Bem, fui tentar colocar algumas coisas de navegação no Computaria (vai aparecer em breve). Pedi umas opiniões no BlueSky, comecei a rascunhar alguma coisa, mandei um print do que eu tinha rascunhado. Então pediram para testar. E é claro que eu precisava resolver isso.

Elementos beta

Meu primeiro pensamento foi: preciso adicionar elementos que seriam visíveis apenas para quem quer usar as funcionalidades beta. Então, comecei pela coisa beta mais básica possível: invisível a quem não é beta.

O mais elaborado seria nem renderizar o HTML, estilo Post facilmente citável, em que o componente é inserido/removido dinamicamente, mas como aqui não era apenas potencialmente um único simples componente, achei que seria melhor sempre ter o HTML disponível e esconder.

Então, fiz isso através de pequenas classes CSS.

Peguei exemplo do https://mademistakes.com/notes/diy-record-cube-back-spacers/, em que para navegar para trás ele colocou um pseudo-elemento ::before com a caption , e de modo similar para navegar para frente com o pseudo-elemento ::after e a caption .

E isso precisa ficar no post, então coloquei no layout de post: assim, toda alteração fica imediatamente visível em todos os posts.

Ok, vamos ver como ficou o experimento, ainda antes de colocar a questão do beta:

<style>
  .nav-next::after {
    content: "→";
  }
  .nav-prev::before {
    content: "←";
  }
  .nav-next {
    flex-basis: 50%;
    justify-content: end;
    text-align: end;
    display: flex;
    gap: .2em;
  }
  .nav-prev {
    flex-basis: 50%;
    justify-content: start;
    text-align: start;
    display: flex;
    gap: .2em;
  }
  .nav-link:visited {
    color: #111;
  }
  .nav-link:link {
    color: #565656;
  }
  .jeff-nav {
    display: flex;
    flex-basis: 100%;
    gap: 2em;
  }
</style>
<nav class="jeff-nav">
    {% if page.previous %}
      <div class="nav-prev"><a class="nav-link" href="{{ page.previous.url | prepend: site.baseurl }}">{{ page.previous.title }}</a></div>
    {% endif %}
    {% if page.next %}
      <div class="nav-next"><a class="nav-link" href="{{ page.next.url | prepend: site.baseurl }}">{{ page.next.title }}</a></div>
    {% endif %}
</nav>

E ficou assim para um post:

Mostrando o trecho da navegação

A parte Liquid pergunta se tem elemento antes/depois. Afinal, se não tiver algo para fazer {{lalala.url}}, a expansão vai virar silenciosamente nada, e eu não quero uma seta para o nada. Justamente para evitar isso que coloquei o condicional na expansão.

Agora, isso ficaria visível sempre. E não é essa a intenção. Vamos marcar para esconder isso!

 <style>
   .nav-next::after {
     content: "→";
   }
   .nav-prev::before {
     content: "←";
   }
   .nav-next {
     flex-basis: 50%;
     justify-content: end;
     text-align: end;
     display: flex;
     gap: .2em;
   }
   .nav-prev {
     flex-basis: 50%;
     justify-content: start;
     text-align: start;
     display: flex;
     gap: .2em;
   }
   .nav-link:visited {
     color: #111;
   }
   .nav-link:link {
     color: #565656;
   }
   .jeff-nav {
     display: flex;
     flex-basis: 100%;
     gap: 2em;
   }
+
+  .beta-hidden[data-beta="hidden"] {
+    display: none
+  }
 </style>
-<nav class="jeff-nav">
+<nav class="jeff-nav beta-hidden beta" data-beta="hidden">
     {% if page.previous %}
-      <div class="nav-prev"><a class="nav-link" href="{{ page.previous.url | prepend: site.baseurl }}">{{ page.previous.title }}</a></div>
+      <div class="nav-prev"><a class="nav-link" href="{{ page.previous.url | prepend: site.baseurl }}">{{ page.previous.title }}</a></div>
     {% endif %}
     {% if page.next %}
