feat(v1.5.0): customização de fontes e correção de cores em tendências
Adiciona sistema de customização de fontes por usuário via CSS custom properties, com preview ao vivo e persistência no banco. Corrige lógica de cores invertida na tabela de receitas em tendências. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -5,6 +5,25 @@ Todas as mudanças notáveis deste projeto serão documentadas neste arquivo.
|
||||
O formato é baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/),
|
||||
e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR/).
|
||||
|
||||
## [1.5.0] - 2026-02-15
|
||||
|
||||
### Adicionado
|
||||
|
||||
- Customização de fontes nas preferências — fonte da interface e fonte de valores monetários configuráveis por usuário
|
||||
- 13 fontes disponíveis: AI Sans, Anthropic Sans, SF Pro Display, SF Pro Rounded, Inter, Geist Sans, Roboto, Reddit Sans, Fira Sans, Ubuntu, JetBrains Mono, Fira Code, IBM Plex Mono
|
||||
- FontProvider com preview ao vivo — troca de fonte aplica instantaneamente via CSS variables, sem necessidade de reload
|
||||
- Fontes Apple SF Pro (Display e Rounded) carregadas localmente com 4 pesos (Regular, Medium, Semibold, Bold)
|
||||
- Colunas `system_font` e `money_font` na tabela `preferencias_usuario`
|
||||
|
||||
### Corrigido
|
||||
|
||||
- Cores de variação invertidas na tabela de receitas em `/relatorios/tendencias` — aumento agora é verde (bom) e diminuição é vermelho (ruim), consistente com a semântica de receita
|
||||
|
||||
### Alterado
|
||||
|
||||
- Sistema de fontes migrado de className direto para CSS custom properties (`--font-app`, `--font-money`) via `@theme inline`
|
||||
- MoneyValues usa `var(--font-money)` em vez de classe fixa, permitindo customização
|
||||
|
||||
## [1.4.1] - 2026-02-15
|
||||
|
||||
### Adicionado
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use server";
|
||||
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { verifyPassword } from "better-auth/crypto";
|
||||
import { and, eq, isNull, ne } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { headers } from "next/headers";
|
||||
import { z } from "zod";
|
||||
import { pagadores, tokensApi } from "@/db/schema";
|
||||
import { account, pagadores, tokensApi } from "@/db/schema";
|
||||
import { auth } from "@/lib/auth/config";
|
||||
import { db, schema } from "@/lib/db";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
@@ -51,8 +52,26 @@ const deleteAccountSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const VALID_FONTS = [
|
||||
"ai-sans",
|
||||
"anthropic-sans",
|
||||
"fira-code",
|
||||
"fira-sans",
|
||||
"geist",
|
||||
"ibm-plex-mono",
|
||||
"inter",
|
||||
"jetbrains-mono",
|
||||
"reddit-sans",
|
||||
"roboto",
|
||||
"sf-pro-display",
|
||||
"sf-pro-rounded",
|
||||
"ubuntu",
|
||||
] as const;
|
||||
|
||||
const updatePreferencesSchema = z.object({
|
||||
disableMagnetlines: z.boolean(),
|
||||
systemFont: z.enum(VALID_FONTS).default("ai-sans"),
|
||||
moneyFont: z.enum(VALID_FONTS).default("ai-sans"),
|
||||
});
|
||||
|
||||
// Actions
|
||||
@@ -235,19 +254,32 @@ export async function updateEmailAction(
|
||||
};
|
||||
}
|
||||
|
||||
// Validar senha tentando fazer changePassword para a mesma senha
|
||||
// Se falhar, a senha atual está incorreta
|
||||
try {
|
||||
await auth.api.changePassword({
|
||||
body: {
|
||||
newPassword: validated.password,
|
||||
currentPassword: validated.password,
|
||||
},
|
||||
headers: await headers(),
|
||||
// Buscar hash da senha no registro de credencial
|
||||
const credentialAccount = await db
|
||||
.select({ password: account.password })
|
||||
.from(account)
|
||||
.where(
|
||||
and(
|
||||
eq(account.userId, session.user.id),
|
||||
eq(account.providerId, "credential"),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const storedHash = credentialAccount[0]?.password;
|
||||
if (!storedHash) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Conta de credencial não encontrada.",
|
||||
};
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword({
|
||||
password: validated.password,
|
||||
hash: storedHash,
|
||||
});
|
||||
} catch (authError: any) {
|
||||
// Se der erro é porque a senha está incorreta
|
||||
console.error("Erro ao validar senha:", authError);
|
||||
|
||||
if (!isValid) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Senha incorreta",
|
||||
@@ -385,6 +417,8 @@ export async function updatePreferencesAction(
|
||||
.update(schema.preferenciasUsuario)
|
||||
.set({
|
||||
disableMagnetlines: validated.disableMagnetlines,
|
||||
systemFont: validated.systemFont,
|
||||
moneyFont: validated.moneyFont,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.preferenciasUsuario.userId, session.user.id));
|
||||
@@ -393,6 +427,8 @@ export async function updatePreferencesAction(
|
||||
await db.insert(schema.preferenciasUsuario).values({
|
||||
userId: session.user.id,
|
||||
disableMagnetlines: validated.disableMagnetlines,
|
||||
systemFont: validated.systemFont,
|
||||
moneyFont: validated.moneyFont,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import { db, schema } from "@/lib/db";
|
||||
|
||||
export interface UserPreferences {
|
||||
disableMagnetlines: boolean;
|
||||
systemFont: string;
|
||||
moneyFont: string;
|
||||
}
|
||||
|
||||
export interface ApiToken {
|
||||
@@ -30,6 +32,8 @@ export async function fetchUserPreferences(
|
||||
const result = await db
|
||||
.select({
|
||||
disableMagnetlines: schema.preferenciasUsuario.disableMagnetlines,
|
||||
systemFont: schema.preferenciasUsuario.systemFont,
|
||||
moneyFont: schema.preferenciasUsuario.moneyFont,
|
||||
})
|
||||
.from(schema.preferenciasUsuario)
|
||||
.where(eq(schema.preferenciasUsuario.userId, userId))
|
||||
|
||||
@@ -61,6 +61,8 @@ export default async function Page() {
|
||||
disableMagnetlines={
|
||||
userPreferences?.disableMagnetlines ?? false
|
||||
}
|
||||
systemFont={userPreferences?.systemFont ?? "ai-sans"}
|
||||
moneyFont={userPreferences?.moneyFont ?? "ai-sans"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { FontProvider } from "@/components/font-provider";
|
||||
import { SiteHeader } from "@/components/header-dashboard";
|
||||
import { PrivacyProvider } from "@/components/privacy-provider";
|
||||
import { AppSidebar } from "@/components/sidebar/app-sidebar";
|
||||
@@ -6,6 +7,7 @@ import { getUserSession } from "@/lib/auth/server";
|
||||
import { fetchDashboardNotifications } from "@/lib/dashboard/notifications";
|
||||
import { fetchPagadoresWithAccess } from "@/lib/pagadores/access";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
import { fetchUserFontPreferences } from "@/lib/preferences/fonts";
|
||||
import { parsePeriodParam } from "@/lib/utils/period";
|
||||
import { fetchPendingInboxCount } from "./pre-lancamentos/data";
|
||||
|
||||
@@ -41,10 +43,17 @@ export default async function DashboardLayout({
|
||||
currentPeriod,
|
||||
);
|
||||
|
||||
// Buscar contagem de pré-lançamentos pendentes
|
||||
const preLancamentosCount = await fetchPendingInboxCount(session.user.id);
|
||||
// Buscar contagem de pré-lançamentos pendentes e preferências de fonte
|
||||
const [preLancamentosCount, fontPrefs] = await Promise.all([
|
||||
fetchPendingInboxCount(session.user.id),
|
||||
fetchUserFontPreferences(session.user.id),
|
||||
]);
|
||||
|
||||
return (
|
||||
<FontProvider
|
||||
systemFont={fontPrefs.systemFont}
|
||||
moneyFont={fontPrefs.moneyFont}
|
||||
>
|
||||
<PrivacyProvider>
|
||||
<SidebarProvider>
|
||||
<AppSidebar
|
||||
@@ -71,5 +80,6 @@ export default async function DashboardLayout({
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</PrivacyProvider>
|
||||
</FontProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Font customization */
|
||||
--font-app: var(--font-ai-sans);
|
||||
--font-money: var(--font-ai-sans);
|
||||
|
||||
/* Base surfaces - warm cream with subtle orange undertone */
|
||||
--background: oklch(96.563% 0.00504 67.275);
|
||||
--foreground: oklch(18% 0.02 45);
|
||||
@@ -71,7 +75,7 @@
|
||||
--sidebar-ring: oklch(69.18% 0.18855 38.353);
|
||||
|
||||
/* Layout */
|
||||
--radius: 0.8rem;
|
||||
--radius: 1rem;
|
||||
|
||||
/* Shadows - warm tinted for cohesion */
|
||||
--shadow-2xs: 0 1px 2px 0px oklch(35% 0.02 45 / 0.04);
|
||||
@@ -160,7 +164,7 @@
|
||||
--sidebar-ring: oklch(69.18% 0.18855 38.353);
|
||||
|
||||
/* Layout */
|
||||
--radius: 0.8rem;
|
||||
--radius: 1rem;
|
||||
|
||||
/* Shadows - deeper for dark mode */
|
||||
--shadow-2xs: 0 1px 2px 0px oklch(0% 0 0 / 0.3);
|
||||
@@ -186,6 +190,8 @@
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--default-font-family: var(--font-app);
|
||||
--default-mono-font-family: var(--font-money);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import type { Metadata } from "next";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { main_font } from "@/public/fonts/font_index";
|
||||
import { allFontVariables } from "@/public/fonts/font_index";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -17,14 +17,11 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang="en" className={allFontVariables} suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-title" content="Opensheets" />
|
||||
</head>
|
||||
<body
|
||||
className={`${main_font.className} antialiased `}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="antialiased" suppressHydrationWarning>
|
||||
<ThemeProvider attribute="class" defaultTheme="light">
|
||||
{children}
|
||||
<Toaster position="top-right" />
|
||||
|
||||
@@ -1,22 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { updatePreferencesAction } from "@/app/(dashboard)/ajustes/actions";
|
||||
import { useFont } from "@/components/font-provider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { FONT_OPTIONS, getFontVariable } from "@/public/fonts/font_index";
|
||||
|
||||
interface PreferencesFormProps {
|
||||
disableMagnetlines: boolean;
|
||||
systemFont: string;
|
||||
moneyFont: string;
|
||||
}
|
||||
|
||||
export function PreferencesForm({ disableMagnetlines }: PreferencesFormProps) {
|
||||
export function PreferencesForm({
|
||||
disableMagnetlines,
|
||||
systemFont: initialSystemFont,
|
||||
moneyFont: initialMoneyFont,
|
||||
}: PreferencesFormProps) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [magnetlinesDisabled, setMagnetlinesDisabled] =
|
||||
useState(disableMagnetlines);
|
||||
const [selectedSystemFont, setSelectedSystemFont] =
|
||||
useState(initialSystemFont);
|
||||
const [selectedMoneyFont, setSelectedMoneyFont] = useState(initialMoneyFont);
|
||||
|
||||
const fontCtx = useFont();
|
||||
|
||||
// Live preview: update CSS vars when font selection changes
|
||||
useEffect(() => {
|
||||
fontCtx.setSystemFont(selectedSystemFont);
|
||||
}, [selectedSystemFont, fontCtx.setSystemFont]);
|
||||
|
||||
useEffect(() => {
|
||||
fontCtx.setMoneyFont(selectedMoneyFont);
|
||||
}, [selectedMoneyFont, fontCtx.setMoneyFont]);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
@@ -24,16 +53,13 @@ export function PreferencesForm({ disableMagnetlines }: PreferencesFormProps) {
|
||||
startTransition(async () => {
|
||||
const result = await updatePreferencesAction({
|
||||
disableMagnetlines: magnetlinesDisabled,
|
||||
systemFont: selectedSystemFont,
|
||||
moneyFont: selectedMoneyFont,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast.success(result.message);
|
||||
// Recarregar a página para aplicar as mudanças nos componentes
|
||||
router.refresh();
|
||||
// Forçar reload completo para garantir que os hooks re-executem
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
} else {
|
||||
toast.error(result.error);
|
||||
}
|
||||
@@ -41,16 +67,103 @@ export function PreferencesForm({ disableMagnetlines }: PreferencesFormProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col space-y-6">
|
||||
<div className="space-y-4 max-w-md">
|
||||
<div className="flex items-center justify-between rounded-lg border border-dashed p-4">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-8">
|
||||
{/* Seção 1: Tipografia */}
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">Tipografia</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Personalize as fontes usadas na interface e nos valores monetários.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fonte do sistema */}
|
||||
<div className="space-y-2 max-w-md">
|
||||
<Label htmlFor="system-font">Fonte do sistema</Label>
|
||||
<Select
|
||||
value={selectedSystemFont}
|
||||
onValueChange={setSelectedSystemFont}
|
||||
>
|
||||
<SelectTrigger id="system-font">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FONT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.key} value={opt.key}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: opt.variable,
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p
|
||||
className="text-sm text-muted-foreground pt-1"
|
||||
style={{
|
||||
fontFamily: getFontVariable(selectedSystemFont),
|
||||
}}
|
||||
>
|
||||
Suas finanças em um só lugar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fonte de valores */}
|
||||
<div className="space-y-2 max-w-md">
|
||||
<Label htmlFor="money-font">Fonte de valores</Label>
|
||||
<Select
|
||||
value={selectedMoneyFont}
|
||||
onValueChange={setSelectedMoneyFont}
|
||||
>
|
||||
<SelectTrigger id="money-font">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FONT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.key} value={opt.key}>
|
||||
<span
|
||||
style={{
|
||||
fontFamily: opt.variable,
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p
|
||||
className="text-sm text-muted-foreground pt-1 tabular-nums"
|
||||
style={{
|
||||
fontFamily: getFontVariable(selectedMoneyFont),
|
||||
}}
|
||||
>
|
||||
R$ 1.234,56
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="border-b" />
|
||||
|
||||
{/* Seção 3: Dashboard */}
|
||||
<section className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">Dashboard</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Opções que afetam a experiência no painel principal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border p-4 max-w-md">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="magnetlines" className="text-base">
|
||||
Desabilitar Magnetlines
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Remove o recurso de linhas magnéticas do sistema. Essa mudança
|
||||
afeta a interface e interações visuais.
|
||||
Remove o recurso de linhas magnéticas do sistema.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -60,7 +173,7 @@ export function PreferencesForm({ disableMagnetlines }: PreferencesFormProps) {
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isPending} className="w-fit">
|
||||
|
||||
80
components/font-provider.tsx
Normal file
80
components/font-provider.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { getFontVariable } from "@/public/fonts/font_index";
|
||||
|
||||
type FontContextValue = {
|
||||
systemFont: string;
|
||||
moneyFont: string;
|
||||
setSystemFont: (key: string) => void;
|
||||
setMoneyFont: (key: string) => void;
|
||||
};
|
||||
|
||||
const FontContext = createContext<FontContextValue | null>(null);
|
||||
|
||||
export function FontProvider({
|
||||
systemFont: initialSystemFont,
|
||||
moneyFont: initialMoneyFont,
|
||||
children,
|
||||
}: {
|
||||
systemFont: string;
|
||||
moneyFont: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [systemFont, setSystemFontState] = useState(initialSystemFont);
|
||||
const [moneyFont, setMoneyFontState] = useState(initialMoneyFont);
|
||||
|
||||
const applyFontVars = useCallback((sys: string, money: string) => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-app",
|
||||
getFontVariable(sys),
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-money",
|
||||
getFontVariable(money),
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
applyFontVars(systemFont, moneyFont);
|
||||
}, [systemFont, moneyFont, applyFontVars]);
|
||||
|
||||
const setSystemFont = useCallback((key: string) => {
|
||||
setSystemFontState(key);
|
||||
}, []);
|
||||
|
||||
const setMoneyFont = useCallback((key: string) => {
|
||||
setMoneyFontState(key);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ systemFont, moneyFont, setSystemFont, setMoneyFont }),
|
||||
[systemFont, moneyFont, setSystemFont, setMoneyFont],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `:root { --font-app: ${getFontVariable(initialSystemFont)}; --font-money: ${getFontVariable(initialMoneyFont)}; }`,
|
||||
}}
|
||||
/>
|
||||
<FontContext value={value}>{children}</FontContext>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function useFont() {
|
||||
const ctx = useContext(FontContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useFont must be used within FontProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils/ui";
|
||||
import { money_font } from "@/public/fonts/font_index";
|
||||
import { usePrivacyMode } from "./privacy-provider";
|
||||
|
||||
type Props = {
|
||||
@@ -24,8 +23,8 @@ function MoneyValues({ amount, className, showPositiveSign = false }: Props) {
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{ fontFamily: "var(--font-money)" }}
|
||||
className={cn(
|
||||
money_font.className,
|
||||
"inline-flex items-baseline transition-all duration-200 tracking-tighter",
|
||||
privacyMode &&
|
||||
"blur-[6px] select-none hover:blur-none focus-within:blur-none",
|
||||
|
||||
@@ -32,6 +32,13 @@ export function CategoryCell({
|
||||
const isIncrease = percentageChange !== null && percentageChange > 0;
|
||||
const isDecrease = percentageChange !== null && percentageChange < 0;
|
||||
|
||||
// Despesa: aumento é ruim (vermelho), diminuição é bom (verde)
|
||||
// Receita: aumento é bom (verde), diminuição é ruim (vermelho)
|
||||
const isPositive =
|
||||
categoryType === "receita" ? isIncrease : isDecrease;
|
||||
const isNegative =
|
||||
categoryType === "receita" ? isDecrease : isIncrease;
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -41,8 +48,8 @@ export function CategoryCell({
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-0.5 text-xs",
|
||||
isIncrease && "text-destructive",
|
||||
isDecrease && "text-success",
|
||||
isNegative && "text-destructive",
|
||||
isPositive && "text-success",
|
||||
)}
|
||||
>
|
||||
{isIncrease && <RiArrowUpSFill className="h-3 w-3" />}
|
||||
@@ -63,8 +70,8 @@ export function CategoryCell({
|
||||
<div
|
||||
className={cn(
|
||||
"font-medium",
|
||||
isIncrease && "text-destructive",
|
||||
isDecrease && "text-success",
|
||||
isNegative && "text-destructive",
|
||||
isPositive && "text-success",
|
||||
)}
|
||||
>
|
||||
Diferença:{" "}
|
||||
|
||||
@@ -107,6 +107,8 @@ export const preferenciasUsuario = pgTable("preferencias_usuario", {
|
||||
.unique()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
disableMagnetlines: boolean("disable_magnetlines").notNull().default(false),
|
||||
systemFont: text("system_font").notNull().default("ai-sans"),
|
||||
moneyFont: text("money_font").notNull().default("ai-sans"),
|
||||
dashboardWidgets: jsonb("dashboard_widgets").$type<{
|
||||
order: string[];
|
||||
hidden: string[];
|
||||
|
||||
2
drizzle/0016_complete_randall.sql
Normal file
2
drizzle/0016_complete_randall.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "preferencias_usuario" ADD COLUMN "system_font" text DEFAULT 'ai-sans' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "preferencias_usuario" ADD COLUMN "money_font" text DEFAULT 'ai-sans' NOT NULL;
|
||||
2191
drizzle/meta/0016_snapshot.json
Normal file
2191
drizzle/meta/0016_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,13 @@
|
||||
"when": 1770332054481,
|
||||
"tag": "0015_concerned_kat_farrell",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "7",
|
||||
"when": 1771166328908,
|
||||
"tag": "0016_complete_randall",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
33
lib/preferences/fonts.ts
Normal file
33
lib/preferences/fonts.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { cache } from "react";
|
||||
import { db, schema } from "@/lib/db";
|
||||
|
||||
export type FontPreferences = {
|
||||
systemFont: string;
|
||||
moneyFont: string;
|
||||
};
|
||||
|
||||
const DEFAULT_FONT_PREFS: FontPreferences = {
|
||||
systemFont: "ai-sans",
|
||||
moneyFont: "ai-sans",
|
||||
};
|
||||
|
||||
export const fetchUserFontPreferences = cache(
|
||||
async (userId: string): Promise<FontPreferences> => {
|
||||
const result = await db
|
||||
.select({
|
||||
systemFont: schema.preferenciasUsuario.systemFont,
|
||||
moneyFont: schema.preferenciasUsuario.moneyFont,
|
||||
})
|
||||
.from(schema.preferenciasUsuario)
|
||||
.where(eq(schema.preferenciasUsuario.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!result[0]) return DEFAULT_FONT_PREFS;
|
||||
|
||||
return {
|
||||
systemFont: result[0].systemFont,
|
||||
moneyFont: result[0].moneyFont,
|
||||
};
|
||||
},
|
||||
);
|
||||
BIN
public/fonts/SF-Pro-Display-Bold.otf
Normal file
BIN
public/fonts/SF-Pro-Display-Bold.otf
Normal file
Binary file not shown.
BIN
public/fonts/SF-Pro-Display-Medium.otf
Normal file
BIN
public/fonts/SF-Pro-Display-Medium.otf
Normal file
Binary file not shown.
BIN
public/fonts/SF-Pro-Display-Regular.otf
Normal file
BIN
public/fonts/SF-Pro-Display-Regular.otf
Normal file
Binary file not shown.
BIN
public/fonts/SF-Pro-Display-Semibold.otf
Normal file
BIN
public/fonts/SF-Pro-Display-Semibold.otf
Normal file
Binary file not shown.
BIN
public/fonts/SF-Pro-Rounded-Bold.otf
Normal file
BIN
public/fonts/SF-Pro-Rounded-Bold.otf
Normal file
Binary file not shown.
BIN
public/fonts/SF-Pro-Rounded-Medium.otf
Normal file
BIN
public/fonts/SF-Pro-Rounded-Medium.otf
Normal file
Binary file not shown.
BIN
public/fonts/SF-Pro-Rounded-Regular.otf
Normal file
BIN
public/fonts/SF-Pro-Rounded-Regular.otf
Normal file
Binary file not shown.
BIN
public/fonts/SF-Pro-Rounded-Semibold.otf
Normal file
BIN
public/fonts/SF-Pro-Rounded-Semibold.otf
Normal file
Binary file not shown.
@@ -1,3 +1,14 @@
|
||||
import {
|
||||
Fira_Code,
|
||||
Fira_Sans,
|
||||
Geist,
|
||||
IBM_Plex_Mono,
|
||||
Inter,
|
||||
JetBrains_Mono,
|
||||
Reddit_Sans,
|
||||
Roboto,
|
||||
Ubuntu,
|
||||
} from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
|
||||
const ai_sans = localFont({
|
||||
@@ -14,14 +25,204 @@ const ai_sans = localFont({
|
||||
},
|
||||
],
|
||||
display: "swap",
|
||||
variable: "--font-ai-sans",
|
||||
});
|
||||
|
||||
const anthropic_sans = localFont({
|
||||
src: "./anthropicSans.woff2",
|
||||
display: "swap",
|
||||
variable: "--font-anthropic-sans",
|
||||
});
|
||||
|
||||
const main_font = ai_sans;
|
||||
const money_font = ai_sans;
|
||||
const sf_pro_display = localFont({
|
||||
src: [
|
||||
{
|
||||
path: "./SF-Pro-Display-Regular.otf",
|
||||
weight: "400",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./SF-Pro-Display-Medium.otf",
|
||||
weight: "500",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./SF-Pro-Display-Semibold.otf",
|
||||
weight: "600",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./SF-Pro-Display-Bold.otf",
|
||||
weight: "700",
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
display: "swap",
|
||||
variable: "--font-sf-pro-display",
|
||||
});
|
||||
|
||||
export { main_font, money_font };
|
||||
const sf_pro_rounded = localFont({
|
||||
src: [
|
||||
{
|
||||
path: "./SF-Pro-Rounded-Regular.otf",
|
||||
weight: "400",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./SF-Pro-Rounded-Medium.otf",
|
||||
weight: "500",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./SF-Pro-Rounded-Semibold.otf",
|
||||
weight: "600",
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
path: "./SF-Pro-Rounded-Bold.otf",
|
||||
weight: "700",
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
display: "swap",
|
||||
variable: "--font-sf-pro-rounded",
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
const geist_sans = Geist({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-geist",
|
||||
});
|
||||
|
||||
const roboto = Roboto({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-roboto",
|
||||
});
|
||||
|
||||
const reddit_sans = Reddit_Sans({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-reddit-sans",
|
||||
});
|
||||
|
||||
const fira_sans = Fira_Sans({
|
||||
weight: ["400", "500", "600", "700"],
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-fira-sans",
|
||||
});
|
||||
|
||||
const ubuntu = Ubuntu({
|
||||
weight: ["400"],
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-ubuntu",
|
||||
});
|
||||
|
||||
const jetbrains_mono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-jetbrains-mono",
|
||||
});
|
||||
|
||||
const fira_code = Fira_Code({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-fira-code",
|
||||
});
|
||||
|
||||
const ibm_plex_mono = IBM_Plex_Mono({
|
||||
weight: ["400", "500", "600"],
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--font-ibm-plex-mono",
|
||||
});
|
||||
|
||||
export type FontOption = {
|
||||
key: string;
|
||||
label: string;
|
||||
variable: string;
|
||||
};
|
||||
|
||||
export const FONT_OPTIONS: FontOption[] = [
|
||||
{ key: "ai-sans", label: "AI Sans", variable: "var(--font-ai-sans)" },
|
||||
{
|
||||
key: "anthropic-sans",
|
||||
label: "Anthropic Sans",
|
||||
variable: "var(--font-anthropic-sans)",
|
||||
},
|
||||
{ key: "fira-code", label: "Fira Code", variable: "var(--font-fira-code)" },
|
||||
{
|
||||
key: "fira-sans",
|
||||
label: "Fira Sans",
|
||||
variable: "var(--font-fira-sans)",
|
||||
},
|
||||
{ key: "geist", label: "Geist Sans", variable: "var(--font-geist)" },
|
||||
{
|
||||
key: "ibm-plex-mono",
|
||||
label: "IBM Plex Mono",
|
||||
variable: "var(--font-ibm-plex-mono)",
|
||||
},
|
||||
{ key: "inter", label: "Inter", variable: "var(--font-inter)" },
|
||||
{
|
||||
key: "jetbrains-mono",
|
||||
label: "JetBrains Mono",
|
||||
variable: "var(--font-jetbrains-mono)",
|
||||
},
|
||||
{
|
||||
key: "reddit-sans",
|
||||
label: "Reddit Sans",
|
||||
variable: "var(--font-reddit-sans)",
|
||||
},
|
||||
{ key: "roboto", label: "Roboto", variable: "var(--font-roboto)" },
|
||||
{
|
||||
key: "sf-pro-display",
|
||||
label: "SF Pro Display",
|
||||
variable: "var(--font-sf-pro-display)",
|
||||
},
|
||||
{
|
||||
key: "sf-pro-rounded",
|
||||
label: "SF Pro Rounded",
|
||||
variable: "var(--font-sf-pro-rounded)",
|
||||
},
|
||||
{ key: "ubuntu", label: "Ubuntu", variable: "var(--font-ubuntu)" },
|
||||
];
|
||||
|
||||
/** @deprecated Use FONT_OPTIONS */
|
||||
export const SYSTEM_FONT_OPTIONS = FONT_OPTIONS;
|
||||
/** @deprecated Use FONT_OPTIONS */
|
||||
export const MONEY_FONT_OPTIONS = FONT_OPTIONS;
|
||||
|
||||
const allFonts = [
|
||||
ai_sans,
|
||||
anthropic_sans,
|
||||
sf_pro_display,
|
||||
sf_pro_rounded,
|
||||
inter,
|
||||
geist_sans,
|
||||
roboto,
|
||||
reddit_sans,
|
||||
fira_sans,
|
||||
ubuntu,
|
||||
jetbrains_mono,
|
||||
fira_code,
|
||||
ibm_plex_mono,
|
||||
];
|
||||
|
||||
export const allFontVariables = allFonts.map((f) => f.variable).join(" ");
|
||||
|
||||
// Backward compatibility
|
||||
export const main_font = ai_sans;
|
||||
export const money_font = ai_sans;
|
||||
|
||||
export function getFontVariable(key: string): string {
|
||||
const option = FONT_OPTIONS.find((o) => o.key === key);
|
||||
return option?.variable ?? "var(--font-ai-sans)";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user