Aparência

Listas e navegação

As peças com que um app organiza o que mostra: a lista e o par chave e valor, a barra de ferramentas, a gaveta de navegação, a busca, o detalhe e o tooltip. Todas em tuff.css; a gaveta é a única que precisa de tuff.js.

Lista

.tuff-lista é uma coluna que rola; cada .tuff-item é uma linha, com role="option" e aria-selected quando a lista é uma escolha. A seleção usa --ds-sel, o mesmo realce do gerenciador de arquivos do ambiente: um destaque próprio aqui faria a lista de um app se comportar diferente da lista ao lado, no mesmo desktop. Um nome comprido trunca com reticências em vez de quebrar a linha.

relatorio-2026.pdf
medicoes.parquet
notas.md
um-nome-bem-comprido-que-nao-cabe-na-coluna-e-por-isso-trunca.tar.gz
<div class="tuff-lista" role="listbox" aria-label="Arquivos">
  <div class="tuff-item" role="option" aria-selected="false"><svg class="tuff-ico tuff-ico--sm"><use href="#ico-file"></use></svg>relatorio-2026.pdf</div>
  <div class="tuff-item" role="option" aria-selected="true"><svg class="tuff-ico tuff-ico--sm"><use href="#ico-file"></use></svg>medicoes.parquet</div>
  <div class="tuff-item" role="option" aria-selected="false"><svg class="tuff-ico tuff-ico--sm"><use href="#ico-file"></use></svg>notas.md</div>
  <div class="tuff-item" role="option" aria-selected="false"><svg class="tuff-ico tuff-ico--sm"><use href="#ico-file"></use></svg>um-nome-bem-comprido-que-nao-cabe-na-coluna-e-por-isso-trunca.tar.gz</div>
</div>

.tuff-item é linha de lista. Um rótulo solto ao lado de um ícone, fora de uma lista, é um span comum; usar a linha para isso ensinaria o contrário.

Chave e valor

O par de leitura, com a hierarquia obrigatória: a chave em --ds-text-dim, o valor em --ds-text. A coluna do valor é monoespaçada com --mono quando o conteúdo é dado (caminho, id, hash): texto proporcional embaralha a leitura de coisa que se compara olhando.

caminho
/home/pesquisador/dados
socket
/run/user/1000/vssh-app.sock
versão
5.0.12
<dl class="tuff-kv tuff-kv--mono">
  <dt>caminho</dt><dd>/home/pesquisador/dados</dd>
  <dt>socket</dt><dd>/run/user/1000/vssh-app.sock</dd>
  <dt>versão</dt><dd>5.0.12</dd>
</dl>

Barra de ferramentas

Uma fileira com divisa embaixo: blocos que mudam juntos, separados por linha. Dentro dela o botão de ícone perde a moldura, e .tuff-barra-espaco empurra o que vem depois para a direita.

4 itens
<div class="tuff-barra">
  <button class="tuff-btn tuff-btn--icone" aria-label="Voltar"><svg class="tuff-ico"><use href="#ico-arrow-left"></use></svg></button>
  <button class="tuff-btn tuff-btn--icone" aria-label="Recarregar"><svg class="tuff-ico"><use href="#ico-refresh"></use></svg></button>
  <span class="tuff-sec-desc">4 itens</span>
  <span class="tuff-barra-espaco"></span>
  <button class="tuff-btn">novo</button>
</div>

A variante --fixa prende a barra no topo enquanto o conteúdo rola por baixo, com fundo opaco: sob uma barra translúcida passa texto rolando. Ela só encosta nas bordas se o contêiner de rolagem não tiver padding, e é por isso que o corpo da gaveta, abaixo, não tem.

A gaveta de navegação

Lateral com as seções, mais o corpo. Um app de lista e detalhe, com um painel ao lado do conteúdo, usa a moldura, que leva a mesma lateral e a transforma em gaveta na tela estreita. Ela é a lateral da janela de Configurações do ambiente, com a mesma medida (208px) e o mesmo realce do item ativo, e é o que faz a lateral de um app parecer a lateral do ambiente. A lateral é persistente: é ela que responde "onde eu estou". Um menu que fecha depois de escolher devolve a pessoa para uma tela sem pista de onde ela está.

