@kaikybrofc/omnizap-system 2.2.10 → 2.3.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 (119) hide show
  1. package/README.md +20 -18
  2. package/app/config/adminIdentity.js +1 -3
  3. package/app/connection/socketController.js +10 -20
  4. package/app/controllers/messageController.js +7 -28
  5. package/app/modules/aiModule/catCommand.js +29 -192
  6. package/app/modules/broadcastModule/noticeCommand.js +28 -97
  7. package/app/modules/gameModule/diceCommand.js +6 -32
  8. package/app/modules/playModule/playCommand.js +57 -258
  9. package/app/modules/quoteModule/quoteCommand.js +2 -4
  10. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1 -13
  11. package/app/modules/statsModule/noMessageCommand.js +16 -84
  12. package/app/modules/statsModule/rankingCommand.js +5 -25
  13. package/app/modules/statsModule/rankingCommon.js +1 -9
  14. package/app/modules/stickerModule/convertToWebp.js +4 -27
  15. package/app/modules/stickerModule/stickerCommand.js +13 -24
  16. package/app/modules/stickerModule/stickerTextCommand.js +13 -25
  17. package/app/modules/stickerPackModule/autoPackCollectorService.js +16 -7
  18. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +20 -36
  19. package/app/modules/stickerPackModule/domainEvents.js +2 -11
  20. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +13 -50
  21. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +2 -15
  22. package/app/modules/stickerPackModule/semanticThemeClusterService.js +14 -41
  23. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +25 -95
  24. package/app/modules/stickerPackModule/stickerAssetRepository.js +12 -31
  25. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +13 -18
  26. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +284 -709
  27. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +27 -106
  28. package/app/modules/stickerPackModule/stickerClassificationService.js +46 -77
  29. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +13 -53
  30. package/app/modules/stickerPackModule/stickerDomainEventBus.js +10 -16
  31. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +13 -34
  32. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +1 -4
  33. package/app/modules/stickerPackModule/stickerObjectStorageService.js +26 -26
  34. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +32 -187
  35. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +6 -15
  36. package/app/modules/stickerPackModule/stickerPackItemRepository.js +6 -32
  37. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +12 -36
  38. package/app/modules/stickerPackModule/stickerPackMessageService.js +12 -40
  39. package/app/modules/stickerPackModule/stickerPackRepository.js +23 -66
  40. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +9 -21
  41. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +10 -40
  42. package/app/modules/stickerPackModule/stickerPackService.js +50 -115
  43. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +2 -21
  44. package/app/modules/stickerPackModule/stickerPackUtils.js +13 -3
  45. package/app/modules/stickerPackModule/stickerStorageService.js +16 -65
  46. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +4 -22
  47. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +14 -29
  48. package/app/modules/systemMetricsModule/pingCommand.js +9 -39
  49. package/app/modules/tiktokModule/tiktokCommand.js +17 -109
  50. package/app/modules/userModule/userCommand.js +2 -88
  51. package/app/observability/metrics.js +5 -16
  52. package/app/services/captchaService.js +1 -6
  53. package/app/services/dbWriteQueue.js +3 -18
  54. package/app/services/featureFlagService.js +2 -8
  55. package/app/services/newsBroadcastService.js +0 -1
  56. package/app/services/queueUtils.js +2 -4
  57. package/app/services/whatsappLoginLinkService.js +7 -9
  58. package/app/store/premiumUserStore.js +1 -2
  59. package/app/utils/antiLink/antiLinkModule.js +3 -233
  60. package/app/utils/logger/loggerModule.js +9 -34
  61. package/app/utils/systemMetrics/systemMetricsModule.js +1 -4
  62. package/database/init.js +1 -8
  63. package/docker-compose.yml +27 -27
  64. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +26 -0
  65. package/docs/seo/satellite-page-template.md +2 -0
  66. package/docs/seo/satellite-pages-phase1.json +40 -177
  67. package/eslint.config.js +2 -15
  68. package/index.js +8 -36
  69. package/ml/clip_classifier/README.md +4 -6
  70. package/observability/alert-rules.yml +12 -12
  71. package/observability/grafana/provisioning/dashboards/dashboards.yml +1 -1
  72. package/package.json +6 -3
  73. package/public/api-docs/index.html +220 -193
  74. package/public/bot-whatsapp-para-grupo/index.html +291 -261
  75. package/public/bot-whatsapp-sem-programar/index.html +291 -261
  76. package/public/comandos/index.html +421 -406
  77. package/public/como-automatizar-avisos-no-whatsapp/index.html +291 -261
  78. package/public/como-criar-comandos-whatsapp/index.html +291 -261
  79. package/public/como-evitar-spam-no-whatsapp/index.html +291 -261
  80. package/public/como-moderar-grupo-whatsapp/index.html +291 -261
  81. package/public/como-organizar-comunidade-whatsapp/index.html +291 -261
  82. package/public/css/github-project-panel.css +13 -8
  83. package/public/css/stickers-admin.css +25 -9
  84. package/public/css/styles.css +23 -16
  85. package/public/index.html +1117 -994
  86. package/public/js/apps/apiDocsApp.js +17 -167
  87. package/public/js/apps/createPackApp.js +69 -332
  88. package/public/js/apps/homeApp.js +94 -74
  89. package/public/js/apps/loginApp.js +3 -12
  90. package/public/js/apps/stickersAdminApp.js +190 -181
  91. package/public/js/apps/stickersApp.js +496 -1397
  92. package/public/js/catalog.js +11 -74
  93. package/public/js/github-panel/components/ErrorState.js +1 -8
  94. package/public/js/github-panel/components/GithubProjectPanel.js +2 -9
  95. package/public/js/github-panel/components/SkeletonPanel.js +1 -11
  96. package/public/js/github-panel/components/StatCard.js +1 -7
  97. package/public/js/github-panel/vendor/react.js +1 -9
  98. package/public/js/runtime/react-runtime.js +1 -9
  99. package/public/licenca/index.html +104 -86
  100. package/public/login/index.html +315 -325
  101. package/public/melhor-bot-whatsapp-para-grupos/index.html +291 -261
  102. package/public/stickers/admin/index.html +14 -19
  103. package/public/stickers/create/index.html +39 -44
  104. package/public/stickers/index.html +96 -107
  105. package/public/termos-de-uso/index.html +142 -115
  106. package/public/user/index.html +347 -350
  107. package/scripts/cache-bust.mjs +5 -24
  108. package/scripts/generate-seo-satellite-pages.mjs +10 -13
  109. package/scripts/run-prettier-all.mjs +25 -0
  110. package/scripts/sticker-catalog-loadtest.mjs +13 -11
  111. package/scripts/sticker-worker-task.mjs +1 -4
  112. package/scripts/sync-readme-snapshot.mjs +3 -2
  113. package/server/controllers/stickerCatalogController.js +67 -5
  114. package/server/http/httpServer.js +2 -10
  115. package/server/routes/stickerCatalog/catalogHandlers/catalogAdminHttp.js +1 -8
  116. package/server/routes/stickerCatalog/catalogHandlers/catalogAuthHttp.js +1 -9
  117. package/server/routes/stickerCatalog/catalogHandlers/catalogPublicHttp.js +10 -11
  118. package/server/routes/stickerCatalog/catalogHandlers/catalogUploadHttp.js +1 -10
  119. package/server/routes/stickerCatalog/catalogRouter.js +11 -13
