Funcionalidade beta no Computaria
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:
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%);
}
//...
}