-      <div class="nav-next"><a class="nav-link" href="{{ page.next.url | prepend: site.baseurl }}">{{ page.next.title }}</a></div>
+      <div class="nav-next"><a class="nav-link" href="{{ page.next.url | prepend: site.baseurl }}">{{ page.next.title }}</a></div>
     {% endif %}
 </nav>

A classe beta coloquei como marcação. Por convenção, os elementos beta são marcados com beta ou beta-*.

Já a classe beta-hidden tem efeito no componente, usado para marcação visual. Note o seletor dele, que foi adicionado:

.beta-hidden[data-beta="hidden"] {
  display: none
}

A parte .beta-hidden simplesmente significa “elemento da classe beta-hidden”. Mas ele não vem sozinho, ele vem com algo entre colchetes: [data-beta="hidden"]. Essa parte entre colchetes permite que o seletor CSS só seja aplicado caso um outro atributo do elemento HTML tenha um valor específico.

Por exemplo, abaixo vou colocar uma seta para a direita e um botão. O botão vai alterar o atributo data-rotation, tal que o novo data-rotation seja o incremento módulo 4 do anterior. E no CSS vou fazer uma marcação para isso:

.rotation[data-rotation="1"] {
    transform: rotate(90deg);
}

.rotation[data-rotation="2"] {
    transform: rotate(180deg);
}

.rotation[data-rotation="3"] {
    transform: rotate(270deg);
}

.rotation {
    width: fit-content;
}

Ou então se quiser rodar para o outro lado:

A transformação em si é aplicada pelo CSS, o JS se preocupa apenas com a questão de povoar logicamente o valor correto no HTML:

function rotate(elementId, delta) {
    if (delta < 0) {
        delta += 4
    }
    const elementRotate = document.getElementById(elementId)

    const rotate = Number(elementRotate.dataset.rotation)
    const newRotate = (rotate + delta)%4
    elementRotate.dataset.rotation = newRotate
}

E para curiosidade, fiz assim para o botão/seta funcionarem:

<div class="rotation" id="seta-1" data-rotation="0"></div>
<button onclick="rotate('seta-1', +1)">RODAR</button>

Mais um ponto de curiosidade, o .rotate { width: fit-content; } foi colocado porque naturalmente a div ocupa um bloco horizontal inteiro. E ao fazer a transformação de rotacionar a div, sem essa limitação do comprimento, ele rotacionava o bloco inteiro, o que causou confusão na primeira vez que eu coloquei isso como exemplo. Para evitar que uma grande parte de espaço vazio fosse rotacionado, coloquei esse limitante para que a div tivesse apenas o tamanho necessário para caber o seu conteúdo.

Seletor com base em atributo foi também usado no artigo Carrossel em markdown no GitHub, mas lá foi mais uma side-quest para depuração do que algo propriamente dito do artigo. Aqui o seletor com base em atributo é essencial.

Agora, como ativar a funcionalidade beta? Bem, temos aqui que o elemento beta está necessariamente escondido por conta do seletor .beta-hidden[data-beta="hidden"]. Em um primeiro momento vamos ignorar a questão de como identificar que a pessoa entrou para ver beta e manipular apenas isso? Da visualização?

Pois bem, abaixo temos um botão, e abaixo do botão um emoji de foguete.

🚀

Se você acessou normalmente, a renderização inicial da página não deve estar exibindo o foguete. Já se acessou com o beta ligado, deve estar vendo. O botão acima vai ligar/desligar a funcionalidade beta, emulando o comportamento de acessar como beta.

Aqui como ficou o source code do foguete e do botão acima:

<script>
function toggleFoguete() {
    const foguete = document.getElementById("foguete")
    const attrAtual = foguete.dataset.beta;
    
    const attrNovo = attrAtual == "hidden"? "true": "hidden"

    foguete.dataset.beta = attrNovo;
}
</script>

