vibespot 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibespot",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "AI-powered HubSpot CMS landing page builder — vibe coding & React converter",
5
5
  "type": "module",
6
6
  "bin": {
package/ui/chat.js CHANGED
@@ -35,6 +35,7 @@ function connectWebSocket() {
35
35
  ws.close();
36
36
  ws = null;
37
37
  }
38
+ brandExtractionPromptShown = false;
38
39
 
39
40
  const protocol = location.protocol === "https:" ? "wss:" : "ws:";
40
41
  ws = new WebSocket(`${protocol}//${location.host}`);
@@ -174,6 +175,18 @@ function handleWsMessage(msg) {
174
175
  case "agentic_prompt":
175
176
  handleAgenticPrompt();
176
177
  break;
178
+ case "suggest_brand_extraction":
179
+ handleSuggestBrandExtraction();
180
+ break;
181
+ case "brand_asset_extracted":
182
+ handleBrandAssetExtracted(msg.assetType);
183
+ break;
184
+ case "brand_extraction_complete":
185
+ handleBrandExtractionComplete();
186
+ break;
187
+ case "brand_extraction_error":
188
+ appendSystemMessage("Brand extraction failed: " + (msg.message || "Unknown error"));
189
+ break;
177
190
  }
178
191
  }
179
192
 
@@ -440,6 +453,53 @@ async function handleAgenticPrompt() {
440
453
  } catch { /* ignore */ }
441
454
  }
442
455
 
456
+ // ---------------------------------------------------------------------------
457
+ // Brand asset extraction prompt
458
+ // ---------------------------------------------------------------------------
459
+
460
+ let brandExtractionPromptShown = false;
461
+
462
+ function handleSuggestBrandExtraction() {
463
+ if (brandExtractionPromptShown) return;
464
+ brandExtractionPromptShown = true;
465
+
466
+ const el = document.createElement("div");
467
+ el.className = "chat-msg chat-msg--system brand-extraction-prompt";
468
+ el.innerHTML = `
469
+ <div class="chat-msg__system" style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
470
+ <span>Extract product context &amp; styleguide from your page? Helps keep future templates consistent.</span>
471
+ <button class="btn btn--sm btn--primary" id="btn-accept-extraction">Extract</button>
472
+ <button class="btn btn--sm btn--outline" id="btn-dismiss-extraction">Dismiss</button>
473
+ </div>
474
+ `;
475
+
476
+ messagesEl.appendChild(el);
477
+ scrollToBottom();
478
+
479
+ el.querySelector("#btn-accept-extraction").addEventListener("click", () => {
480
+ el.querySelector(".brand-extraction-prompt__actions").innerHTML =
481
+ '<span class="brand-extraction-prompt__status">Extracting...</span>';
482
+ if (ws && ws.readyState === WebSocket.OPEN) {
483
+ ws.send(JSON.stringify({ type: "extract_brand_assets" }));
484
+ }
485
+ });
486
+
487
+ el.querySelector("#btn-dismiss-extraction").addEventListener("click", () => {
488
+ el.remove();
489
+ });
490
+ }
491
+
492
+ function handleBrandAssetExtracted(assetType) {
493
+ const labelMap = { themeContext: "Product context", styleguide: "Styleguide", brandvoice: "Brand voice" };
494
+ const label = labelMap[assetType] || assetType;
495
+ appendSystemMessage(`${label} extracted and saved.`);
496
+ }
497
+
498
+ function handleBrandExtractionComplete() {
499
+ const prompt = document.querySelector(".brand-extraction-prompt");
500
+ if (prompt) prompt.remove();
501
+ }
502
+
443
503
  // ---------------------------------------------------------------------------
444
504
  // File attachments
445
505
  // ---------------------------------------------------------------------------