@@ -43,11 +43,7 @@ const removeControlChars = (value) => String(value || '').replace(/[\u0000-\u001
43
43
 
44
44
  const sanitizePackNameInput = (value, maxLength = 120) => removeControlChars(value).slice(0, maxLength);
45
45
 
46
- const sanitizePackName = (value, maxLength = 120) =>
47
- removeControlChars(value)
48
- .replace(/\s+/g, ' ')
49
- .trim()
50
- .slice(0, maxLength);
46
+ const sanitizePackName = (value, maxLength = 120) => removeControlChars(value).replace(/\s+/g, ' ').trim().slice(0, maxLength);
51
47
 
52
48
  const toBytesLabel = (bytes) => `${Math.round(Number(bytes || 0) / 1024)} KB`;
53
49
  const normalizeTag = (value) =>
@@ -322,16 +318,8 @@ const uploadStickerWithRetry = async (params) => {
322
318
 
323
319
  function StepPill({ step, active, done }) {
324
320
  return html`
325
- <div className=${`flex min-w-0 items-center gap-1.5 rounded-xl border px-2.5 py-1.5 transition sm:gap-2 sm:rounded-2xl sm:px-3 sm:py-2 ${
326
- active
327
- ? 'border-accent/50 bg-accent/10 text-accent'
328
- : done
329
- ? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
330
- : 'border-line/70 bg-panelSoft/80 text-slate-300'
331
- }`}>
332
- <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-black/25 text-[11px] font-extrabold sm:h-6 sm:w-6 sm:text-xs">
333
- ${done ? '✓' : step.id}
334
- </span>
321
+ <div className=${`flex min-w-0 items-center gap-1.5 rounded-xl border px-2.5 py-1.5 transition sm:gap-2 sm:rounded-2xl sm:px-3 sm:py-2 ${active ? 'border-accent/50 bg-accent/10 text-accent' : done ? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300' : 'border-line/70 bg-panelSoft/80 text-slate-300'}`}>
322
+ <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-black/25 text-[11px] font-extrabold sm:h-6 sm:w-6 sm:text-xs"> ${done ? '✓' : step.id} </span>
335
323
  <p className="truncate text-[10px] font-semibold sm:text-[11px]">${step.title}</p>
336
324
  </div>
337
325
  `;
@@ -347,18 +335,8 @@ function FloatingField({ label, value, onChange, maxLength, hint = '', multiline
347
335
  <label className="block">
348
336
  <span className="mb-1.5 inline-block text-xs font-semibold text-slate-300">${label}</span>
349
337
  <div className="relative">
350
- <${Tag}
351
- className=${`w-full rounded-2xl border bg-panel/70 px-3.5 py-2.5 text-sm text-slate-100 outline-none transition placeholder:text-transparent md:bg-panel/80 md:px-4 md:py-3 ${
352
- atLimit ? 'border-rose-400/60 focus:border-rose-300' : 'border-line focus:border-accent/60'
353
- } ${multiline ? 'min-h-[96px] max-h-44 resize-none overflow-y-auto md:min-h-[110px] md:max-h-52' : 'h-11 md:h-12'}`}
354
- placeholder=${label}
355
- value=${value}
356
- maxlength=${maxLength}
357
- onInput=${onChange}
358
- />
359
- <span className="pointer-events-none absolute left-3.5 top-[-9px] rounded-md bg-base px-1.5 text-[10px] font-semibold uppercase tracking-[.08em] text-slate-400 md:left-4 md:bg-panel md:px-2">
360
- ${label}
361
- </span>
338
+ <${Tag} className=${`w-full rounded-2xl border bg-panel/70 px-3.5 py-2.5 text-sm text-slate-100 outline-none transition placeholder:text-transparent md:bg-panel/80 md:px-4 md:py-3 ${atLimit ? 'border-rose-400/60 focus:border-rose-300' : 'border-line focus:border-accent/60'} ${multiline ? 'min-h-[96px] max-h-44 resize-none overflow-y-auto md:min-h-[110px] md:max-h-52' : 'h-11 md:h-12'}`} placeholder=${label} value=${value} maxlength=${maxLength} onInput=${onChange} />
339
+ <span className="pointer-events-none absolute left-3.5 top-[-9px] rounded-md bg-base px-1.5 text-[10px] font-semibold uppercase tracking-[.08em] text-slate-400 md:left-4 md:bg-panel md:px-2"> ${label} </span>
362
340
  </div>
363
341
  <div className="mt-1.5 flex items-center justify-between gap-3 text-[11px]">
364
342
  <span className="line-clamp-2 text-slate-400">${hint}</span>
@@ -370,27 +348,11 @@ function FloatingField({ label, value, onChange, maxLength, hint = '', multiline
370
348
 
371
349
  function StickerThumb({ item, index, selectedCoverId, onSetCover, onRemove, onDragStart, onDropOn }) {
372
350
  return html`
373
- <article
374
- draggable=${true}
375
- onDragStart=${() => onDragStart(item.id)}
376
- onDragOver=${(e) => e.preventDefault()}
377
- onDrop=${() => onDropOn(item.id)}
378
- className="group relative overflow-hidden rounded-2xl border border-line bg-panelSoft"
379
- >
380
- ${item.mediaKind === 'video'
381
- ? html`<video src=${item.dataUrl} muted=${true} playsInline=${true} preload="metadata" className="aspect-square w-full object-cover bg-slate-900/80"></video>`
382
- : html`<img src=${item.dataUrl} alt=${item.file.name} className="aspect-square w-full object-contain bg-slate-900/80" />`}
351
+ <article draggable=${true} onDragStart=${() => onDragStart(item.id)} onDragOver=${(e) => e.preventDefault()} onDrop=${() => onDropOn(item.id)} className="group relative overflow-hidden rounded-2xl border border-line bg-panelSoft">
352
+ ${item.mediaKind === 'video' ? html`<video src=${item.dataUrl} muted=${true} playsinline=${true} preload="metadata" className="aspect-square w-full object-cover bg-slate-900/80"></video>` : html`<img src=${item.dataUrl} alt=${item.file.name} className="aspect-square w-full object-contain bg-slate-900/80" />`}
383
353
  <span className="absolute left-2 top-2 rounded-md bg-black/50 px-1.5 py-0.5 text-[10px] font-bold">#${index + 1}</span>
384
354
  <div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 bg-gradient-to-t from-black/80 to-transparent p-2">
385
- <button
386
- type="button"
387
- onClick=${() => onSetCover(item.id)}
388
- className=${`rounded-lg px-2 py-1 text-[10px] font-bold ${
389
- selectedCoverId === item.id ? 'bg-accent text-slate-900' : 'bg-white/15 text-slate-100'
390
- }`}
391
- >
392
- ${selectedCoverId === item.id ? 'Capa' : 'Definir capa'}
393
- </button>
355
+ <button type="button" onClick=${() => onSetCover(item.id)} className=${`rounded-lg px-2 py-1 text-[10px] font-bold ${selectedCoverId === item.id ? 'bg-accent text-slate-900' : 'bg-white/15 text-slate-100'}`}>${selectedCoverId === item.id ? 'Capa' : 'Definir capa'}</button>
394
356
  <button type="button" onClick=${() => onRemove(item.id)} className="rounded-lg bg-rose-500/80 px-2 py-1 text-[10px] font-bold text-white">Remover</button>
395
357
  </div>
396
358
  </article>
@@ -406,11 +368,7 @@ function PackPreviewPanel({ preview, quality, compact = false }) {
406
368
  <p className=${`${compact ? 'text-base' : 'text-lg'} line-clamp-2 font-display font-bold`}>${preview.name}</p>
407
369
  <p className="line-clamp-2 text-sm text-slate-300">${preview.description || 'Descrição do pack aparecerá aqui.'}</p>
408
370
  <p className="text-xs text-slate-400">por ${preview.publisher}</p>
409
- <div className="flex flex-wrap items-center gap-1">
410
- ${preview.tags.length
411
- ? preview.tags.map((tag) => html`<span key=${tag} className="rounded-full border border-line/70 px-2 py-0.5 text-[10px] text-slate-300">#${tag}</span>`)
412
- : html`<span className="text-[10px] text-slate-500">Adicione tags para melhorar descoberta</span>`}
413
- </div>
371
+ <div className="flex flex-wrap items-center gap-1">${preview.tags.length ? preview.tags.map((tag) => html`<span key=${tag} className="rounded-full border border-line/70 px-2 py-0.5 text-[10px] text-slate-300">#${tag}</span>`) : html`<span className="text-[10px] text-slate-500">Adicione tags para melhorar descoberta</span>`}</div>
414
372
  <div className="flex flex-wrap items-center gap-1.5 text-xs">
415
373
  <span className="rounded-full border border-line/70 px-2 py-1 text-slate-300">${preview.visibility}</span>
416
374
  <span className="rounded-full border border-line/70 px-2 py-1 text-slate-300">🧩 ${preview.stickerCount}</span>
@@ -485,37 +443,15 @@ function CreatePackApp() {
485
443
  [authRedirecting, buildLoginRedirectUrl],
486
444
  );
487
445
 
488
- const canStep2 = useMemo(
489
- () =>
490
- sanitizePackName(name, limits.pack_name_max_length).length > 0 &&
491
- hasGoogleLogin &&
492
- googleSessionChecked &&
493
- !authRedirecting,
494
- [name, limits.pack_name_max_length, hasGoogleLogin, googleSessionChecked, authRedirecting],
495
- );
446
+ const canStep2 = useMemo(() => sanitizePackName(name, limits.pack_name_max_length).length > 0 && hasGoogleLogin && googleSessionChecked && !authRedirecting, [name, limits.pack_name_max_length, hasGoogleLogin, googleSessionChecked, authRedirecting]);
496
447
  const canStep3 = useMemo(() => files.length > 0, [files.length]);
497
448
  const publishReady = canStep2 && canStep3 && !busy;
498
449
  const completionPercentage = Math.round((step / STEPS.length) * 100);
499
- const failedUploadsCount = useMemo(
500
- () => files.reduce((acc, item) => (uploadMap[item.id]?.status === 'error' ? acc + 1 : acc), 0),
501
- [files, uploadMap],
502
- );
503
- const pendingUploadsCount = useMemo(
504
- () => files.reduce((acc, item) => (uploadMap[item.id]?.status === 'done' ? acc : acc + 1), 0),
505
- [files, uploadMap],
506
- );
450
+ const failedUploadsCount = useMemo(() => files.reduce((acc, item) => (uploadMap[item.id]?.status === 'error' ? acc + 1 : acc), 0), [files, uploadMap]);
451
+ const pendingUploadsCount = useMemo(() => files.reduce((acc, item) => (uploadMap[item.id]?.status === 'done' ? acc : acc + 1), 0), [files, uploadMap]);
507
452
  const hasPartialUploadedSession = Boolean(activeSession?.packKey && pendingUploadsCount > 0 && pendingUploadsCount < files.length);
508
- const backendPackStatus = String(
509
- backendPublishState?.status || result?.status || activeSession?.created?.status || '',
510
- ).toLowerCase();
511
- const publishLabel =
512
- backendPackStatus === 'failed'
513
- ? '🛠️ Reparar pack'
514
- : failedUploadsCount > 0
515
- ? `🔁 Reenviar falhas (${failedUploadsCount})`
516
- : hasPartialUploadedSession
517
- ? '▶ Retomar envio'
518
- : '🚀 Publicar Pack';
453
+ const backendPackStatus = String(backendPublishState?.status || result?.status || activeSession?.created?.status || '').toLowerCase();
454
+ const publishLabel = backendPackStatus === 'failed' ? '🛠️ Reparar pack' : failedUploadsCount > 0 ? `🔁 Reenviar falhas (${failedUploadsCount})` : hasPartialUploadedSession ? '▶ Retomar envio' : '🚀 Publicar Pack';
519
455
 
520
456
  const suggestedFromText = useMemo(() => {
521
457
  const haystack = `${name} ${description}`
@@ -663,8 +599,7 @@ function CreatePackApp() {
663
599
  return;
664
600
  }
665
601
 
666
- const restoredName =
667
- typeof parsed.name === 'string' ? sanitizePackNameInput(parsed.name, DEFAULT_LIMITS.pack_name_max_length) : '';
602
+ const restoredName = typeof parsed.name === 'string' ? sanitizePackNameInput(parsed.name, DEFAULT_LIMITS.pack_name_max_length) : '';
668
603
  if (restoredName) setName(restoredName);
669
604
  if (typeof parsed.description === 'string') setDescription(parsed.description);
670
605
  if (typeof parsed.publisher === 'string') setPublisher(parsed.publisher);
@@ -685,8 +620,12 @@ function CreatePackApp() {
685
620
  },
686
621
  hash: /^[a-f0-9]{64}$/.test(String(item.hash || '').toLowerCase()) ? String(item.hash || '').toLowerCase() : '',
687
622
  mediaKind:
688
- String(item.type || '').toLowerCase().startsWith('video/') ||
689
- String(item.name || '').toLowerCase().match(/\.(mp4|webm|mov|m4v)$/i)
623
+ String(item.type || '')
624
+ .toLowerCase()
625
+ .startsWith('video/') ||
626
+ String(item.name || '')
627
+ .toLowerCase()
628
+ .match(/\.(mp4|webm|mov|m4v)$/i)
690
629
  ? 'video'
691
630
  : 'image',
692
631
  dataUrl: String(item.dataUrl),
@@ -809,9 +748,7 @@ function CreatePackApp() {
809
748
 
810
749
  const uploads = Array.isArray(publishState.uploads) ? publishState.uploads : [];
811
750
  const uploadsById = new Map(uploads.map((entry) => [String(entry.upload_id || ''), entry]));
812
- const uploadsByHash = new Map(
813
- uploads.filter((entry) => entry?.sticker_hash).map((entry) => [String(entry.sticker_hash || ''), entry]),
814
- );
751
+ const uploadsByHash = new Map(uploads.filter((entry) => entry?.sticker_hash).map((entry) => [String(entry.sticker_hash || ''), entry]));
815
752
 
816
753
  setUploadMap((prev) => {
817
754
  const next = { ...prev };
@@ -887,23 +824,14 @@ function CreatePackApp() {
887
824
  const lowerName = String(file.name || '').toLowerCase();
888
825
  const lowerType = String(file.type || '').toLowerCase();
889
826
  const isImage = lowerType.startsWith('image/');
890
- const isVideo =
891
- lowerType.startsWith('video/') ||
892
- lowerName.endsWith('.mp4') ||
893
- lowerName.endsWith('.webm') ||
894
- lowerName.endsWith('.mov') ||
895
- lowerName.endsWith('.m4v');
827
+ const isVideo = lowerType.startsWith('video/') || lowerName.endsWith('.mp4') || lowerName.endsWith('.webm') || lowerName.endsWith('.mov') || lowerName.endsWith('.m4v');
896
828
  if (!isImage && !isVideo) return false;
897
829
  const maxBytes = Number(limits.sticker_upload_source_max_bytes || 0);
898
830
  return Number(file.size || 0) <= maxBytes;
899
831
  });
900
832
 
901
833
  if (!filtered.length) {
902
- setError(
903
- `Envie imagem ou vídeo até ${toBytesLabel(
904
- limits.sticker_upload_source_max_bytes,
905
- )}. O sistema converte automaticamente para webp.`,
906
- );
834
+ setError(`Envie imagem ou vídeo até ${toBytesLabel(limits.sticker_upload_source_max_bytes)}. O sistema converte automaticamente para webp.`);
907
835
  return;
908
836
  }
909
837
 
@@ -923,8 +851,12 @@ function CreatePackApp() {
923
851
  file,
924
852
  hash: await computeDataUrlSha256(dataUrl),
925
853
  mediaKind:
926
- String(file.type || '').toLowerCase().startsWith('video/') ||
927
- String(file.name || '').toLowerCase().match(/\.(mp4|webm|mov|m4v)$/i)
854
+ String(file.type || '')
855
+ .toLowerCase()
856
+ .startsWith('video/') ||
857
+ String(file.name || '')
858
+ .toLowerCase()
859
+ .match(/\.(mp4|webm|mov|m4v)$/i)
928
860
  ? 'video'
929
861
  : 'image',
930
862
  dataUrl,
@@ -1395,33 +1327,20 @@ function CreatePackApp() {
1395
1327
  }
1396
1328
  };
1397
1329
 
1398
- const visibilityHelp =
1399
- visibility === 'private'
1400
- ? 'Privado: apenas você acessa este pack.'
1401
- : visibility === 'unlisted'
1402
- ? 'Não listado: acesso por link direto.'
1403
- : 'Público: aparece no catálogo para descoberta.';
1330
+ const visibilityHelp = visibility === 'private' ? 'Privado: apenas você acessa este pack.' : visibility === 'unlisted' ? 'Não listado: acesso por link direto.' : 'Público: aparece no catálogo para descoberta.';
1404
1331
 
1405
1332
  const uploadProgressTotal = Math.max(0, Number(progress.total || files.length || 0));
1406
1333
  const uploadProgressDone = Math.max(0, Math.min(uploadProgressTotal || 0, Number(progress.current || 0)));
1407
- const uploadProgressPercent = Math.max(
1408
- 0,
1409
- Math.min(100, Math.round((uploadProgressDone / Math.max(1, uploadProgressTotal || 1)) * 100)),
1410
- );
1334
+ const uploadProgressPercent = Math.max(0, Math.min(100, Math.round((uploadProgressDone / Math.max(1, uploadProgressTotal || 1)) * 100)));
1411
1335
  const uploadHasFailures = failedUploadsCount > 0;
1412
1336
  const backendStateFailed = backendPackStatus === 'failed';
1413
- const publishCompleted = Boolean(
1414
- result && String(backendPackStatus || result?.status || '').toLowerCase() === PACK_STATUS_PUBLISHED && !busy,
1415
- );
1337
+ const publishCompleted = Boolean(result && String(backendPackStatus || result?.status || '').toLowerCase() === PACK_STATUS_PUBLISHED && !busy);
1416
1338
  const showUploadProgressCard = step === 3 && busy;
1417
1339
  const showUploadFailureCard = step === 3 && !busy && (uploadHasFailures || backendStateFailed);
1418
- const publishedPackUrl =
1419
- String(result?.web_url || activeSession?.webUrl || '').trim() ||
1420
- (result?.pack_key ? `${webPath}/${encodeURIComponent(String(result.pack_key || ''))}` : '');
1340
+ const publishedPackUrl = String(result?.web_url || activeSession?.webUrl || '').trim() || (result?.pack_key ? `${webPath}/${encodeURIComponent(String(result.pack_key || ''))}` : '');
1421
1341
  const finalStepPrimaryLabel = publishCompleted ? '👁 Ver pack criado' : publishLabel;
1422
1342
  const mobilePrimaryActionLabel = step < 3 ? 'Continuar' : finalStepPrimaryLabel;
1423
- const mobilePrimaryActionClass =
1424
- step < 3 ? 'bg-accent text-slate-900' : 'bg-accent2 text-slate-900';
1343
+ const mobilePrimaryActionClass = step < 3 ? 'bg-accent text-slate-900' : 'bg-accent2 text-slate-900';
1425
1344
  const toggleMobilePreview = () => setMobilePreviewOpen((prev) => !prev);
1426
1345
  const openCreatedPack = () => {
1427
1346
  if (!publishedPackUrl) return;
@@ -1449,21 +1368,11 @@ function CreatePackApp() {
1449
1368
  <span className="hidden rounded-full border border-line/60 bg-panel/70 px-3 py-1 sm:inline-flex">🧩 Até ${limits.stickers_per_pack} stickers</span>
1450
1369
  <span className="hidden rounded-full border border-line/60 bg-panel/70 px-3 py-1 sm:inline-flex">📦 Até ${limits.packs_per_owner} packs</span>
1451
1370
  <span className="hidden rounded-full border border-line/60 bg-panel/70 px-3 py-1 md:inline-flex">✍ ${limits.pack_name_max_length} caracteres no nome</span>
1452
- <button
1453
- type="button"
1454
- onClick=${restartCreateFlow}
1455
- disabled=${busy}
1456
- className="h-8 rounded-full border border-line/70 bg-panel/70 px-3 text-[11px] font-semibold text-slate-200 disabled:opacity-60"
1457
- title="Limpar rascunho local e recomeçar"
1458
- >
1459
- Recomeçar
1460
- </button>
1371
+ <button type="button" onClick=${restartCreateFlow} disabled=${busy} className="h-8 rounded-full border border-line/70 bg-panel/70 px-3 text-[11px] font-semibold text-slate-200 disabled:opacity-60" title="Limpar rascunho local e recomeçar">Recomeçar</button>
1461
1372
  </div>
1462
1373
  </header>
1463
1374
 
1464
- <div className="mb-3 grid grid-cols-3 gap-2 md:mb-5">
1465
- ${STEPS.map((item) => html`<${StepPill} key=${item.id} step=${item} active=${step === item.id} done=${step > item.id} />`)}
1466
- </div>
1375
+ <div className="mb-3 grid grid-cols-3 gap-2 md:mb-5">${STEPS.map((item) => html`<${StepPill} key=${item.id} step=${item} active=${step === item.id} done=${step > item.id} />`)}</div>
1467
1376
  <div className="mb-4 md:mb-6">
1468
1377
  <div className="mb-1 flex items-center justify-between text-[11px] font-semibold text-slate-400">
1469
1378
  <span>Progresso</span>
@@ -1479,42 +1388,17 @@ function CreatePackApp() {
1479
1388
  ${step === 1
1480
1389
  ? html`
1481
1390
  <div className="space-y-3 md:space-y-4">
1482
- <${FloatingField}
1483
- label="Nome do pack"
1484
- value=${name}
1485
- maxLength=${limits.pack_name_max_length}
1486
- hint="Use um nome curto e fácil de encontrar."
1487
- onChange=${(e) => setName(sanitizePackNameInput(e.target.value, limits.pack_name_max_length))}
1488
- />
1489
- <${FloatingField}
1490
- label="Descrição"
1491
- value=${description}
1492
- multiline=${true}
1493
- maxLength=${limits.description_max_length}
1494
- hint="Explique o tema do pack em uma frase curta"
1495
- onChange=${(e) => setDescription(clampInputText(e.target.value, limits.description_max_length))}
1496
- />
1497
- <${FloatingField}
1498
- label="Autor"
1499
- value=${publisher}
1500
- maxLength=${limits.publisher_max_length}
1501
- hint="Como seu nome será exibido no catálogo."
1502
- onChange=${(e) => setPublisher(clampInputText(e.target.value, limits.publisher_max_length))}
1503
- />
1391
+ <${FloatingField} label="Nome do pack" value=${name} maxLength=${limits.pack_name_max_length} hint="Use um nome curto e fácil de encontrar." onChange=${(e) => setName(sanitizePackNameInput(e.target.value, limits.pack_name_max_length))} />
1392
+ <${FloatingField} label="Descrição" value=${description} multiline=${true} maxLength=${limits.description_max_length} hint="Explique o tema do pack em uma frase curta" onChange=${(e) => setDescription(clampInputText(e.target.value, limits.description_max_length))} />
1393
+ <${FloatingField} label="Autor" value=${publisher} maxLength=${limits.publisher_max_length} hint="Como seu nome será exibido no catálogo." onChange=${(e) => setPublisher(clampInputText(e.target.value, limits.publisher_max_length))} />
1504
1394
  <div className="rounded-2xl border border-line/70 bg-panel/70 p-3 md:p-4">
1505
1395
  <div className="flex items-start justify-between gap-3">
1506
1396
  <div>
1507
1397
  <p className="text-xs font-semibold uppercase tracking-[.08em] text-slate-400">Sessão da conta</p>
1508
1398
  <p className="mt-1 text-sm font-semibold text-slate-100">Conta Google vinculada</p>
1509
- <p className="mt-1 text-xs text-slate-400">
1510
- ${googleSessionChecked
1511
- ? 'Sua sessão foi validada para criar packs.'
1512
- : 'Validando sua sessão para liberar a criação.'}
1513
- </p>
1399
+ <p className="mt-1 text-xs text-slate-400">${googleSessionChecked ? 'Sua sessão foi validada para criar packs.' : 'Validando sua sessão para liberar a criação.'}</p>
1514
1400
  </div>
1515
- ${hasGoogleLogin
1516
- ? html`<span className="rounded-full border border-emerald-400/40 bg-emerald-400/10 px-2.5 py-1 text-[11px] font-semibold text-emerald-300">Conectado</span>`
1517
- : html`<span className="rounded-full border border-amber-400/40 bg-amber-400/10 px-2.5 py-1 text-[11px] font-semibold text-amber-300">Validando</span>`}
1401
+ ${hasGoogleLogin ? html`<span className="rounded-full border border-emerald-400/40 bg-emerald-400/10 px-2.5 py-1 text-[11px] font-semibold text-emerald-300">Conectado</span>` : html`<span className="rounded-full border border-amber-400/40 bg-amber-400/10 px-2.5 py-1 text-[11px] font-semibold text-amber-300">Validando</span>`}
1518
1402
  </div>
1519
1403
  <div className="mt-3 rounded-xl border border-line/70 bg-panelSoft/80 p-2.5 md:p-3">
1520
1404
  <p className="truncate text-sm font-semibold text-slate-100">${googleAuth.user?.name || 'Conta Google'}</p>
@@ -1525,30 +1409,16 @@ function CreatePackApp() {
1525
1409
  <span className="mb-2 inline-block text-xs font-semibold text-slate-300">Tags do pack</span>
1526
1410
  <div className="rounded-2xl border border-line/70 bg-panelSoft/80 px-3 py-3">
1527
1411
  <div className="mb-2 flex flex-wrap gap-2">
1528
- ${tags.map((tag) => html`
1529
- <button
1530
- key=${tag}
1531
- type="button"
1532
- onClick=${() => removeTag(tag)}
1533
- className="inline-flex items-center gap-1 rounded-full border border-accent/40 bg-accent/10 px-2.5 py-1 text-[11px] font-semibold text-accent"
1534
- title="Remover tag"
1535
- >
1536
- #${tag}
1537
- <span aria-hidden="true">×</span>
1538
- </button>
1539
- `)}
1412
+ ${tags.map(
1413
+ (tag) => html`
1414
+ <button key=${tag} type="button" onClick=${() => removeTag(tag)} className="inline-flex items-center gap-1 rounded-full border border-accent/40 bg-accent/10 px-2.5 py-1 text-[11px] font-semibold text-accent" title="Remover tag">
1415
+ #${tag}
1416
+ <span aria-hidden="true">×</span>
1417
+ </button>
1418
+ `,
1419
+ )}
1540
1420
  </div>
1541
- <input
1542
- type="text"
1543
- value=${tagInput}
1544
- maxlength=${40}
1545
- onInput=${(e) => setTagInput(String(e.target.value || ''))}
1546
- onKeyDown=${onTagInputKeyDown}
1547
- onBlur=${() => addTag(tagInput)}
1548
- placeholder=${tags.length >= MAX_MANUAL_TAGS ? `Limite de ${MAX_MANUAL_TAGS} tags` : 'Digite e pressione Enter para adicionar'}
1549
- disabled=${tags.length >= MAX_MANUAL_TAGS}
1550
- className="h-11 w-full rounded-xl border border-line/70 bg-panel/80 px-3 text-sm outline-none transition focus:border-accent/60 disabled:opacity-60"
1551
- />
1421
+ <input type="text" value=${tagInput} maxlength=${40} onInput=${(e) => setTagInput(String(e.target.value || ''))} onKeyDown=${onTagInputKeyDown} onBlur=${() => addTag(tagInput)} placeholder=${tags.length >= MAX_MANUAL_TAGS ? `Limite de ${MAX_MANUAL_TAGS} tags` : 'Digite e pressione Enter para adicionar'} disabled=${tags.length >= MAX_MANUAL_TAGS} className="h-11 w-full rounded-xl border border-line/70 bg-panel/80 px-3 text-sm outline-none transition focus:border-accent/60 disabled:opacity-60" />
1552
1422
  ${tagInput.trim() && tags.length < MAX_MANUAL_TAGS && tagTypeaheadSuggestions.length
1553
1423
  ? html`
1554
1424
  <div className="mt-2 rounded-xl border border-line/70 bg-panel/70 p-2">
@@ -1556,56 +1426,26 @@ function CreatePackApp() {
1556
1426
  <p className="text-[10px] font-semibold uppercase tracking-[.08em] text-slate-400">Sugestões</p>
1557
1427
  <p className="text-[10px] text-slate-500">Tab completa a primeira</p>
1558
1428
  </div>
1559
- <div className="flex flex-wrap gap-1.5">
1560
- ${tagTypeaheadSuggestions.map((tag) => html`
1561
- <button
1562
- key=${`typeahead-${tag}`}
1563
- type="button"
1564
- onMouseDown=${(e) => e.preventDefault()}
1565
- onClick=${() => addTag(tag)}
1566
- className="rounded-full border border-accent/35 bg-accent/10 px-2 py-1 text-[10px] font-semibold text-accent transition hover:border-accent/60"
1567
- >
1568
- #${tag}
1569
- </button>
1570
- `)}
1571
- </div>
1429
+ <div className="flex flex-wrap gap-1.5">${tagTypeaheadSuggestions.map((tag) => html` <button key=${`typeahead-${tag}`} type="button" onMouseDown=${(e) => e.preventDefault()} onClick=${() => addTag(tag)} className="rounded-full border border-accent/35 bg-accent/10 px-2 py-1 text-[10px] font-semibold text-accent transition hover:border-accent/60">#${tag}</button> `)}</div>
1572
1430
  </div>
1573
1431
  `
1574
1432
  : null}
1575
1433
  <p className="mt-2 text-[11px] text-slate-400">${tags.length}/${MAX_MANUAL_TAGS} tags selecionadas.</p>
1576
- <div className="mt-2 flex flex-wrap gap-1.5">
1577
- ${suggestedFromText.map((tag) => html`
1578
- <button
1579
- key=${tag}
1580
- type="button"
1581
- onMouseDown=${(e) => e.preventDefault()}
1582
- onClick=${() => addTag(tag)}
1583
- className="rounded-full border border-line bg-panel px-2 py-1 text-[10px] font-semibold text-slate-300 transition hover:border-accent/50 hover:text-accent"
1584
- >
1585
- + ${tag}
1586
- </button>
1587
- `)}
1588
- </div>
1434
+ <div className="mt-2 flex flex-wrap gap-1.5">${suggestedFromText.map((tag) => html` <button key=${tag} type="button" onMouseDown=${(e) => e.preventDefault()} onClick=${() => addTag(tag)} className="rounded-full border border-line bg-panel px-2 py-1 text-[10px] font-semibold text-slate-300 transition hover:border-accent/50 hover:text-accent">+ ${tag}</button> `)}</div>
1589
1435
  </div>
1590
1436
  </label>
1591
1437
  <label className="block">
1592
1438
  <span className="mb-2 inline-block text-xs font-semibold text-slate-300">Visibilidade</span>
1593
- <select
1594
- value=${visibility}
1595
- onChange=${(e) => setVisibility(String(e.target.value || 'public'))}
1596
- className="h-11 w-full rounded-2xl border border-line/70 bg-panelSoft/80 px-4 text-sm outline-none focus:border-accent/60 md:h-12"
1597
- >
1439
+ <select value=${visibility} onChange=${(e) => setVisibility(String(e.target.value || 'public'))} className="h-11 w-full rounded-2xl border border-line/70 bg-panelSoft/80 px-4 text-sm outline-none focus:border-accent/60 md:h-12">
1598
1440
  <option value="public">Público</option>
1599
1441
  <option value="unlisted">Não listado</option>
1600
1442
  <option value="private">Privado</option>
1601
1443
  </select>
1602
1444
  <p className="mt-2 text-[11px] text-slate-400">${visibilityHelp}</p>
1603
1445
  </label>
1604
-
1605
1446
  </div>
1606
1447
  `
1607
1448
  : null}
1608
-
1609
1449
  ${step === 2
1610
1450
  ? html`
1611
1451
  <div className="space-y-3 md:space-y-4">
@@ -1619,11 +1459,7 @@ function CreatePackApp() {
1619
1459
  className=${`rounded-2xl border border-dashed p-4 text-center transition md:rounded-3xl md:border-2 md:p-6 ${dragActive ? 'border-accent bg-accent/10' : 'border-line/70 bg-panelSoft/80'}`}
1620
1460
  >
1621
1461
  <p className="text-sm font-bold md:text-base">Arraste e solte seus stickers aqui</p>
1622
- <p className="mt-1 text-xs text-slate-400">
1623
- Imagens e vídeos até ${toBytesLabel(
1624
- limits.sticker_upload_source_max_bytes,
1625
- )} cada (conversão automática para .webp)
1626
- </p>
1462
+ <p className="mt-1 text-xs text-slate-400">Imagens e vídeos até ${toBytesLabel(limits.sticker_upload_source_max_bytes)} cada (conversão automática para .webp)</p>
1627
1463
  <input
1628
1464
  id="webp-upload"
1629
1465
  type="file"
@@ -1643,26 +1479,10 @@ function CreatePackApp() {
1643
1479
  <span>Arraste para reordenar • toque para definir capa</span>
1644
1480
  </div>
1645
1481
 
1646
- ${files.length
1647
- ? html`
1648
- <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 sm:gap-3 lg:grid-cols-4">
1649
- ${files.map((item, index) => html`<${StickerThumb}
1650
- key=${item.id}
1651
- item=${item}
1652
- index=${index}
1653
- selectedCoverId=${coverId}
1654
- onSetCover=${setCoverId}
1655
- onRemove=${removeSticker}
1656
- onDragStart=${setDraggingStickerId}
1657
- onDropOn=${(targetId) => reorderStickers(draggingStickerId, targetId)}
1658
- />`)}
1659
- </div>
1660
- `
1661
- : html`<p className="rounded-2xl border border-line/70 bg-panelSoft/80 p-3 text-center text-sm text-slate-400 md:p-4">Nenhum sticker selecionado ainda.</p>`}
1482
+ ${files.length ? html` <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 sm:gap-3 lg:grid-cols-4">${files.map((item, index) => html`<${StickerThumb} key=${item.id} item=${item} index=${index} selectedCoverId=${coverId} onSetCover=${setCoverId} onRemove=${removeSticker} onDragStart=${setDraggingStickerId} onDropOn=${(targetId) => reorderStickers(draggingStickerId, targetId)} />`)}</div> ` : html`<p className="rounded-2xl border border-line/70 bg-panelSoft/80 p-3 text-center text-sm text-slate-400 md:p-4">Nenhum sticker selecionado ainda.</p>`}
1662
1483
  </div>
1663
1484
  `
1664
1485
  : null}
1665
-
1666
1486
  ${step === 3
1667
1487
  ? html`
1668
1488
  <div className="space-y-3 md:space-y-4">
@@ -1672,9 +1492,7 @@ function CreatePackApp() {
1672
1492
  <h3 className="font-display text-base font-bold md:text-lg">Revisão final</h3>
1673
1493
  <p className="mt-0.5 text-xs text-slate-400">Confira os dados antes de publicar.</p>
1674
1494
  </div>
1675
- <span className="rounded-full border border-line/70 bg-panel/60 px-2.5 py-1 text-[11px] font-semibold text-slate-300">
1676
- ${files.length} stickers
1677
- </span>
1495
+ <span className="rounded-full border border-line/70 bg-panel/60 px-2.5 py-1 text-[11px] font-semibold text-slate-300"> ${files.length} stickers </span>
1678
1496
  </div>
1679
1497
  <div className="mt-3 grid gap-1.5 text-sm text-slate-300">
1680
1498
  <p className="truncate"><span className="text-slate-400">Nome:</span> <strong>${preview.name}</strong></p>
@@ -1693,38 +1511,18 @@ function CreatePackApp() {
1693
1511
  <div className="mt-2 h-2 overflow-hidden rounded-full bg-slate-900/70">
1694
1512
  <div className="h-full bg-accent transition-all" style=${{ width: `${uploadProgressPercent}%` }}></div>
1695
1513
  </div>
1696
- <p className="mt-2 text-xs text-slate-400">
1697
- ${publishPhase === 'creating'
1698
- ? 'Criando pack...'
1699
- : publishPhase === 'uploading'
1700
- ? `${uploadProgressDone}/${uploadProgressTotal || files.length || 0} enviados`
1701
- : publishPhase === 'processing'
1702
- ? 'Validando consistência e capa do pack...'
1703
- : publishPhase === 'publishing'
1704
- ? 'Publicando pack no marketplace...'
1705
- : `${uploadProgressDone}/${uploadProgressTotal || files.length || 0} concluídos`}
1706
- </p>
1514
+ <p className="mt-2 text-xs text-slate-400">${publishPhase === 'creating' ? 'Criando pack...' : publishPhase === 'uploading' ? `${uploadProgressDone}/${uploadProgressTotal || files.length || 0} enviados` : publishPhase === 'processing' ? 'Validando consistência e capa do pack...' : publishPhase === 'publishing' ? 'Publicando pack no marketplace...' : `${uploadProgressDone}/${uploadProgressTotal || files.length || 0} concluídos`}</p>
1707
1515
  </div>
1708
1516
  `
1709
1517
  : null}
1710
-
1711
1518
  ${showUploadFailureCard
1712
1519
  ? html`
1713
1520
  <div className="rounded-2xl border border-rose-400/25 bg-rose-400/5 p-3 text-sm">
1714
- <p className="font-semibold text-rose-200">
1715
- ${backendStateFailed
1716
- ? 'O pack entrou em estado de falha no backend.'
1717
- : `${failedUploadsCount} sticker(s) falharam no envio.`}
1718
- </p>
1719
- <p className="mt-1 text-xs text-rose-200/80">
1720
- ${backendStateFailed
1721
- ? `Use "${publishLabel}" para reparar e concluir a publicação.`
1722
- : `Toque em "${publishLabel}" para reenviar apenas as falhas.`}
1723
- </p>
1521
+ <p className="font-semibold text-rose-200">${backendStateFailed ? 'O pack entrou em estado de falha no backend.' : `${failedUploadsCount} sticker(s) falharam no envio.`}</p>
1522
+ <p className="mt-1 text-xs text-rose-200/80">${backendStateFailed ? `Use "${publishLabel}" para reparar e concluir a publicação.` : `Toque em "${publishLabel}" para reenviar apenas as falhas.`}</p>
1724
1523
  </div>
1725
1524
  `
1726
1525
  : null}
1727
-
1728
1526
  ${publishCompleted
1729
1527
  ? html`
1730
1528
  <div className="rounded-2xl border border-emerald-400/25 bg-emerald-400/5 p-3 text-sm text-emerald-100 md:p-4">
@@ -1753,98 +1551,37 @@ function CreatePackApp() {
1753
1551
 
1754
1552
  <div className="mt-3 lg:hidden">
1755
1553
  <div className="rounded-2xl border border-line/70 bg-panel/80 p-3">
1756
- <button
1757
- type="button"
1758
- onClick=${toggleMobilePreview}
1759
- className="flex h-11 w-full items-center justify-between gap-3 rounded-xl border border-line/70 bg-panelSoft/70 px-3 text-left"
1760
- aria-expanded=${mobilePreviewOpen ? 'true' : 'false'}
1761
- >
1554
+ <button type="button" onClick=${toggleMobilePreview} className="flex h-11 w-full items-center justify-between gap-3 rounded-xl border border-line/70 bg-panelSoft/70 px-3 text-left" aria-expanded=${mobilePreviewOpen ? 'true' : 'false'}>
1762
1555
  <div>
1763
1556
  <p className="text-xs font-semibold uppercase tracking-[.08em] text-slate-400">Preview</p>
1764
1557
  <p className="text-sm font-semibold text-slate-100">${preview.name}</p>
1765
1558
  </div>
1766
1559
  <span className="text-xs font-semibold text-accent">${mobilePreviewOpen ? 'Ocultar' : 'Mostrar'}</span>
1767
1560
  </button>
1768
- ${mobilePreviewOpen
1769
- ? html`<div className="mt-3"><${PackPreviewPanel} preview=${preview} quality=${quality} compact=${true} /></div>`
1770
- : html`<p className="mt-2 text-xs text-slate-400">Toque para visualizar capa, descrição e score do pack.</p>`}
1561
+ ${mobilePreviewOpen ? html`<div className="mt-3"><${PackPreviewPanel} preview=${preview} quality=${quality} compact=${true} /></div>` : html`<p className="mt-2 text-xs text-slate-400">Toque para visualizar capa, descrição e score do pack.</p>`}
1771
1562
  </div>
1772
1563
  </div>
1773
1564
 
1774
- ${error
1775
- ? html`<div className="mt-3 rounded-2xl border border-rose-400/25 bg-rose-400/5 px-3 py-2.5 text-sm text-rose-200 md:mt-4 md:px-4 md:py-3">${error}</div>`
1776
- : null}
1565
+ ${error ? html`<div className="mt-3 rounded-2xl border border-rose-400/25 bg-rose-400/5 px-3 py-2.5 text-sm text-rose-200 md:mt-4 md:px-4 md:py-3">${error}</div>` : null}
1777
1566
  </div>
1778
1567
 
1779
1568
  <div className="fixed inset-x-0 bottom-0 z-30 border-t border-line/70 bg-panel/95 p-3 backdrop-blur md:hidden">
1780
1569
  <div className="mx-auto w-full max-w-7xl">
1781
1570
  <div className="mb-2 flex items-center justify-between gap-2">
1782
- <button
1783
- type="button"
1784
- className="h-8 rounded-full border border-line/70 bg-panelSoft/80 px-3 text-xs font-semibold text-slate-200 disabled:opacity-60"
1785
- onClick=${restartCreateFlow}
1786
- disabled=${busy}
1787
- title="Limpar rascunho local"
1788
- >
1789
- Recomeçar
1790
- </button>
1791
- <button
1792
- type="button"
1793
- className="h-8 rounded-full border border-line/70 bg-panelSoft/60 px-3 text-xs font-semibold text-slate-300"
1794
- onClick=${toggleMobilePreview}
1795
- aria-expanded=${mobilePreviewOpen ? 'true' : 'false'}
1796
- >
1797
- ${mobilePreviewOpen ? 'Ocultar preview' : 'Preview'}
1798
- </button>
1571
+ <button type="button" className="h-8 rounded-full border border-line/70 bg-panelSoft/80 px-3 text-xs font-semibold text-slate-200 disabled:opacity-60" onClick=${restartCreateFlow} disabled=${busy} title="Limpar rascunho local">Recomeçar</button>
1572
+ <button type="button" className="h-8 rounded-full border border-line/70 bg-panelSoft/60 px-3 text-xs font-semibold text-slate-300" onClick=${toggleMobilePreview} aria-expanded=${mobilePreviewOpen ? 'true' : 'false'}>${mobilePreviewOpen ? 'Ocultar preview' : 'Preview'}</button>
1799
1573
  </div>
1800
1574
  <div className="grid grid-cols-[1fr_1.45fr] gap-2">
1801
- <button
1802
- type="button"
1803
- className="h-11 rounded-xl border border-line/70 bg-panelSoft/80 text-sm font-bold disabled:opacity-60"
1804
- onClick=${prevStep}
1805
- disabled=${step === 1 || busy}
1806
- >
1807
- Voltar
1808
- </button>
1809
- ${step < 3
1810
- ? html`
1811
- <button
1812
- type="button"
1813
- className=${`h-11 rounded-xl text-sm font-extrabold disabled:opacity-60 ${mobilePrimaryActionClass}`}
1814
- onClick=${nextStep}
1815
- disabled=${busy}
1816
- >
1817
- ${mobilePrimaryActionLabel}
1818
- </button>
1819
- `
1820
- : html`
1821
- <button
1822
- type="button"
1823
- className=${`h-11 rounded-xl text-sm font-extrabold disabled:opacity-60 ${mobilePrimaryActionClass}`}
1824
- onClick=${handleFinalStepPrimaryAction}
1825
- disabled=${finalStepPrimaryDisabled}
1826
- >
1827
- ${mobilePrimaryActionLabel}
1828
- </button>
1829
- `}
1575
+ <button type="button" className="h-11 rounded-xl border border-line/70 bg-panelSoft/80 text-sm font-bold disabled:opacity-60" onClick=${prevStep} disabled=${step === 1 || busy}>Voltar</button>
1576
+ ${step < 3 ? html` <button type="button" className=${`h-11 rounded-xl text-sm font-extrabold disabled:opacity-60 ${mobilePrimaryActionClass}`} onClick=${nextStep} disabled=${busy}>${mobilePrimaryActionLabel}</button> ` : html` <button type="button" className=${`h-11 rounded-xl text-sm font-extrabold disabled:opacity-60 ${mobilePrimaryActionClass}`} onClick=${handleFinalStepPrimaryAction} disabled=${finalStepPrimaryDisabled}>${mobilePrimaryActionLabel}</button> `}
1830
1577
  </div>
1831
1578
  </div>
1832
1579
  </div>
1833
1580
 
1834
1581
  <div className="mt-6 hidden items-center justify-end gap-2 px-6 pb-6 md:flex">
1835
- <button
1836
- type="button"
1837
- className="h-10 rounded-xl border border-line/70 bg-panelSoft/80 px-4 text-sm font-bold disabled:opacity-60"
1838
- onClick=${restartCreateFlow}
1839
- disabled=${busy}
1840
- title="Limpar rascunho local e recomeçar"
1841
- >
1842
- Recomeçar
1843
- </button>
1582
+ <button type="button" className="h-10 rounded-xl border border-line/70 bg-panelSoft/80 px-4 text-sm font-bold disabled:opacity-60" onClick=${restartCreateFlow} disabled=${busy} title="Limpar rascunho local e recomeçar">Recomeçar</button>
1844
1583
  <button type="button" className="h-11 rounded-xl border border-line/70 bg-panelSoft/80 px-5 text-sm font-bold" onClick=${prevStep} disabled=${step === 1 || busy}>Voltar</button>
1845
- ${step < 3
1846
- ? html`<button type="button" className="h-11 rounded-xl bg-accent px-5 text-sm font-extrabold text-slate-900" onClick=${nextStep} disabled=${busy}>Próximo passo</button>`
1847
- : html`<button type="button" className="h-11 rounded-xl bg-accent2 px-5 text-sm font-extrabold text-slate-900 disabled:opacity-60" onClick=${handleFinalStepPrimaryAction} disabled=${finalStepPrimaryDisabled}>${finalStepPrimaryLabel}</button>`}
1584
+ ${step < 3 ? html`<button type="button" className="h-11 rounded-xl bg-accent px-5 text-sm font-extrabold text-slate-900" onClick=${nextStep} disabled=${busy}>Próximo passo</button>` : html`<button type="button" className="h-11 rounded-xl bg-accent2 px-5 text-sm font-extrabold text-slate-900 disabled:opacity-60" onClick=${handleFinalStepPrimaryAction} disabled=${finalStepPrimaryDisabled}>${finalStepPrimaryLabel}</button>`}
1848
1585
  </div>
1849
1586
  </div>
1850
1587
  `;