<button onclick="toggleFoguete()">FOGUETE!!!</button>

<p class="beta-hidden beta" data-beta="hidden" id="foguete">
🚀
</p>

O acesso de modo geral é isso, então como fazer para pegar todos os elementos com a classe de marcação beta e alterar o atributo data-beta? Simples, document.getElementsByClassName("beta")! Isso nos dá um iterável, agora basta colocar para rodar e alterar o atributo:

function showBeta() {
    const betaElements = document.getElementsByClassName("beta")
    for (const betaElement of betaElements) {
        betaElement.dataset.beta = true"
    }
}

Como não tenho acesso a controlar renderização no server-side, já que o Computaria é SSG, preciso dar um jeito de identificar isso apenas no front-end. Como eu quero compartilhar isso via link, preciso de algo no link que indique se é beta ou não. O fragmento poderia ser usado para esse fim? Definitivamente, mas o fragmento tem outro papel na renderização do browser, que é a navegação para o lugar correto.

Para passar adiante a opção de beta, preferi colocar no link no query param indicando que é uma funcionalidade beta. No Post facilmente citável foi colocado como localStorage para habilitar a funcionalidade específica, mas o localStorage exige uma intervenção maior do que o simples clicar em um link para ativar a visualização beta, portanto não atinge o objetivo de ter um compartilhamento beta.

Além disso, eu preciso esperar o carregamento da página. E sabe como eu espero o carregamento da página para pegar todos os elementos? Usando <script defer>! Mais explicações nesse post: Deixando a pipeline visível para acompanhar deploy do blog.

E com esse defer nasceu um novo script a ser carregado, meta.js!

Ele será disparado apenas quando houver a carga da página. Aqui estou assumindo que a página sofrerá uma carga completa, se por acaso eu alterar o Computaria e isso não for mais verdade, deixar uma carga incremental que só puxa o resto da página sob demanda, preciso revisitar o como eu faço para habilitar os componentes beta.

Como o script vai ser disparado na carga, preciso colocar nele a função ao ser chamada. Então, dentro da função, eu verifica se ele precisa liberar a funcionalidade beta e, necessitando, rodar o que eu preciso para deixar as coisas betas visíveis:

function enableBeta() {
    const queryParams = new URLSearchParams(window.location.search.substring(1))
    if (queryParams.get("beta") === "true") {
        showBeta();
    }
}

function showBeta() {
    const betaElements = document.getElementsByClassName("beta")
    for (const betaElement of betaElements) {
        betaElement.dataset.beta = true
    }
}

enableBeta()

No caso, o mecanismo escolhido foi o query param, com ?beta=true. Para lidar com a complexidade de serializar e desserializar os query params, usei a função nativa do JS new URLSearchParams(...), tal qual fiz em Deixando a pipeline visível para acompanhar deploy do blog.

Links

Mas… a estratégia anterior não manteria a pessoa no estado “visualizando o beta”. Na primeira navegação feita, ele iria sair do beta. Então coloquei para ver beta nos links mais óbvios primeiro: os links de navegação.

Para fazer a carga dinâmica dos links beta, devo admitir que fui preguiçoso, e que por hora funciona. Primeiro, identifiquei os links de navegação com a classe de marcação beta-link:

 <nav class="jeff-nav beta-hidden beta" data-beta="hidden">
     {% if page.previous %}
-      <div class="nav-prev"><a class="nav-link" href="{{ page.previous.url | prepend: site.baseurl }}">{{ page.previous.title }}</a></div>
+      <div class="nav-prev"><a class="nav-link beta-link" href="{{ page.previous.url | prepend: site.baseurl }}">{{ page.previous.title }}</a></div>
     {% endif %}
     {% if page.next %}
