@drunkcoding/agents-and-skills 0.0.13 → 0.0.14

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 (78) hide show
  1. package/.claude-plugin/marketplace.json +9 -6
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/plugins/html-effectiveness/.claude-plugin/plugin.json +1 -1
  5. package/plugins/html-effectiveness/README.md +9 -4
  6. package/plugins/html-effectiveness/agents/report-builder.md +4 -0
  7. package/plugins/html-effectiveness/assets/base.css +40 -20
  8. package/plugins/html-effectiveness/docs/template-gallery.md +28 -0
  9. package/plugins/html-effectiveness/scripts/_tmp.js +37 -0
  10. package/plugins/html-effectiveness/scripts/catalog.js +47 -0
  11. package/plugins/html-effectiveness/scripts/extract.js +104 -0
  12. package/plugins/html-effectiveness/scripts/render.js +2 -2
  13. package/plugins/html-effectiveness/skills/html-effectiveness/SKILL.md +4 -4
  14. package/plugins/html-effectiveness/templates/01-exploration-code-approaches.html.tmpl +400 -0
  15. package/plugins/html-effectiveness/templates/02-exploration-visual-designs.html.tmpl +456 -0
  16. package/plugins/html-effectiveness/templates/03-code-review-pr.html.tmpl +581 -0
  17. package/plugins/html-effectiveness/templates/04-code-understanding.html.tmpl +443 -0
  18. package/plugins/html-effectiveness/templates/05-design-system.html.tmpl +578 -0
  19. package/plugins/html-effectiveness/templates/06-component-variants.html.tmpl +557 -0
  20. package/plugins/html-effectiveness/templates/07-prototype-animation.html.tmpl +417 -0
  21. package/plugins/html-effectiveness/templates/08-prototype-interaction.html.tmpl +363 -0
  22. package/plugins/html-effectiveness/templates/09-slide-deck.html.tmpl +535 -0
  23. package/plugins/html-effectiveness/templates/10-svg-illustrations.html.tmpl +444 -0
  24. package/plugins/html-effectiveness/templates/11-status-report.html.tmpl +152 -0
  25. package/plugins/html-effectiveness/templates/12-incident-report.html.tmpl +547 -0
  26. package/plugins/html-effectiveness/templates/13-flowchart-diagram.html.tmpl +361 -0
  27. package/plugins/html-effectiveness/templates/14-research-feature-explainer.html.tmpl +348 -0
  28. package/plugins/html-effectiveness/templates/15-research-concept-explainer.html.tmpl +334 -0
  29. package/plugins/html-effectiveness/templates/16-implementation-plan.html.tmpl +657 -0
  30. package/plugins/html-effectiveness/templates/17-pr-writeup.html.tmpl +553 -0
  31. package/plugins/html-effectiveness/templates/18-editor-triage-board.html.tmpl +541 -0
  32. package/plugins/html-effectiveness/templates/19-editor-feature-flags.html.tmpl +627 -0
  33. package/plugins/html-effectiveness/templates/20-editor-prompt-tuner.html.tmpl +687 -0
  34. package/plugins/html-effectiveness/templates/manifest.json +436 -2
  35. package/plugins/html-effectiveness/tests/_tmp.test.js +58 -0
  36. package/plugins/html-effectiveness/tests/fixtures/01-exploration-code-approaches.data.json +3 -0
  37. package/plugins/html-effectiveness/tests/fixtures/02-exploration-visual-designs.data.json +3 -0
  38. package/plugins/html-effectiveness/tests/fixtures/03-code-review-pr.data.json +3 -0
  39. package/plugins/html-effectiveness/tests/fixtures/04-code-understanding.data.json +3 -0
  40. package/plugins/html-effectiveness/tests/fixtures/05-design-system.data.json +3 -0
  41. package/plugins/html-effectiveness/tests/fixtures/06-component-variants.data.json +3 -0
  42. package/plugins/html-effectiveness/tests/fixtures/07-prototype-animation.data.json +3 -0
  43. package/plugins/html-effectiveness/tests/fixtures/08-prototype-interaction.data.json +3 -0
  44. package/plugins/html-effectiveness/tests/fixtures/09-slide-deck.data.json +3 -0
  45. package/plugins/html-effectiveness/tests/fixtures/10-svg-illustrations.data.json +3 -0
  46. package/plugins/html-effectiveness/tests/fixtures/11-status-report.data.json +37 -0
  47. package/plugins/html-effectiveness/tests/fixtures/12-incident-report.data.json +3 -0
  48. package/plugins/html-effectiveness/tests/fixtures/13-flowchart-diagram.data.json +3 -0
  49. package/plugins/html-effectiveness/tests/fixtures/14-research-feature-explainer.data.json +3 -0
  50. package/plugins/html-effectiveness/tests/fixtures/15-research-concept-explainer.data.json +3 -0
  51. package/plugins/html-effectiveness/tests/fixtures/16-implementation-plan.data.json +3 -0
  52. package/plugins/html-effectiveness/tests/fixtures/17-pr-writeup.data.json +3 -0
  53. package/plugins/html-effectiveness/tests/fixtures/18-editor-triage-board.data.json +3 -0
  54. package/plugins/html-effectiveness/tests/fixtures/19-editor-feature-flags.data.json +3 -0
  55. package/plugins/html-effectiveness/tests/fixtures/20-editor-prompt-tuner.data.json +3 -0
  56. package/plugins/html-effectiveness/tests/manifest.test.js +61 -0
  57. package/plugins/plugin-validator/.claude-plugin/plugin.json +1 -1
  58. package/plugins/plugin-validator/skills/validate-agents/SKILL.md +2 -2
  59. package/plugins/plugin-validator/skills/validate-commands/SKILL.md +2 -2
  60. package/plugins/plugin-validator/skills/validate-hooks/SKILL.md +57 -18
  61. package/plugins/team-superpower/.claude-plugin/plugin.json +7 -39
  62. package/plugins/team-superpower/README.md +73 -19
  63. package/plugins/team-superpower/agents/backend-developer.md +33 -0
  64. package/plugins/team-superpower/agents/designer.md +5 -1
  65. package/plugins/team-superpower/agents/frontend-developer.md +33 -0
  66. package/plugins/team-superpower/agents/planner.md +31 -11
  67. package/plugins/team-superpower/agents/qa-engineer.md +30 -0
  68. package/plugins/team-superpower/agents/reviewer.md +37 -21
  69. package/plugins/team-superpower/agents/security-engineer.md +30 -0
  70. package/plugins/team-superpower/agents/software-architect.md +30 -0
  71. package/plugins/team-superpower/assets/ESCALATION.md +19 -5
  72. package/plugins/team-superpower/assets/SESSION_README.md +31 -11
  73. package/plugins/team-superpower/commands/team-feature-resume.md +40 -5
  74. package/plugins/team-superpower/commands/team-feature.md +148 -22
  75. package/plugins/team-superpower/hooks/hooks.json +0 -3
  76. package/plugins/tech-graph/.claude-plugin/plugin.json +1 -1
  77. package/plugins/tech-graph/agents/tech-graph.md +8 -0
  78. package/plugins/team-superpower/agents/implementer.md +0 -46