package/ui/dashboard.js CHANGED
@@ -273,32 +273,32 @@ document.getElementById("dashboard-preview-close").addEventListener("click", clo
273
273
  // Brand assets
274
274
  // ---------------------------------------------------------------------------
275
275
 
276
+ const ASSET_LABELS = { styleguide: "Styleguide", brandvoice: "Brand Voice", themeContext: "Product Context" };
277
+ const ASSET_FILES = { styleguide: "styleguide.md", brandvoice: "brandvoice.md", themeContext: "theme-context.md" };
278
+ const ASSET_FLAGS = { styleguide: "hasStyleguide", brandvoice: "hasBrandvoice", themeContext: "hasThemeContext" };
279
+
276
280
  function renderBrandAssets(assets) {
277
- const sgIcon = document.getElementById("brand-icon-styleguide");
278
- const bvIcon = document.getElementById("brand-icon-brandvoice");
279
-
280
- if (assets.hasStyleguide) {
281
- sgIcon.textContent = "\u2713";
282
- sgIcon.classList.add("brand-asset-upload__icon--done");
283
- } else {
284
- sgIcon.textContent = "+";
285
- sgIcon.classList.remove("brand-asset-upload__icon--done");
286
- }
281
+ for (const [type, flagKey] of Object.entries(ASSET_FLAGS)) {
282
+ const card = document.querySelector(`.brand-asset-card[data-asset="${type}"]`);
283
+ if (!card) continue;
284
+ const icon = card.querySelector(".brand-asset-card__icon");
285
+ const hasAsset = !!assets[flagKey];
286
+
287
+ if (hasAsset) {
288
+ icon.textContent = "\u2713";
289
+ icon.classList.add("brand-asset-card__icon--done");
290
+ } else {
291
+ icon.textContent = "+";
292
+ icon.classList.remove("brand-asset-card__icon--done");
293
+ }
287
294
 
288
- if (assets.hasBrandvoice) {
289
- bvIcon.textContent = "\u2713";
290
- bvIcon.classList.add("brand-asset-upload__icon--done");
291
- } else {
292
- bvIcon.textContent = "+";
293
- bvIcon.classList.remove("brand-asset-upload__icon--done");
295
+ // Toggle which action set is visible on hover
296
+ const actions = card.querySelector(".brand-asset-card__actions");
297
+ const manage = card.querySelector(".brand-asset-card__manage");
298
+ if (actions) actions.classList.toggle("hidden", hasAsset);
299
+ if (manage) manage.classList.toggle("hidden", !hasAsset);
294
300
  }
295
301
 
296
- // Show/hide action buttons based on asset existence
297
- const sgActions = document.getElementById("brand-actions-styleguide");
298
- if (sgActions) sgActions.classList.toggle("hidden", !assets.hasStyleguide);
299
- const bvActions = document.getElementById("brand-actions-brandvoice");
300
- if (bvActions) bvActions.classList.toggle("hidden", !assets.hasBrandvoice);
301
-
302
302
  // Humanify toggle
303
303
  const humanifyCheckbox = document.getElementById("humanify-checkbox");
304
304
  if (humanifyCheckbox) {
@@ -306,37 +306,24 @@ function renderBrandAssets(assets) {
306
306
  }
307
307
  }
308
308
 
309
- async function viewStyleguide() {
309
+ async function viewBrandAsset(type) {
310
310
  try {
311
311
  const res = await fetch("/api/brand-assets");
312
312
  const data = await res.json();
313
- if (data.styleguide) {
314
- await vibeViewContent(data.styleguide, "Styleguide", "styleguide.md");
313
+ const content = data[type];
314
+ if (content) {
315
+ await vibeViewContent(content, ASSET_LABELS[type], ASSET_FILES[type]);
315
316
  } else {
316
- await vibeAlert("No styleguide found.", "Info");
317
+ await vibeAlert(`No ${ASSET_LABELS[type].toLowerCase()} found.`, "Info");
317
318
  }
318
319
  } catch (err) {
319
- await vibeAlert("Failed to load styleguide: " + err.message, "Error");
320
- }
321
- }
322
-
323
- async function viewBrandvoice() {
324
- try {
325
- const res = await fetch("/api/brand-assets");
326
- const data = await res.json();
327
- if (data.brandvoice) {
328
- await vibeViewContent(data.brandvoice, "Brand Voice", "brandvoice.md");
329
- } else {
330
- await vibeAlert("No brand voice found.", "Info");
331
- }
332
- } catch (err) {
333
- await vibeAlert("Failed to load brand voice: " + err.message, "Error");
320
+ await vibeAlert(`Failed to load: ${err.message}`, "Error");
334
321
  }
335
322
  }
336
323
 
337
324
  async function deleteBrandAsset(type) {
338
- const label = type === "styleguide" ? "styleguide" : "brand voice";
339
- const ok = await vibeConfirm(`Remove ${label}?`, "This will delete the file from disk.", { confirmLabel: "Remove", confirmClass: "btn--danger" });
325
+ const label = ASSET_LABELS[type] || type;
326
+ const ok = await vibeConfirm(`Remove ${label.toLowerCase()}?`, "This will delete the file from disk.", { confirmLabel: "Remove", confirmClass: "btn--danger" });
340
327
  if (!ok) return;
341
328
  try {
342
329
  const res = await fetch("/api/brand-assets", {
@@ -352,10 +339,57 @@ async function deleteBrandAsset(type) {
352
339
  }
353
340
  }
354
341
 
355
- document.getElementById("btn-view-styleguide")?.addEventListener("click", viewStyleguide);
356
- document.getElementById("btn-view-brandvoice")?.addEventListener("click", viewBrandvoice);
357
- document.getElementById("btn-delete-styleguide")?.addEventListener("click", () => deleteBrandAsset("styleguide"));
358
- document.getElementById("btn-delete-brandvoice")?.addEventListener("click", () => deleteBrandAsset("brandvoice"));
342
+ async function extractBrandAsset(type, card) {
343
+ const labelEl = card.querySelector(".brand-asset-card__label");
344
+ const origLabel = labelEl?.textContent;
345
+ if (labelEl) labelEl.textContent = "Extracting...";
346
+ card.classList.add("brand-asset-card--extracting");
347
+ try {
348
+ const res = await fetch("/api/brand-assets/extract", {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/json" },
351
+ body: JSON.stringify({ type }),
352
+ });
353
+ const data = await res.json();
354
+ if (data.ok && data.content) {
355
+ await refreshDashboard();
356
+ const view = await vibeConfirm(
357
+ `${ASSET_LABELS[type]} extracted.`,
358
+ "Would you like to view it?",
359
+ { confirmLabel: "View", confirmClass: "btn--primary" },
360
+ );
361
+ if (view) await vibeViewContent(data.content, ASSET_LABELS[type], ASSET_FILES[type]);
362
+ } else {
363
+ await vibeAlert(data.error || "Nothing to extract — generate some modules first.", "Info");
364
+ }
365
+ } catch (err) {
366
+ await vibeAlert("Extraction failed: " + err.message, "Error");
367
+ } finally {
368
+ card.classList.remove("brand-asset-card--extracting");
369
+ if (labelEl) labelEl.textContent = origLabel;
370
+ }
371
+ }
372
+
373
+ // Event delegation for brand asset cards
374
+ document.getElementById("dashboard-brand-assets")?.addEventListener("click", (e) => {
375
+ const card = e.target.closest(".brand-asset-card");
376
+ if (!card) return;
377
+ const type = card.dataset.asset;
378
+ if (!type) return;
379
+
380
+ const action = e.target.closest("[data-action]")?.dataset?.action;
381
+ if (action === "view") { viewBrandAsset(type); return; }
382
+ if (action === "delete") { deleteBrandAsset(type); return; }
383
+ if (action === "extract") { extractBrandAsset(type, card); return; }
384
+ });
385
+
386
+ // File upload via label inside cards
387
+ document.getElementById("dashboard-brand-assets")?.addEventListener("change", (e) => {
388
+ if (e.target.type !== "file") return;
389
+ const card = e.target.closest(".brand-asset-card");
390
+ if (!card || !e.target.files[0]) return;
391
+ handleBrandFileSelected(card.dataset.asset, e.target.files[0]);
392
+ });
359
393
 
360
394
  // ---------------------------------------------------------------------------
361
395
  // Actions
@@ -541,33 +575,42 @@ document.getElementById("dashboard-deploy-btn").addEventListener("click", () =>
541
575
  }
542
576
  });
543
577
 
544
- // Brand asset file inputs
545
- document.getElementById("brand-upload-styleguide").querySelector("input").addEventListener("change", (e) => {
546
- if (e.target.files[0]) handleBrandFileSelected("styleguide", e.target.files[0]);
547
- });
548
- document.getElementById("brand-upload-brandvoice").querySelector("input").addEventListener("change", (e) => {
549
- if (e.target.files[0]) handleBrandFileSelected("brandvoice", e.target.files[0]);
550
- });
551
-
552
- // Extract design from theme
553
- document.getElementById("btn-extract-design")?.addEventListener("click", async () => {
554
- const btn = document.getElementById("btn-extract-design");
578
+ // Extract All button
579
+ document.getElementById("btn-extract-all")?.addEventListener("click", async () => {
580
+ const btn = document.getElementById("btn-extract-all");
555
581
  const origText = btn.textContent;
556
582
  btn.textContent = "Extracting...";
557
583
  btn.disabled = true;
558
584
 
585
+ // Mark all cards as extracting
586
+ const cards = document.querySelectorAll(".brand-asset-card");
587
+ const savedLabels = new Map();
588
+ cards.forEach((card) => {
589
+ const labelEl = card.querySelector(".brand-asset-card__label");
590
+ if (labelEl) {
591
+ savedLabels.set(card, labelEl.textContent);
592
+ labelEl.textContent = "Extracting...";
593
+ }
594
+ card.classList.add("brand-asset-card--extracting");
595
+ });
596
+
559
597
  try {
560
598
  const res = await fetch("/api/brand-assets/extract", {
561
599
  method: "POST",
562
600
  headers: { "Content-Type": "application/json" },
563
- body: JSON.stringify({}),
601
+ body: JSON.stringify({ type: "all" }),
564
602
  });
565
603
  const data = await res.json();
566
604
  if (data.ok) {
567
605
  await refreshDashboard();
568
- const view = await vibeConfirm("Design system extracted and saved as styleguide.", "Would you like to view it?", { confirmLabel: "View Styleguide", confirmClass: "btn--primary" });
569
- if (view && data.styleguide) {
570
- await vibeViewContent(data.styleguide, "Styleguide", "styleguide.md");
606
+ const extracted = data.extracted || {};
607
+ const names = Object.entries(extracted)
608
+ .filter(([, v]) => v)
609
+ .map(([k]) => ASSET_LABELS[k] || k);
610
+ if (names.length > 0) {
611
+ await vibeAlert(`Extracted: ${names.join(", ")}`, "Done");
612
+ } else {
613
+ await vibeAlert("Nothing to extract \u2014 generate some modules first.", "Info");
571
614
  }
572
615
  } else {
573
616
  await vibeAlert(data.error || "Extraction failed", "Error");
@@ -577,6 +620,11 @@ document.getElementById("btn-extract-design")?.addEventListener("click", async (
577
620
  } finally {
578
621
  btn.textContent = origText;
579
622
  btn.disabled = false;
623
+ cards.forEach((card) => {
624
+ card.classList.remove("brand-asset-card--extracting");
625
+ const labelEl = card.querySelector(".brand-asset-card__label");
626
+ if (labelEl && savedLabels.has(card)) labelEl.textContent = savedLabels.get(card);
627
+ });
580
628
  }
581
629
  });
582
630
 
package/ui/index.html CHANGED
@@ -205,29 +205,49 @@
205
205
  <span class="brand-asset-toggle__label">Humanify</span>
206
206
  <span class="brand-asset-toggle__tooltip" data-tooltip="Strips AI-sounding copy: removes em dashes, banned words like 'delve' and 'leverage', cliché openers, and forced enthusiasm. Makes your landing page read like a human wrote it.">?</span>
207
207
  </div>
208
- <div class="brand-asset-row">
209
- <label class="brand-asset-upload" id="brand-upload-styleguide">
210
- <input type="file" accept=".md,.txt" hidden>
211
- <span class="brand-asset-upload__icon" id="brand-icon-styleguide">+</span>
212
- <span class="brand-asset-upload__label">styleguide.md</span>
213
- </label>
214
- <span class="brand-asset-actions hidden" id="brand-actions-styleguide">
215
- <button class="brand-asset-action" title="View" id="btn-view-styleguide"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
216
- <button class="brand-asset-action brand-asset-action--delete" title="Remove" id="btn-delete-styleguide">&times;</button>
217
- </span>
208
+ <div class="brand-asset-card" data-asset="styleguide">
209
+ <div class="brand-asset-card__base">
210
+ <span class="brand-asset-card__icon" id="brand-icon-styleguide">+</span>
211
+ <span class="brand-asset-card__label">styleguide</span>
212
+ </div>
213
+ <div class="brand-asset-card__actions">
214
+ <label class="brand-asset-card__btn" title="Upload .md/.txt file"><input type="file" accept=".md,.txt" hidden>Upload</label>
215
+ <button class="brand-asset-card__btn" data-action="extract" title="AI-extract from theme">Extract</button>
216
+ </div>
217
+ <div class="brand-asset-card__manage hidden">
218
+ <button class="brand-asset-card__btn" data-action="view" title="View">View</button>
219
+ <button class="brand-asset-card__btn brand-asset-card__btn--danger" data-action="delete" title="Remove">Delete</button>
220
+ </div>
218
221
  </div>
219
- <div class="brand-asset-row">
220
- <label class="brand-asset-upload" id="brand-upload-brandvoice">
221
- <input type="file" accept=".md,.txt" hidden>
222
- <span class="brand-asset-upload__icon" id="brand-icon-brandvoice">+</span>
223
- <span class="brand-asset-upload__label">brandvoice.md</span>
224
- </label>
225
- <span class="brand-asset-actions hidden" id="brand-actions-brandvoice">
226
- <button class="brand-asset-action" title="View" id="btn-view-brandvoice"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>
227
- <button class="brand-asset-action brand-asset-action--delete" title="Remove" id="btn-delete-brandvoice">&times;</button>
228
- </span>
222
+ <div class="brand-asset-card" data-asset="brandvoice">
223
+ <div class="brand-asset-card__base">
224
+ <span class="brand-asset-card__icon" id="brand-icon-brandvoice">+</span>
225
+ <span class="brand-asset-card__label">brand voice</span>
226
+ </div>
227
+ <div class="brand-asset-card__actions">
228
+ <label class="brand-asset-card__btn" title="Upload .md/.txt file"><input type="file" accept=".md,.txt" hidden>Upload</label>
229
+ <button class="brand-asset-card__btn" data-action="extract" title="AI-extract from page copy">Extract</button>
230
+ </div>
231
+ <div class="brand-asset-card__manage hidden">
232
+ <button class="brand-asset-card__btn" data-action="view" title="View">View</button>
233
+ <button class="brand-asset-card__btn brand-asset-card__btn--danger" data-action="delete" title="Remove">Delete</button>
234
+ </div>
235
+ </div>
236
+ <div class="brand-asset-card" data-asset="themeContext">
237
+ <div class="brand-asset-card__base">
238
+ <span class="brand-asset-card__icon" id="brand-icon-themeContext">+</span>
239
+ <span class="brand-asset-card__label">product context</span>
240
+ </div>
241
+ <div class="brand-asset-card__actions">
242
+ <label class="brand-asset-card__btn" title="Upload .md/.txt file"><input type="file" accept=".md,.txt" hidden>Upload</label>
243
+ <button class="brand-asset-card__btn" data-action="extract" title="AI-extract from page content">Extract</button>
244
+ </div>
245
+ <div class="brand-asset-card__manage hidden">
246
+ <button class="brand-asset-card__btn" data-action="view" title="View">View</button>
247
+ <button class="brand-asset-card__btn brand-asset-card__btn--danger" data-action="delete" title="Remove">Delete</button>
248
+ </div>
229
249
  </div>
230
- <button class="btn btn--sm btn--outline" id="btn-extract-design" title="Auto-extract design system from this theme's CSS and modules">Extract from Theme</button>
250
+ <button class="btn btn--sm btn--outline" id="btn-extract-all" title="AI-extract all brand assets from this theme">Extract All</button>
231
251
  <button class="btn btn--sm btn--outline" id="btn-import-reference" title="Import design from another HubSpot theme or local folder">Import Reference</button>
232
252
  </div>
233
253
  </section>
package/ui/styles.css CHANGED
@@ -807,69 +807,90 @@ body { display: flex; }
807
807
  gap: 12px;
808
808
  }
809
809
 
810
- .brand-asset-upload {
810
+ /* Brand asset cards — ghost buttons with hover-expand actions */
811
+ .brand-asset-card {
811
812
  display: flex;
812
- align-items: center;
813
- gap: 8px;
814
- padding: 10px 16px;
813
+ align-items: stretch;
815
814
  border: 1px dashed var(--border);
816
815
  border-radius: var(--radius);
817
- cursor: pointer;
818
- transition: border-color 0.15s ease;
816
+ overflow: hidden;
817
+ transition: border-color 0.15s;
819
818
  }
820
-
821
- .brand-asset-upload:hover {
819
+ .brand-asset-card:hover {
822
820
  border-color: var(--accent);
823
821
  }
824
-
825
- .brand-asset-upload__icon {
826
- width: 24px;
827
- height: 24px;
822
+ .brand-asset-card__base {
823
+ display: flex;
824
+ align-items: center;
825
+ gap: 8px;
826
+ padding: 8px 14px;
827
+ }
828
+ .brand-asset-card__icon {
829
+ width: 22px;
830
+ height: 22px;
828
831
  display: flex;
829
832
  align-items: center;
830
833
  justify-content: center;
831
834
  border-radius: 50%;
832
835
  background: var(--bg-hover);
833
836
  color: var(--text-dim);
834
- font-size: 14px;
837
+ font-size: 13px;
835
838
  font-weight: 500;
839
+ flex-shrink: 0;
836
840
  }
837
-
838
- .brand-asset-upload__icon--done {
841
+ .brand-asset-card__icon--done {
839
842
  background: rgba(74,222,128,0.15);
840
843
  color: var(--success);
841
844
  }
845
+ .brand-asset-card__label {
846
+ font-size: 13px;
847
+ color: var(--text);
848
+ white-space: nowrap;
849
+ }
842
850
 
843
- .brand-asset-row {
851
+ /* Slide-in actions on hover */
852
+ .brand-asset-card__actions,
853
+ .brand-asset-card__manage {
844
854
  display: flex;
845
- align-items: center;
846
- gap: 4px;
855
+ max-width: 0;
856
+ overflow: hidden;
857
+ opacity: 0;
858
+ transition: max-width 0.2s ease, opacity 0.15s ease;
847
859
  }
848
- .brand-asset-actions {
849
- display: flex;
850
- gap: 2px;
860
+ .brand-asset-card:hover > .brand-asset-card__actions:not(.hidden),
861
+ .brand-asset-card:hover > .brand-asset-card__manage:not(.hidden) {
862
+ max-width: 180px;
863
+ opacity: 1;
851
864
  }
852
- .brand-asset-action {
853
- background: none;
865
+
866
+ .brand-asset-card__btn {
867
+ display: flex;
868
+ align-items: center;
869
+ padding: 6px 10px;
870
+ font-size: 12px;
871
+ font-weight: 500;
854
872
  border: none;
855
- color: var(--text-muted);
873
+ border-left: 1px solid var(--border);
874
+ background: var(--bg-hover);
875
+ color: var(--text-dim);
856
876
  cursor: pointer;
857
- font-size: 14px;
858
- padding: 2px 4px;
859
- border-radius: var(--radius-sm);
860
- transition: color 0.15s, background 0.15s;
877
+ white-space: nowrap;
878
+ transition: background 0.12s, color 0.12s;
861
879
  }
862
- .brand-asset-action:hover {
880
+ .brand-asset-card__btn:hover {
881
+ background: var(--bg-active);
863
882
  color: var(--accent);
864
- background: var(--bg-hover);
865
883
  }
866
- .brand-asset-action--delete:hover {
884
+ .brand-asset-card__btn--danger:hover {
867
885
  color: var(--error);
868
886
  }
887
+ /* Hide file input inside label */
888
+ .brand-asset-card__btn input[type="file"] { display: none; }
869
889
 
870
- .brand-asset-upload__label {
871
- font-size: 13px;
872
- color: var(--text);
890
+ /* Extracting state */
891
+ .brand-asset-card--extracting {
892
+ opacity: 0.6;
893
+ pointer-events: none;
873
894
  }
874
895
 
875
896
  /* Back button in dashboard topbar */