Communitygithub.com

AxelBeary/Inkglean

Padrões de arquitetura para plataformas multi-inquilino de comissão/mercado — páginas de usuário com template, widgets incorporáveis e hospedagem de páginas personalizadas. Aborda o sistema de páginas em três camadas para oferecer templates hospedados enquanto permite integração com sites externos.

O que é Inkglean?

Esta skill fornece uma arquitetura completa e testada para construção de plataformas de comissão multi-inquilino (artistas, criadores, freelancers) com um sistema de páginas públicas em três camadas. Ela detalha como suportar criadores em diferentes níveis de habilidade técnica: desde templates Vue embutidos com temas CSS (Camada 1), passando por widgets incorporáveis com um simples <script> (Camada 2), até a hospedagem de páginas HTML/CSS/JS personalizadas (Camada 3). A skill documenta decisões arquiteturais cruciais, como a separação de layout e paleta de cores em eixos ortogonais para gerar 16 combinações visuais a partir de 8 definições, garantindo que 'dois templates que diferem apenas na cor não são dois templates'. Além disso, aborda padrões avançados de UI/UX como formulários de pedido extraíveis (useOrderForm), substituição por arrastar com confirmação contextual (drag & drop), pré-visualização de templates via parâmetros de URL, um histórico de atividades com timeline diferenciada e temas de cores adaptáveis ao modo claro/escuro. A arquitetura prioriza a reutilização de componentes de lógica (composables) sobre duplicação de código, permitindo que múltiplos templates de página de pedido ou página inicial consumam a mesma lógica de negócio, variando apenas na apresentação visual. Os padrões de CSS incluem easing bounce global e modelo físico de botão de três estados para uma experiência de usuário refinada.

Funciona com~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/AxelBeary/Inkglean/tree/HEAD/docs/comms/archive/skills/software-development/commission-platform-architecture

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Arquitetura de Plataforma de Comissão

Plataformas de comissão multi-inquilino onde cada usuário (artista, criador, freelancer) possui uma página pública com capacidade de realizar pedidos. Esta skill cobre a decisão arquitetural de oferecer tanto templates hospedados integrados quanto permitir que criadores com seus próprios sites possam integrar.

Sistema de Página em Três Camadas

O padrão arquitetural central: suportar criadores em três níveis de habilidade técnica e independência.

┌─────────────────────────────────────────────────┐
│  Camada 3: Hospedagem de Página Personalizada   │
│  Envie seu próprio HTML/CSS/JS → servido em      │
│  /artist/alice (arquivos estáticos de uploads/)   │
├─────────────────────────────────────────────────┤
│  Camada 2: Widget Incorporável                   │
│  Cole uma tag <script> no seu site existente     │
│  → Botão "Encomendar Comissão" + formulário       │
├─────────────────────────────────────────────────┤
│  Camada 1: Templates Integrados                  │
│  Escolha uma variante de layout, zero configuração│
│  → Página completa em /artist/alice              │
└─────────────────────────────────────────────────┘

Camada 1: Templates Integrados

Para quem é: Criadores que não possuem site próprio ou não querem mantê-lo.

Implementação: Componentes Vue com temas via CSS Variables.

// Rota: /artist/:subdomain
routes.get('/artist/:subdomain', async (req, reply) => {
  const artist = await artistService.getBySubdomain(req.params.subdomain)
  
  if (artist.page_type === 'default') {
    // Renderiza SPA Vue integrado (ou SSR)
    // O campo page_theme controla qual variante de template
    return reply.sendFile('index.html')
  }
  // ... fallback para camadas 2-3 tratadas em outro lugar
})

Template = Layout × Paleta (eixos ortogonais) — a decisão de modelagem mais importante. Se dois "templates" diferem principalmente na cor, eles NÃO são dois templates — eles são um layout usando duas cores. Agrupar a cor na dimensão do template infla a contagem de templates com código de estrutura duplicado (os usuários notarão: "essas páginas parecem a mesma em cores diferentes"). Divida em eixos que o artista escolhe independentemente:

EixoQuem escolheExemplosControla
Layout (estrutura)Artistaclassic (estúdio de duas colunas) / gallery (museu em tela cheia) / folio (landing page única) / atelier (estúdio acolhedor em estilo livro)Tratamento de abertura, lógica de colunas, formato da galeria, navegação
Paleta (clima)Artistapaper / ink / dusk / moss — cada uma definida com variantes clara E escuraCores de fundo/superfície/texto da página (variáveis CSS --pal-*)
Acento + modoVisitanteSeletor de acento de 5 cores + preferência claro/escuro--color-primary, html.dark

Regras-chave:

  • Uma paleta é um clima com variantes clara+escura, nunca "o tema escuro". O artista escolhe ink; visitantes em modo claro veem cinza claro, visitantes em modo escuro veem carvão — o clima permanece consistente, o brilho segue o visitante.
  • A cor de acento (botões/links/destaques) sempre usa a variável de acento do visitante (--color-primary), nunca as variáveis de paleta — os dois sistemas permanecem ortogonais e ambos continuam funcionando.
  • 4 layouts × 4 paletas = 16 aparências distintas a partir de 8 definições.
  • Layouts devem diferir estruturalmente (abertura diferente, lógica de colunas diferente) ou devem ser mesclados em um único layout.

Arquétipos de layout que funcionam — cada um abre com o ativo mais distintivo do artista (sua arte), não um herói genérico centrado em nome+bio+botões:

LayoutAberturaToque característico
galleryObra de arte em tela cheia, nome como pequena placa no cantoGaleria editorial alternando grande/pequeno, zoom ao passar o mouse, clique para pré-visualizar
classicBanner com obra principal, nome sobreposto à arteDuas colunas no desktop, cartão informativo fixo com CTA de comissão sempre visível
folioTela dividida: texto à esquerda, obras empilhadas à direitaDestaque de navegação baseado em rolagem, hamburger no mobile + CTA fixo na parte inferior
atelierEspalhado em estilo livro acolhedor, tipografia serifada (Noto Serif SC)Fundo com textura de papel, margens editoriais, espaçamento generoso, foco na arte com clima literário

