10 Commits

Author SHA1 Message Date
Felipe Coutinho
c5df97f7aa chore(setup): reduzir logo e colocar nome ASCII lado a lado
Logo reduzido de 19 para 10 linhas (seleção dos frames-chave).
Nome do projeto em ASCII art posicionado ao lado direito do logo,
centralizado verticalmente. Tagline abaixo do bloco.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 14:05:53 +00:00
Felipe Coutinho
3476fda4db chore(setup): adicionar banner ASCII do logo e corrigir script db:extensions
Substitui o header simples pelo logo em ASCII art na cor primária
(laranja) com nome e tagline centralizados. Corrige chamada
db:enableExtensions → db:extensions após renomeio do script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:55:26 +00:00
Felipe Coutinho
519b673ae5 chore(release): publicar versão 2.1.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:44:21 +00:00
Felipe Coutinho
303b8bedd4 chore(config): limpeza de tsconfig.json e .vscode/settings.json
Reformata arrays no tsconfig para multi-line. Remove configurações
obsoletas do .vscode (explorerExclude.backup, eslint.enable,
typescript.preferences.organizeImportsCollation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:44:17 +00:00
Felipe Coutinho
f2b9b16896 chore(package): renomear scripts e remover dependências Vercel
Renomeia mockup→db:seed, db:enableExtensions→db:extensions e remove
o script dev-env. Remove @vercel/analytics e @vercel/speed-insights.
Atualiza README com o novo nome do script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:44:13 +00:00
Felipe Coutinho
6eba35542b chore(logo): remover prop showVersion e atualizar logo_small.png
Remove a prop showVersion do componente Logo e seu uso na sidebar.
Aplica iconFilterClass também no variant compact. Atualiza a imagem
logo_small.png.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:44:10 +00:00
Felipe Coutinho
f5e95ffba6 chore(analytics): substituir Vercel Analytics por Umami self-hosted
Remove @vercel/analytics e @vercel/speed-insights e adiciona o script
do Umami self-hosted no layout raiz, restrito ao domínio openmonetis.com.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:44:03 +00:00
Felipe Coutinho
a75bb86eec refactor(navbar): extrair NavbarShell e adicionar variante navbar no Button
Unifica a estrutura da navbar entre o app e a landing page via novo
componente NavbarShell. Centraliza estilos de botões da navbar na
variante `navbar` do Button, eliminando nav-styles.ts e as classes
inline duplicadas. AnimatedThemeToggler, RefreshPageButton e MobileNav
passam a aceitar prop `variant` para adaptar ao contexto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:43:59 +00:00
Felipe Coutinho
a3b858621f fix(transactions): preservar período salvo ao editar lançamento de cartão
No modal de edição, o período não era recalculado com base no fechamento
do cartão, garantindo que o valor salvo no banco seja sempre exibido.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 17:51:43 +00:00
Felipe Coutinho
fee2a2c9f5 fix(build): corrigir erros de tipo introduzidos pelo TypeScript 6.0
- Adiciona src/global.d.ts com declare module '*.css' para suportar
  side-effect imports de CSS com moduleResolution bundler
- Adiciona ignoreDeprecations "6.0" no tsconfig para silenciar aviso
  de depreciação do baseUrl (será removido no TS 7)
- Corrige cast de .message em better-auth 1.5.6, cujo tipo passou a
  ser string | RawError em chamadas de passkey

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 15:21:56 +00:00
26 changed files with 231 additions and 254 deletions

View File

@@ -12,7 +12,6 @@
"**/.next": true, "**/.next": true,
".next": true ".next": true
}, },
"explorerExclude.backup": {},
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
@@ -25,9 +24,7 @@
"[json]": { "[json]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
}, },
"eslint.enable": false,
"prettier.enable": false, "prettier.enable": false,
"typescript.preferences.organizeImportsCollation": "ordinal",
"editor.fontSize": 15, "editor.fontSize": 15,
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.json-language-features"

View File

