openclacky 1.3.2 → 1.3.4

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/Dockerfile +3 -0
  4. data/README.md +1 -1
  5. data/README_JA.md +237 -0
  6. data/docs/rich_ui_guide.md +277 -0
  7. data/docs/rich_ui_refactor_plan.md +396 -0
  8. data/lib/clacky/agent/llm_caller.rb +10 -4
  9. data/lib/clacky/agent/session_serializer.rb +52 -7
  10. data/lib/clacky/agent/time_machine.rb +247 -26
  11. data/lib/clacky/agent.rb +15 -3
  12. data/lib/clacky/api_extension.rb +262 -0
  13. data/lib/clacky/api_extension_loader.rb +156 -0
  14. data/lib/clacky/cli.rb +93 -3
  15. data/lib/clacky/client.rb +38 -13
  16. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  17. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  18. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  19. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  20. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  21. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  22. data/lib/clacky/default_skills/media-gen/SKILL.md +37 -10
  23. data/lib/clacky/idle_compression_timer.rb +3 -1
  24. data/lib/clacky/locales/en.rb +26 -0
  25. data/lib/clacky/locales/i18n.rb +26 -0
  26. data/lib/clacky/locales/zh.rb +26 -0
  27. data/lib/clacky/media/openai_compat.rb +64 -1
  28. data/lib/clacky/message_history.rb +9 -0
  29. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  30. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  31. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  32. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  33. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  34. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  35. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  36. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  37. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  38. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  39. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  40. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  41. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  42. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  43. data/lib/clacky/rich_ui.rb +57 -0
  44. data/lib/clacky/rich_ui_controller.rb +3 -1549
  45. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  46. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  47. data/lib/clacky/server/git_panel.rb +115 -0
  48. data/lib/clacky/server/http_server.rb +547 -15
  49. data/lib/clacky/server/server_master.rb +6 -4
  50. data/lib/clacky/server/session_registry.rb +1 -1
  51. data/lib/clacky/shell_hook_loader.rb +1 -1
  52. data/lib/clacky/tools/edit.rb +14 -2
  53. data/lib/clacky/ui2/ui_controller.rb +7 -0
  54. data/lib/clacky/version.rb +1 -1
  55. data/lib/clacky/web/app.css +511 -101
  56. data/lib/clacky/web/app.js +95 -14
  57. data/lib/clacky/web/components/code-editor.js +197 -0
  58. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  59. data/lib/clacky/web/{onboard.js → components/onboard.js} +18 -2
  60. data/lib/clacky/web/core/aside.js +117 -0
  61. data/lib/clacky/web/core/ext.js +387 -0
  62. data/lib/clacky/web/features/backup/store.js +92 -0
  63. data/lib/clacky/web/features/backup/view.js +94 -0
  64. data/lib/clacky/web/features/billing/store.js +163 -0
  65. data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
  66. data/lib/clacky/web/features/brand/store.js +110 -0
  67. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  68. data/lib/clacky/web/features/channels/store.js +103 -0
  69. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  70. data/lib/clacky/web/features/creator/store.js +81 -0
  71. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  72. data/lib/clacky/web/features/mcp/store.js +158 -0
  73. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  74. data/lib/clacky/web/features/model-tester/store.js +77 -0
  75. data/lib/clacky/web/features/model-tester/view.js +7 -0
  76. data/lib/clacky/web/features/profile/store.js +170 -0
  77. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  78. data/lib/clacky/web/features/share/store.js +145 -0
  79. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  80. data/lib/clacky/web/features/skills/store.js +331 -0
  81. data/lib/clacky/web/features/skills/view.js +581 -0
  82. data/lib/clacky/web/features/tasks/store.js +135 -0
  83. data/lib/clacky/web/features/tasks/view.js +241 -0
  84. data/lib/clacky/web/features/trash/store.js +242 -0
  85. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  86. data/lib/clacky/web/features/version/store.js +165 -0
  87. data/lib/clacky/web/features/version/view.js +323 -0
  88. data/lib/clacky/web/features/workspace/store.js +99 -0
  89. data/lib/clacky/web/features/workspace/view.js +305 -0
  90. data/lib/clacky/web/i18n.js +68 -6
  91. data/lib/clacky/web/index.html +113 -62
  92. data/lib/clacky/web/sessions.js +493 -39
  93. data/lib/clacky/web/settings.js +143 -49
  94. data/lib/clacky/web/skills.js +3 -863
  95. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  96. data/lib/clacky/web/ws-dispatcher.js +7 -3
  97. data/lib/clacky.rb +17 -1
  98. metadata +81 -20
  99. data/lib/clacky/web/backup.js +0 -119
  100. data/lib/clacky/web/model-tester.js +0 -66
  101. data/lib/clacky/web/tasks.js +0 -373
  102. data/lib/clacky/web/version.js +0 -449
  103. data/lib/clacky/web/workspace.js +0 -316
  104. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  105. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  106. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  107. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -96,6 +96,10 @@ const Settings = (() => {
96
96
  <span class="model-card-grid-name">${_esc(displayName)}</span>
97
97
  ${isDefault ? `<span class="badge badge-default">${I18n.t("settings.models.badge.default")}</span>` : ""}
98
98
  ${isLite ? `<span class="badge badge-lite">${I18n.t("settings.models.badge.lite")}</span>` : ""}
99
+ ${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default" style="margin-left:auto">
100
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
101
+ <span>${I18n.t("settings.models.btn.setDefault")}</span>
102
+ </button>` : ""}
99
103
  </div>
100
104
  <div class="model-card-grid-provider">${_esc(providerName)}</div>
101
105
  ${model.api_key_masked ? `<div class="model-card-grid-model">${_esc(model.api_key_masked)}</div>` : ""}
@@ -113,6 +117,10 @@ const Settings = (() => {
113
117
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
114
118
  <span>${I18n.t("settings.models.btn.edit")}</span>
115
119
  </button>
120
+ <button class="btn-card-grid-action" data-index="${index}" data-action="duplicate">
121
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
122
+ <span>${I18n.t("settings.models.btn.duplicate")}</span>
123
+ </button>
116
124
  ${_models.length > 1 ? `<button class="btn-card-grid-action btn-card-grid-action-danger" data-index="${index}" data-action="delete">
117
125
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
118
126
  <span>${I18n.t("settings.models.btn.delete")}</span>
@@ -120,11 +128,7 @@ const Settings = (() => {
120
128
  </div>
121
129
  </div>
122
130
  <div class="model-card-grid-footer">
123
- ${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default">
124
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
125
- <span>${I18n.t("settings.models.btn.setDefault")}</span>
126
- </button>` : `<span></span>`}
127
- ${websiteUrl ? `<a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer">
131
+ ${websiteUrl ? `<a class="model-card-grid-link" href="${_esc(websiteUrl)}" target="_blank" rel="noopener noreferrer" style="margin-left:auto">
128
132
  ${I18n.t("settings.models.link.topUp")}
129
133
  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 17L17 7"/><path d="M8 7h9v9"/></svg>
130
134
  </a>` : ""}
@@ -140,10 +144,11 @@ const Settings = (() => {
140
144
  btn.addEventListener("click", () => {
141
145
  const action = btn.dataset.action;
142
146
  switch (action) {
143
- case "edit": _openModal(index); break;
144
- case "test": _testModel(index); break;
145
- case "delete": _removeModel(index); break;
146
- case "default": _setAsDefault(index); break;
147
+ case "edit": _openModal(index); break;
148
+ case "test": _testModel(index); break;
149
+ case "delete": _removeModel(index); break;
150
+ case "default": _setAsDefault(index); break;
151
+ case "duplicate": _openModalDuplicate(index); break;
147
152
  }
148
153
  });
149
154
  });
@@ -157,6 +162,7 @@ const Settings = (() => {
157
162
  const indexInput = document.getElementById("model-modal-index");
158
163
 
159
164
  indexInput.value = index;
165
+ document.getElementById("model-modal-source-id").value = "";
160
166
 
161
167
  // Populate provider dropdown
162
168
  _populateModalProviderDropdown();
@@ -169,7 +175,14 @@ const Settings = (() => {
169
175
  document.getElementById("model-modal-baseurl").value = model.base_url || "";
170
176
  document.getElementById("model-modal-apikey").value = model.api_key_masked || "";
171
177
  document.getElementById("model-modal-default-field").style.display = "";
172
- document.getElementById("model-modal-set-default").checked = (model.type === "default");
178
+ // Lock the checkbox when this is the only configured model: the system
179
+ // must always have one default (backend re-promotes on save), so
180
+ // unchecking would be a silent no-op. Force-checked + disabled makes
181
+ // the constraint visible without any extra copy.
182
+ const setDefaultCb = document.getElementById("model-modal-set-default");
183
+ const isOnlyModel = _models.length === 1;
184
+ setDefaultCb.checked = isOnlyModel ? true : (model.type === "default");
185
+ setDefaultCb.disabled = isOnlyModel;
173
186
 
174
187
  // Set provider dropdown value
175
188
  const matched = _findProviderByBaseUrl(model.base_url);
@@ -190,6 +203,9 @@ const Settings = (() => {
190
203
  // Default to checked for new models — most users want their first/new
191
204
  // model to take over as the default.
192
205
  document.getElementById("model-modal-set-default").checked = true;
206
+ // Reset disabled flag in case the previous open was edit-mode on the
207
+ // sole-model lock path.
208
+ document.getElementById("model-modal-set-default").disabled = false;
193
209
 
194
210
  // Reset provider dropdown
195
211
  _modalSelectedProviderId = null;
@@ -222,6 +238,30 @@ const Settings = (() => {
222
238
  document.body.style.overflow = "";
223
239
  }
224
240
 
241
+ function _openModalDuplicate(index) {
242
+ const source = _models[index];
243
+ if (!source) return;
244
+
245
+ _openModal(-1);
246
+
247
+ document.getElementById("model-modal-source-id").value = source.id || "";
248
+ document.getElementById("model-modal-title").textContent = I18n.t("settings.models.modal.duplicate");
249
+ document.getElementById("model-modal-model").value = source.model || "";
250
+ document.getElementById("model-modal-baseurl").value = source.base_url || "";
251
+ document.getElementById("model-modal-apikey").value = source.api_key_masked || "";
252
+
253
+ const matched = _findProviderByBaseUrl(source.base_url);
254
+ _modalSelectedProviderId = matched ? matched.id : (source.anthropic_format ? "anthropic" : null);
255
+ const providerValue = document.getElementById("model-modal-provider-value");
256
+ if (matched) {
257
+ providerValue.textContent = matched.name;
258
+ providerValue.classList.remove("placeholder");
259
+
260
+ const promoHint = document.getElementById("model-modal-promo-hint");
261
+ if (matched.id !== "openclacky") promoHint.classList.remove("visible");
262
+ }
263
+ }
264
+
225
265
  function _populateModalProviderDropdown() {
226
266
  const dropdown = document.getElementById("model-modal-provider-dropdown");
227
267
  dropdown.innerHTML = `
@@ -323,13 +363,14 @@ const Settings = (() => {
323
363
  const isNew = index < 0;
324
364
  const existing = isNew ? {} : (_models[index] || {});
325
365
  const existingId = existing.id || null;
366
+ const sourceId = document.getElementById("model-modal-source-id").value || null;
326
367
 
327
368
  // Step 1: Test first
328
369
  saveBtn.textContent = I18n.t("settings.models.btn.testing");
329
370
  _showModalTestResult(null, "");
330
371
 
331
372
  const result = await ModelTester.testConnection({
332
- model, base_url, api_key, index, id: existingId, anthropic_format
373
+ model, base_url, api_key, index, id: existingId || sourceId, anthropic_format
333
374
  });
334
375
 
335
376
  if (result.rewrote) {
@@ -365,10 +406,14 @@ const Settings = (() => {
365
406
  }
366
407
 
367
408
  if (!hasId && !payload.api_key) {
368
- saveBtn.textContent = I18n.t("settings.models.btn.save");
369
- saveBtn.disabled = false;
370
- _showModalTestResult(false, I18n.t("settings.models.placeholder.apikey"));
371
- return;
409
+ if (sourceId) {
410
+ payload.source_id = sourceId;
411
+ } else {
412
+ saveBtn.textContent = I18n.t("settings.models.btn.save");
413
+ saveBtn.disabled = false;
414
+ _showModalTestResult(false, I18n.t("settings.models.placeholder.apikey"));
415
+ return;
416
+ }
372
417
  }
373
418
 
374
419
  const saveResult = await ModelTester.saveModel(payload, { existingId: hasId ? existingId : null });
@@ -395,6 +440,25 @@ const Settings = (() => {
395
440
  el.className = `model-test-result ${ok ? "result-ok" : "result-fail"}`;
396
441
  }
397
442
 
443
+ function _positionDropdownFixed(dropdown, anchor) {
444
+ const rect = anchor.getBoundingClientRect();
445
+ dropdown.style.position = "fixed";
446
+ dropdown.style.top = (rect.bottom + 4) + "px";
447
+ dropdown.style.left = rect.left + "px";
448
+ dropdown.style.width = rect.width + "px";
449
+ dropdown.style.right = "auto";
450
+ dropdown.style.zIndex = "9999";
451
+ }
452
+
453
+ function _resetDropdownPosition(dropdown) {
454
+ dropdown.style.position = "";
455
+ dropdown.style.top = "";
456
+ dropdown.style.left = "";
457
+ dropdown.style.width = "";
458
+ dropdown.style.right = "";
459
+ dropdown.style.zIndex = "";
460
+ }
461
+
398
462
  function _initModal() {
399
463
  // Close button
400
464
  document.getElementById("model-modal-close").addEventListener("click", _closeModal);
@@ -448,34 +512,58 @@ const Settings = (() => {
448
512
  // Model dropdown functionality
449
513
  const modelDropdownBtn = document.getElementById("model-modal-model-dropdown-btn");
450
514
  const modelDropdown = document.getElementById("model-modal-model-dropdown");
515
+ const modelCombobox = document.getElementById("model-modal-model-combobox");
451
516
  const modelInput = document.getElementById("model-modal-model");
452
517
 
518
+ function _openModelDropdown() {
519
+ _closeBaseUrlDropdown();
520
+ _updateModalModelDropdown();
521
+ _positionDropdownFixed(modelDropdown, modelCombobox);
522
+ modelDropdown.style.display = "block";
523
+ document.body.appendChild(modelDropdown);
524
+ }
525
+
526
+ function _closeModelDropdown() {
527
+ modelDropdown.style.display = "none";
528
+ _resetDropdownPosition(modelDropdown);
529
+ modelCombobox.appendChild(modelDropdown);
530
+ }
531
+
453
532
  modelDropdownBtn.addEventListener("click", (e) => {
454
533
  e.stopPropagation();
455
- const isOpen = modelDropdown.style.display === "block";
456
- document.querySelectorAll(".model-name-dropdown, .base-url-dropdown").forEach(d => {
457
- d.style.display = "none";
458
- });
459
- if (!isOpen) {
460
- _updateModalModelDropdown();
461
- modelDropdown.style.display = "block";
534
+ if (modelDropdown.style.display === "block") {
535
+ _closeModelDropdown();
536
+ } else {
537
+ _openModelDropdown();
462
538
  }
463
539
  });
464
540
 
465
541
  // Base URL dropdown functionality
466
542
  const baseUrlDropdownBtn = document.getElementById("model-modal-baseurl-dropdown-btn");
467
543
  const baseUrlDropdown = document.getElementById("model-modal-baseurl-dropdown");
544
+ const baseUrlCombobox = document.getElementById("model-modal-baseurl-combobox");
468
545
  const baseUrlInput = document.getElementById("model-modal-baseurl");
469
546
 
547
+ function _openBaseUrlDropdown() {
548
+ _closeModelDropdown();
549
+ _updateModalBaseUrlDropdown();
550
+ _positionDropdownFixed(baseUrlDropdown, baseUrlCombobox);
551
+ baseUrlDropdown.style.display = "block";
552
+ document.body.appendChild(baseUrlDropdown);
553
+ }
554
+
555
+ function _closeBaseUrlDropdown() {
556
+ baseUrlDropdown.style.display = "none";
557
+ _resetDropdownPosition(baseUrlDropdown);
558
+ baseUrlCombobox.appendChild(baseUrlDropdown);
559
+ }
560
+
470
561
  baseUrlDropdownBtn.addEventListener("click", (e) => {
471
562
  e.stopPropagation();
472
- const isOpen = baseUrlDropdown.style.display === "block";
473
- document.querySelectorAll(".model-name-dropdown, .base-url-dropdown").forEach(d => {
474
- d.style.display = "none";
475
- });
476
- if (!isOpen) {
477
- _updateModalBaseUrlDropdown();
478
- baseUrlDropdown.style.display = "block";
563
+ if (baseUrlDropdown.style.display === "block") {
564
+ _closeBaseUrlDropdown();
565
+ } else {
566
+ _openBaseUrlDropdown();
479
567
  }
480
568
  });
481
569
 
@@ -484,10 +572,14 @@ const Settings = (() => {
484
572
  _updateModalModelDropdown();
485
573
  });
486
574
 
487
- // Close all modal dropdowns on document click
488
- document.addEventListener("click", () => {
489
- modelDropdown.style.display = "none";
490
- baseUrlDropdown.style.display = "none";
575
+ // Close dropdowns on outside click
576
+ document.addEventListener("mousedown", (e) => {
577
+ if (!modelCombobox.contains(e.target) && !modelDropdown.contains(e.target)) {
578
+ _closeModelDropdown();
579
+ }
580
+ if (!baseUrlCombobox.contains(e.target) && !baseUrlDropdown.contains(e.target)) {
581
+ _closeBaseUrlDropdown();
582
+ }
491
583
  });
492
584
  }
493
585
 
@@ -1463,22 +1555,24 @@ const Settings = (() => {
1463
1555
  // ── Init ──────────────────────────────────────────────────────────────────────
1464
1556
 
1465
1557
  function _initTabs() {
1466
- const tabs = document.querySelectorAll("#settings-tabs .settings-tab");
1467
- const contents = document.querySelectorAll("#settings-body .settings-tab-content");
1468
-
1469
- tabs.forEach(tab => {
1470
- tab.addEventListener("click", () => {
1471
- const targetTab = tab.dataset.tab;
1472
-
1473
- // Update tab buttons
1474
- tabs.forEach(t => t.classList.toggle("active", t.dataset.tab === targetTab));
1475
-
1476
- // Update tab content panels
1477
- contents.forEach(c => {
1478
- const isActive = c.dataset.tabContent === targetTab;
1479
- c.classList.toggle("active", isActive);
1480
- c.style.display = isActive ? "" : "none";
1481
- });
1558
+ const bar = document.getElementById("settings-tabs");
1559
+ if (!bar) return;
1560
+
1561
+ // Delegated so extension tabs (mounted into the settings.tabs slot after
1562
+ // this runs) switch correctly without re-binding.
1563
+ bar.addEventListener("click", (e) => {
1564
+ const tab = e.target.closest(".settings-tab");
1565
+ if (!tab || !bar.contains(tab)) return;
1566
+ const targetTab = tab.dataset.tab;
1567
+ if (!targetTab) return;
1568
+
1569
+ document.querySelectorAll("#settings-tabs .settings-tab").forEach(t =>
1570
+ t.classList.toggle("active", t.dataset.tab === targetTab));
1571
+
1572
+ document.querySelectorAll("#settings-body .settings-tab-content").forEach(c => {
1573
+ const isActive = c.dataset.tabContent === targetTab;
1574
+ c.classList.toggle("active", isActive);
1575
+ c.style.display = isActive ? "" : "none";
1482
1576
  });
1483
1577
  });
1484
1578
  }