Arquitetura de kit compartilhado (comprovada): templates nunca leem nomes de campos do backend diretamente — um composable adaptador (useArtistData(props)) fornece imgUrl(), statusText() (i18n), socialLinks, heroArtwork; componentes Tpl* reutilizáveis recebem variantes via props (TplHero variant="banner|fullscreen|split", TplGallery layout="grid|editorial|masonry") com slots como pontos de extensão (#addons na grade de níveis). Veja references/template-system.md para o padrão completo, incluindo correções de temporização de componentes assíncronos e mapeamento de IDs legados.

Sobreposição de cor de acento definida pelo artista (padrão R49): O artista pode definir uma cor de acento persistente (lista branca de 5 cores, armazenada como artists.accent_color TEXT) que substitui a escolha do ThemePicker do visitante em sua página pública. Implementação em ArtistHome.vue:

const ACCENT_INDEX = { '#34dbcb': '1', '#34c2db': '2', '#3498db': '3', '#346edb': '4', '#3445db': '5' }
const accentOverride = computed(() => {
  const raw = previewAccent.value || artist.value?.accentColor
  return raw ? (ACCENT_INDEX[String(raw).toLowerCase()] || null) : null
})
// watch(immediate) + onUnmounted: salvar/restaurar document.documentElement.dataset.accent

Decisões-chave: (1) Injeção de atributo global data-accent — NENHUMA alteração de variável CSS por template necessária (theme.css já possui seletores data-accent="1"~"5" com variantes clara+escura). Adicionar suporte a acento em todos os 4 templates = zero alterações de arquivo de template. (2) Acento do artista tem prioridade sobre o ThemePicker do visitante enquanto na página do artista; a escolha do visitante é restaurada em onUnmounted. (3) Backend valida contra lista branca de 5 cores + null (limpar); normalização toLowerCase() na escrita. (4) UI de configurações: botões circulares de amostra (32px) com ✓ de selecionado + botão "默认" (limpar). NÃO usar el-color-picker (cor livre quebraria a lista branca). (5) Decisão C61: acento afeta apenas páginas voltadas ao cliente, NÃO o painel do artista.

Modo de pré-visualização via parâmetros de URL (padrão R50): O botão de configurações "预览主页" abre /artist/:subdomain?_tpl=X&_pal=Y&_accent=Z em uma nova janela. ArtistHome.vue lê route.query._tpl/_pal/_accent como substituições da camada de renderização (nunca escreve na camada de dados). Um banner de aviso fixo ("预览模式 — 修改尚未保存") é exibido quando qualquer parâmetro de pré-visualização está presente. Os computados de template/paleta/acento recaem em: previewParam || artist.field || default. Seguro para acesso público — parâmetros de pré-visualização afetam apenas a renderização, todos os dados vêm da API pública.

Linha do tempo de atividades substituindo lista plana de notas (padrão R40): Ao mesclar registros de mudança de status (notas do sistema) e notas do artista em uma única visão cronológica, use el-timeline com diferenciação de tipo/oco:

<el-timeline-item :type="note.created_by === 'system' ? 'info' : (note.image_path ? 'success' : 'primary')"
  :hollow="note.created_by === 'system'" :timestamp="formatDate(note.created_at)" placement="top">

Indicadores de tipo: 🔄 mudança de status / 📝 nota / 🖼 nota com imagem. Notas do sistema recebem classe tl-item--system (cor suave, fonte menor). Botão de excluir (R46) aparece apenas para notas não do sistema, revelado ao passar o mouse com fallback de toque @media (hover: none). A caixa de entrada é movida para o final da linha do tempo. Zero alterações no backend — as notas do sistema já existem em order_notes com created_by='system'. A barra de operações (avançar/retroceder/cancelar) permanece FORA do cartão da linha do tempo.

Substituição por arrastar com confirmação dependente do contexto (R53 vs R55): Dois recursos de arrastar-para-substituir no mesmo aplicativo têm REQUISITOS de confirmação DIFERENTES — NÃO os unifique:

R53 Imagem de foco do quadro de filaR55 Imagem de exemplo de nível
Confirmação de substituiçãoNão (a imagem antiga permanece na galeria do pedido)Sim — ElMessageBox.confirm (imagem antiga é perdida)
MotivoSubstituir o foco ≠ excluir a imagemSubstituir o exemplo = imagem antiga irrecuperável
Implementação: infraestrutura de arrastar compartilhada (dragover.prevent/dragleave com contains(relatedTarget) anti-flicker/drop.prevent + input de arquivo oculto), mas a função de upload verifica if (row.example_image) { await ElMessageBox.confirm(...) } apenas para R55. Ambas reutilizam as APIs de upload existentes. O el-image preview-src-list deve ser removido da imagem de foco do R53 para evitar que ele engula o evento de clique (mesma armadilha do R18).

Extrair composable de formulário de pedido (padrão R58-1): Quando o formulário de pedido ultrapassa ~400 linhas (carregamento de dados + precificação + validação + upload + envio + recuperação de rascunho), extraia TODA a lógica de negócios em um composable compartilhado (useOrderForm.js). O componente de página torna-se puro layout (~300 linhas de template + estilo, o script é apenas const of = useOrderForm(subdomain, formRef) + desestruturação). O composable retorna um objeto plano de refs/reactives/métodos agrupados por preocupação: carregamento de dados (artist, tiers, rulesContent, loading), estado do formulário + regras de validação, estado de envio (submitting, showSuccess, resultNo, submit), imagens de referência (refFileList, handleRefUpload, handleRefRemove), precificação (addonSelections, pricePreview, pricingExpanded, selectedTier, onTierChange) e HTML sanitizado das regras. Por que isso importa para multi-template: quando a página de pedido suporta múltiplos templates de layout (passo a passo, estilo recibo, estilo revista), cada template é uma casca de layout fina que chama o MESMO composable — zero duplicação de lógica. O composable é o "núcleo", os templates são "pele". Este é o mesmo padrão de adaptador de useArtistData para a página inicial, aplicado ao formulário de pedido. Detalhe de implementação chave: o composable recebe formRef (a ref do el-form do template) como parâmetro porque a validação (formRef.value.validate()) deve chamar o elemento form do template — o composable possui a lógica, mas NÃO o DOM.

Layout de formulário de pedido passo a passo (padrão R58-2, comprovado): O layout de pedido padrão é um fluxo guiado de três etapas construído inteiramente sobre useOrderForm() (zero alterações no composable do R58-1 — valida a extração):

  • Indicador de etapa: três pontos numerados + linhas de conexão. Atual: ampliado + primário. Concluída: ✓ + preenchimento primário. Pendente: cinza oco. Mobile (≤860px): apenas pontos.
  • Etapa 1 — Escolher nível: grade de cartões responsiva (repeat(auto-fill, minmax(240px, 1fr))) substituindo el-select. Selecionado: transição --ease-bounce + carimbo giratório ✓ (@keyframes tier-stamp-in: escala 0→1.2→1, rotação -15°→0°). A divulgação progressiva do R14 permanece dentro da etapa 1.
  • Etapa 2 — Descrever + enviar: textarea + tags de inspiração (R58-4: chips padrão codificados, clique anexa com vírgula inteligente + truncamento de 2000 caracteres) + upload de referência (arquivo + colar inalterados).
  • Etapa 3 — Contato: QQ + nome + aceitação das regras.
  • Cartão de resumo fixo: coluna direita em widescreen (280px fixo), nível + detalhamento + total + parcelas em tempo real. Mobile: parte inferior da etapa ativa.
  • Confirmação de recibo (R58-3): enviar → validar → el-dialog de recibo de café (teleportado para o body, CSS global namespace receipt-* em templates.css). Papel #fdfbf5, Courier monoespaçado, bordas em zigue-zague (repeating-conic-gradient), separadores tracejados, decoração de código de barras, chips de parcelas. Tema claro fixo (metáfora do papel). Cancelar retorna à etapa 3; confirmar chama submit().
  • Copiar informações do pedido (R58-5): diálogo de sucesso "复制约稿信息" → área de transferência multilinha (número do pedido + nível + complementos + total).
  • Navegação entre etapas: "下一步" valida a etapa atual antes de avançar; "上一步" sempre permitido; sem pular etapas.

Página de pedido multi-template (arquitetura R58): A página de pedido suporta templates de layout selecionáveis pelo artista, independentemente do template da página inicial. Backend (comprovado, migração v16): artists.order_template_id TEXT DEFAULT 'default' — validação de lista branca na camada de serviço (ORDER_TEMPLATES = ['default'], estender o array ao adicionar templates). GET /api/artists/:subdomain retorna orderTemplateId: artist.order_template_id || 'default'. PUT /api/artist/profile aceita orderTemplateId (mapeado via keyMap para order_template_id). Valores inválidos geram INVALID_ORDER_TEMPLATE (409). String vazia recai em 'default'. Frontend: A página de configurações do artista mostra um seletor de template em estilo cartão (mesmo padrão de UI da seleção de template da página inicial). Roteamento do lado do cliente: OrderForm.vue lê artist.orderTemplateId e renderiza o componente de layout correspondente via <component :is="..."> ou um mapa de registro de template. Todos os componentes de layout compartilham useOrderForm() — eles diferem APENAS no arranjo visual (indicadores de etapa, layout do cartão, estilo do diálogo de confirmação). Adicionar um novo template de pedido = um novo arquivo .vue com uma casca de layout + uma entrada no array da lista branca do backend, zero alterações de lógica. Pré-visualização: reutiliza o mecanismo de parâmetro de URL _tpl da pré-visualização da página inicial.

Padrão de contato QQ (R58-6): Duas opções para contato QQ: "跳转QQ" (window.open('tencent://message/?uin=' + encodeURIComponent(qq), '_self')) + "复制QQ" (navigator.clipboard.writeText(qq) com toast). Fallback da área de transferência: em caso de falha (permissões negadas, não-HTTPS), exibe ElMessage.warning(qq) — o próprio número QQ como mensagem de aviso, para que o usuário possa copiar manualmente do toast. Aplicado em dois lugares: diálogo de sucesso do lado do cliente (contactQq do artista) e detalhe do pedido do lado do artista (qq do cliente). i18n: chaves separadas por contexto (orderForm.jumpQq vs orderDetail.jumpQq) porque a mensagem do toast difere ("QQ号已复制" vs "客户QQ已复制").

Padrões de atualização de CSS (lote v0.16, comprovados):

  • Easing bounce global: --ease-bounce: cubic-bezier(.34, 1.56, .64, 1) em :root. Use apenas para transições de transform/box-shadow (nunca opacity — não pode ultrapassar 1.0). Fallback no código consumidor: var(--ease-bounce, cubic-bezier(0.22, 1, 0.36, 1)).
  • Modelo físico de botão de três estados: .el-button:hover:not(:disabled):not(.is-loading) { transform: translateY(-3px); box-shadow: deeper } + :active { transform: translateY(-1px) scale(0.98) }. As exclusões :not() impedem que botões desabilitados/carregando animem.
  • Layout fluido com clamp() (padrão C73-1): elementos sensíveis (fontes, padding, gap, tamanhos de cartão) usam clamp(tamanhoMin, escalaFluida, tamanhoMax). A escala fluida deriva de calc(valorMin + (valorMax - valorMin) * ((100vw - 375px) / (1440px - 375px))). Exemplo para fonte primária: --fs-primary: clamp(0.875rem, calc(0.75rem + 0.5vw), 1.125rem). Evite tipografia baseada em vw puro (incontrolável em telas muito largas/muito estreitas). Este padrão substitui consultas de mídia para elementos individuais — consultas de mídia são reservadas para mudanças de layout (quebra de coluna, alternância de navegação).
  • Animação de entrada de cartão com base no índice :nth-child (padrão C73-2): cartões em uma grade (níveis, portfólio) entram com atraso escalonado: animation: card-in 0.6s var(--ease-bounce) both; animation-delay: calc(var(--i, 0) * 80ms). O índice --i é definido por style="--i: 0" inline no template (Vue :style="{ '--i': index }"). Nenhum JS de interseção necessário para animações de entrada simples.

Camada 2: Widget Incorporável (<iframe> incorporado)

Para quem é: Criadores com site próprio que desejam um formulário de comissão sem redirecionar os visitantes.

Implementação: widget baseado em <iframe> carregado via POST /api/embed/token (token de uso único, 15 min de expiração). O iframe é dimensionado para max-width: 100%; height: 500px (pode ser ajustado pelo criador via atributos data-*). O widget de comissão completo (seleção de nível, formulário, checkout) é servido como uma SPA Vue independente (dist/embed.html), comunicando-se com o servidor principal via tokens de API em curto prazo.

Geração de token do widget:

// POST /api/embed/token
app.post('/api/embed/token', async (req, reply) => {
  const { artistSubdomain } = req.body
  const token = crypto.randomBytes(32).toString('hex')
  // Armazenar token com tempo de expiração + artistSubdomain
  await redis.set(`embed:${token}`, artistSubdomain, 'EX', 900) // 15 min
  return { token }
})

Script de integração do cliente: o criador coloca esta tag em sua página:

<script src="https://suaplataforma.com/embed.js" data-artist="subdomain-do-artista" defer></script>

Isso renderiza um invólucro de iframe estilizado com fallbacks de acessibilidade.

Camada 3: Hospedagem de Página Personalizada

Para quem é: Criadores que desejam controle total sobre sua página pública na plataforma.

Implementação: Armazenamento de arquivos estáticos enviados, servidos em /artist/:subdomain.

Rota fallback:

// Fallback em routes.get('/artist/:subdomain')
if (artist.page_type === 'custom') {
  const filePath = path.join(config.uploadsDir, artist.subdomain, 'index.html')
  if (fs.existsSync(filePath)) {
    return reply.sendFile(filePath)
  }
  // Fallback para 404: servir página de erro personalizada ou padrão
}

Fluxo de upload: Os criadores enviam um arquivo ZIP contendo index.html, style.css, script.js e quaisquer ativos estáticos (imagens, fontes) através de um endpoint de upload na página de configurações. O backend extrai e valida o conteúdo antes de servir.

Validação (itens obrigatórios):

  • Arquivo raiz index.html — repositório principal da página.
  • Sem caminhos de diretório maliciosos (impedir traversal com path.resolve + path.relative).
  • Tamanho máximo: 10MB por envio.
  • HTML sanitizado (tags permitidas: divisões, seções, cabeçalhos, parágrafos, listas, links, imagens, mídia incorporada; script/style permitido apenas em arquivos dedicados; nenhum eval() no conteúdo injetado).

Estrutura de diretórios no backend:

uploads/
  └── {subdomain}/
      ├── index.html
      ├── style.css
      └── assets/
          └── logo.png

Endpoints da API

MétodoRotaDescrição
GET/api/artists/:subdomainObter dados públicos do artista (inclui pageType, orderTemplateId, etc.)
PUT/api/artist/profileAtualizar perfil do artista (inclui pageType, orderTemplateId, accentColor, etc.)
POST/api/embed/tokenGerar token de uso único para widget incorporado
POST/api/upload/custom-pageEnviar arquivos ZIP da página personalizada
POST/api/orderCriar um novo pedido de comissão

Variáveis CSS e Temas

O sistema de temas funciona através de variáveis CSS definidas em :root e substituídas por dados do artista/seleções do visitante. Paletas definem as cores base; temas claro/escuro e acentos do visitante ajustam-nas através de seletores CSS.

Exemplo de definição de paleta (estilo de cores) :

:root {
  --pal-paper-light-bg: #fefcf5;
  --pal-paper-light-text: #3a3a3a;
  --pal-paper-dark-bg: #1e1e1e;
  --pal-paper-dark-text: #e0e0e0;
  /* ... */
}

/* O tema escuro substitui as cores base por variantes escuras da paleta */
html.dark {
  --page-bg: var(--pal-paper-dark-bg);
  --page-text: var(--pal-paper-dark-text);
}

/* As cores de acento são definidas separadamente */
[data-accent="1"] { --color-primary: #34dbcb; }
[data-accent="2"] { --color-primary: #34c2db; }
/* ... */

Considerações de Segurança

  • Todos os uploads de página personalizada são verificados contra traversal de diretório e conteúdo malicioso.
  • Tokens de widget expiram após 15 minutos — nenhuma exposição de longa duração.
  • A sobreposição de acento do artista é validada contra uma lista branca no backend — sem cores arbitrárias.
  • A pré-visualização via parâmetros de URL nunca persiste dados; é apenas uma substituição de renderização do lado do cliente.

Etapas de Migração

Ao adicionar novos recursos (por exemplo, novo template de layout, nova paleta):

  1. Adicionar ao array de lista branca no backend (serviço e validação).
  2. Adicionar arquivo de componente no frontend.
  3. Registrar no mapeamento de template (se aplicável).
  4. Adicionar opção na UI de configurações.
  5. Testar com parâmetros de URL de pré-visualização.

Referências: references/template-system.md, references/component-architecture.md

Habilidades Relacionadas