MelissaConversas: a11y + perf tagsForThread + DRY (channelMeta + KANBAN_COLUMNS shared)

A11y no parent:
- aria-label em botoes icon-only do header (Recarregar dinamico, Buscar
  compact, Close); tooltip vira title que SR ignora
- aria-hidden=true em icones decorativos (header title, search input,
  subheader info-circle, kanban col head, empty state, button icons)
- aria-busy reativo no mw-col__body durante loading
- aria-label dinamico no count do kanban ("3 conversas em Urgente")
- aria-expanded + aria-controls no menu mobile button
- aria-label no input de busca
- role=note no subheader explicativo
- :inert="(drawerOpen && isMobile) || null" no <section class="mw-page">
  — focus trap real: drawer aberto torna conteudo de fundo inerte
  (boolean attr via || null pra Vue 3.4 serializar correto)

A11y no Sidebar:
- aria-hidden=true em todos icones decorativos restantes (filter title
  icons, list/bell/user/user-minus, channel icons, filter-slash, etc)

Perf — tagsForThread cacheado:
- Antes era chamado in-template (2x por card, recriava array a cada
  render). Agora tagsByThreadKey computed Map: lookup O(1) por card,
  recompute so quando threadTagsMap ou tagById muda. EMPTY_TAGS frozen
  evita criar arrays novos pra threads sem tags.

DRY — channelMeta + KANBAN_COLUMNS shared:
- src/utils/channelMeta.js (novo): CHANNEL_OPTIONS frozen + channelIcon
  + channelLabel. Antes channelIcon estava em 3 lugares (parent, Sidebar,
  Card); CHANNEL_OPTIONS em 2 (parent, Sidebar). Agora 1.
- useConversations.js: exporta KANBAN_COLUMNS frozen (metadata canonica:
  key + label + icon + color). Antes parent+Sidebar tinham copias locais
  de 8 linhas cada + composable tinha KANBAN_ORDER separado. Agora
  KANBAN_ORDER deriva de KANBAN_COLUMNS.

Drift eliminado: 3 fontes -> 1 pra channelIcon, 2 -> 1 pra
CHANNEL_OPTIONS, 2 -> 1 pra KANBAN_COLUMNS (KANBAN_ORDER ainda interno
ao composable mas derivado).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-07 16:37:46 -03:00
parent 250e946084
commit cc7841bd1f
5 changed files with 116 additions and 69 deletions
+34
View File
@@ -0,0 +1,34 @@
/*
* channelMeta — fonte unica de canais de comunicacao (WhatsApp/SMS/E-mail).
* --------------------------------------------------------------------
* Antes, os 3 consumidores (MelissaConversas parent, Sidebar, Card)
* tinham copias locais de CHANNEL_OPTIONS + channelIcon. Drift risk
* pequeno mas existia — adicionar 'telegram' exigia mexer em N lugares.
* Aqui centraliza.
*
* NAO eh um composable Vue (no useXxx) porque sao constantes puras
* e funcoes sem reactivity — arquivo utility eh suficiente.
*/
export const CHANNEL_OPTIONS = Object.freeze([
Object.freeze({ label: 'Todos', value: null }),
Object.freeze({ label: 'WhatsApp', value: 'whatsapp' }),
Object.freeze({ label: 'SMS', value: 'sms' }),
Object.freeze({ label: 'E-mail', value: 'email' })
]);
const ICON_MAP = Object.freeze({
whatsapp: 'pi-whatsapp',
sms: 'pi-comment',
email: 'pi-envelope'
});
export function channelIcon(channel) {
return ICON_MAP[channel] || 'pi-comment';
}
export function channelLabel(channel) {
if (!channel) return 'Todos';
const opt = CHANNEL_OPTIONS.find((o) => o.value === channel);
return opt?.label || channel;
}