-      <div class="nav-next"><a class="nav-link" href="{{ page.next.url | prepend: site.baseurl }}">{{ page.next.title }}</a></div>
+      <div class="nav-next"><a class="nav-link beta-link" href="{{ page.next.url | prepend: site.baseurl }}">{{ page.next.title }}</a></div>
     {% endif %}
 </nav>

Então, posso simplesmente iterar por todos os elementos com essa classe e colocar a query beta neles. No caso, estou assumindo que a query beta seja a mesma da página atual, mesmo que isso não esteja certo é um atalho prático. Para tal, coloquei nas ações para disparar quando identificado que precisamos habiltiar as funcionalidade beta um disparo relativo aos links:

function enableBeta() {
    const queryParams = new URLSearchParams(window.location.search.substring(1))
    if (queryParams.get("beta") === "true") {
        showBeta();
        // adicionado abaixo para lidar com os beta-links
        changeBetaLink()
    }
}

function changeBetaLink() {
    const betaElements = document.getElementsByClassName("beta-link")
    for (const betaElement of betaElements) {
        if (betaElement.hasAttribute("href")) {
            betaElement.href += window.location.search
        }
    }
}

Mas isso resolvia apenas a parte de navegação dentro do post. No momento que fosse retornar ao topo e clicasse no link do header do Computaria, já era. Então… preciso alterar no próprio default.html para carregar o meta.js.

Além disso, componentes como main-link, os links básicos de navegação no header.html e os links de navegação para tags no tags.html precisaram ser marcados também.

Páginas de rascunho

Testando a questão da navegação, lembrei de que eu possuo Rascunhos publicados em Jekyll. Os rascunhos em tese não deveriam ser acessíveis via indicação de navegação, apenas quando compartilhado o link direto.

Então… como ficamos em relação a inclusão do previous e next? Bem, esses links não levam em consideração isso de que o elemento é um rascunho. Então eu preciso iterar até encontrar um elemento não rascunho (ou chegar ao fim, seja lá de qual lado).

Mas Liquid não permite que eu itere… mas ele tem include! E será que eu posso fazer inclusão recursiva? Um mesmo elemento se incluir? A priori, não vejo motivo para isso não ocorrer.

Vamos primeiro separar o componente corretamente:

 <nav class="jeff-nav beta-hidden beta" data-beta="hidden">
     {% if page.previous %}
-      <div class="nav-prev"><a class="nav-link beta-link" href="{{ page.previous.url | prepend: site.baseurl }}">{{ page.previous.title }}</a></div>
+      {% include component/prev-link.html post=page.previous %}
     {% endif %}
     {% if page.next %}
-      <div class="nav-next"><a class="nav-link beta-link" href="{{ page.next.url | prepend: site.baseurl }}">{{ page.next.title }}</a></div>
+      {% include component/next-link.html post=page.next %}
     {% endif %}
 </nav>

Aqui, usei o mesmo mecanismo do main-link introduzido no Posts patrocinados para passar objeto complexo para baixo. E assim ficou o primeiro rascinho do componente prev-link.md:

{% assign post=include.post %}
<div class="nav-prev"><a class="nav-link beta-link" href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a></div>

Muito bem, eu só isolei o problema. Agora, vamos fazer o salto recursivo? Primeiramente, só exibo o conteúdo se não for rascunho:

{% assign post=include.post %}
{% unless post.draft == "true" %}
    <div class="nav-prev"><a class="nav-link beta-link" href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a></div>
{% endunless %}

Muito bem, já tenho meu caso baso. Agora, para a chamada recursiva, caso o de cima não seja verdade:

{% assign post=include.post %}
{% unless post.draft == "true" %}
    <div class="nav-prev"><a class="nav-link beta-link" href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a></div>
{% else %}
    {% include component/prev-link.html post=post.previous %}
{% endunless %}

Mas… isso não leva em consideração os casos extremos, de começo e fim. Eu só estou chamando o componente se o previous/next deles existirem. Então posso adicionar isso como condição!