@@ -7,6 +7,27 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
## [Unreleased] ## [Unreleased]
## [2.1.1] - 2026-03-29
### Adicionado
- Navbar: novo componente `NavbarShell` que unifica a estrutura da barra de navegação entre o app e a landing page
- UI: nova variante `navbar` no componente `Button`, centralizando os estilos de botões usados dentro da navbar
- Analytics: integração com Umami self-hosted via script tag no layout raiz
### Alterado
- Navbar: `AnimatedThemeToggler` e `RefreshPageButton` passam a aceitar prop `variant` para adaptar estilos ao contexto (navbar ou sidebar)
- Navbar: estilos inline duplicados de `nav-styles.ts` migrados para a variante `navbar` do Button
- Logo: prop `showVersion` removida; prop `colorIcon` passa a aplicar filtro de cor também no variant `compact`
- Scripts: `mockup` renomeado para `db:seed`; `db:enableExtensions` renomeado para `db:extensions`; script `dev-env` removido
- Landing: `MobileNav` simplificado com a remoção da prop `triggerClassName`
### Removido
- Navbar: arquivo `nav-styles.ts` removido após migração dos estilos para a variante `navbar`
- Dependências: `@vercel/analytics` e `@vercel/speed-insights` removidos (substituídos pelo Umami self-hosted)
## [2.1.0] - 2026-03-28 ## [2.1.0] - 2026-03-28
### Adicionado ### Adicionado

View File

@@ -156,7 +156,7 @@ O script irá:
```bash ```bash
docker compose up db -d docker compose up db -d
pnpm db:enableExtensions pnpm db:extensions
``` ```
4. **Execute as migrations e inicie** 4. **Execute as migrations e inicie**

View File