Fotos
O corpo rola; a lateral fica parada.
<div class="tuff-gaveta" style="height:260px">
  <div class="tuff-gaveta-veu"></div>
  <nav class="tuff-gaveta-lateral" aria-label="Seções">
    <div class="tuff-gaveta-cap">Biblioteca</div>
    <button class="tuff-gaveta-item tuff-gaveta-item--ativo" data-alvo="ex-fotos"><svg class="tuff-ico"><use href="#ico-image"></use></svg><span>Fotos</span></button>
    <button class="tuff-gaveta-item" data-alvo="ex-videos"><svg class="tuff-ico"><use href="#ico-video"></use></svg><span>Vídeos</span></button>
    <div class="tuff-gaveta-cap">App</div>
    <button class="tuff-gaveta-item" data-alvo="ex-config"><svg class="tuff-ico"><use href="#ico-settings"></use></svg><span>Configurações</span></button>
  </nav>
  <div class="tuff-gaveta-corpo">
    <div class="tuff-barra tuff-barra--fixa">
      <button class="tuff-btn tuff-btn--icone tuff-gaveta-abrir" data-tuff-gaveta-abrir aria-label="Seções"><svg class="tuff-ico"><use href="#ico-menu"></use></svg></button>
      <strong>Fotos</strong>
    </div>
    <div class="tuff-gaveta-conteudo">
      <section id="ex-fotos"><div class="tuff-sec-desc">O corpo rola; a lateral fica parada.</div></section>
    </div>
  </div>
</div>

O que cada parte faz:

classepapel
.tuff-gavetaa raiz: um flex, e um contêiner (container-type: inline-size)
.tuff-gaveta-laterala coluna de 208px com as seções; .tuff-gaveta-cap é a legenda de família, .tuff-gaveta-item cada entrada, --ativo a corrente
.tuff-gaveta-corpoo que rola, sem padding, para uma barra fixa poder encostar nas bordas
.tuff-gaveta-conteudoo respiro do conteúdo, separado do contêiner de rolagem
.tuff-gaveta-veuo véu que cobre o corpo quando a gaveta abre estreita
.tuff-gaveta-abriro botão que abre; some quando a lateral é persistente, porque um controle que não faz nada ensina a não confiar em controle nenhum

Estreita (o contêiner com menos de 640px), a lateral vira gaveta: position: absolute, nunca fixed, porque um app é um iframe dentro de uma janela do ambiente, e com absolute ela pertence ao contêiner. O ponto de virada é @container, e não @media: o que decide é a largura do contêiner, e um app pode ter a gaveta dentro de um painel de 400px numa tela de 4K. Fechada, a lateral fica visibility: hidden e sai da ordem de tabulação, para quem navega por teclado não cair dentro de um menu que não está na tela.

O comportamento vem de tuff.js:

const gaveta = TuffGaveta.ligar(document.querySelector('.tuff-gaveta'));
// gaveta.abrir(), gaveta.fechar(), gaveta.destruir()

ligar faz o que CSS não faz: devolve o foco para quem abriu, fecha no Esc e no véu, marca o item da seção visível enquanto a página rola (cada item aponta a seção pelo data-alvo), e fecha a gaveta depois de um clique só quando ela está em modo gaveta; persistente, ela responde "onde eu estou", e fechá-la apagaria a resposta no momento em que a pessoa acabou de perguntar.

Busca

Um input type="search" com a lupa embutida. A cruz nativa do WebKit sai: ela é branca, quadrada e não tem nada a ver com o resto.

<input class="tuff-busca" type="search" placeholder="filtrar…" aria-label="Filtrar">

Detalhe

Um details nativo: abre sem JavaScript, é encontrável pelo Ctrl+F do navegador e já anuncia o estado a um leitor de tela. A seta padrão sai e volta como pseudo-elemento, para girar.

Por que o app não vê meus arquivos
O backend roda como você no servidor, com acesso POSIX. O seletor existe para você dizer qual pasta, e a permissão já havia.
Aberto de início
Com o atributo open no details.
<details class="tuff-detalhe">
  <summary>Por que o app não vê meus arquivos</summary>
  <div class="tuff-detalhe-corpo">
    O backend roda como você no servidor, com acesso POSIX. O seletor existe para você dizer qual
    pasta, e a permissão já havia.
  </div>
</details>
<details class="tuff-detalhe" open>
  <summary>Aberto de início</summary>
  <div class="tuff-detalhe-corpo">Com o atributo <code>open</code> no <code>details</code>.</div>
</details>

Tooltip

O title nativo é desenhado pelo sistema: no Windows, uma caixinha clara com fonte do sistema, no meio de uma janela escura. Este é desenhado pela folha, a partir de data-tuff-dica, e aparece sob o ponteiro e com o foco do teclado.

python3-pip
<span class="tuff-tag" data-tuff-dica="declarado em requiredPackages" tabindex="0">python3-pip</span>
<button class="tuff-btn tuff-btn--icone" data-tuff-dica="Recarregar a lista" aria-label="Recarregar"><svg class="tuff-ico"><use href="#ico-refresh"></use></svg></button>

O elemento precisa de um rótulo acessível próprio (aria-label, ou texto visível): um tooltip em CSS puro não é anunciado por leitor de tela, e usá-lo como única fonte da informação a esconderia de quem mais precisa dela. Ele é reforço, nunca o único caminho.