Files
amare/app/Providers/Filament/AdminPanelProvider.php
Manoel Freitas 63c0269489 feat: aplicar identidade Heritage Editorial ao painel Filament (MAN-118) (#40)
* feat: aplicar identidade Heritage Editorial ao painel Filament

O painel admin ainda usava os defaults de fábrica do Filament (Amber,
Zinc, Inter Variable, dark mode ligado) enquanto o site público já
seguia o design system Heritage Editorial há várias entregas — a
inconsistência ficava evidente para quem navegava entre as duas áreas
e não havia nenhum teste travando a configuração do painel.

- Cores primary/gray/danger/warning/success viram arrays explícitos de
  11 tons (não Color::hex()/string), a única forma de preservar os
  hexadecimais exatos do DESIGN.md — Color::hex() decompõe a cor e
  remonta lightness/chroma por uma tabela fixa, perdendo a cor real.
  Os tons foram escolhidos verificando empiricamente com
  ButtonComponentColorMap qual shade o botão sólido realmente usa
  (600/500/50), e o tom 50 do primary é um verde-oliva claro (não
  branco puro) porque BadgeComponent usa bg-color-50 diretamente e um
  branco puro tornaria o badge quase invisível sobre o Papel Marfim.
- EB Garamond self-hosted via LocalFontProvider (sans/mono/serif), sem
  reintroduzir uma requisição externa ao fonts.bunny.net — o hook
  HEAD_END reaproveita o mesmo <x-fonts /> do site público.
- Novo tema Vite (resources/css/filament/admin/theme.css) zera radius
  e shadow só no escopo do Filament, incluindo a variável --radius
  "bare" (usada em ~227 regras do próprio Filament) e um override
  manual para o CSS pré-compilado do tooltip Tippy.js, que não é
  alcançado pelo @theme.
- Dark mode desativado: o Heritage Editorial é uma paleta única.
- Teste de regressão (AdminPanelBrandParityTest) pinando cores, fontes
  e configuração do tema, incluindo uma renderização real de
  /admin/login — foi essa renderização que pegou um bug real: passar
  a família já entre aspas simples quebrava a declaração CSS
  (--font-family: ''EB Garamond''), silenciosamente caindo para
  ui-sans-serif.

Bundle público verificado byte a byte: nenhuma declaração CSS
existente mudou de valor (apenas classes novas e não usadas pelo site
público foram adicionadas ao app.css, um efeito colateral inerte de
ter uma segunda entrada Tailwind no mesmo build do Vite).

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

* fix(build): copiar CSS do Filament no estágio frontend e neutralizar sombras

Dois defeitos encontrados ao revisar a paridade visual do painel.

O primeiro impedia o build da imagem. O `resources/css/filament/admin/
theme.css` importa o CSS não compilado do próprio Filament, e o estágio
`frontend` do Dockerfile copia apenas `package.json`, `vite.config.js`,
`resources` e `public` — nunca `vendor`. O `npm run build` falhava na
resolução do import, o que derrubaria os jobs `container` e `browser` do
CI. Agora a subárvore `vendor/filament` é copiada do estágio do composer,
em vez de todo o `vendor`, para manter o contexto pequeno.

O segundo é silencioso e mais interessante. O plugin do Tailwind compartilha
um único contexto entre todas as entradas do build, então o `@source
app/Filament/**` da entrada do painel torna visível o uso de `shadow-*`
do Filament e o Tailwind passa a emitir seus valores padrão de
`--shadow-sm/md/lg` também no bundle público. Verificado por diff do
`app.css` construído com e sem a entrada do tema.

Nada no site público usa utilitário de sombra hoje, então a renderização
não muda e os baselines visuais seguem válidos. Mas deixar valores reais
de sombra definidos no CSS que vai para produção permitiria que um
`shadow-sm` futuro em elemento público violasse a Tonal Layer Rule do
DESIGN.md em silêncio — e o HeritageEditorialTokensTest só inspeciona
arquivos de origem, então não pegaria.

Os tokens de sombra passam a ser fixados em transparente no `@theme` do
`app.css`, com teste que falha se algum deixar de ser. A regra continua
verdadeira no artefato que realmente ship.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code

---------

Co-authored-by: manoel.neto <manoel.neto@creditas.com>
2026-08-10 11:09:01 -03:00

233 lines
8.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Providers\Filament;
use App\Filament\Pages\Auth\RequestPasswordReset;
use Filament\FontProviders\LocalFontProvider;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages\Dashboard;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\View\PanelsRenderHook;
use Filament\Widgets\AccountWidget;
use Filament\Widgets\FilamentInfoWidget;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class AdminPanelProvider extends PanelProvider
{
/**
* Heritage Editorial — Oliva Herança primary ramp.
*
* Filament needs an explicit 11-stop shade array to preserve exact brand
* hexes (a bare string/`Color::hex()` runs the value through
* `Color::generatePalette()`, which discards its lightness and maps a
* fixed per-shade lightness/chroma table instead — see MAN-118 survey).
* Shade 600 pins `#556B2F` (Oliva Herança) and shade 700 pins `#3E5219`
* (Oliva Profundo). `Filament\Support\View\Components\ColorMaps\
* ButtonComponentColorMap` was used to verify empirically (not assumed)
* that a solid primary button resolves to `bg: 600, hover:bg: 500, text:
* 50` at 5.42:1 / 4.57:1 contrast (WCAG AA). Shade 50 is a pale olive
* tint rather than pure white deliberately: `BadgeComponent`/
* `badge.css` apply `bg-color-50` directly as a badge's background
* (independent of the button map above), and a pure-white 50 would
* render a "primary" badge as an all-but-invisible pill against the
* Papel Marfim (`#FBF9F4`) panel body.
*
* @return array<int, string>
*/
private function primaryColor(): array
{
return [
50 => '#F3F5EC',
100 => '#E7EBDA',
200 => '#D4DCBE',
300 => '#8B9D77', // Sálvia Silenciosa
400 => '#6E8354',
500 => '#61763A',
600 => '#556B2F', // Oliva Herança
700 => '#3E5219', // Oliva Profundo
800 => '#2E3D13',
900 => '#1F290D',
950 => '#141B08',
];
}
/**
* Heritage Editorial — paper/ink neutral ramp, replacing Filament's
* stock Zinc gray so panel chrome (sidebar, topbar, body background,
* borders) matches the public site's paper tones instead of true gray.
* Anchored at the exact tokens where DESIGN.md defines them (50/100/200
* = Papel Marfim/Profundo/Arquivo, 300 = Linha Botânica, 500 = Tinta
* Suave, 900 = Tinta Oliva); the remaining stops are interpolated to
* keep the ramp monotonic.
*
* @return array<int, string>
*/
private function grayColor(): array
{
return [
50 => '#FBF9F4', // Papel Marfim
100 => '#F0EEE9', // Papel Profundo
200 => '#E4E2DD', // Papel Arquivo
300 => '#C5C8B8', // Linha Botânica
400 => '#919486',
500 => '#5D6155', // Tinta Suave
600 => '#43453D',
700 => '#32342E',
800 => '#252622',
900 => '#1B1C19', // Tinta Oliva
950 => '#121210',
];
}
/**
* Semantic ramps built the same way as the primary/gray ramps above:
* an explicit 11-stop array (never `Color::hex()`) anchored so the
* existing `--amare-color-{success,warning,error}` hex lands exactly on
* shade 600 — verified against `ButtonComponentColorMap` to resolve to
* `bg: 600` with white text at WCAG AA contrast, same as primary.
*
* @return array<int, string>
*/
private function dangerColor(): array
{
return [
50 => '#F7EDED',
100 => '#EBD1D1',
200 => '#D8A8A8',
300 => '#C88484',
400 => '#B85F5F',
500 => '#A73B3B',
600 => '#991B1B',
700 => '#7D1616',
800 => '#651212',
900 => '#4C0E0E',
950 => '#3A0A0A',
];
}
/**
* @return array<int, string>
*/
private function warningColor(): array
{
return [
50 => '#F6F0EC',
100 => '#E9D9CF',
200 => '#D6B6A3',
300 => '#C4987D',
400 => '#B37956',
500 => '#A15B30',
600 => '#92400E',
700 => '#78340B',
800 => '#602A09',
900 => '#492007',
950 => '#371805',
];
}
/**
* @return array<int, string>
*/
private function successColor(): array
{
return [
50 => '#ECF3EF',
100 => '#D0E0D6',
200 => '#A6C4B2',
300 => '#81AC91',
400 => '#5C9371',
500 => '#377B50',
600 => '#166534',
700 => '#12532B',
800 => '#0F4322',
900 => '#0B321A',
950 => '#082614',
];
}
public function panel(Panel $panel): Panel
{
return $panel
->default()
->id('admin')
->path('admin')
->login()
->passwordReset(requestAction: RequestPasswordReset::class)
->brandName('Amare Assessoria')
->brandLogo(fn (): string => asset('brand/lockup-on-light.webp'))
->brandLogoHeight('2rem')
->colors([
'primary' => $this->primaryColor(),
'gray' => $this->grayColor(),
'danger' => $this->dangerColor(),
'warning' => $this->warningColor(),
'success' => $this->successColor(),
])
// Heritage Editorial is a single paper palette by design (see
// DESIGN.md) — no dark-mode variant exists, so the switcher is
// removed rather than left pointing at an unstyled dark theme.
->darkMode(false)
// Self-hosted EB Garamond, same family as the public site.
// LocalFontProvider is pinned explicitly: HasFont::getFontProvider()
// otherwise defaults a custom family to BunnyFontProvider, which
// would emit a live request to fonts.bunny.net from the panel.
// LocalFontProvider renders no <link>/@font-face itself (no $url
// given), so the actual face comes from the render hook below,
// reusing the public site's own <x-fonts /> component/manifest.
//
// The monospace and serif faces are set too, not just the base
// (sans) family: Filament exposes them via the separate
// ->monoFont()/->serifFont() calls below, and the `KeyValue`
// field (used in ManageSiteSettings) renders in the monospace
// face directly — left unset, that field would silently fall
// back to a system monospace stack instead of EB Garamond,
// breaking Heritage Editorial's single-voice typography.
->font('EB Garamond', provider: LocalFontProvider::class)
->monoFont('EB Garamond', provider: LocalFontProvider::class)
->serifFont('EB Garamond', provider: LocalFontProvider::class)
->renderHook(
PanelsRenderHook::HEAD_END,
fn (): string => view('components.fonts')->render(),
)
// Compiled Filament theme entry — sharp corners, flat surfaces
// (see resources/css/filament/admin/theme.css for the full
// rationale and the two vendored-CSS exceptions it can't reach).
->viteTheme('resources/css/filament/admin/theme.css')
->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources')
->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages')
->pages([
Dashboard::class,
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
->widgets([
AccountWidget::class,
FilamentInfoWidget::class,
])
->middleware([
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
PreventRequestForgery::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}