Ajuda do Up Web DS
Como classificar, importar, criar, validar e distribuir componentes do Design System interno da Up Web Studio.
Importar não significa aprovar.
Todo item externo entra como matéria-prima e precisa passar por normalização, examples, testes, preview e validação antes de stable.
Introdução
O Up Web DS é uma camada de normalização entre fontes externas, componentes internos e os produtos da equipe. Nada entra por conveniência: cada item declara o que é, de onde veio, sob qual licença, e o que já foi verificado nele.
Esta página é a referência rápida para pessoas. AGENTS.md continua sendo a referência de engenharia para agentes, e as duas descrevem a mesma política — a taxonomia e as tabelas aqui são lidas de src/catalog/taxonomy.ts, o mesmo módulo que o CLI e o ds:validate usam para aceitar ou recusar um item.
Como classificar um item
A metadata tem três eixos independentes. Responder aos três é o que transforma um arquivo em um item do design system.
- type
- Onde o item vive na arquitetura.
- category
- O que o item faz na interface.
- domain
- Em qual contexto de produto ele ganha significado.
Os três eixos são independentes. Nunca use um deles para representar outro: category não é layer, domain não é categoria, e type não é o lugar onde o arquivo mora.
Guia de decisão
- É uma unidade genérica de UI?
- primitive
- É só uma aparência diferente de um primitive existente?
- recipe
- É uma composição reutilizável com significado?
- component
- Resolve um fluxo recorrente?
- pattern
- É uma seção grande de página?
- block
- É um ponto de partida de página ou app?
- template
Type
type é o nível de abstração — o que o item é arquiteturalmente. São seis valores, e a lista é fechada.
layout não é um type. Uma estrutura pequena e genérica é primitive com category: 'layout'; uma estrutura grande é block ou template. Foundation também não é um type: tokens, tipografia e a escala de motion vivem na folha de estilo da Foundation, não em um diretório de item.
Category
category é a família funcional: o que o item faz na interface. Não representa layer e não representa domínio.
Primitives e recipes
Um Select abre um popup para funcionar, mas a função é escolher um valor: ele é selection, não overlay.
Por domínio
Componentes de domínio não são forçados nas categorias de primitive quando isso destrói o significado. O domínio application reaproveita as famílias funcionais: uma Command Palette é overlay e uma Filter Bar é selection.
Domain
domain é o contexto de produto em que o item ganha significado. primitive e recipe são sempre null; os demais types exigem um domínio.
Domínio é classificação, nunca comportamento: nenhum item muda o que faz em runtime por causa do seu domain.
Exemplos de classificação
Na interface, domain: null aparece como Domain-free. No código continua sendo null.
Lifecycle
Todo item nasce incoming e sobe por prova, não por decisão. Somente stable entra no registry consumível.
- incomingAparece no catálogo, fora do registry.Scaffold ou matéria-prima recém-ingerida. A normalização ainda não aconteceu.
- experimentalAparece no catálogo, fora do registry.Implementação em avaliação: existe, funciona, mas o contrato ainda pode mudar.
- stableÚnico status publicado no registry consumível.Contrato aprovado. Exige metadata, examples, testes, tokens, acessibilidade, dark mode, responsivo, ícones semânticos, motion e validators verdes.
- deprecatedAparece no catálogo, não é distribuído como item novo.Mantido por compatibilidade e documentação, com substituto planejado.
Adicionar do shadcn/ui
O caminho tem duas etapas separadas de propósito: inspecionar não baixa nada para src/, e promover é uma decisão explícita.
1. Inspecionar
Mostra staging, licença, dependências, ícones semânticos, normalizações conhecidas e findings — sem escrever em src/.
2. Adicionar — fluxo interativo
O CLI pergunta se deve criar o scaffold. Responder `y` cria o item em src/ como incoming.
3. Adicionar — fluxo explícito
Sem --promote, ds:add escreve apenas em .tmp/ds-ingest (staging) e .ds/ingestion (proveniência).
Um scaffold, não dois
Não execute --promote depois de já responder y ao scaffold. O item já existe e o CLI reporta conflito — corretamente.
Outros exemplos
Adicionar de outras fontes
A fonte não define o type. Um item do Magic UI pode ser uma recipe, um do Kibo pode ser um component, e o mesmo registry pode fornecer os dois — a classificação é decidida depois do inspect, com o código à vista.
Magic UI — recipe sobre primitive
Se é aparência de Button, é uma recipe do Button oficial. Não se cria um segundo primitive.
Kibo UI — componente composto
A categoria final é confirmada semanticamente depois do inspect, não deduzida da fonte.
Coss UI — primitive que já existe
Comportamento genérico melhor → incorporar ao primitive oficial. Aparência diferente → recipe.
Não existe Button2, CossButton nem ButtonV2. Se o item externo tem comportamento genérico melhor, ele é incorporado ao primitive existente para que todas as recipes herdem.
Cult UI e Chánh Đại
Criar do zero
Antes de implementar: procure primitives existentes, componha Button/Radio/Badge em vez de duplicar comportamento de foundation, e não acople o item a Firebase, Supabase, Prisma, Server Actions ou autenticação. Um item recebe dados e devolve eventos por props.
O scaffold nasce incoming: implementação, examples, testes e revisão de acessibilidade são do autor.
Adicionar código licenciado
Fontes comerciais não são baixadas automaticamente. O código chega por --from-file, sob revisão humana de licença.
Acrescente --promote quando a revisão terminar; sem ele o item fica em staging.
Uso não é redistribuição
Tailwind Plus e outras bibliotecas comerciais nascem distribution: 'restricted'. O build do registry as exclui automaticamente (DS004). Usar em uma entrega da Up Web Studio não dá direito de redistribuir por registry.
Restrito não é isento: o código licenciado passa pelos mesmos tokens, primitives, API pública, light/dark, acessibilidade, responsividade, testes e preview que qualquer outro item.
Quando o componente já existe
Se ds:add reportar conflito, não force outro nome. Compare.
Preservação da API upstream
Normalizar um item externo preserva, por padrão, a API pública observável, o comportamento funcional e a identidade visual dele. O que pode mudar é a implementação: primitives, imports, tokens, semantic icons, acessibilidade, foco, reduced motion e infraestrutura.
base é implementação, não API
base diz qual primitive a recipe reutiliza por dentro. Ela nunca é a origem da API pública. Uma recipe cuja API virou ComponentProps<typeof Dialog> deixou de ser o componente que foi ingerido — é isso que DS_API_003 reporta.
ds:compare separa quatro dimensões: compatibilidade de API, reuso de implementação, delegação de capability e preservação visual.
Uma capability que vive no primitive base aparece como delegated(dialog), não como ausente. Não copie uma implementação ruim do upstream só para as colunas baterem: um role="button" manual não é melhor que o Dialog oficial.
Uma quebra pode acontecer — registrada
Grave "publicApi": { "preserve": false } no .ds/ingestion/<source>-<id>.json, com o motivo. É o único jeito de DS_API_001 e DS_API_003 pararem de reportar, e deixa a decisão auditável ao lado da proveniência.
Normalização obrigatória
A automação detecta e relata; ela não decide semântica. Qual token um azul representa é decisão de quem tem contexto de produto. Esta é a lista que um item precisa fechar antes de reivindicar stable.
Classificação e origem
- type, category e domain corretos
- source e licença registrados
- primitives existentes reutilizados
- API pública normalizada
Foundation
- tokens semânticos de cor, radius e sombra
- ícones semânticos do adapter
- motion semântico (duration-fast / default / slow)
- light mode
- dark mode
- responsivo
Acessibilidade
- teclado
- foco visível
- leitor de tela quando aplicável
- touch quando aplicável
- reduced motion quando aplicável
Prova
- examples
- testes unitários e de integração
- E2E quando a afirmação depende de layout
- preview
Remover um item
ds:remove tira o item e tudo que foi gerado a partir dele. Nunca tira o que ele apenas usa: primitive base, hook compartilhado, semantic icon, style capability, token da Foundation e dependência npm ficam.
Sem flag nenhuma, ds:remove não altera um único arquivo.
Regenera catalog, docs, registry e cli:sync, e depois roda ds:validate.
Consumidores bloqueiam
Se alguma coisa fora do item ainda importa o módulo, usa o id como string ou renderiza o componente, a remoção normal para e não escreve nada. --force existe para quando isso é exatamente o que se quer.
Documentação do item
A página de um item ganha Usage, Props e Composition quando elas existem — e nenhuma delas é escrita na página. npm run ds:docs deriva tudo do source e grava src/catalog/docs.generated.ts; o catálogo lê dado puro.
Props herdadas não entram na tabela. Uma prop declarada em @types/react é atributo de HTML, e listar as 285 de um <button> enterra as duas que são do design system — elas viram uma linha de “Estende”. Prop semântica que uma biblioteca de comportamento adiciona (render, onValueChange) é prop do item e é documentada.
Composition é declarada, nunca inferida
A regra não é “mais de um export”: Button exporta Button e buttonVariants e não tem composição. Ela existe quando há uma estrutura recomendada de pai e filho entre partes públicas — e aí a árvore vive na metadata.
Export utilitário (buttonVariants, TabsPrimitive) não é componente: não entra na árvore nem na tabela.
Em incoming e experimental os cinco são aviso; em stable são erro. Documentação quebrada não chega ao registry, e um acervo em normalização não trava o CI.
Particles
Um Particle é uma composição copiável de itens que já existem, construída sobre um example real, publicada em /particles. Ele mostra como primitives, recipes e os demais itens se combinam para resolver uma necessidade concreta de interface — um date picker, uma paleta de comandos, um painel de filtros.
Particle não é um type
A lista de types continua fechada em seis valores. Particle é documentação executável: não entra na taxonomia, não entra no registry como item, não tem lifecycle próprio e não se instala pela CLI. up-web-ds add continua instalando itens reais.
Como publicar um example na galeria
Uma linha na metadata do item dono do example. O código continua vivendo em *.examples.tsx — nunca uma string de JSX no .meta.ts, e nunca uma pasta paralela com cópias.
Depois, npm run ds:docs. O resto — id, uses, código, readiness — é derivado.
Tags
Tags existem só para descoberta em /particles. Elas não substituem nem restringem type, category e domain, e não entram em src/catalog/taxonomy.ts. São lowercase e kebab-case: date-picker, nunca Date Picker ou date_picker. A interface imprime o rótulo; a metadata guarda o slug. A lista de tags e a contagem de cada uma são derivadas dos Particles existentes.
Readiness
Um Particle não tem status próprio: ele é tão pronto quanto os itens que compõe. Se algum deles é deprecated, o Particle é deprecated; se todos são stable, ele é ready; caso contrário, experimental. Os dois últimos aparecem no card como aviso em texto — nunca só por cor. Nada disso é escrito na metadata, e é por isso que um item que muda de status muda a galeria na mesma passada.
Quando promover a item do catálogo
Continua Particle enquanto for só uma composição útil: um exemplo que ninguém precisa versionar, instalar ou evoluir como unidade. Vira item do catálogo quando ganha API pública própria, precisa de testes como unidade, precisa de lifecycle ou passa a ser instalado por projetos. Aí vale a taxonomia normal — primitive, recipe, component, pattern, block, template — e o docs.particles sai.
Não confunda com docs.composition: aquela árvore descreve como as partes públicas de um item se relacionam (TabsList > TabsTrigger). Particle descreve como itens distintos se combinam (Button + Popover + Input). E Particle também não é recipe: uma recipe é uma variação visual de um primitive, não uma composição.
Como os cinco anteriores: aviso em incoming e experimental, erro em stable. Um example sem código próprio continua sendo DS_DOCS_005 — o Particle apenas não é gerado, em vez de virar um segundo aviso sobre o mesmo fato.
Registrar um papel de ícone
Componente do DS não importa lucide-react: importa o papel de que precisa de @/lib/up-web-icons. O papel é o contrato — close, chevron-down, folder-open — e qual glifo o desenha é decisão do projeto que consome, em components.json → iconLibrary. Registrar um papel novo é preencher essa equivalência nos quatro providers e levá-la até quem depende dela.
O passo a passo
SemanticIconName sai daqui. Papel não declarado não compila no mapping nem em metadata.icons.
Equivalência de significado, não de nome: o Phosphor desenha um caret onde o Lucide desenha um chevron, e os dois dizem a mesma coisa.
Antes de gerar e antes de validar, não depois: a resolução lê packages/cli/src/icons/mappings.generated.ts, então pular este passo valida o mapping de ontem e o validador acusa um nome que você já corrigiu.
Reescreve src/lib/up-web-icons.tsx com os papéis que os itens declaram.
Gera o adapter de cada provider num fixture e roda o tsc contra os pacotes reais. Glifo que a biblioteca não exporta reprova aqui, e não no projeto que instalou.
O icons.json leva o papel para quem instala. É por isso que papel novo não exige CLI nova.
ds:catalog e ds:docs entram junto quando o item que usa o papel também mudou.
O adapter só exporta o papel que alguém usa
ds:icons gera exatamente os papéis declarados em metadata.icons. Enquanto nenhum item importar o papel novo, o passo 4 não escreve nada — e isso não é falha: o mapping já está válido e publicável. Quando o item passar a usá-lo, ele importa de @/lib/up-web-icons e declara icons: ['folder-open'] na metadata. Código e metadata em desacordo é DS_ICON_005, nos dois sentidos.
A validação prova que o glifo existe, não que ele significa a coisa certa
O tsc reprova um nome que a biblioteca não exporta. Um nome que existe e quer dizer outra coisa passa: IconFolderOff é “pasta desativada”, não “pasta aberta”, e compila igual. Escolher o glifo é a única parte deste fluxo que nenhum comando faz por você.
Cobertura é governada pela maturidade de quem usa o papel, não pelo mapping (requireEveryProviderFrom em .ds/icons.ts). Papel usado por item stable sem os quatro providers é erro e barra a distribuição; usado só por incoming ou experimental, é aviso; sem nenhum item usando, é informativo.
Trocar o glifo de um papel que já existe
Corrigir o glifo de um papel é o mesmo fluxo sem o passo 1: o papel já está declarado, e o que muda é a equivalência. A diferença está do outro lado — o projeto que já instalou guarda o mapping em .up-web-ds.json, como um lockfile, e essa cópia não se atualiza sozinha.
Mesma ordem do passo a passo. ds:registry é o que publica o valor novo: sem ele, o registry continua servindo o glifo antigo.
init é o refresh: ele relê o icons.json do registry e regrava os papéis que o projeto já usa com o mapping publicado hoje. É idempotente.
icons sync não busca valor novo — ele é local por definição
icons sync e icons migrate não falam com o registry: os dois regeneram o adapter a partir do que .up-web-ds.json já guarda, e é justamente por isso que funcionam offline e para papel que a CLI instalada nunca conheceu. Para um glifo que mudou de valor, o que atualiza o registro é init — ou o próximo add, que também resolve contra o registry.
Validação
npm run verify encadeia biome, typecheck, testes, ds:catalog --check, ds:docs --check, ds:validate, ds:icons:validate, registry:validate e o build da CLI. É o portão que precisa estar verde antes de concluir qualquer tarefa — npm run check continua funcionando como alias.
verify:full antes de abrir a PR; ds:registry quando o item realmente virou stable.
Fluxo até a Pull Request
A regressão completa roda na sua máquina, antes da PR. O GitHub Actions fica com as verificações leves e determinísticas, e nada é executado duas vezes entre local, PR, main e Vercel.
- Desenvolvimentolocal
npm run verifyA cada ajuste relevante. Lint, tipos, testes unitários e políticas do DS — sem build e sem browser. - Antes da PRlocal
npm run verify:fullObrigatório. Soma o build de produção e o Playwright completo: axe em todas as rotas, page-overflow e os specs de cada item. - Pull RequestGitHub Actions
npm run verifyO check Verify roda o mesmo comando do desenvolvimento. Um commit novo cancela o run anterior da mesma PR. - Merge em mainGitHubnenhum jobNada se repete: o merge só leva para a main o que o check Verify já aprovou.
- DeployVercel
npm run buildRefaz e valida o registry e gera o build de produção. Não roda testes nem instala browser.
O CI não roda Playwright — de propósito.
Enquanto o repositório for privado, de uso interno e com equipe pequena, a regressão completa é responsabilidade de quem abre a PR. Um check Verify verde não prova contraste, layout nem acessibilidade: só npm run verify:full prova.
Passo a passo
O Chromium do Playwright é instalado uma vez por máquina; a suíte não usa outro browser.
Rode quantas vezes quiser: não faz build nem abre browser.
O segundo comando roda o spec do item contra o npm run dev que já está aberto, sem esperar um build de produção.
É a etapa demorada: build de produção e toda a suíte de Playwright, em desktop e mobile. Se falhar, npm run test:e2e:report abre o relatório.
Ou pela interface do GitHub. A main recebe código só por PR, nunca por push direto.
Se falhar, rode npm run verify: é exatamente o que o CI executa. Corrija e volte ao passo 2.
Depois de ajustar uma PR aberta
Quando não dá para rodar localmente
Roda o mesmo npm run verify:full em GitHub Actions e anexa o relatório do Playwright. É manual e não bloqueia o merge. Pela interface: Actions → Full regression → Run workflow.
verify:full não roda em hook de pre-push: um git push não pode prender o trabalho por vários minutos. A validação completa é uma ação consciente antes da PR.
Publicação no registry
Catálogo e registry respondem perguntas diferentes. O preview documenta o que a equipe está construindo; o registry distribui o que um produto pode instalar.
Interface oficial de consumo
Só este CLI lê components.json → iconLibrary, instala o provider certo e gera o adapter de ícones. O registry por baixo é infraestrutura que ele consome.
Não é o caminho oficial
npx shadcn add @up-web-ds/dialog instala os arquivos sem resolver a biblioteca de ícones do projeto. O componente chega sem o adapter que ele importa — e, sem a credencial que o CLI configura, sem chegar a lugar nenhum.
O item chega como um arquivo, na pasta que o components.json do projeto declara — nunca num caminho literal, porque quem decide se @ui é src/components/ui ou app/ui é o consumidor. A taxonomia daqui não viaja: os imports entre itens são reescritos no build, e o index.ts de cada item não é publicado.
Desvio deliberado, não política de layout: o shadcn aplica --path a todos os arquivos daquele install, e os imports dentro deles continuam resolvendo pelos aliases do components.json. Um item por vez.
Onboarding de um projeto novo
A CLI vem do GitHub Packages privado; os componentes vêm do registry autenticado na Vercel. São duas credenciais diferentes, para dois serviços diferentes — o PAT do GitHub nunca é enviado para o registry.
Uma vez por máquina, com permissão mínima de leitura de packages. Nunca comite um PAT.
O pacote leva só a CLI compilada: nenhum componente viaja dentro dele.
Fica no perfil do usuário, com permissão 0600. Nunca no projeto, nunca no components.json.
Idempotente: configura registries["@up-web-ds"], registra a biblioteca de ícones e gera o adapter, sem tocar no resto do arquivo.
O token nunca entra no projeto
components.json guarda o nome da variável de ambiente, e o shadcn a expande no momento do fetch. Em CI, basta definir UP_WEB_DS_REGISTRY_TOKEN.
Novos componentes e a CLI
Componente novo não pede CLI nova. O icons.json do registry leva tudo o que a instalação precisa: o glifo de cada ícone nos quatro providers, o CSS de cada style capability, os hooks e os itens base. A CLI instalada lê isso no momento do add. A cópia que vem dentro do pacote serve só de reserva.
Quando um componente entra no DS
Quem mantém o design system faz isto uma vez por componente, depois que ele vira stable.
Obrigatório quando o item trouxe ícone ou capability nova. Nos outros casos não muda nada, e o check recusa uma cópia velha.
Reprova se o item usa ícone sem mapping nos quatro providers, capability não descrita, base não publicado ou hook ausente, e diz o que falta.
Escreve registry.json e registry/icons.json. Recusa gerar um manifest com dependência que o registry não carrega.
O merge da PR na main dispara o deploy da Vercel, que roda npm run build e refaz e valida o registry antes de servir /r/*. A CLI não é republicada.
No projeto que consome
Com a CLI que já está no projeto. Ícones, CSS e hooks do item vêm do registry. O .up-web-ds.json passa a guardar o mapping dos ícones usados, e icons sync e icons migrate continuam funcionando offline.
Quando atualizar a CLI
Publicar uma versão da CLI
Publicar é uma decisão explícita: nenhum merge publica a CLI sozinho.
minor para componente ou comando novo, patch para correção (SemVer). Não cria commit nem tag, e a versão fica declarada só em packages/cli/package.json.
Typecheck, build, fronteira do pacote e npm pack --dry-run.
Depois do merge na main. Pela interface: Actions → Publish Consumer CLI → Run workflow. Com -f dry_run=true, roda todos os gates sem publicar.
Atualizar a CLI em um projeto
Idempotente. Grava no .up-web-ds.json o mapping atual dos ícones que o projeto já usa, e depois é só voltar ao add.