@@ -1,11 +1,10 @@
{ {
"name": "openmonetis", "name": "openmonetis",
"version": "2.1.0", "version": "2.1.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"dev-env": "tsx scripts/dev.ts", "db:seed": "tsx scripts/mock-data.ts",
"mockup": "tsx scripts/mock-data.ts",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "biome check .", "lint": "biome check .",
@@ -14,7 +13,7 @@
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:enableExtensions": "tsx scripts/postgres/enable-extensions.ts", "db:extensions": "tsx scripts/postgres/enable-extensions.ts",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"docker:up": "docker compose up --build", "docker:up": "docker compose up --build",
"docker:up:db": "docker compose up -d db", "docker:up:db": "docker compose up -d db",
@@ -61,8 +60,6 @@
"@remixicon/react": "4.9.0", "@remixicon/react": "4.9.0",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "^3.13.23", "@tanstack/react-virtual": "^3.13.23",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"ai": "^6.0.141", "ai": "^6.0.141",
"better-auth": "1.5.6", "better-auth": "1.5.6",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",

71
pnpm-lock.yaml generated
View File

@@ -104,12 +104,6 @@ importers:
'@tanstack/react-virtual': '@tanstack/react-virtual':
specifier: ^3.13.23 specifier: ^3.13.23
version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@vercel/analytics':
specifier: ^2.0.1
version: 2.0.1(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
'@vercel/speed-insights':
specifier: ^2.0.0
version: 2.0.0(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
ai: ai:
specifier: ^6.0.141 specifier: ^6.0.141
version: 6.0.141(zod@4.3.6) version: 6.0.141(zod@4.3.6)
@@ -2666,65 +2660,10 @@ packages:
'@types/whatwg-url@13.0.0': '@types/whatwg-url@13.0.0':
resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==}
'@vercel/analytics@2.0.1':
resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==}
peerDependencies:
'@remix-run/react': ^2
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
nuxt: '>= 3'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@remix-run/react':
optional: true
'@sveltejs/kit':
optional: true
next:
optional: true
nuxt:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
'@vercel/oidc@3.1.0': '@vercel/oidc@3.1.0':
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
'@vercel/speed-insights@2.0.0':
resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==}
peerDependencies:
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
nuxt: '>= 3'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@sveltejs/kit':
optional: true
next:
optional: true
nuxt:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
adler-32@1.3.1: adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
@@ -6557,18 +6496,8 @@ snapshots:
'@types/webidl-conversions': 7.0.3 '@types/webidl-conversions': 7.0.3
optional: true optional: true
'@vercel/analytics@2.0.1(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
optionalDependencies:
next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
'@vercel/oidc@3.1.0': {} '@vercel/oidc@3.1.0': {}
'@vercel/speed-insights@2.0.0(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
optionalDependencies:
next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4
adler-32@1.3.1: {} adler-32@1.3.1: {}
ai@6.0.141(zod@4.3.6): ai@6.0.141(zod@4.3.6):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -21,6 +21,7 @@ const c = {
red: "\x1b[31m", red: "\x1b[31m",
yellow: "\x1b[33m", yellow: "\x1b[33m",
cyan: "\x1b[36m", cyan: "\x1b[36m",
orange: "\x1b[38;5;214m",
}; };
const sym = { const sym = {
@@ -81,10 +82,38 @@ function abort(msg) {
// ─── Header ────────────────────────────────────────────────────────────────── // ─── Header ──────────────────────────────────────────────────────────────────
console.log(` const logoLines = [
${c.bold}${c.cyan} OpenMonetis — Setup${c.reset} ".............................+@@@@@@@@@@=.............................",
${c.dim}Gestão financeira self-hosted${c.reset} ".............................@@@@@@@@@@@:.............................",
`); "...................+@@@@@@*-:@@@@@@@@@@%...=@@@@@@-...................",
"..................@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%..................",
"................=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+................",
"...................-=+%@@@@@@@@@@@@@@@@@@@@@*:........................",
".......................#@@@@@@@@@@@@@@@@@@@@@@@+......................",
"....................%@@@@@@@@@@@@@%#@@@@@@@@@@@@*.....................",
"....................+@@@@@@@@@@@......*@@@@@@#........................",
".........................:#@@=...........+#...........................",
];
const nameLines = [
" ___ __ __ _ _ ",
" / _ \\ _ __ ___ _ __ | \\/ | ___ _ __ ___| |_(_)___ ",
" | | | | '_ \\ / _ \\ '_ \\| |\\/| |/ _ \\| '_ \\ / _ \\ __| / __|",
" | |_| | |_) | __/ | | | | | | (_) | | | | __/ |_| \\__ \\",
" \\___/| .__/ \\___|_| |_|_| |_|\\___/|_| |_|\\___|\\__|_|___/",
" |_| ",
];
const nameStart = Math.floor((logoLines.length - nameLines.length) / 2);
console.log();
for (let i = 0; i < logoLines.length; i++) {
const logoCol = c.orange + logoLines[i].replaceAll(".", " ").substring(14, 56).padEnd(42) + c.reset;
const nameIdx = i - nameStart;
const nameCol = nameIdx >= 0 && nameIdx < nameLines.length ? nameLines[nameIdx] : "";
console.log(logoCol + " " + nameCol);
}
console.log(`\n${" ".repeat(46)}${c.dim}Gestão financeira · self-hosted${c.reset}\n`);
// ─── ETAPA 1: Verificações do sistema ──────────────────────────────────────── // ─── ETAPA 1: Verificações do sistema ────────────────────────────────────────
@@ -329,7 +358,7 @@ if (useLocalDocker) {
// Extensões // Extensões
s = spinner("Habilitando extensões do banco..."); s = spinner("Habilitando extensões do banco...");
try { try {
run("pnpm db:enableExtensions", { cwd: targetDir }); run("pnpm db:extensions", { cwd: targetDir });
s.stop("Extensões habilitadas"); s.stop("Extensões habilitadas");
} catch { } catch {
s.fail("Falha ao habilitar extensões"); s.fail("Falha ao habilitar extensões");

View File

@@ -17,7 +17,6 @@ import {
extraFeatures, extraFeatures,
getMetricsItems, getMetricsItems,
mainFeatures, mainFeatures,
navbarActionClassName,
navLinks, navLinks,
pwaHighlights, pwaHighlights,
stackItems, stackItems,
@@ -27,6 +26,7 @@ import { landingImages } from "@/features/landing/images";
import { fetchGitHubStats } from "@/features/landing/queries"; import { fetchGitHubStats } from "@/features/landing/queries";
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler"; import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
import { Logo } from "@/shared/components/logo"; import { Logo } from "@/shared/components/logo";
import { NavbarShell } from "@/shared/components/navigation/navbar/navbar-shell";
import { Badge } from "@/shared/components/ui/badge"; import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
import { Card, CardContent } from "@/shared/components/ui/card"; import { Card, CardContent } from "@/shared/components/ui/card";
@@ -50,10 +50,7 @@ export default async function Page() {
return ( return (
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
{/* Navigation */} {/* Navigation */}
<header className="sticky top-0 z-50 flex h-16 shrink-0 items-center bg-primary"> <NavbarShell>
<div className="relative z-10 max-w-8xl mx-auto px-4 w-full flex h-full items-center justify-between">
<Logo variant="compact" invertTextOnDark={false} />
{/* Center Navigation Links */} {/* Center Navigation Links */}
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2"> <nav className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
{navLinks.map(({ href, label }) => ( {navLinks.map(({ href, label }) => (
@@ -67,8 +64,8 @@ export default async function Page() {
))} ))}
</nav> </nav>
<nav className="flex items-center gap-2 md:gap-3"> <nav className="ml-auto flex items-center gap-2 md:gap-3">
<AnimatedThemeToggler className={navbarActionClassName} /> <AnimatedThemeToggler variant="navbar" />
{!isPublicDomain && {!isPublicDomain &&
(session?.user ? ( (session?.user ? (
<Link prefetch href="/dashboard" className="hidden md:block"> <Link prefetch href="/dashboard" className="hidden md:block">
@@ -104,11 +101,9 @@ export default async function Page() {
<MobileNav <MobileNav
isPublicDomain={isPublicDomain} isPublicDomain={isPublicDomain}
isLoggedIn={!!session?.user} isLoggedIn={!!session?.user}
triggerClassName="border border-black/10 text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20"
/> />
</nav> </nav>
</div> </NavbarShell>
</header>
{/* Hero Section */} {/* Hero Section */}
<section className="relative overflow-hidden pt-14 md:pt-20 lg:pt-24 pb-0"> <section className="relative overflow-hidden pt-14 md:pt-20 lg:pt-24 pb-0">

View File

@@ -1,5 +1,3 @@
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { ThemeProvider } from "@/shared/components/providers/theme-provider"; import { ThemeProvider } from "@/shared/components/providers/theme-provider";
import { Toaster } from "@/shared/components/ui/sonner"; import { Toaster } from "@/shared/components/ui/sonner";
@@ -28,14 +26,18 @@ export default function RootLayout({
> >
<head> <head>
<meta name="apple-mobile-web-app-title" content="OpenMonetis" /> <meta name="apple-mobile-web-app-title" content="OpenMonetis" />
<script
defer
src="https://umami.felipecoutinho.com/script.js"
data-website-id="ea438854-a014-42ea-b416-0a8321471f0f"
data-domains="openmonetis.com"
/>
</head> </head>
<body className="subpixel-antialiased" suppressHydrationWarning> <body className="subpixel-antialiased" suppressHydrationWarning>
<ThemeProvider attribute="class" defaultTheme="light"> <ThemeProvider attribute="class" defaultTheme="light">
{children} {children}
<Toaster position="top-right" /> <Toaster position="top-right" />
</ThemeProvider> </ThemeProvider>
<Analytics />
<SpeedInsights />
</body> </body>
</html> </html>
); );

View File

@@ -125,7 +125,7 @@ export function LoginForm({ className, ...props }: DivProps) {
}); });
if (passkeyError) { if (passkeyError) {
setError(passkeyError.message || "Erro ao entrar com passkey."); setError((passkeyError.message as string) || "Erro ao entrar com passkey.");
setLoadingPasskey(false); setLoadingPasskey(false);
} }
} }

View File

@@ -24,24 +24,18 @@ const navLinks = [
interface MobileNavProps { interface MobileNavProps {
isPublicDomain: boolean; isPublicDomain: boolean;
isLoggedIn: boolean; isLoggedIn: boolean;
triggerClassName?: string;
} }
export function MobileNav({ export function MobileNav({ isPublicDomain, isLoggedIn }: MobileNavProps) {
isPublicDomain,
isLoggedIn,
triggerClassName,
}: MobileNavProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<div className="md:hidden"> <div className="md:hidden">
<Button <Button
variant="ghost" variant="navbar"
size="icon" size="icon-sm"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
aria-label="Abrir menu" aria-label="Abrir menu"
className={triggerClassName}
> >
<RiMenuLine className="size-5" /> <RiMenuLine className="size-5" />
</Button> </Button>

View File

@@ -35,9 +35,6 @@ export type FeatureItem = {
colorVar: string; colorVar: string;
}; };
export const navbarActionClassName =
"border-black/10 bg-transparent text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20 data-[state=open]:bg-black/10 data-[state=open]:text-black";
export const navLinks = [ export const navLinks = [
{ href: "#telas", label: "conheça as telas" }, { href: "#telas", label: "conheça as telas" },
{ href: "#funcionalidades", label: "funcionalidades" }, { href: "#funcionalidades", label: "funcionalidades" },

View File

@@ -73,7 +73,7 @@ export function PasskeysForm() {
const { data, error: fetchError } = const { data, error: fetchError } =
await authClient.passkey.listUserPasskeys(); await authClient.passkey.listUserPasskeys();
if (fetchError) { if (fetchError) {
setError(fetchError.message || "Erro ao carregar passkeys."); setError((fetchError.message as string) || "Erro ao carregar passkeys.");
return; return;
} }
setPasskeys( setPasskeys(
@@ -111,7 +111,7 @@ export function PasskeysForm() {
name: addName.trim() || undefined, name: addName.trim() || undefined,
}); });
if (addError) { if (addError) {
setError(addError.message || "Erro ao registrar passkey."); setError((addError.message as string) || "Erro ao registrar passkey.");
return; return;
} }
setAddName(""); setAddName("");
@@ -134,7 +134,7 @@ export function PasskeysForm() {
name: editName.trim(), name: editName.trim(),
}); });
if (renameError) { if (renameError) {
setError(renameError.message || "Erro ao renomear passkey."); setError((renameError.message as string) || "Erro ao renomear passkey.");
return; return;
} }
setEditingId(null); setEditingId(null);
@@ -156,7 +156,7 @@ export function PasskeysForm() {
id: deleteId, id: deleteId,
}); });
if (deleteError) { if (deleteError) {
setError(deleteError.message || "Erro ao remover passkey."); setError((deleteError.message as string) || "Erro ao remover passkey.");
return; return;
} }
setDeleteId(null); setDeleteId(null);

View File

@@ -116,8 +116,9 @@ export function TransactionDialog({
}, },
); );
// Derive credit card period on open when cardId is pre-filled // Derive credit card period on open when cardId is pre-filled (create only)
if ( if (
mode !== "update" &&
initial.paymentMethod === "Cartão de crédito" && initial.paymentMethod === "Cartão de crédito" &&
initial.cardId && initial.cardId &&
initial.purchaseDate initial.purchaseDate

1
src/global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module "*.css";

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { RiMoonClearLine, RiSunLine } from "@remixicon/react"; import { RiMoonClearLine, RiSunLine } from "@remixicon/react";
import type { VariantProps } from "class-variance-authority";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { buttonVariants } from "@/shared/components/ui/button"; import { buttonVariants } from "@/shared/components/ui/button";
@@ -13,11 +14,13 @@ import { cn } from "@/shared/utils/ui";
interface AnimatedThemeTogglerProps interface AnimatedThemeTogglerProps
extends React.ComponentPropsWithoutRef<"button"> { extends React.ComponentPropsWithoutRef<"button"> {
duration?: number; duration?: number;
variant?: VariantProps<typeof buttonVariants>["variant"];
} }
export const AnimatedThemeToggler = ({ export const AnimatedThemeToggler = ({
className, className,
duration = 400, duration = 400,
variant = "ghost",
...props ...props
}: AnimatedThemeTogglerProps) => { }: AnimatedThemeTogglerProps) => {
const [isDark, setIsDark] = useState(false); const [isDark, setIsDark] = useState(false);
@@ -84,10 +87,10 @@ export const AnimatedThemeToggler = ({
onClick={toggleTheme} onClick={toggleTheme}
data-state={isDark ? "dark" : "light"} data-state={isDark ? "dark" : "light"}
className={cn( className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }), buttonVariants({ variant, size: "icon-sm" }),
"group relative text-muted-foreground transition-all duration-200", "group relative transition-all duration-200",
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40", variant === "ghost" &&
"data-[state=open]:bg-accent/60 data-[state=open]:text-foreground", "text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40 data-[state=open]:bg-accent/60 data-[state=open]:text-foreground",
className, className,
)} )}
{...props} {...props}

View File

@@ -1,20 +1,20 @@
import Image from "next/image"; import Image from "next/image";
import { version } from "@/package.json";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
interface LogoProps { interface LogoProps {
variant?: "full" | "small" | "compact"; variant?: "full" | "small" | "compact";
className?: string; className?: string;
showVersion?: boolean; /** Apenas nos variants "full" e "compact" */
invertTextOnDark?: boolean; invertTextOnDark?: boolean;
/** Exibe o ícone na cor original, sem filtro preto */ /** Exibe o ícone na cor original, sem filtro preto. Apenas nos variants "full" e "compact" */
colorIcon?: boolean; colorIcon?: boolean;
} }
const iconFilterClass = "brightness-0 saturate-0";
export function Logo({ export function Logo({
variant = "full", variant = "full",
className, className,
showVersion = false,
invertTextOnDark = true, invertTextOnDark = true,
colorIcon = false, colorIcon = false,
}: LogoProps) { }: LogoProps) {
@@ -26,10 +26,7 @@ export function Logo({
alt="OpenMonetis" alt="OpenMonetis"
width={32} width={32}
height={32} height={32}
className={cn( className={cn("object-contain", !colorIcon && iconFilterClass)}
"object-contain",
!colorIcon && "brightness-0 saturate-0",
)}
priority priority
/> />
<Image <Image
@@ -67,7 +64,7 @@ export function Logo({
alt="OpenMonetis" alt="OpenMonetis"
width={28} width={28}
height={28} height={28}
className="object-contain" className={cn("object-contain", !colorIcon && iconFilterClass)}
priority priority
/> />
<Image <Image
@@ -78,11 +75,6 @@ export function Logo({
className={cn("object-contain", invertTextOnDark && "dark:invert")} className={cn("object-contain", invertTextOnDark && "dark:invert")}
priority priority
/> />
{showVersion && (
<span className="text-[9px] font-medium text-muted-foreground">
{version}
</span>
)}
</div> </div>
); );
} }

View File

@@ -1,10 +1,9 @@
import Link from "next/link";
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler"; import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
import { Logo } from "@/shared/components/logo";
import { NotificationBell } from "@/shared/components/navigation/navbar/notification-bell"; import { NotificationBell } from "@/shared/components/navigation/navbar/notification-bell";
import { RefreshPageButton } from "@/shared/components/refresh-page-button"; import { RefreshPageButton } from "@/shared/components/refresh-page-button";
import type { DashboardNotificationsSnapshot } from "@/shared/lib/types/notifications"; import type { DashboardNotificationsSnapshot } from "@/shared/lib/types/notifications";
import { NavMenu } from "./nav-menu"; import { NavMenu } from "./nav-menu";
import { NavbarShell } from "./navbar-shell";
import { NavbarUser } from "./navbar-user"; import { NavbarUser } from "./navbar-user";
type AppNavbarProps = { type AppNavbarProps = {
@@ -19,9 +18,6 @@ type AppNavbarProps = {
notificationsSnapshot: DashboardNotificationsSnapshot; notificationsSnapshot: DashboardNotificationsSnapshot;
}; };
const navbarActionClassName =
"border-black/10 bg-transparent text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20 data-[state=open]:bg-black/10 data-[state=open]:text-black";
export function AppNavbar({ export function AppNavbar({
user, user,
pagadorAvatarUrl, pagadorAvatarUrl,
@@ -29,14 +25,8 @@ export function AppNavbar({
notificationsSnapshot, notificationsSnapshot,
}: AppNavbarProps) { }: AppNavbarProps) {
return ( return (
<header className="fixed top-0 left-0 right-0 z-50 flex h-16 shrink-0 items-center bg-primary"> <NavbarShell logoHref="/dashboard" fixed>
<div className="relative z-10 mx-auto flex h-full w-full max-w-8xl items-center gap-4 px-4">
<Link href="/dashboard" className="shrink-0 mr-1">
<Logo variant="compact" invertTextOnDark={false} />
</Link>
<NavMenu /> <NavMenu />
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<NotificationBell <NotificationBell
notifications={notificationsSnapshot.notifications} notifications={notificationsSnapshot.notifications}
@@ -45,12 +35,10 @@ export function AppNavbar({
budgetNotifications={notificationsSnapshot.budgetNotifications} budgetNotifications={notificationsSnapshot.budgetNotifications}
preLancamentosCount={preLancamentosCount} preLancamentosCount={preLancamentosCount}
/> />
<RefreshPageButton className={navbarActionClassName} /> <RefreshPageButton variant="navbar" />
<AnimatedThemeToggler className={navbarActionClassName} /> <AnimatedThemeToggler variant="navbar" />
</div> </div>
<NavbarUser user={user} pagadorAvatarUrl={pagadorAvatarUrl} /> <NavbarUser user={user} pagadorAvatarUrl={pagadorAvatarUrl} />
</div> </NavbarShell>
</header>
); );
} }

View File

@@ -20,13 +20,18 @@ import {
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
} from "@/shared/components/ui/sheet"; } from "@/shared/components/ui/sheet";
import { cn } from "@/shared/utils/ui";
import { MobileLink, MobileSectionLabel } from "./mobile-link"; import { MobileLink, MobileSectionLabel } from "./mobile-link";
import { NavDropdown } from "./nav-dropdown"; import { NavDropdown } from "./nav-dropdown";
import { NAV_SECTIONS } from "./nav-items"; import { NAV_SECTIONS } from "./nav-items";
import { NavPill } from "./nav-pill"; import { NavPill } from "./nav-pill";
import { triggerActiveClass, triggerClass } from "./nav-styles";
import { MobileTools, NavToolsDropdown } from "./nav-tools"; import { MobileTools, NavToolsDropdown } from "./nav-tools";
const triggerClass =
"h-8! rounded-md! px-2! py-0! text-sm! font-medium! bg-transparent! shadow-none! lowercase! [&_svg]:text-current! text-black/75! hover:text-black! hover:bg-black/10! focus:text-black! focus:bg-black/10! focus-visible:ring-black/20! data-[state=open]:text-black! data-[state=open]:bg-black/10!";
const triggerActiveClass = "bg-black/15! text-black!";
export function NavMenu() { export function NavMenu() {
const pathname = usePathname(); const pathname = usePathname();
const [sheetOpen, setSheetOpen] = useState(false); const [sheetOpen, setSheetOpen] = useState(false);
@@ -55,7 +60,10 @@ export function NavMenu() {
return ( return (
<NavigationMenuItem key={section.label}> <NavigationMenuItem key={section.label}>
<NavigationMenuTrigger <NavigationMenuTrigger
className={`${triggerClass} ${isSectionActive ? triggerActiveClass : ""}`} className={cn(
triggerClass,
isSectionActive && triggerActiveClass,
)}
> >
{section.label} {section.label}
</NavigationMenuTrigger> </NavigationMenuTrigger>
@@ -82,9 +90,9 @@ export function NavMenu() {
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}> <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
variant="ghost" variant="navbar"
size="icon" size="icon-sm"
className="-order-1 border border-black/10 text-black/75 shadow-none md:hidden hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20" className="-order-1 md:hidden"
> >
<RiMenuLine className="size-5" /> <RiMenuLine className="size-5" />
<span className="sr-only">Abrir menu</span> <span className="sr-only">Abrir menu</span>

View File

@@ -1,9 +1,9 @@
"use client"; "use client";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { buttonVariants } from "@/shared/components/ui/button";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
import { NavLink } from "./nav-link"; import { NavLink } from "./nav-link";
import { linkActive, linkBase, linkIdle } from "./nav-styles";
type NavPillProps = { type NavPillProps = {
href: string; href: string;
@@ -23,7 +23,11 @@ export function NavPill({ href, preservePeriod, children }: NavPillProps) {
<NavLink <NavLink
href={href} href={href}
preservePeriod={preservePeriod} preservePeriod={preservePeriod}
className={cn(linkBase, isActive ? linkActive : linkIdle)} className={cn(
buttonVariants({ variant: "navbar", size: "sm" }),
"lowercase",
isActive && "bg-black/15 text-black",
)}
> >
{children} {children}
</NavLink> </NavLink>

View File

@@ -1,29 +0,0 @@
export const linkBase =
"inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-all lowercase";
export const linkIdle = "text-black/75 hover:bg-black/10 hover:text-black";
export const linkActive = "bg-black/15 text-black";
export const triggerActiveClass = ["bg-black/15!", "text-black!"].join(" ");
export const triggerClass = [
"h-8!",
"rounded-md!",
"px-2!",
"py-0!",
"text-sm!",
"font-medium!",
"bg-transparent!",
"text-black/75!",
"hover:text-black!",
"hover:bg-black/10!",
"focus:text-black!",
"focus:bg-black/10!",
"focus-visible:ring-black/20!",
"data-[state=open]:text-black!",
"data-[state=open]:bg-black/10!",
"shadow-none!",
"[&_svg]:text-current!",
"lowercase!",
].join(" ");

View File

@@ -0,0 +1,33 @@
import Link from "next/link";
import { Logo } from "@/shared/components/logo";
type NavbarShellProps = {
logoHref?: string;
fixed?: boolean;
children: React.ReactNode;
};
export function NavbarShell({
logoHref,
fixed = false,
children,
}: NavbarShellProps) {
const positionClass = fixed ? "fixed top-0 left-0 right-0" : "sticky top-0";
return (
<header
className={`${positionClass} z-50 flex h-16 shrink-0 items-center bg-primary`}
>
<div className="relative z-10 mx-auto flex h-full w-full max-w-8xl items-center gap-4 px-4">
{logoHref ? (
<Link href={logoHref} className="shrink-0">
<Logo variant="compact" invertTextOnDark={false} />
</Link>
) : (
<Logo variant="compact" invertTextOnDark={false} />
)}
{children}
</div>
</header>
);
}

View File

@@ -78,6 +78,6 @@ function LogoContent() {
const isCollapsed = state === "collapsed"; const isCollapsed = state === "collapsed";
return ( return (
<Logo variant={isCollapsed ? "small" : "full"} showVersion={!isCollapsed} /> <Logo variant={isCollapsed ? "small" : "full"} />
); );
} }

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { RiRefreshLine } from "@remixicon/react"; import { RiRefreshLine } from "@remixicon/react";
import type { VariantProps } from "class-variance-authority";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTransition } from "react"; import { useTransition } from "react";
import { buttonVariants } from "@/shared/components/ui/button"; import { buttonVariants } from "@/shared/components/ui/button";
@@ -11,10 +12,12 @@ import {
} from "@/shared/components/ui/tooltip"; } from "@/shared/components/ui/tooltip";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
type RefreshPageButtonProps = React.ComponentPropsWithoutRef<"button">; type RefreshPageButtonProps = React.ComponentPropsWithoutRef<"button"> &
Pick<VariantProps<typeof buttonVariants>, "variant">;
export function RefreshPageButton({ export function RefreshPageButton({
className, className,
variant = "ghost",
...props ...props
}: RefreshPageButtonProps) { }: RefreshPageButtonProps) {
const router = useRouter(); const router = useRouter();
@@ -36,10 +39,10 @@ export function RefreshPageButton({
aria-label="Atualizar página" aria-label="Atualizar página"
title="Atualizar página" title="Atualizar página"
className={cn( className={cn(
buttonVariants({ variant: "ghost", size: "icon-sm" }), buttonVariants({ variant, size: "icon-sm" }),
"size-8 text-muted-foreground transition-all duration-200", "transition-all duration-200",
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40", variant === "ghost" &&
"disabled:pointer-events-none disabled:opacity-50", "text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
className, className,
)} )}
{...props} {...props}

View File

@@ -19,6 +19,8 @@ const buttonVariants = cva(
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
navbar:
"bg-transparent text-black/75 shadow-none hover:bg-black/10 hover:text-black focus-visible:ring-black/20",
}, },
size: { size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3", default: "h-9 px-4 py-2 has-[>svg]:px-3",

View File

@@ -1,8 +1,13 @@
{ {
"compilerOptions": { "compilerOptions": {
"ignoreDeprecations": "6.0",
"baseUrl": ".", "baseUrl": ".",
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -20,7 +25,10 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*", "./*"] "@/*": [
"./src/*",
"./*"
]
} }
}, },
"include": [ "include": [
@@ -34,5 +42,7 @@
".next/types/**/*.ts", ".next/types/**/*.ts",
".next/dev/types/**/*.ts" ".next/dev/types/**/*.ts"
], ],
"exclude": ["node_modules"] "exclude": [
"node_modules"
]
} }