O Liquid me permite chamar usar elsif tal qual Ruby, no lugar de abrir um novo if dentro do else. O código fica mais expressivo dessa forma:

{% assign post=include.post %}
{% unless post.draft == "true" %}
    <div class="nav-prev"><a class="nav-link beta-link" href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a></div>
{% elsif post.previous %}
    {% include component/prev-link.html post=post.previous %}
{% endunless %}

E assim conseguimos fazer a magia. A chamada recursiva eventualmente chega no final, seja porquê não há mais posts seguintes ou porque ele achou um post que não seja marcado como draft.

Removendo beta

Depois de algumas idas e vindas e testes, o Camilo Micheletto ajustou o CSS e também adicionou um botão de “back to top”. Com isso estou julgando que não tem mais nada a testar com beta. Assim, por hora, vou colocar a navegação como algo definitivo.

Para isso, nada como colocar o estilo no lugar correto. Criei um novo lugar só para registrar as coisas de navegação, o _navigation.scss e adicionei nos módulos usados por main.scss.

No componente em si, basta remover as menções de que aquilo é um elemento beta, como as classes beta e beta-hidden (não beta-link, o contexto de beta-link é de gerar links que também apontem para beta habilitado).

Ligeiro ajuste no SASS

No último ajuste feito no SASS, Atualizando o SASS do Computaria, feito para atualizar a versão da lang usada, fiz uma mudança para lidar com os ajustes de brilho:

$grey-color-light-raw: color.adjust($grey-color, $lightness: 40%);
$grey-color-light: color.change($grey-color-light-raw,
    $red: math.round(color.channel($grey-color-light-raw, "red")),
    $green: math.round(color.channel($grey-color-light-raw, "green")),
    $blue: math.round(color.channel($grey-color-light-raw, "blue"))
);

Eu fiz isso para ter números inteiros, no lugar de um CSS com rgb(r, g, b) com pontos decimais. Para evitar ter esse retrabalho toda vida, criei uma função para lidar com isso.

Em SASS, para criar a função você usa o comando @function, e para retornar você usa o comando @return dentro da @function. Os parâmetros são nomeados e posicionais. Usei isso para usar a variação de lightness usada. Então, no lugar do esquema acima, o uso que eu espero é algo assim:

$grey-color-light: adjust-lightness-and-round($grey-color, $lightness-delta: 40%);

Portanto, a evolução foi essa:

-$grey-color-light-raw: color.adjust($grey-color, $lightness: 40%);
-$grey-color-light: color.change($grey-color-light-raw,
-    $red: math.round(color.channel($grey-color-light-raw, "red")),
-    $green: math.round(color.channel($grey-color-light-raw, "green")),
-    $blue: math.round(color.channel($grey-color-light-raw, "blue"))
-);
+$grey-color-light: adjust-lightness-and-round($grey-color, $lightness-delta: 40%);

E a função? Bem, ela continua funcionando com o mesmo pensamento: cria a cor “crua” e então seleciona os canais de cores independentemente para vermelho/verde/azul:

@function adjust-lightness-and-round($color, $lightness-delta) {
    $color-adjust-raw: color.adjust($color, $lightness: $lightness-delta);
    @return color.change($color-adjust-raw,
        $red: math.round(color.channel($color-adjust-raw, "red")),
        $green: math.round(color.channel($color-adjust-raw, "green")),
        $blue: math.round(color.channel($color-adjust-raw, "blue"))
    );
}

Para usar fora do _common.scss, como em _base.scss, basta colocar o nome do módulo usada no @use. Como o comum é usar @use "common" as c;, basta chamar a função c.adjust-lightness-and-round.

Esse ajuste foi feito para tags <a> visitadas, para não ter mais números decimais na cor:

 a {
     //...
     &:visited {
-        color: color.adjust(c.$brand-color, $lightness: -15%);
+        color: c.adjust-lightness-and-round(c.$brand-color, $lightness-delta: -15%);
     }
     //...
 }