@@ -0,0 +1,627 @@
1
+ <!--
2
+ template: 19-editor-feature-flags
3
+ upstream: 19-editor-feature-flags.html
4
+ slots:
5
+ title: string # replaces first <h1> innerHTML
6
+ -->
7
+ <div class="wrap">
8
+
9
+ <header>
10
+ <div class="eyebrow">Acme / editor / feature-flags</div>
11
+ <h1>{{title}}</h1>
12
+ <p class="sub">A form-based editor for the production feature-flag config.
13
+ Toggle flags, fix dependency warnings as they surface, then copy out
14
+ only the lines that changed.</p>
15
+ </header>
16
+
17
+ <div class="layout">
18
+ <div class="banner" id="banner">
19
+ <span class="glyph">&#9888;</span>
20
+ <span id="bannerText">1 flag is enabled without its prerequisite</span>
21
+ </div>
22
+
23
+ <div class="form-col" id="formCol"><!-- groups injected --></div>
24
+
25
+ <aside class="sidebar">
26
+ <div>
27
+ <div class="side-label">Pending changes</div>
28
+ <div class="side-count" id="sideCount">0 changed</div>
29
+ </div>
30
+ <pre class="diff" id="diff"><span class="empty">// no changes yet</span></pre>
31
+ <div class="btn-stack">
32
+ <button class="btn primary" id="copyDiff" disabled>Copy diff</button>
33
+ <button class="btn ghost" id="copyJson">Copy full JSON</button>
34
+ <button class="btn ghost" id="reset">Reset</button>
35
+ </div>
36
+ </aside>
37
+ </div>
38
+
39
+ </div>
40
+
41
+ <style>
42
+ * { box-sizing: border-box; }
43
+ .wrap {
44
+ max-width: 1080px;
45
+ margin: 0 auto;
46
+ padding: 48px 32px 64px;
47
+ }
48
+ /* ---------- header ---------- */
49
+ header { margin-bottom: 28px; }
50
+ .eyebrow {
51
+ font-family: var(--mono);
52
+ font-size: 11px;
53
+ letter-spacing: 0.08em;
54
+ text-transform: uppercase;
55
+ color: var(--gray-500);
56
+ }
57
+ h1 {
58
+ font-family: var(--serif);
59
+ font-weight: 500;
60
+ font-size: 30px;
61
+ letter-spacing: -0.01em;
62
+ margin: 6px 0 4px;
63
+ }
64
+ .sub {
65
+ color: var(--gray-500);
66
+ max-width: 600px;
67
+ }
68
+ /* ---------- layout ---------- */
69
+ .layout {
70
+ display: grid;
71
+ grid-template-columns: 1fr 320px;
72
+ gap: 24px;
73
+ align-items: start;
74
+ }
75
+ @media (max-width: 880px) {
76
+ .layout { grid-template-columns: 1fr; }
77
+ .sidebar { position: static !important; }
78
+ }
79
+ /* ---------- warning banner ---------- */
80
+ .banner {
81
+ grid-column: 1 / -1;
82
+ display: none;
83
+ align-items: baseline;
84
+ gap: 8px;
85
+ padding: 10px 14px;
86
+ border: 1.5px solid #E8C2AE;
87
+ background: #F8E9E0;
88
+ color: var(--clay-d);
89
+ border-radius: 10px;
90
+ font-size: 13px;
91
+ }
92
+ .banner.show { display: flex; }
93
+ .banner .glyph { font-size: 13px; line-height: 1; transform: translateY(1px); }
94
+ /* ---------- groups / panels ---------- */
95
+ .form-col { display: flex; flex-direction: column; gap: 16px; }
96
+ .group {
97
+ background: var(--white);
98
+ border: 1.5px solid var(--gray-200);
99
+ border-radius: 12px;
100
+ overflow: hidden;
101
+ }
102
+ .group-head {
103
+ font-family: var(--mono);
104
+ font-size: 11px;
105
+ letter-spacing: 0.08em;
106
+ text-transform: uppercase;
107
+ color: var(--gray-500);
108
+ padding: 12px 18px 11px;
109
+ border-bottom: 1.5px solid var(--gray-50);
110
+ display: flex;
111
+ justify-content: space-between;
112
+ gap: 12px;
113
+ }
114
+ .group-head .name { color: var(--gray-800); }
115
+ .group-head .meta { letter-spacing: 0.04em; }
116
+ /* ---------- flag rows ---------- */
117
+ .flag {
118
+ display: flex;
119
+ align-items: flex-start;
120
+ gap: 16px;
121
+ padding: 14px 18px;
122
+ border-bottom: 1px solid var(--gray-50);
123
+ border-left: 3px solid transparent;
124
+ transition: border-left-color 140ms ease, background 140ms ease;
125
+ }
126
+ .flag:last-child { border-bottom: none; }
127
+ .flag.warn {
128
+ border-left-color: var(--clay);
129
+ background: #FBF3EE;
130
+ }
131
+ .flag-info { flex: 1; min-width: 0; }
132
+ .flag-key {
133
+ font-family: var(--mono);
134
+ font-size: 13px;
135
+ color: var(--gray-800);
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 7px;
139
+ flex-wrap: wrap;
140
+ }
141
+ .dot {
142
+ display: none;
143
+ width: 6px;
144
+ height: 6px;
145
+ border-radius: 50%;
146
+ background: var(--clay);
147
+ flex: none;
148
+ }
149
+ .flag.changed .dot { display: inline-block; }
150
+ .rollout {
151
+ font-family: var(--mono);
152
+ font-size: 10px;
153
+ letter-spacing: 0.03em;
154
+ color: var(--gray-500);
155
+ background: var(--gray-50);
156
+ border: 1px solid var(--gray-200);
157
+ border-radius: 999px;
158
+ padding: 1px 7px 2px;
159
+ }
160
+ .flag-desc {
161
+ color: var(--gray-500);
162
+ font-size: 12.5px;
163
+ margin-top: 2px;
164
+ }
165
+ .req-chip {
166
+ display: inline-flex;
167
+ align-items: baseline;
168
+ gap: 5px;
169
+ margin-top: 7px;
170
+ font-family: var(--mono);
171
+ font-size: 11px;
172
+ color: var(--gray-500);
173
+ background: var(--gray-50);
174
+ border: 1px solid var(--gray-200);
175
+ border-radius: 999px;
176
+ padding: 2px 9px 3px;
177
+ }
178
+ .req-chip .warn-glyph { display: none; }
179
+ .flag.warn .req-chip {
180
+ color: var(--clay-d);
181
+ background: #F8E1D5;
182
+ border-color: #E8C2AE;
183
+ }
184
+ .flag.warn .req-chip .warn-glyph { display: inline; }
185
+ /* ---------- toggle switch ---------- */
186
+ .toggle {
187
+ position: relative;
188
+ flex: none;
189
+ width: 38px;
190
+ height: 22px;
191
+ margin-top: 1px;
192
+ }
193
+ .toggle input {
194
+ position: absolute;
195
+ inset: 0;
196
+ width: 100%;
197
+ height: 100%;
198
+ margin: 0;
199
+ opacity: 0;
200
+ cursor: pointer;
201
+ }
202
+ .toggle .track {
203
+ position: absolute;
204
+ inset: 0;
205
+ background: var(--gray-200);
206
+ border-radius: 999px;
207
+ transition: background 160ms ease;
208
+ pointer-events: none;
209
+ }
210
+ .toggle .track::after {
211
+ content: "";
212
+ position: absolute;
213
+ top: 3px;
214
+ left: 3px;
215
+ width: 16px;
216
+ height: 16px;
217
+ border-radius: 50%;
218
+ background: var(--white);
219
+ box-shadow: 0 1px 2px rgba(20,20,19,0.18);
220
+ transition: transform 160ms ease;
221
+ }
222
+ .toggle input:checked + .track { background: var(--olive); }
223
+ .toggle input:checked + .track::after { transform: translateX(16px); }
224
+ .toggle input:focus-visible + .track {
225
+ outline: 2px solid var(--slate);
226
+ outline-offset: 2px;
227
+ }
228
+ /* ---------- sidebar ---------- */
229
+ .sidebar {
230
+ position: sticky;
231
+ top: 24px;
232
+ background: var(--white);
233
+ border: 1.5px solid var(--gray-200);
234
+ border-radius: 12px;
235
+ padding: 18px;
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 14px;
239
+ }
240
+ .side-label {
241
+ font-family: var(--mono);
242
+ font-size: 11px;
243
+ letter-spacing: 0.08em;
244
+ text-transform: uppercase;
245
+ color: var(--gray-500);
246
+ }
247
+ .side-count {
248
+ font-family: var(--serif);
249
+ font-weight: 500;
250
+ font-size: 19px;
251
+ letter-spacing: -0.01em;
252
+ }
253
+ .side-count .warn-n { color: var(--clay-d); }
254
+ .side-count .sep { color: var(--gray-200); margin: 0 4px; }
255
+ .diff {
256
+ margin: 0;
257
+ font-family: var(--mono);
258
+ font-size: 12px;
259
+ line-height: 1.7;
260
+ background: var(--gray-50);
261
+ border: 1px solid var(--gray-200);
262
+ border-radius: 8px;
263
+ padding: 12px 14px;
264
+ overflow-x: auto;
265
+ max-height: 320px;
266
+ overflow-y: auto;
267
+ white-space: pre;
268
+ }
269
+ .diff .minus { color: var(--gray-500); }
270
+ .diff .plus { color: var(--olive); }
271
+ .diff .empty { color: var(--gray-500); }
272
+ .btn {
273
+ display: block;
274
+ width: 100%;
275
+ border: none;
276
+ border-radius: 999px;
277
+ font-family: var(--mono);
278
+ font-size: 12px;
279
+ padding: 9px 16px;
280
+ cursor: pointer;
281
+ text-align: center;
282
+ transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
283
+ }
284
+ .btn.primary {
285
+ background: var(--slate);
286
+ color: var(--ivory);
287
+ }
288
+ .btn.primary:hover:not(:disabled) { background: var(--gray-800); }
289
+ .btn.primary:disabled {
290
+ background: var(--gray-200);
291
+ color: var(--gray-500);
292
+ cursor: not-allowed;
293
+ }
294
+ .btn.primary.copied { background: var(--olive); }
295
+ .btn.ghost {
296
+ background: transparent;
297
+ color: var(--gray-800);
298
+ border: 1.5px solid var(--gray-200);
299
+ }
300
+ .btn.ghost:hover { border-color: var(--gray-500); }
301
+ .btn.ghost.copied { border-color: var(--olive); color: var(--olive); }
302
+ .btn-stack { display: flex; flex-direction: column; gap: 8px; }
303
+ </style>
304
+
305
+ <script>
306
+ "use strict";
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // data — the production flag set, as it exists right now
310
+ // ---------------------------------------------------------------------------
311
+
312
+ const GROUPS = [
313
+ {
314
+ id: "onboarding",
315
+ label: "Onboarding",
316
+ flags: [
317
+ { key: "onboarding.checklist_v2",
318
+ desc: "New three-step setup checklist replacing the modal tour.",
319
+ on: true },
320
+ { key: "onboarding.invite_nudge",
321
+ desc: "Nudge owners to invite teammates after creating their first project.",
322
+ on: true },
323
+ { key: "onboarding.workspace_templates",
324
+ desc: "Offer prebuilt workspace templates during signup.",
325
+ on: false, requires: "onboarding.checklist_v2" },
326
+ { key: "onboarding.skip_email_verify",
327
+ desc: "Let SSO-domain users in before email verification completes.",
328
+ on: false }
329
+ ]
330
+ },
331
+ {
332
+ id: "sync",
333
+ label: "Sync engine",
334
+ flags: [
335
+ { key: "sync.delta_compression",
336
+ desc: "Send field-level deltas instead of full document snapshots.",
337
+ on: true },
338
+ { key: "sync.offline_queue_v2",
339
+ desc: "Persist offline edits to IndexedDB and replay on reconnect.",
340
+ on: false, requires: "sync.delta_compression" },
341
+ { key: "sync.presence_cursors",
342
+ desc: "Show live collaborator cursors in board and doc views.",
343
+ on: true },
344
+ { key: "sync.conflict_banner",
345
+ desc: "Surface a merge banner instead of silently last-write-wins.",
346
+ on: false, requires: "sync.offline_queue_v2" },
347
+ { key: "sync.binary_ws_frames",
348
+ desc: "Switch the realtime channel to binary WebSocket frames.",
349
+ on: false, rollout: 10 }
350
+ ]
351
+ },
352
+ {
353
+ id: "billing",
354
+ label: "Billing",
355
+ flags: [
356
+ { key: "billing.usage_meter",
357
+ desc: "Show per-seat usage meter on the workspace billing page.",
358
+ on: false },
359
+ { key: "billing.annual_discount_banner",
360
+ desc: "Promote the annual-plan discount in the upgrade flow.",
361
+ on: true },
362
+ { key: "billing.proration_preview",
363
+ desc: "Preview the prorated charge before confirming a plan change.",
364
+ on: false, requires: "billing.usage_meter" },
365
+ { key: "billing.dunning_emails_v3",
366
+ desc: "Use the rewritten dunning sequence with a 14-day grace window.",
367
+ on: true, rollout: 25 }
368
+ ]
369
+ },
370
+ {
371
+ id: "internal",
372
+ label: "Internal",
373
+ flags: [
374
+ { key: "internal.shadow_traffic",
375
+ desc: "Mirror 1% of API traffic to the staging cluster for diffing.",
376
+ on: false },
377
+ { key: "internal.query_tracing",
378
+ desc: "Attach OpenTelemetry spans to every Postgres query.",
379
+ on: true },
380
+ { key: "internal.kill_switch_ui",
381
+ desc: "Expose the emergency kill-switch panel in the admin console.",
382
+ on: true, requires: "internal.query_tracing" }
383
+ ]
384
+ }
385
+ ];
386
+
387
+ // flat lookup, plus a frozen copy of initial state
388
+ const FLAGS = {};
389
+ const INITIAL = {};
390
+ GROUPS.forEach(g => g.flags.forEach(f => {
391
+ FLAGS[f.key] = f;
392
+ INITIAL[f.key] = f.on;
393
+ }));
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // render the form once; state lives in the checkboxes
397
+ // ---------------------------------------------------------------------------
398
+
399
+ const formCol = document.getElementById("formCol");
400
+ const banner = document.getElementById("banner");
401
+ const bannerTxt = document.getElementById("bannerText");
402
+ const sideCount = document.getElementById("sideCount");
403
+ const diffEl = document.getElementById("diff");
404
+ const copyDiffBtn = document.getElementById("copyDiff");
405
+ const copyJsonBtn = document.getElementById("copyJson");
406
+ const resetBtn = document.getElementById("reset");
407
+
408
+ function esc(s) {
409
+ return String(s).replace(/[&<>"]/g, c =>
410
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
411
+ }
412
+
413
+ function buildForm() {
414
+ formCol.innerHTML = "";
415
+ for (const g of GROUPS) {
416
+ const panel = document.createElement("section");
417
+ panel.className = "group";
418
+ panel.dataset.group = g.id;
419
+
420
+ const head = document.createElement("div");
421
+ head.className = "group-head";
422
+ head.innerHTML =
423
+ '<span class="name">' + esc(g.label) + '</span>' +
424
+ '<span class="meta" data-meta></span>';
425
+ panel.appendChild(head);
426
+
427
+ for (const f of g.flags) {
428
+ const row = document.createElement("div");
429
+ row.className = "flag";
430
+ row.dataset.key = f.key;
431
+
432
+ const rolloutBadge = (typeof f.rollout === "number")
433
+ ? '<span class="rollout">' + f.rollout + '%</span>'
434
+ : "";
435
+
436
+ const reqChip = f.requires
437
+ ? '<div class="req-chip"><span class="warn-glyph">&#9888;</span>' +
438
+ '<span>requires ' + esc(f.requires) + '</span></div>'
439
+ : "";
440
+
441
+ row.innerHTML =
442
+ '<label class="toggle">' +
443
+ '<input type="checkbox" data-key="' + esc(f.key) + '"' +
444
+ (f.on ? " checked" : "") +
445
+ ' aria-label="' + esc(f.key) + '">' +
446
+ '<span class="track"></span>' +
447
+ '</label>' +
448
+ '<div class="flag-info">' +
449
+ '<div class="flag-key"><span class="dot"></span>' +
450
+ '<span>' + esc(f.key) + '</span>' + rolloutBadge +
451
+ '</div>' +
452
+ '<div class="flag-desc">' + esc(f.desc) + '</div>' +
453
+ reqChip +
454
+ '</div>';
455
+
456
+ panel.appendChild(row);
457
+ }
458
+ formCol.appendChild(panel);
459
+ }
460
+ }
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // state helpers
464
+ // ---------------------------------------------------------------------------
465
+
466
+ function currentState() {
467
+ const state = {};
468
+ formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
469
+ state[cb.dataset.key] = cb.checked;
470
+ });
471
+ return state;
472
+ }
473
+
474
+ function computeDiff(state) {
475
+ const lines = [];
476
+ for (const g of GROUPS) {
477
+ for (const f of g.flags) {
478
+ if (state[f.key] !== INITIAL[f.key]) {
479
+ lines.push({ key: f.key, from: INITIAL[f.key], to: state[f.key] });
480
+ }
481
+ }
482
+ }
483
+ return lines;
484
+ }
485
+
486
+ function diffText(lines) {
487
+ return lines.map(l =>
488
+ '- "' + l.key + '": ' + l.from + '\n' +
489
+ '+ "' + l.key + '": ' + l.to
490
+ ).join("\n");
491
+ }
492
+
493
+ function fullJson(state) {
494
+ // grouped, ordered, pretty-printed — what you'd actually paste into the file
495
+ let out = "{\n";
496
+ GROUPS.forEach((g, gi) => {
497
+ g.flags.forEach((f, fi) => {
498
+ const last = gi === GROUPS.length - 1 && fi === g.flags.length - 1;
499
+ out += ' "' + f.key + '": ' + state[f.key] + (last ? "" : ",") + "\n";
500
+ });
501
+ });
502
+ out += "}";
503
+ return out;
504
+ }
505
+
506
+ // ---------------------------------------------------------------------------
507
+ // the live update pass
508
+ // ---------------------------------------------------------------------------
509
+
510
+ function update() {
511
+ const state = currentState();
512
+ let warnCount = 0;
513
+
514
+ // per-row decorations
515
+ for (const g of GROUPS) {
516
+ let onCount = 0;
517
+ for (const f of g.flags) {
518
+ const row = formCol.querySelector('.flag[data-key="' + CSS.escape(f.key) + '"]');
519
+ const on = state[f.key];
520
+ if (on) onCount++;
521
+
522
+ const changed = on !== INITIAL[f.key];
523
+ row.classList.toggle("changed", changed);
524
+
525
+ let warn = false;
526
+ if (f.requires && on && !state[f.requires]) warn = true;
527
+ row.classList.toggle("warn", warn);
528
+ if (warn) warnCount++;
529
+ }
530
+ const meta = formCol.querySelector(
531
+ '.group[data-group="' + g.id + '"] [data-meta]');
532
+ meta.textContent = g.flags.length + " flags, " + onCount + " on";
533
+ }
534
+
535
+ // diff + sidebar
536
+ const lines = computeDiff(state);
537
+ if (lines.length === 0) {
538
+ diffEl.innerHTML = '<span class="empty">// no changes yet</span>';
539
+ copyDiffBtn.disabled = true;
540
+ } else {
541
+ diffEl.innerHTML = lines.map(l =>
542
+ '<span class="minus">- "' + esc(l.key) + '": ' + l.from + '</span>\n' +
543
+ '<span class="plus">+ "' + esc(l.key) + '": ' + l.to + '</span>'
544
+ ).join("\n");
545
+ copyDiffBtn.disabled = false;
546
+ }
547
+
548
+ sideCount.innerHTML =
549
+ lines.length + " changed" +
550
+ '<span class="sep">&middot;</span>' +
551
+ '<span class="' + (warnCount ? "warn-n" : "") + '">' +
552
+ warnCount + " warning" + (warnCount === 1 ? "" : "s") +
553
+ "</span>";
554
+
555
+ // banner
556
+ if (warnCount > 0) {
557
+ bannerTxt.textContent =
558
+ warnCount === 1
559
+ ? "1 flag is enabled without its prerequisite"
560
+ : warnCount + " flags are enabled without their prerequisites";
561
+ banner.classList.add("show");
562
+ } else {
563
+ banner.classList.remove("show");
564
+ }
565
+ }
566
+
567
+ // ---------------------------------------------------------------------------
568
+ // clipboard + buttons
569
+ // ---------------------------------------------------------------------------
570
+
571
+ function flash(btn, label) {
572
+ const orig = btn.textContent;
573
+ btn.textContent = label;
574
+ btn.classList.add("copied");
575
+ btn.disabled = true;
576
+ setTimeout(() => {
577
+ btn.textContent = orig;
578
+ btn.classList.remove("copied");
579
+ btn.disabled = false;
580
+ update(); // restore disabled state on copyDiff if needed
581
+ }, 1200);
582
+ }
583
+
584
+ function writeClipboard(text) {
585
+ if (navigator.clipboard && navigator.clipboard.writeText) {
586
+ return navigator.clipboard.writeText(text);
587
+ }
588
+ // fallback for file:// contexts without clipboard permission
589
+ const ta = document.createElement("textarea");
590
+ ta.value = text;
591
+ ta.style.position = "fixed";
592
+ ta.style.left = "-9999px";
593
+ document.body.appendChild(ta);
594
+ ta.select();
595
+ try { document.execCommand("copy"); } catch (e) { /* ignore */ }
596
+ document.body.removeChild(ta);
597
+ return Promise.resolve();
598
+ }
599
+
600
+ copyDiffBtn.addEventListener("click", () => {
601
+ const lines = computeDiff(currentState());
602
+ if (!lines.length) return;
603
+ writeClipboard(diffText(lines)).then(() => flash(copyDiffBtn, "Copied ✓"));
604
+ });
605
+
606
+ copyJsonBtn.addEventListener("click", () => {
607
+ writeClipboard(fullJson(currentState())).then(() => flash(copyJsonBtn, "Copied ✓"));
608
+ });
609
+
610
+ resetBtn.addEventListener("click", () => {
611
+ formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
612
+ cb.checked = INITIAL[cb.dataset.key];
613
+ });
614
+ update();
615
+ });
616
+
617
+ formCol.addEventListener("change", e => {
618
+ if (e.target.matches("input[type=checkbox]")) update();
619
+ });
620
+
621
+ // ---------------------------------------------------------------------------
622
+ // boot
623
+ // ---------------------------------------------------------------------------
624
+
625
+ buildForm();
626
+ update();
627
+ </script>