[{"data":1,"prerenderedAt":2135},["ShallowReactive",2],{"project:\u002Ffui-content:pt-br":3,"project:prev:\u002Ffui-content:pt-br":2129,"project:next:\u002Ffui-content:pt-br":2132},{"id":4,"title":5,"body":6,"category":2097,"company":2100,"cover":2101,"date":2102,"description":2103,"extension":2104,"featured":401,"image":2105,"isPublic":401,"links":2106,"meta":2110,"navigation":401,"order":909,"path":2111,"period":2112,"role":2113,"seo":2116,"stack":2117,"status":2120,"stem":2121,"tags":2122,"__hash__":2128},"projects_ptBR\u002Fprojects\u002Ffui-content.md","fui-content",{"type":7,"value":8,"toc":2075},"minimark",[9,30,33,38,175,202,206,217,265,268,331,357,361,374,466,519,522,572,576,587,590,962,980,1165,1172,1176,1191,1194,1262,1289,1319,1331,1340,1347,1351,1365,1376,1381,1392,1408,1441,1445,1451,1461,1465,1468,1478,1482,1496,1506,1522,1526,1533,1540,1551,1554,1617,1620,1624,1697,1701,1704,1776,1816,1819,1836,1871,1877,1905,1908,2019,2032,2036,2039,2042,2046,2064,2071],[10,11,12,13,17,18,21,22,24,25,29],"p",{},"Todo o resto neste portfólio — cada ",[14,15,16],"code",{},"fui-btn",", ",[14,19,20],{},"fui-card",", cada troca de tema no seletor de morfismo lá em cima — vem daqui. ",[14,23,5],{}," é o design system SCSS que comecei a construir em abril de 2026: um monorepo com tokens, utilitários e cinco morfismos visuais completos, publicado como pacotes independentes no npm. Este projeto é, ao mesmo tempo, o mais técnico e o mais pessoal do portfólio — não tem cliente, não tem prazo de agência, só a pergunta que me fez começar: dá pra ter cinco identidades visuais completamente diferentes — vidro, argila, plástico extrudado, hardware físico, flat puro — trocando ",[26,27,28],"strong",{},"um único import",", sem duplicar HTML, sem framework acoplado, e sem sacrificar contraste WCAG em nenhuma delas?",[10,31,32],{},"A resposta, depois de mais de 170 commits, é sim. E o processo de chegar lá ensinou mais sobre CSS moderno do que qualquer projeto de cliente que já toquei.",[34,35,37],"h2",{"id":36},"por-baixo-do-capo","Por baixo do capô",[39,40,41,54],"table",{},[42,43,44],"thead",{},[45,46,47,51],"tr",{},[48,49,50],"th",{},"Camada",[48,52,53],{},"Tecnologia",[55,56,57,81,94,107,120,138,151,162],"tbody",{},[45,58,59,63],{},[60,61,62],"td",{},"Linguagem",[60,64,65,73,74,17,77,80],{},[66,67,72],"a",{"href":68,"rel":69,"target":71},"https:\u002F\u002Fsass-lang.com\u002Fdocumentation\u002F",[70],"nofollow","_blank","SCSS\u002FSass"," — ",[14,75,76],{},"@use",[14,78,79],{},"@forward",", funções e mixins nativos",[45,82,83,86],{},[60,84,85],{},"Monorepo",[60,87,88,93],{},[66,89,92],{"href":90,"rel":91,"target":71},"https:\u002F\u002Fdocs.npmjs.com\u002Fcli\u002Fv10\u002Fusing-npm\u002Fworkspaces",[70],"npm workspaces"," — 5 pacotes publicados independentemente",[45,95,96,99],{},[60,97,98],{},"Distribuição",[60,100,101,106],{},[66,102,105],{"href":103,"rel":104,"target":71},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fpackages",[70],"GitHub Packages"," — registry npm privado por pacote",[45,108,109,112],{},[60,110,111],{},"Testes",[60,113,114,119],{},[66,115,118],{"href":116,"rel":117,"target":71},"https:\u002F\u002Fwww.oddbird.net\u002Ftrue\u002F",[70],"sass-true"," via Jest — 307 testes, incluindo contraste WCAG calculado",[45,121,122,125],{},[60,123,124],{},"Qualidade",[60,126,127,132,133],{},[66,128,131],{"href":129,"rel":130,"target":71},"https:\u002F\u002Fstylelint.io\u002F",[70],"Stylelint"," + ",[66,134,137],{"href":135,"rel":136,"target":71},"https:\u002F\u002Fprettier.io\u002F",[70],"Prettier",[45,139,140,143],{},[60,141,142],{},"CI\u002FCD",[60,144,145,150],{},[66,146,149],{"href":147,"rel":148,"target":71},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions",[70],"GitHub Actions"," — build\u002Flint\u002Ftest em todo push, publish por tag",[45,152,153,156],{},[60,154,155],{},"Preview local",[60,157,158,159],{},"Docker + Traefik, servido em ",[14,160,161],{},"fui.localhost",[45,163,164,167],{},[60,165,166],{},"Commits",[60,168,169,174],{},[66,170,173],{"href":171,"rel":172,"target":71},"https:\u002F\u002Fwww.conventionalcommits.org\u002F",[70],"Conventional Commits"," via Husky + commitlint",[10,176,177,178,181,182,185,186,189,190,193,194,17,196,17,198,201],{},"A ideia central é simples de enunciar e difícil de sustentar: ",[26,179,180],{},"um tema = um morfismo",". Nenhum componente carrega classe de morfismo (",[14,183,184],{},".fui-glass .fui-btn"," não existe) — instalar ",[14,187,188],{},"@allangboliveira\u002Fui-theme-glass"," e trocar ",[14,191,192],{},"data-theme"," já é suficiente para todo o vocabulário de componentes (",[14,195,16],{},[14,197,20],{},[14,199,200],{},"fui-input","...) virar vidro. Trocar de morfismo em produção significa trocar de pacote, não reescrever HTML.",[34,203,205],{"id":204},"seis-camadas-uma-ordem-que-nao-pode-quebrar","Seis camadas, uma ordem que não pode quebrar",[10,207,208,209,216],{},"O CSS inteiro do sistema é organizado em ",[66,210,213],{"href":211,"rel":212,"target":71},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@layer",[70],[14,214,215],{},"@layer",", declarados uma única vez, logo no primeiro arquivo que qualquer consumidor carrega:",[218,219,224],"pre",{"className":220,"code":221,"language":222,"meta":223,"style":223},"language-scss shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F\u002F packages\u002Fbase\u002Flayout\u002F_grid.scss\n@layer fui.layout, fui.base, fui.components, fui.utilities;\n","scss","",[14,225,226,235],{"__ignoreMap":223},[227,228,231],"span",{"class":229,"line":230},"line",1,[227,232,234],{"class":233},"sLBg1","\u002F\u002F packages\u002Fbase\u002Flayout\u002F_grid.scss\n",[227,236,238,242,246,249,252,254,257,259,262],{"class":229,"line":237},2,[227,239,241],{"class":240},"suds8","@layer fui",[227,243,245],{"class":244},"s-5SL",".layout",[227,247,248],{"class":240},", fui",[227,250,251],{"class":244},".base",[227,253,248],{"class":240},[227,255,256],{"class":244},".components",[227,258,248],{"class":240},[227,260,261],{"class":244},".utilities",[227,263,264],{"class":240},";\n",[10,266,267],{},"A ordem entre camadas é a regra mais importante do projeto inteiro — e a que mais fácil se quebra sem querer:",[269,270,271,278,294,300,313,319],"ol",{},[272,273,274,277],"li",{},[26,275,276],{},"tokens"," — variáveis SCSS de compile-time (paleta, espaçamento, tipografia)",[272,279,280,283,284,17,287,17,290,293],{},[26,281,282],{},"utilities"," — classes atômicas (",[14,285,286],{},"fui-bg-primary",[14,288,289],{},"fui-p-4",[14,291,292],{},"fui-shadow-md",")",[272,295,296,299],{},[26,297,298],{},"layout"," — grid, breakpoints, container",[272,301,302,305,306,309,310,293],{},[26,303,304],{},"themes"," — geração dos temas (",[14,307,308],{},"[data-theme=\"light\"]"," \u002F ",[14,311,312],{},"[data-theme=\"dark\"]",[272,314,315,318],{},[26,316,317],{},"base"," — reset e estilos de elemento HTML puro",[272,320,321,73,324,17,326,17,328,330],{},[26,322,323],{},"components",[14,325,16],{},[14,327,20],{},[14,329,200],{},"...",[10,332,333,335,336,340,341,343,344,347,348,350,351,356],{},[14,334,215],{}," resolve em ordem de ",[337,338,339],"em",{},"declaração",", não de especificidade — por isso essa lista, uma vez fixada, é contrato: se um pacote de tema declarasse sua própria ordem de ",[14,342,215],{}," diferente da do base, o browser resolveria a cascata errado dependendo de qual ",[14,345,346],{},"\u003Clink>"," o parser encontrasse primeiro. (Essa exata categoria de bug — ordem de ",[14,349,215],{}," não fixada de forma determinística — é a mesma classe de problema que resolvi no lado consumidor, aqui no ao.dev, documentada no ",[66,352,355],{"href":353,"rel":354,"target":71},"https:\u002F\u002Fgithub.com\u002FAllanGBOliveira\u002Fao.dev",[70],"CLAUDE.md do próprio projeto"," como \"bug real de bleed em reload frio\". A lição de um projeto virou prevenção estrutural no outro.)",[34,358,360],{"id":359},"tokens-em-duas-camadas-sass-em-compile-time-css-custom-properties-em-runtime","Tokens em duas camadas — Sass em compile-time, CSS custom properties em runtime",[10,362,363,364,369,370,373],{},"Todo valor visual do sistema nasce como variável Sass e é emitido como ",[66,365,368],{"href":366,"rel":367,"target":71},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F--*",[70],"CSS custom property",". A dualidade é deliberada: o Sass resolve funções, deep-merge de mapas e cálculo de contraste ",[26,371,372],{},"antes"," de qualquer byte chegar ao browser; o CSS var existe para o consumidor poder sobrescrever em runtime sem recompilar Sass.",[218,375,377],{"className":220,"code":376,"language":222,"meta":223,"style":223},"\u002F\u002F packages\u002Fbase\u002Ftokens\u002F_colors.scss — customização do consumidor\n$palette-overrides: () !default;\n\n\u002F\u002F ao.dev customiza assim, em fui.scss:\n@use '@allangboliveira\u002Fui-themes-base' with (\n  $palette-overrides: (\"brand\": (500: #ff6b35, 600: #e55a2b))\n);\n",[14,378,379,384,396,403,409,424,460],{"__ignoreMap":223},[227,380,381],{"class":229,"line":230},[227,382,383],{"class":233},"\u002F\u002F packages\u002Fbase\u002Ftokens\u002F_colors.scss — customização do consumidor\n",[227,385,386,390,394],{"class":229,"line":237},[227,387,389],{"class":388},"soyes","$palette-overrides: () ",[227,391,393],{"class":392},"sHUrx","!default",[227,395,264],{"class":240},[227,397,399],{"class":229,"line":398},3,[227,400,402],{"emptyLinePlaceholder":401},true,"\n",[227,404,406],{"class":229,"line":405},4,[227,407,408],{"class":233},"\u002F\u002F ao.dev customiza assim, em fui.scss:\n",[227,410,412,414,418,421],{"class":229,"line":411},5,[227,413,76],{"class":392},[227,415,417],{"class":416},"sT6z2"," '@allangboliveira\u002Fui-themes-base'",[227,419,420],{"class":392}," with",[227,422,423],{"class":240}," (\n",[227,425,427,430,433,436,438,441,444,447,449,452,454,457],{"class":229,"line":426},6,[227,428,429],{"class":388},"  $palette-overrides",[227,431,432],{"class":240},": (",[227,434,435],{"class":416},"\"brand\"",[227,437,432],{"class":240},[227,439,440],{"class":244},"500",[227,442,443],{"class":240},": ",[227,445,446],{"class":244},"#ff6b35",[227,448,17],{"class":240},[227,450,451],{"class":244},"600",[227,453,443],{"class":240},[227,455,456],{"class":244},"#e55a2b",[227,458,459],{"class":240},"))\n",[227,461,463],{"class":229,"line":462},7,[227,464,465],{"class":240},");\n",[218,467,471],{"className":468,"code":469,"language":470,"meta":223,"style":223},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F* o que chega ao browser, já resolvido *\u002F\n:root {\n  --fui-color-primary: #7c3aed;\n  --fui-color-on-primary: #fafafa; \u002F* calculado, não hardcoded — ver seção seguinte *\u002F\n}\n","css",[14,472,473,478,486,498,514],{"__ignoreMap":223},[227,474,475],{"class":229,"line":230},[227,476,477],{"class":233},"\u002F* o que chega ao browser, já resolvido *\u002F\n",[227,479,480,483],{"class":229,"line":237},[227,481,482],{"class":244},":root",[227,484,485],{"class":240}," {\n",[227,487,488,491,493,496],{"class":229,"line":398},[227,489,490],{"class":388},"  --fui-color-primary",[227,492,443],{"class":240},[227,494,495],{"class":244},"#7c3aed",[227,497,264],{"class":240},[227,499,500,503,505,508,511],{"class":229,"line":405},[227,501,502],{"class":388},"  --fui-color-on-primary",[227,504,443],{"class":240},[227,506,507],{"class":244},"#fafafa",[227,509,510],{"class":240},"; ",[227,512,513],{"class":233},"\u002F* calculado, não hardcoded — ver seção seguinte *\u002F\n",[227,515,516],{"class":229,"line":411},[227,517,518],{"class":240},"}\n",[10,520,521],{},"Dois níveis de customização coexistem por design:",[523,524,525,542],"ul",{},[272,526,527,73,530,533,534,537,538,541],{},[26,528,529],{},"Sass compile-time",[14,531,532],{},"$palette-overrides"," e funções como ",[14,535,536],{},"shadow-color-for()","\u002F",[14,539,540],{},"text-color-for()",": derivação automática, span completo de recálculo (cor de texto, cor de sombra, tudo se ajusta).",[272,543,544,547,548,551,552,555,556,559,560,563,564,567,568,571],{},[26,545,546],{},"CSS runtime"," — sobrescrever ",[14,549,550],{},"--fui-color-surface"," direto via ",[14,553,554],{},"\u003Cstyle>",": a cascata sempre vence, mas só o que tem fallback via ",[14,557,558],{},"color-mix()"," (como a sombra) se readapta sozinho. Texto ",[26,561,562],{},"não"," — CSS não tem ",[14,565,566],{},"color-contrast()"," com suporte real ainda, então sobrescrever cor de superfície em runtime exige atualizar ",[14,569,570],{},"--fui-color-on-surface"," manualmente. Documentei essa assimetria explicitamente para não virar surpresa de quem usa a lib.",[34,573,575],{"id":574},"paleta-com-contraste-wcag-garantido-calculado-nao-adivinhado","Paleta com contraste WCAG garantido — calculado, não adivinhado",[10,577,578,579,582,583,586],{},"Esse foi o problema mais interessante do projeto inteiro. Toda paleta de design system tem o mesmo risco: alguém escolhe ",[14,580,581],{},"blue-500"," como cor de texto sobre ",[14,584,585],{},"blue-600"," como fundo, parece bonito no Figma, e falha WCAG AA (4.5:1) sem ninguém perceber até uma auditoria de acessibilidade pegar.",[10,588,589],{},"A solução aqui foi eliminar a adivinhação: uma função Sass calcula luminância relativa e razão de contraste em compile-time, e decide sozinha se o texto deve ser claro ou escuro.",[218,591,593],{"className":220,"code":592,"language":222,"meta":223,"style":223},"\u002F\u002F packages\u002Fbase\u002Futilities\u002F_a11y.scss\n@function luminance($color) {\n  \u002F\u002F fórmula de luminância relativa (WCAG 2.1)\n  $r: red($color) \u002F 255;\n  $g: green($color) \u002F 255;\n  $b: blue($color) \u002F 255;\n  \u002F\u002F ... gamma correction por canal\n  @return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;\n}\n\n@function contrast-ratio($fg, $bg) {\n  $l1: luminance($fg) + 0.05;\n  $l2: luminance($bg) + 0.05;\n  @return math.max($l1, $l2) \u002F math.min($l1, $l2);\n}\n\n@function text-color-for($bg) {\n  \u002F\u002F retorna neutral-50 ou neutral-950 — o que tiver MAIOR contraste contra $bg\n  $contrast-light: contrast-ratio($neutral-50, $bg);\n  $contrast-dark: contrast-ratio($neutral-950, $bg);\n  @return if($contrast-light > $contrast-dark, $neutral-50, $neutral-950);\n}\n",[14,594,595,600,617,622,646,668,690,695,733,738,743,763,788,810,855,860,865,879,885,907,928,957],{"__ignoreMap":223},[227,596,597],{"class":229,"line":230},[227,598,599],{"class":233},"\u002F\u002F packages\u002Fbase\u002Futilities\u002F_a11y.scss\n",[227,601,602,605,608,611,614],{"class":229,"line":237},[227,603,604],{"class":392},"@function",[227,606,607],{"class":244}," luminance",[227,609,610],{"class":240},"(",[227,612,613],{"class":388},"$color",[227,615,616],{"class":240},") {\n",[227,618,619],{"class":229,"line":398},[227,620,621],{"class":233},"  \u002F\u002F fórmula de luminância relativa (WCAG 2.1)\n",[227,623,624,627,629,632,634,636,639,641,644],{"class":229,"line":405},[227,625,626],{"class":388},"  $r",[227,628,443],{"class":240},[227,630,631],{"class":244},"red",[227,633,610],{"class":240},[227,635,613],{"class":388},[227,637,638],{"class":240},") ",[227,640,537],{"class":392},[227,642,643],{"class":244}," 255",[227,645,264],{"class":240},[227,647,648,651,653,656,658,660,662,664,666],{"class":229,"line":411},[227,649,650],{"class":388},"  $g",[227,652,443],{"class":240},[227,654,655],{"class":244},"green",[227,657,610],{"class":240},[227,659,613],{"class":388},[227,661,638],{"class":240},[227,663,537],{"class":392},[227,665,643],{"class":244},[227,667,264],{"class":240},[227,669,670,673,675,678,680,682,684,686,688],{"class":229,"line":426},[227,671,672],{"class":388},"  $b",[227,674,443],{"class":240},[227,676,677],{"class":244},"blue",[227,679,610],{"class":240},[227,681,613],{"class":388},[227,683,638],{"class":240},[227,685,537],{"class":392},[227,687,643],{"class":244},[227,689,264],{"class":240},[227,691,692],{"class":229,"line":462},[227,693,694],{"class":233},"  \u002F\u002F ... gamma correction por canal\n",[227,696,698,701,704,707,710,713,716,718,721,723,726,728,731],{"class":229,"line":697},8,[227,699,700],{"class":392},"  @return",[227,702,703],{"class":244}," 0.2126",[227,705,706],{"class":392}," *",[227,708,709],{"class":388}," $r",[227,711,712],{"class":392}," +",[227,714,715],{"class":244}," 0.7152",[227,717,706],{"class":392},[227,719,720],{"class":388}," $g",[227,722,712],{"class":392},[227,724,725],{"class":244}," 0.0722",[227,727,706],{"class":392},[227,729,730],{"class":388}," $b",[227,732,264],{"class":240},[227,734,736],{"class":229,"line":735},9,[227,737,518],{"class":240},[227,739,741],{"class":229,"line":740},10,[227,742,402],{"emptyLinePlaceholder":401},[227,744,746,748,751,753,756,758,761],{"class":229,"line":745},11,[227,747,604],{"class":392},[227,749,750],{"class":244}," contrast-ratio",[227,752,610],{"class":240},[227,754,755],{"class":388},"$fg",[227,757,17],{"class":240},[227,759,760],{"class":388},"$bg",[227,762,616],{"class":240},[227,764,766,769,771,774,776,778,780,783,786],{"class":229,"line":765},12,[227,767,768],{"class":388},"  $l1",[227,770,443],{"class":240},[227,772,773],{"class":244},"luminance",[227,775,610],{"class":240},[227,777,755],{"class":388},[227,779,638],{"class":240},[227,781,782],{"class":392},"+",[227,784,785],{"class":244}," 0.05",[227,787,264],{"class":240},[227,789,791,794,796,798,800,802,804,806,808],{"class":229,"line":790},13,[227,792,793],{"class":388},"  $l2",[227,795,443],{"class":240},[227,797,773],{"class":244},[227,799,610],{"class":240},[227,801,760],{"class":388},[227,803,638],{"class":240},[227,805,782],{"class":392},[227,807,785],{"class":244},[227,809,264],{"class":240},[227,811,813,815,818,821,824,826,829,831,834,836,838,840,842,845,847,849,851,853],{"class":229,"line":812},14,[227,814,700],{"class":392},[227,816,817],{"class":388}," math",[227,819,820],{"class":240},".",[227,822,823],{"class":244},"max",[227,825,610],{"class":240},[227,827,828],{"class":388},"$l1",[227,830,17],{"class":240},[227,832,833],{"class":388},"$l2",[227,835,638],{"class":240},[227,837,537],{"class":392},[227,839,817],{"class":388},[227,841,820],{"class":240},[227,843,844],{"class":244},"min",[227,846,610],{"class":240},[227,848,828],{"class":388},[227,850,17],{"class":240},[227,852,833],{"class":388},[227,854,465],{"class":240},[227,856,858],{"class":229,"line":857},15,[227,859,518],{"class":240},[227,861,863],{"class":229,"line":862},16,[227,864,402],{"emptyLinePlaceholder":401},[227,866,868,870,873,875,877],{"class":229,"line":867},17,[227,869,604],{"class":392},[227,871,872],{"class":244}," text-color-for",[227,874,610],{"class":240},[227,876,760],{"class":388},[227,878,616],{"class":240},[227,880,882],{"class":229,"line":881},18,[227,883,884],{"class":233},"  \u002F\u002F retorna neutral-50 ou neutral-950 — o que tiver MAIOR contraste contra $bg\n",[227,886,888,891,893,896,898,901,903,905],{"class":229,"line":887},19,[227,889,890],{"class":388},"  $contrast-light",[227,892,443],{"class":240},[227,894,895],{"class":244},"contrast-ratio",[227,897,610],{"class":240},[227,899,900],{"class":388},"$neutral-50",[227,902,17],{"class":240},[227,904,760],{"class":388},[227,906,465],{"class":240},[227,908,910,913,915,917,919,922,924,926],{"class":229,"line":909},20,[227,911,912],{"class":388},"  $contrast-dark",[227,914,443],{"class":240},[227,916,895],{"class":244},[227,918,610],{"class":240},[227,920,921],{"class":388},"$neutral-950",[227,923,17],{"class":240},[227,925,760],{"class":388},[227,927,465],{"class":240},[227,929,931,933,936,938,941,944,947,949,951,953,955],{"class":229,"line":930},21,[227,932,700],{"class":392},[227,934,935],{"class":244}," if",[227,937,610],{"class":240},[227,939,940],{"class":388},"$contrast-light",[227,942,943],{"class":388}," >",[227,945,946],{"class":388}," $contrast-dark",[227,948,17],{"class":240},[227,950,900],{"class":388},[227,952,17],{"class":240},[227,954,921],{"class":388},[227,956,465],{"class":240},[227,958,960],{"class":229,"line":959},22,[227,961,518],{"class":240},[10,963,964,965,968,969,972,973,976,977,979],{},"O resultado prático: ",[26,966,967],{},"88 variantes de botão com contraste garantido"," — 8 escalas de cor × 11 degraus (",[14,970,971],{},"50"," a ",[14,974,975],{},"950",") — cada uma testada. A suíte de testes (",[14,978,118],{}," via Jest) roda essa verificação para cada combinação real:",[218,981,985],{"className":982,"code":983,"language":984,"meta":223,"style":223},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast","$ npm test\n\n  A11y — button palette WCAG AA\n    Button palette — WCAG AA contrast (4.5:1)\n      neutral\n        ✓ neutral-50 has a compliant text choice\n        ✓ neutral-100 has a compliant text choice\n        ...\n      blue \u002F green \u002F red \u002F yellow \u002F purple \u002F ...\n        ✓ (11 degraus cada, todos compliant)\n\nTests:       307 passed, 307 total\n","bash",[14,986,987,998,1002,1022,1042,1047,1070,1087,1092,1123,1144,1148],{"__ignoreMap":223},[227,988,989,992,995],{"class":229,"line":230},[227,990,991],{"class":388},"$",[227,993,994],{"class":416}," npm",[227,996,997],{"class":416}," test\n",[227,999,1000],{"class":229,"line":237},[227,1001,402],{"emptyLinePlaceholder":401},[227,1003,1004,1007,1010,1013,1016,1019],{"class":229,"line":398},[227,1005,1006],{"class":388},"  A11y",[227,1008,1009],{"class":416}," —",[227,1011,1012],{"class":416}," button",[227,1014,1015],{"class":416}," palette",[227,1017,1018],{"class":416}," WCAG",[227,1020,1021],{"class":416}," AA\n",[227,1023,1024,1027,1029,1031,1033,1036,1039],{"class":229,"line":405},[227,1025,1026],{"class":388},"    Button",[227,1028,1015],{"class":416},[227,1030,1009],{"class":416},[227,1032,1018],{"class":416},[227,1034,1035],{"class":416}," AA",[227,1037,1038],{"class":416}," contrast",[227,1040,1041],{"class":240}," (4.5:1)\n",[227,1043,1044],{"class":229,"line":411},[227,1045,1046],{"class":388},"      neutral\n",[227,1048,1049,1052,1055,1058,1061,1064,1067],{"class":229,"line":426},[227,1050,1051],{"class":388},"        ✓",[227,1053,1054],{"class":416}," neutral-50",[227,1056,1057],{"class":416}," has",[227,1059,1060],{"class":416}," a",[227,1062,1063],{"class":416}," compliant",[227,1065,1066],{"class":416}," text",[227,1068,1069],{"class":416}," choice\n",[227,1071,1072,1074,1077,1079,1081,1083,1085],{"class":229,"line":462},[227,1073,1051],{"class":388},[227,1075,1076],{"class":416}," neutral-100",[227,1078,1057],{"class":416},[227,1080,1060],{"class":416},[227,1082,1063],{"class":416},[227,1084,1066],{"class":416},[227,1086,1069],{"class":416},[227,1088,1089],{"class":229,"line":697},[227,1090,1091],{"class":244},"        ...\n",[227,1093,1094,1097,1100,1103,1105,1108,1110,1113,1115,1118,1120],{"class":229,"line":735},[227,1095,1096],{"class":388},"      blue",[227,1098,1099],{"class":416}," \u002F",[227,1101,1102],{"class":416}," green",[227,1104,1099],{"class":416},[227,1106,1107],{"class":416}," red",[227,1109,1099],{"class":416},[227,1111,1112],{"class":416}," yellow",[227,1114,1099],{"class":416},[227,1116,1117],{"class":416}," purple",[227,1119,1099],{"class":416},[227,1121,1122],{"class":416}," ...\n",[227,1124,1125,1127,1130,1133,1136,1139,1141],{"class":229,"line":740},[227,1126,1051],{"class":388},[227,1128,1129],{"class":240}," (11 ",[227,1131,1132],{"class":416},"degraus",[227,1134,1135],{"class":416}," cada,",[227,1137,1138],{"class":416}," todos",[227,1140,1063],{"class":416},[227,1142,1143],{"class":240},")\n",[227,1145,1146],{"class":229,"line":745},[227,1147,402],{"emptyLinePlaceholder":401},[227,1149,1150,1153,1156,1159,1162],{"class":229,"line":765},[227,1151,1152],{"class":388},"Tests:",[227,1154,1155],{"class":244},"       307",[227,1157,1158],{"class":416}," passed,",[227,1160,1161],{"class":244}," 307",[227,1163,1164],{"class":416}," total\n",[10,1166,1167,1168,1171],{},"Não é teste de snapshot visual — é teste matemático rodando a mesma fórmula de contraste que o WCAG define, contra cada célula real da paleta, toda vez que alguém roda ",[14,1169,1170],{},"npm test",". Se um dia eu adicionar uma cor nova à paleta e ela quebrar contraste em algum degrau, o CI pega antes de eu nem notar visualmente.",[34,1173,1175],{"id":1174},"sombra-e-superficie-o-problema-que-pure-black-escondia","Sombra e superfície — o problema que pure black escondia",[10,1177,1178,1179,1182,1183,1186,1187,1190],{},"Um dos primeiros bugs reais do projeto: em dark mode, ",[14,1180,1181],{},"box-shadow"," sobre ",[14,1184,1185],{},"neutral-900"," (quase preto puro) fica ",[26,1188,1189],{},"invisível"," — preto sobre preto, mesmo com blur generoso. Cards e botões perdiam toda a elevação visual assim que o usuário trocava pra dark.",[10,1192,1193],{},"A correção não foi aumentar blur ou opacidade às cegas — foi recalibrar os próprios degraus de superfície:",[39,1195,1196,1209],{},[42,1197,1198],{},[45,1199,1200,1203,1206],{},[48,1201,1202],{},"Papel",[48,1204,1205],{},"Light",[48,1207,1208],{},"Dark",[55,1210,1211,1228,1245],{},[45,1212,1213,1218,1223],{},[60,1214,1215],{},[14,1216,1217],{},"surface",[60,1219,1220],{},[14,1221,1222],{},"neutral-50",[60,1224,1225],{},[14,1226,1227],{},"neutral-800",[45,1229,1230,1235,1240],{},[60,1231,1232],{},[14,1233,1234],{},"surface-alt",[60,1236,1237],{},[14,1238,1239],{},"neutral-100",[60,1241,1242],{},[14,1243,1244],{},"neutral-700",[45,1246,1247,1252,1257],{},[60,1248,1249],{},[14,1250,1251],{},"surface-raised",[60,1253,1254],{},[14,1255,1256],{},"#fff",[60,1258,1259],{},[14,1260,1261],{},"neutral-600",[10,1263,1264,1265,537,1267,1269,1270,1272,1273,1276,1277,1280,1281,1284,1285,1288],{},"Mover a base do dark mode de ",[14,1266,1185],{},[14,1268,975],{}," para ",[14,1271,1227],{}," (um cinza, não um quase-preto) abriu espaço pra sombra ter contra o que se destacar. Em paralelo, criei um token semântico ",[14,1274,1275],{},"color-shadow"," — light usa ",[14,1278,1279],{},"rgba(0,0,0,0.15)",", dark usa ",[14,1282,1283],{},"rgba(0,0,0,0.55)"," — porque a mesma opacidade de sombra que é sutil sobre branco fica imperceptível sobre cinza escuro. Uma função (",[14,1286,1287],{},"shadow-color-for($bg)",") deriva essa opacidade automaticamente pela luminância do fundo, com thresholds específicos:",[218,1290,1292],{"className":220,"code":1291,"language":222,"meta":223,"style":223},"\u002F\u002F luminância \u003C 0.03 → opacity 0.55\n\u002F\u002F luminância \u003C 0.10 → opacity 0.45\n\u002F\u002F luminância \u003C 0.30 → opacity 0.30\n\u002F\u002F luminância \u003C 0.65 → opacity 0.20\n\u002F\u002F caso contrário     → opacity 0.15\n",[14,1293,1294,1299,1304,1309,1314],{"__ignoreMap":223},[227,1295,1296],{"class":229,"line":230},[227,1297,1298],{"class":233},"\u002F\u002F luminância \u003C 0.03 → opacity 0.55\n",[227,1300,1301],{"class":229,"line":237},[227,1302,1303],{"class":233},"\u002F\u002F luminância \u003C 0.10 → opacity 0.45\n",[227,1305,1306],{"class":229,"line":398},[227,1307,1308],{"class":233},"\u002F\u002F luminância \u003C 0.30 → opacity 0.30\n",[227,1310,1311],{"class":229,"line":405},[227,1312,1313],{"class":233},"\u002F\u002F luminância \u003C 0.65 → opacity 0.20\n",[227,1315,1316],{"class":229,"line":411},[227,1317,1318],{"class":233},"\u002F\u002F caso contrário     → opacity 0.15\n",[10,1320,1321,1322,1324,1325,1327,1328,1330],{},"E, como fallback pra quando um consumidor sobrescreve ",[14,1323,550],{}," via CSS puro sem saber da função Sass, todo ",[14,1326,1181],{}," do sistema carrega um ",[14,1329,558],{}," de segurança:",[218,1332,1334],{"className":468,"code":1333,"language":470,"meta":223,"style":223},"--fui-shadow-md: 0 4px 6px -1px var(--fui-color-shadow, color-mix(in srgb, var(--fui-color-surface) 75%, black));\n",[14,1335,1336],{"__ignoreMap":223},[227,1337,1338],{"class":229,"line":230},[227,1339,1333],{"class":240},[10,1341,1342,1343,1346],{},"Se ",[14,1344,1345],{},"--fui-color-shadow"," não existir, o browser deriva uma sombra plausível a partir da própria cor de superfície — nunca preto-sobre-preto, mesmo em customização de última hora.",[34,1348,1350],{"id":1349},"cinco-morfismos-o-mesmo-componente-luz-e-escuro-ao-mesmo-tempo","Cinco morfismos, o mesmo componente, luz e escuro ao mesmo tempo",[1352,1353,1354],"blockquote",{},[10,1355,1356,1357,1360,1361,1364],{},"Instalar e importar o pacote já aplica o morfismo a todos os componentes. Sem ",[14,1358,1359],{},".fui-glass",", sem ",[14,1362,1363],{},".fui-skeu"," no HTML — o tema é o pacote, não uma classe.",[10,1366,1367,1368,1371,1372,1375],{},"Cada morfismo é um pacote próprio (",[14,1369,1370],{},"@allangboliveira\u002Fui-theme-{glass,clay,neu,skeu}","), com paridade completa de componentes: button, card, input, checkbox, radio, select, tabs, navbar, pagination, tag, content. A mesma marcação HTML muda de identidade inteira conforme qual pacote está instalado. As capturas abaixo são todas da mesma seção da página de testes do sistema — a tabela de estados e de cores semânticas do botão — com ",[26,1373,1374],{},"light e dark lado a lado na mesma imagem",", porque comparar os dois modos ao mesmo tempo é o teste visual mais honesto que existe pra um design system multi-tema.",[1377,1378,1380],"h3",{"id":1379},"skeuomorfismo-hardware-fisico","Skeuomorfismo — hardware físico",[10,1382,1383,1384,1387,1388,1391],{},"Perspectiva lateral, como um teclado físico: gradiente convexo, quatro camadas de sombra para transição suave entre estados, e um sinal de \"pressionado\" real — ",[14,1385,1386],{},"translateY(1px)"," combinado com ",[14,1389,1390],{},"border-style: inset"," e o gradiente invertido, não só uma sombra menor.",[1393,1394,1397,1405],"figure",{"className":1395},[1396],"rehype-figure",[1398,1399],"img",{"alt":1400,"height":1401,"src":1402,"title":1403,"width":1404},"Botão skeuomórfico com gradiente convexo, capturado em light e dark lado a lado na mesma tela de testes",675,"\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-skeu.png","Skeuomorfismo — light e dark",1200,[1406,1407,1400],"figcaption",{},[10,1409,1410,1411,1414,1415,1418,1419,17,1422,17,1425,17,1428,17,1431,17,1434,1436,1437,1440],{},"O card skeu vai além do botão: simula um índice físico 3×5 polegadas, com ",[26,1412,1413],{},"linhas de pauta horizontais ligadas por padrão"," (",[14,1416,1417],{},"fui-card-lined",") e variantes de cor de papel (",[14,1420,1421],{},"new",[14,1423,1424],{},"cream",[14,1426,1427],{},"aged",[14,1429,1430],{},"dirty",[14,1432,1433],{},"yellow",[14,1435,677],{},") — inclusive uma linha vertical de margem opcional (",[14,1438,1439],{},"fui-card-margin",", o efeito \"caderno universitário\").",[1377,1442,1444],{"id":1443},"glassmorfismo-vidro-e-translucidez","Glassmorfismo — vidro e translucidez",[10,1446,1447,1450],{},[14,1448,1449],{},"backdrop-filter: blur()"," de verdade, com opacidade calibrada por componente — 15% no card contra 50% no botão, porque a área de leitura de um card precisa de mais transparência de fundo sem perder legibilidade do texto sobreposto.",[1393,1452,1454,1459],{"className":1453},[1396],[1398,1455],{"alt":1456,"height":1401,"src":1457,"title":1458,"width":1404},"Botão glassmórfico translúcido com backdrop-filter, capturado em light e dark lado a lado","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-glass.png","Glassmorfismo — light e dark",[1406,1460,1456],{},[1377,1462,1464],{"id":1463},"claymorfismo-argila-inflada","Claymorfismo — argila inflada",[10,1466,1467],{},"Sombras HSL coloridas (não cinza) derivadas da própria cor do componente, raios de borda generosos (26–50px) e uma escala de profundidade progressiva — o efeito \"inflado\" que dá nome ao morfismo.",[1393,1469,1471,1476],{"className":1470},[1396],[1398,1472],{"alt":1473,"height":1401,"src":1474,"title":1475,"width":1404},"Botão claymórfico com sombra colorida e bordas bem arredondadas, capturado em light e dark lado a lado","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-clay.png","Claymorfismo — light e dark",[1406,1477,1473],{},[1377,1479,1481],{"id":1480},"neumorfismo-plastico-extrudado","Neumorfismo — plástico extrudado",[10,1483,1484,1485,1488,1489,1492,1493,1495],{},"Perspectiva top-down (câmera de cima, não de lado — por isso ",[26,1486,1487],{},"sem"," ",[14,1490,1491],{},"translateY"," no estado ativo, diferente do skeu). Fundo obrigatoriamente igual à cor de superfície, com sombra dupla via ",[14,1494,558],{}," simulando luz vinda de um único ângulo.",[1393,1497,1499,1504],{"className":1498},[1396],[1398,1500],{"alt":1501,"height":1401,"src":1502,"title":1503,"width":1404},"Botão neumórfico extrudado do próprio fundo, capturado em light e dark lado a lado","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-neu.png","Neumorfismo — light e dark",[1406,1505,1501],{},[10,1507,1508,1509,1512,1513,1515,1516,1518,1519,1521],{},"A própria página de testes documenta uma limitação real desse morfismo, direto num aviso visível acima da tabela de cores semânticas: nas variantes ",[14,1510,1511],{},"soft"," e ",[14,1514,26],{},", o fundo é sempre transparente e a cor do papel vira a cor do texto — e nem toda combinação de ",[14,1517,1511],{}," (degrau 400) ou ",[14,1520,26],{}," (dessaturado) passa WCAG AA. A recomendação documentada é usar o tier padrão quando contraste garantido importa mais que a variação visual. Prefiro deixar essa limitação exposta a esconder atrás de um componente que parece perfeito e não é.",[1377,1523,1525],{"id":1524},"flat-a-base-sem-morfismo","Flat — a base sem morfismo",[10,1527,1528,1529,1532],{},"Sem pacote de tema instalado, ",[14,1530,1531],{},"@allangboliveira\u002Fui-themes-base"," sozinho já é um design system completo e utilizável — flat, sem sombra decorativa, só os tokens e a base. É o morfismo que quase ninguém \"escolhe\" conscientemente, mas que precisa funcionar perfeitamente sozinho, porque é o piso sobre o qual todos os outros quatro são construídos. A screenshot dela é a que abre este case study, ali na barra lateral desta página.",[34,1534,1536,1539],{"id":1535},"border-aware-padding-quando-a-borda-rouba-espaco-da-borda",[14,1537,1538],{},"border-aware-padding"," — quando a borda rouba espaço da borda",[10,1541,1542,1543,1546,1547,1550],{},"Um bug sutil de consistência: dois botões com o mesmo ",[14,1544,1545],{},"padding"," declarado, um sem borda e outro com ",[14,1548,1549],{},"border: 2px solid",", renderizam com áreas internas visualmente diferentes — a borda \"come\" espaço do padding, então o texto do botão com borda mais grossa parece mais apertado.",[10,1552,1553],{},"A correção virou um mixin usado em todo componente com borda variável entre morfismos:",[218,1555,1557],{"className":220,"code":1556,"language":222,"meta":223,"style":223},"@mixin border-aware-padding($padding-base, $border-ref, $border-actual) {\n  padding: calc(#{$padding-base} + #{$border-ref} - #{$border-actual});\n}\n",[14,1558,1559,1585,1613],{"__ignoreMap":223},[227,1560,1561,1564,1568,1570,1573,1575,1578,1580,1583],{"class":229,"line":230},[227,1562,1563],{"class":392},"@mixin",[227,1565,1567],{"class":1566},"sKwhi"," border-aware-padding",[227,1569,610],{"class":240},[227,1571,1572],{"class":388},"$padding-base",[227,1574,17],{"class":240},[227,1576,1577],{"class":388},"$border-ref",[227,1579,17],{"class":240},[227,1581,1582],{"class":388},"$border-actual",[227,1584,616],{"class":240},[227,1586,1587,1590,1592,1595,1597,1600,1602,1605,1608,1611],{"class":229,"line":237},[227,1588,1589],{"class":244},"  padding",[227,1591,443],{"class":240},[227,1593,1594],{"class":244},"calc",[227,1596,610],{"class":240},[227,1598,1599],{"class":388},"#{$padding-base}",[227,1601,712],{"class":392},[227,1603,1604],{"class":388}," #{$border-ref}",[227,1606,1607],{"class":392}," -",[227,1609,1610],{"class":388}," #{$border-actual}",[227,1612,465],{"class":240},[227,1614,1615],{"class":229,"line":398},[227,1616,518],{"class":240},[10,1618,1619],{},"A fórmula compensa a diferença entre uma borda de referência (a que o design foi calibrado para) e a borda real do morfismo atual — skeu e clay tendem a usar bordas mais grossas para o efeito de relevo, glass quase não usa borda nenhuma. Sem esse mixin, cada morfismo precisaria de um valor de padding manualmente recalibrado; com ele, o mesmo token de espaçamento produz a mesma área interna percebida em qualquer morfismo.",[34,1621,1623],{"id":1622},"testado-a-serio-nao-so-compilado","Testado a sério, não só compilado",[523,1625,1626,1654,1660],{},[272,1627,1628,1631,1632,1635,1636,17,1639,17,1642,1645,1646,1649,1650,1653],{},[26,1629,1630],{},"307 testes"," via ",[66,1633,118],{"href":116,"rel":1634,"target":71},[70]," rodando dentro do Jest — funções puras (",[14,1637,1638],{},"px-to-rem",[14,1640,1641],{},"map-range",[14,1643,1644],{},"fluid","), mixins (",[14,1647,1648],{},"transition"," respeitando ",[14,1651,1652],{},"prefers-reduced-motion","), tokens (paleta, opacidade, raio, tipografia) e os testes de contraste WCAG já descritos.",[272,1655,1656,1659],{},[26,1657,1658],{},"Stylelint + Prettier"," obrigatórios no CI — formatação e convenções de SCSS não dependem de review manual.",[272,1661,1662,1665,1666,1669,1670,1673,1674,1676,1677,1680,1681,1684,1685,1688,1689,1692,1693,1696],{},[26,1663,1664],{},"IBM Equal Access"," guiou correções reais de acessibilidade na própria lib, não só no consumidor: indicadores que dependiam só de ",[14,1667,1668],{},"background-image"," ganharam fallback em ",[14,1671,1672],{},"forced-colors: active"," (Windows High Contrast Mode não renderiza ",[14,1675,1668],{}," decorativo), e o ",[14,1678,1679],{},"_content.scss"," nunca declara ",[14,1682,1683],{},"background-color"," nem ",[14,1686,1687],{},"color"," no ",[14,1690,1691],{},"\u003Cpre>"," — essa responsabilidade fica ",[26,1694,1695],{},"exclusivamente"," com o Shiki do lado consumidor, exatamente a mesma regra de divisão que documentei aqui no ao.dev.",[34,1698,1700],{"id":1699},"cicd-ordem-de-deploy-e-regra-nao-sugestao","CI\u002FCD — ordem de deploy é regra, não sugestão",[10,1702,1703],{},"O monorepo publica cinco pacotes independentes, cada um com sua própria tag de versão:",[39,1705,1706,1716],{},[42,1707,1708],{},[45,1709,1710,1713],{},[48,1711,1712],{},"Tag",[48,1714,1715],{},"Publica",[55,1717,1718,1729,1740,1752,1764],{},[45,1719,1720,1725],{},[60,1721,1722],{},[14,1723,1724],{},"base\u002Fv0.3.0-canary.36",[60,1726,1727],{},[14,1728,1531],{},[45,1730,1731,1736],{},[60,1732,1733],{},[14,1734,1735],{},"theme-glass\u002Fv0.2.0-canary.17",[60,1737,1738],{},[14,1739,188],{},[45,1741,1742,1747],{},[60,1743,1744],{},[14,1745,1746],{},"theme-clay\u002Fv0.2.0-canary.18",[60,1748,1749],{},[14,1750,1751],{},"@allangboliveira\u002Fui-theme-clay",[45,1753,1754,1759],{},[60,1755,1756],{},[14,1757,1758],{},"theme-neu\u002Fv0.2.0-canary.18",[60,1760,1761],{},[14,1762,1763],{},"@allangboliveira\u002Fui-theme-neu",[45,1765,1766,1771],{},[60,1767,1768],{},[14,1769,1770],{},"theme-skeu\u002Fv0.2.0-canary.17",[60,1772,1773],{},[14,1774,1775],{},"@allangboliveira\u002Fui-theme-skeu",[218,1777,1779],{"className":982,"code":1778,"language":984,"meta":223,"style":223},"git tag base\u002Fv0.3.0 && git push origin base\u002Fv0.3.0\n# dispara o workflow \"CD — Publish\", que roda lint + test\n# + compile check antes de qualquer `npm publish`\n",[14,1780,1781,1806,1811],{"__ignoreMap":223},[227,1782,1783,1786,1789,1792,1795,1797,1800,1803],{"class":229,"line":230},[227,1784,1785],{"class":388},"git",[227,1787,1788],{"class":416}," tag",[227,1790,1791],{"class":416}," base\u002Fv0.3.0",[227,1793,1794],{"class":240}," && ",[227,1796,1785],{"class":388},[227,1798,1799],{"class":416}," push",[227,1801,1802],{"class":416}," origin",[227,1804,1805],{"class":416}," base\u002Fv0.3.0\n",[227,1807,1808],{"class":229,"line":237},[227,1809,1810],{"class":233},"# dispara o workflow \"CD — Publish\", que roda lint + test\n",[227,1812,1813],{"class":229,"line":398},[227,1814,1815],{"class":233},"# + compile check antes de qualquer `npm publish`\n",[10,1817,1818],{},"Duas regras de disciplina que aprendi a sério, não de manual:",[269,1820,1821,1830],{},[272,1822,1823,1826,1827,1829],{},[26,1824,1825],{},"Um único commit para todo bump de versão entre pacotes que dependem uns dos outros"," — evita um tema publicado apontando pra uma versão de ",[14,1828,317],{}," que ainda não existe no registry.",[272,1831,1832,1835],{},[26,1833,1834],{},"No máximo dois morfismos publicados em paralelo"," — publicar os quatro ao mesmo tempo multiplica a superfície de algo dar errado numa janela só, sem ganho real de velocidade.",[10,1837,1838,1839,1842,1843,1846,1847,1850,1851,17,1854,17,1857,17,1860,1863,1864,1867,1868,1870],{},"Todo push (não só em ",[14,1840,1841],{},"main",") já roda o job ",[14,1844,1845],{},"Compile, Lint & Test"," — compila os cinco pacotes com ",[14,1848,1849],{},"sass",", checa formatação, lint e os 307 testes. Nenhuma branch de ",[14,1852,1853],{},"feat\u002F",[14,1855,1856],{},"fix\u002F",[14,1858,1859],{},"chore\u002F",[14,1861,1862],{},"refactor\u002F"," ou ",[14,1865,1866],{},"docs\u002F"," escapa disso. E desde abril, um ruleset ativo no repositório bloqueia push direto em ",[14,1869,1841],{}," — até para mim: qualquer mudança, mesmo de documentação, precisa passar por Pull Request com o check verde. É a mesma disciplina que peço em qualquer projeto de cliente, só que aqui automatizada e inegociável.",[34,1872,1874,1876],{"id":1873},"fui-content-o-wrapper-que-da-nome-ao-projeto",[14,1875,5],{}," — o wrapper que dá nome ao projeto",[10,1878,1879,1880,1883,1884,17,1886,17,1888,17,1890,17,1892,17,1895,17,1898,1900,1901,1904],{},"O nome do design system inteiro vem de uma única classe: ",[14,1881,1882],{},".fui-content",", o wrapper de contexto de leitura que estiliza qualquer HTML gerado por CMS, editor rico ou pipeline Markdown — ",[14,1885,34],{},[14,1887,10],{},[14,1889,218],{},[14,1891,1352],{},[14,1893,1894],{},"kbd",[14,1896,1897],{},"mark",[14,1899,1393],{},", tudo, sem depender de nome de framework. É literalmente o que renderiza este parágrafo que você está lendo agora, dentro do ",[14,1902,1903],{},"\u003Carticle class=\"fui-content\">"," do ao.dev.",[10,1906,1907],{},"Toda propriedade visual do wrapper é uma CSS custom property, e os morfismos sobrescrevem só as variáveis — nunca seletor, nunca propriedade direta:",[218,1909,1911],{"className":220,"code":1910,"language":222,"meta":223,"style":223},".fui-content {\n  --fui-content-code-bg:        var(--fui-color-surface-alt);\n  --fui-content-code-shadow:    none;   \u002F\u002F hook de morfismo\n  --fui-content-code-backdrop:  none;   \u002F\u002F hook de morfismo (glass)\n  --fui-content-quote-accent:   var(--fui-color-primary);\n  --fui-content-mark-bg-image:  none;   \u002F\u002F hook de morfismo (gradiente \"caneta\")\n  --fui-content-hr-shadow:      none;   \u002F\u002F hook de morfismo (efeito sulco)\n}\n",[14,1912,1913,1919,1937,1954,1969,1986,2000,2015],{"__ignoreMap":223},[227,1914,1915,1917],{"class":229,"line":230},[227,1916,1882],{"class":244},[227,1918,485],{"class":240},[227,1920,1921,1924,1927,1930,1932,1935],{"class":229,"line":237},[227,1922,1923],{"class":388},"  --fui-content-code-bg",[227,1925,1926],{"class":240},":        ",[227,1928,1929],{"class":244},"var",[227,1931,610],{"class":240},[227,1933,1934],{"class":388},"--fui-color-surface-alt",[227,1936,465],{"class":240},[227,1938,1939,1942,1945,1948,1951],{"class":229,"line":398},[227,1940,1941],{"class":388},"  --fui-content-code-shadow",[227,1943,1944],{"class":240},":    ",[227,1946,1947],{"class":244},"none",[227,1949,1950],{"class":240},";   ",[227,1952,1953],{"class":233},"\u002F\u002F hook de morfismo\n",[227,1955,1956,1959,1962,1964,1966],{"class":229,"line":405},[227,1957,1958],{"class":388},"  --fui-content-code-backdrop",[227,1960,1961],{"class":240},":  ",[227,1963,1947],{"class":244},[227,1965,1950],{"class":240},[227,1967,1968],{"class":233},"\u002F\u002F hook de morfismo (glass)\n",[227,1970,1971,1974,1977,1979,1981,1984],{"class":229,"line":411},[227,1972,1973],{"class":388},"  --fui-content-quote-accent",[227,1975,1976],{"class":240},":   ",[227,1978,1929],{"class":244},[227,1980,610],{"class":240},[227,1982,1983],{"class":388},"--fui-color-primary",[227,1985,465],{"class":240},[227,1987,1988,1991,1993,1995,1997],{"class":229,"line":426},[227,1989,1990],{"class":388},"  --fui-content-mark-bg-image",[227,1992,1961],{"class":240},[227,1994,1947],{"class":244},[227,1996,1950],{"class":240},[227,1998,1999],{"class":233},"\u002F\u002F hook de morfismo (gradiente \"caneta\")\n",[227,2001,2002,2005,2008,2010,2012],{"class":229,"line":462},[227,2003,2004],{"class":388},"  --fui-content-hr-shadow",[227,2006,2007],{"class":240},":      ",[227,2009,1947],{"class":244},[227,2011,1950],{"class":240},[227,2013,2014],{"class":233},"\u002F\u002F hook de morfismo (efeito sulco)\n",[227,2016,2017],{"class":229,"line":697},[227,2018,518],{"class":240},[10,2020,2021,2022,2025,2026,2028,2029,2031],{},"Cada ",[14,2023,2024],{},"\u002F\u002F hook de morfismo"," é uma var que só ganha valor real dentro do arquivo ",[14,2027,1679],{}," específico de cada tema — o base declara a variável vazia, o morfismo a preenche. ",[14,2030,1882],{}," nunca precisa saber qual morfismo está ativo; só expõe os pontos de extensão.",[34,2033,2035],{"id":2034},"o-que-virou-aodev","O que virou ao.dev",[10,2037,2038],{},"Esse projeto e o portfólio que você está navegando nasceram como o mesmo projeto na prática — cada componente que termino aqui desbloqueia uma página real do ao.dev. A ordem de prioridade nunca foi \"o que é mais interessante de construir\", foi sempre \"o que a próxima página do portfólio precisa\": Button primeiro (usado em toda parte), Card em seguida (toda listagem), Badge\u002FTag (dentro de todo card), Navbar, campos de formulário, Timeline (a página Sobre), Meta block e Breadcrumb (case study), Code block (blog técnico), Avatar.",[10,2040,2041],{},"O seletor de morfismo no topo deste próprio site — a engrenagem que troca entre skeu, glass, neu, clay e flat em tempo real — é o teste de aceitação mais honesto que a lib podia ter: se trocar de tema aqui, ao vivo, sem reload, sem flash, sem quebrar contraste em nenhum dos cinco, o design system está fazendo o que prometeu.",[34,2043,2045],{"id":2044},"o-que-ainda-falta","O que ainda falta",[10,2047,2048,2049,2052,2053,537,2056,2059,2060,2063],{},"O roadmap documentado inclui uma camada \"pro\" opcional — texturas procedurais via ",[14,2050,2051],{},"filter"," + SVG (",[14,2054,2055],{},"feTurbulence",[14,2057,2058],{},"feDisplacementMap",") aplicadas em pseudo-elemento: granulado fosco pro glass, textura de plástico\u002Fborracha pro neu, metal\u002Fcouro\u002Fmadeira pro skeu, microgrão pro clay. Nada disso foi implementado ainda — a decisão deliberada foi terminar paridade de componentes nos cinco morfismos primeiro, e só então considerar a camada de textura como pacote adicional opcional (",[14,2061,2062],{},".fui-texture","), sem tocar no comportamento base.",[10,2065,2066,2067,2070],{},"Este repositório estava marcado como privado desde o primeiro commit — parte do processo de publicar este próprio portfólio foi justamente tirar essa lib do armário: remover o ",[14,2068,2069],{},"private",", girar os tokens que só faziam sentido enquanto ninguém mais podia ver o código, e deixar público o design system que sustenta, literalmente, a página que você está lendo agora.",[2072,2073,2074],"style",{},"html pre.shiki code .sLBg1, html code.shiki .sLBg1{--shiki-default:#66707B;--shiki-dark:#BDC4CC}html pre.shiki code .suds8, html code.shiki .suds8{--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .s-5SL, html code.shiki .s-5SL{--shiki-default:#023B95;--shiki-dark:#91CBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .soyes, html code.shiki .soyes{--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .sHUrx, html code.shiki .sHUrx{--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .sT6z2, html code.shiki .sT6z2{--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .sKwhi, html code.shiki .sKwhi{--shiki-default:#622CBC;--shiki-dark:#DBB7FF}",{"title":223,"searchDepth":237,"depth":237,"links":2076},[2077,2078,2079,2080,2081,2082,2089,2091,2092,2093,2095,2096],{"id":36,"depth":237,"text":37},{"id":204,"depth":237,"text":205},{"id":359,"depth":237,"text":360},{"id":574,"depth":237,"text":575},{"id":1174,"depth":237,"text":1175},{"id":1349,"depth":237,"text":1350,"children":2083},[2084,2085,2086,2087,2088],{"id":1379,"depth":398,"text":1380},{"id":1443,"depth":398,"text":1444},{"id":1463,"depth":398,"text":1464},{"id":1480,"depth":398,"text":1481},{"id":1524,"depth":398,"text":1525},{"id":1535,"depth":237,"text":2090},"border-aware-padding — quando a borda rouba espaço da borda",{"id":1622,"depth":237,"text":1623},{"id":1699,"depth":237,"text":1700},{"id":1873,"depth":237,"text":2094},"fui-content — o wrapper que dá nome ao projeto",{"id":2034,"depth":237,"text":2035},{"id":2044,"depth":237,"text":2045},[2098,2099],"open-source","design-system","Pessoal","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-cover.png","2026-07-12","Design system SCSS que estiliza HTML nativo com cinco morfismos intercambiáveis — glass, clay, neu, skeu e flat — contraste WCAG AA calculado em compile-time e zero classe extra no HTML. É a lib que veste este próprio portfólio.","md","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-detail.png",[2107],{"label":2108,"url":2109},"github.com\u002FAllanGBOliveira\u002Ffront-ui-scss-themes","https:\u002F\u002Fgithub.com\u002FAllanGBOliveira\u002Ffront-ui-scss-themes",{},"\u002Fprojects\u002Ffui-content","2026 – atual",[2114,2115],"Criador","Mantenedor",{"title":5,"description":2103},[72,92,105,2118,131,137,2119,149],"sass-true (Jest)","Docker","in-development","projects\u002Ffui-content",[2123,2124,2125,85,2126,2127],"SCSS","Design System","Sass","Acessibilidade","Open Source","HsBoi4AmNL3A4kn7DcR9GNw-4SgXwBuNM-X6oHG6F_A",{"path":2130,"title":2131,"order":887},"\u002Fprojects\u002F3c-disparos-em-massa","Disparos em Massa · Grupo 3C",{"path":2133,"title":2134,"order":930},"\u002Fprojects\u002Fao-dev","ao.dev",1784312150195]