feat: add brand assets, design tokens and light/dark theme
- Use real logo/isotype SVGs across app and emails - Token-based color system aligned with landing (brand scale + semantic tokens) - ThemeProvider (light/dark/system) with header toggle and settings card - Embed logo in transactional emails; centralize email colors
BIN
apps/backend/src/assets/logotipo-y-organica.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
@@ -1,9 +1,31 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import nodemailer, { type Transporter } from 'nodemailer'
|
||||
|
||||
type VerificationEmailData = { email: string; url: string; name?: string }
|
||||
type PasswordResetEmailData = { email: string; url: string; name?: string }
|
||||
type InvitationEmailData = { email: string; url: string; organizationName: string }
|
||||
|
||||
// Colores de marca (mismo sistema que la web: index.css de apps/web).
|
||||
const BRAND_600 = '#2544ea'
|
||||
const TEXT_STRONG = '#0f172a'
|
||||
const TEXT_MUTED = '#475569'
|
||||
const ON_ACCENT = '#ffffff'
|
||||
const LINK_BG = BRAND_600
|
||||
|
||||
// Logotipo embebido como data URI para que los mails no dependan de hosting.
|
||||
function loadLogoTypoDataUri(): string {
|
||||
try {
|
||||
const base64 = readFileSync(
|
||||
new URL('../assets/logotipo-y-organica.png', import.meta.url),
|
||||
).toString('base64')
|
||||
return `data:image/png;base64,${base64}`
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const LOGO_DATA_URI = loadLogoTypoDataUri()
|
||||
|
||||
interface EmailProvider {
|
||||
sendVerificationEmail(data: VerificationEmailData): Promise<void>
|
||||
sendPasswordResetEmail(data: PasswordResetEmailData): Promise<void>
|
||||
@@ -30,11 +52,15 @@ class SMTPEmailProvider implements EmailProvider {
|
||||
}
|
||||
|
||||
private layout(title: string, body: string) {
|
||||
const header = LOGO_DATA_URI
|
||||
? `<img src="${LOGO_DATA_URI}" alt="Gruperly" width="252" height="83" style="height:36px;width:auto;display:block;margin:0 auto;" />`
|
||||
: '<h2 style="margin:0 0 16px;">Gruperly</h2>'
|
||||
|
||||
return `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 480px; margin: 0 auto; padding: 24px; color: #0a2540;">
|
||||
<h2 style="margin: 0 0 16px;">Gruperly</h2>
|
||||
<h3 style="margin: 0 0 8px;">${title}</h3>
|
||||
<p style="line-height: 1.6; color: #334155;">${body}</p>
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 480px; margin: 0 auto; padding: 24px; color: ${TEXT_MUTED};">
|
||||
<div style="text-align:center;margin-bottom:20px;">${header}</div>
|
||||
<h3 style="margin: 0 0 8px; color: ${TEXT_STRONG};">${title}</h3>
|
||||
<p style="line-height: 1.6; color: ${TEXT_MUTED};">${body}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -49,7 +75,7 @@ class SMTPEmailProvider implements EmailProvider {
|
||||
}
|
||||
|
||||
async sendVerificationEmail({ email, url, name }: VerificationEmailData) {
|
||||
const link = `<a href="${url}" style="display:inline-block;background:#1e90ff;color:#fff;text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;">Verificar email</a>`
|
||||
const link = `<a href="${url}" style="display:inline-block;background:${LINK_BG};color:${ON_ACCENT};text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;">Verificar email</a>`
|
||||
await this.send(
|
||||
email,
|
||||
'Verifica tu email en Gruperly',
|
||||
@@ -61,7 +87,7 @@ class SMTPEmailProvider implements EmailProvider {
|
||||
}
|
||||
|
||||
async sendPasswordResetEmail({ email, url, name }: PasswordResetEmailData) {
|
||||
const link = `<a href="${url}" style="display:inline-block;background:#1e90ff;color:#fff;text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;">Restablecer contraseña</a>`
|
||||
const link = `<a href="${url}" style="display:inline-block;background:${LINK_BG};color:${ON_ACCENT};text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;">Restablecer contraseña</a>`
|
||||
await this.send(
|
||||
email,
|
||||
'Restablece tu contraseña en Gruperly',
|
||||
@@ -73,7 +99,7 @@ class SMTPEmailProvider implements EmailProvider {
|
||||
}
|
||||
|
||||
async sendOrganizationInvitation({ email, url, organizationName }: InvitationEmailData) {
|
||||
const link = `<a href="${url}" style="display:inline-block;background:#1e90ff;color:#fff;text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;">Unirme a ${organizationName}</a>`
|
||||
const link = `<a href="${url}" style="display:inline-block;background:${LINK_BG};color:${ON_ACCENT};text-decoration:none;padding:12px 24px;border-radius:12px;font-weight:600;">Unirme a ${organizationName}</a>`
|
||||
await this.send(
|
||||
email,
|
||||
`Te invitaron a ${organizationName} en Gruperly`,
|
||||
|
||||
@@ -4,9 +4,28 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gruperly</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem('theme');
|
||||
var isDark =
|
||||
stored === 'dark' ||
|
||||
(stored !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
if (isDark) document.documentElement.classList.add('dark');
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
BIN
apps/web/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
12
apps/web/public/favicon.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="718.5 93 144 144">
|
||||
<title>Gruperly</title>
|
||||
<style>
|
||||
.branch { fill: #171D54; }
|
||||
.diag { fill: #93B4FD; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.branch { fill: #EEF4FF; }
|
||||
}
|
||||
</style>
|
||||
<path class="branch" d="M755 111 H767.4 Q771.3 111 772.6 114.8 C776.5 126.2 781.3 137.6 787.6 148.9 Q789.2 151.9 785.8 152.5 C781.9 153.2 777.8 153.4 774 152.9 Q770.2 152.4 768.5 149 C762.9 138 757.9 126.7 753.1 115.3 Q751.3 111 755 111 Z"/>
|
||||
<path class="diag" d="M812.5 111 H824.4 Q828.8 111 827.2 115.1 C817.2 140.2 807.3 167 796.4 194.6 C790.1 210.7 783.2 219 767.2 219 C764.2 219 761.5 218.7 759 218.2 Q756.6 217.7 757 214.7 L758 205.1 Q758.3 202.4 761.1 203 C763.1 203.5 765.2 203.9 767 203.9 C774.9 203.9 778 198.7 781.6 189.4 C791.9 162.5 800.1 138.1 808.4 114 Q809.5 111 812.5 111 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 885 B |
6
apps/web/src/assets/brand/isotipo-y-organica-oscuro.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="718.5 93 144 144">
|
||||
<title>Gruperly — isotipo y orgánica</title>
|
||||
<desc>Y orgánica con rama corta blanco azulado #EEF4FF y diagonal larga celeste #93B4FD, separadas, sobre fondo transparente.</desc>
|
||||
<path fill="#EEF4FF" d="M755 111 H767.4 Q771.3 111 772.6 114.8 C776.5 126.2 781.3 137.6 787.6 148.9 Q789.2 151.9 785.8 152.5 C781.9 153.2 777.8 153.4 774 152.9 Q770.2 152.4 768.5 149 C762.9 138 757.9 126.7 753.1 115.3 Q751.3 111 755 111 Z"/>
|
||||
<path fill="#93B4FD" d="M812.5 111 H824.4 Q828.8 111 827.2 115.1 C817.2 140.2 807.3 167 796.4 194.6 C790.1 210.7 783.2 219 767.2 219 C764.2 219 761.5 218.7 759 218.2 Q756.6 217.7 757 214.7 L758 205.1 Q758.3 202.4 761.1 203 C763.1 203.5 765.2 203.9 767 203.9 C774.9 203.9 778 198.7 781.6 189.4 C791.9 162.5 800.1 138.1 808.4 114 Q809.5 111 812.5 111 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 885 B |
6
apps/web/src/assets/brand/isotipo-y-organica.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="718.5 93 144 144">
|
||||
<title>Gruperly — isotipo y orgánica</title>
|
||||
<desc>Y orgánica con rama corta azul noche #171D54 y diagonal larga celeste #93B4FD, separadas, sobre fondo transparente.</desc>
|
||||
<path fill="#171D54" d="M755 111 H767.4 Q771.3 111 772.6 114.8 C776.5 126.2 781.3 137.6 787.6 148.9 Q789.2 151.9 785.8 152.5 C781.9 153.2 777.8 153.4 774 152.9 Q770.2 152.4 768.5 149 C762.9 138 757.9 126.7 753.1 115.3 Q751.3 111 755 111 Z"/>
|
||||
<path fill="#93B4FD" d="M812.5 111 H824.4 Q828.8 111 827.2 115.1 C817.2 140.2 807.3 167 796.4 194.6 C790.1 210.7 783.2 219 767.2 219 C764.2 219 761.5 218.7 759 218.2 Q756.6 217.7 757 214.7 L758 205.1 Q758.3 202.4 761.1 203 C763.1 203.5 765.2 203.9 767 203.9 C774.9 203.9 778 198.7 781.6 189.4 C791.9 162.5 800.1 138.1 808.4 114 Q809.5 111 812.5 111 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 881 B |
7
apps/web/src/assets/brand/logotipo-y-organica-oscuro.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="624" height="206" viewBox="250 42 624 206">
|
||||
<title>Gruperly — y orgánica bicolor</title>
|
||||
<desc>Rama corta azul noche y diagonal larga celeste, separadas, con curvas suaves y terminales redondeados.</desc>
|
||||
<path fill="#EEF4FF" d="m 358,111 v 65.55 q 0,9.75 -2.4,17.55 -2.4,7.95 -7.5,13.35 -4.95,5.55 -12.6,8.55 -7.65,3 -18.15,3 -4.35,0 -9.3,-0.75 -4.8,-0.75 -9.6,-2.25 -4.65,-1.5 -9,-3.75 -4.35,-2.25 -7.8,-5.1 L 292,193.2 q 5.25,4.95 11.85,7.5 6.6,2.7 13.35,2.7 6.45,0 10.8,-1.95 4.5,-1.8 7.05,-5.1 2.7,-3.3 3.75,-7.8 1.2,-4.5 1.2,-9.9 v -5.25 h -0.3 q -3.9,5.25 -10.05,8.1 -6,2.7 -13.5,2.7 -8.1,0 -14.7,-3 -6.6,-3 -11.25,-8.1 -4.5,-5.1 -7.05,-11.85 -2.55,-6.9 -2.55,-14.55 0,-7.8 2.4,-14.7 2.55,-6.9 7.05,-12 4.65,-5.1 11.1,-8.1 6.6,-3 14.7,-3 7.65,0 14.25,3.15 6.6,3.15 10.5,9.75 h 0.3 V 111 Z m -38.4,12.6 q -4.95,0 -8.85,1.8 -3.75,1.8 -6.45,4.95 -2.55,3 -4.05,7.2 -1.35,4.2 -1.35,9 0,4.35 1.35,8.4 1.5,4.05 4.05,7.35 2.7,3.15 6.45,5.1 3.9,1.95 8.7,1.95 4.95,0 8.85,-1.8 4.05,-1.95 6.75,-5.1 2.85,-3.15 4.35,-7.2 1.5,-4.2 1.5,-8.7 0,-4.65 -1.5,-8.85 -1.5,-4.2 -4.35,-7.35 -2.7,-3.15 -6.6,-4.95 -3.9,-1.8 -8.85,-1.8 z m 55,-12.6 h 17.25 v 12 h 0.3 q 3,-6.3 8.4,-10.2 5.4,-3.9 12.6,-3.9 1.05,0 2.25,0.15 1.2,0 2.1,0.3 v 16.5 q -1.8,-0.45 -3.15,-0.6 -1.2,-0.15 -2.4,-0.15 -6.15,0 -9.9,2.25 -3.75,2.25 -5.85,5.4 -2.1,3.15 -2.85,6.45 -0.75,3.3 -0.75,5.25 V 183 h -18 z m 115.30004,72 h -17.1 v -11.55 h -0.3 q -2.4,5.4 -8.4,9.6 -5.85,4.05 -13.95,4.05 -7.05,0 -12.15,-2.4 -4.95,-2.55 -8.25,-6.6 -3.15,-4.05 -4.65,-9.3 -1.5,-5.25 -1.5,-10.8 v -45 h 18 v 39.9 q 0,3.15 0.45,6.6 0.45,3.45 1.95,6.3 1.5,2.7 4.05,4.5 2.7,1.8 7.05,1.8 4.2,0 7.35,-1.65 3.3,-1.8 5.25,-4.5 2.1,-2.7 3.15,-6.15 1.05,-3.6 1.05,-7.2 V 111 h 18 z m 33.85011,-72 v 10.05 h 0.45 q 1.35,-2.1 3.45,-4.2 2.25,-2.25 5.4,-3.9 3.15,-1.8 7.05,-2.85 4.05,-1.2 9,-1.2 7.65,0 14.1,3 6.45,2.85 11.1,7.95 4.65,5.1 7.2,12 2.55,6.9 2.55,14.85 0,7.95 -2.55,15 -2.4,6.9 -7.05,12.15 -4.5,5.1 -11.1,8.25 -6.45,3 -14.7,3 -7.65,0 -14.1,-3.15 -6.3,-3.15 -9.75,-8.55 h -0.3 V 219 h -18 V 111 Z m 42,35.7 q 0,-4.2 -1.35,-8.25 -1.2,-4.2 -3.75,-7.5 -2.55,-3.3 -6.45,-5.25 -3.9,-2.1 -9.15,-2.1 -4.95,0 -8.85,2.1 -3.9,2.1 -6.75,5.4 -2.7,3.3 -4.2,7.5 -1.35,4.2 -1.35,8.4 0,4.2 1.35,8.4 1.5,4.05 4.2,7.35 2.85,3.3 6.75,5.4 3.9,1.95 8.85,1.95 5.25,0 9.15,-2.1 3.9,-2.1 6.45,-5.4 2.55,-3.3 3.75,-7.5 1.35,-4.2 1.35,-8.4 z m 81.3999,-6.9 q 0,-3.45 -1.05,-6.6 -0.9,-3.15 -3,-5.55 -2.1,-2.4 -5.4,-3.75 -3.15,-1.5 -7.5,-1.5 -8.1,0 -13.8,4.95 -5.55,4.8 -6.15,12.45 z m 18,8.1 q 0,1.2 0,2.4 0,1.2 -0.15,2.4 h -54.75 q 0.3,3.9 1.95,7.2 1.8,3.15 4.65,5.55 2.85,2.25 6.45,3.6 3.6,1.35 7.5,1.35 6.75,0 11.4,-2.4 4.65,-2.55 7.65,-6.9 l 12,9.6 q -10.65,14.4 -30.9,14.4 -8.4,0 -15.45,-2.55 -7.05,-2.7 -12.3,-7.5 -5.1,-4.8 -8.1,-11.7 -2.85,-7.05 -2.85,-15.9 0,-8.7 2.85,-15.75 3,-7.2 8.1,-12.15 5.1,-5.1 12,-7.8 7.05,-2.85 15.15,-2.85 7.5,0 13.8,2.55 6.45,2.4 11.1,7.35 4.65,4.8 7.2,12.15 2.7,7.2 2.7,16.95 z M 677.54997,111 h 17.25 v 12 h 0.3 q 3,-6.3 8.4,-10.2 5.4,-3.9 12.6,-3.9 1.05,0 2.25,0.15 1.2,0 2.1,0.3 v 16.5 q -1.8,-0.45 -3.15,-0.6 -1.2,-0.15 -2.4,-0.15 -6.15,0 -9.9,2.25 -3.75,2.25 -5.85,5.4 -2.1,3.15 -2.85,6.45 -0.75,3.3 -0.75,5.25 V 183 h -18 z m 49.90007,-41.4 h 18 V 183 h -18 z"/>
|
||||
<path fill="#EEF4FF" d="M755 111 H767.4 Q771.3 111 772.6 114.8 C776.5 126.2 781.3 137.6 787.6 148.9 Q789.2 151.9 785.8 152.5 C781.9 153.2 777.8 153.4 774 152.9 Q770.2 152.4 768.5 149 C762.9 138 757.9 126.7 753.1 115.3 Q751.3 111 755 111 Z"/>
|
||||
<path fill="#93B4FD" d="M812.5 111 H824.4 Q828.8 111 827.2 115.1 C817.2 140.2 807.3 167 796.4 194.6 C790.1 210.7 783.2 219 767.2 219 C764.2 219 761.5 218.7 759 218.2 Q756.6 217.7 757 214.7 L758 205.1 Q758.3 202.4 761.1 203 C763.1 203.5 765.2 203.9 767 203.9 C774.9 203.9 778 198.7 781.6 189.4 C791.9 162.5 800.1 138.1 808.4 114 Q809.5 111 812.5 111 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
7
apps/web/src/assets/brand/logotipo-y-organica.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="624" height="206" viewBox="250 42 624 206">
|
||||
<title>Gruperly — y orgánica bicolor</title>
|
||||
<desc>Rama corta azul noche y diagonal larga celeste, separadas, con curvas suaves y terminales redondeados.</desc>
|
||||
<path fill="#171D54" d="m 358,111 v 65.55 q 0,9.75 -2.4,17.55 -2.4,7.95 -7.5,13.35 -4.95,5.55 -12.6,8.55 -7.65,3 -18.15,3 -4.35,0 -9.3,-0.75 -4.8,-0.75 -9.6,-2.25 -4.65,-1.5 -9,-3.75 -4.35,-2.25 -7.8,-5.1 L 292,193.2 q 5.25,4.95 11.85,7.5 6.6,2.7 13.35,2.7 6.45,0 10.8,-1.95 4.5,-1.8 7.05,-5.1 2.7,-3.3 3.75,-7.8 1.2,-4.5 1.2,-9.9 v -5.25 h -0.3 q -3.9,5.25 -10.05,8.1 -6,2.7 -13.5,2.7 -8.1,0 -14.7,-3 -6.6,-3 -11.25,-8.1 -4.5,-5.1 -7.05,-11.85 -2.55,-6.9 -2.55,-14.55 0,-7.8 2.4,-14.7 2.55,-6.9 7.05,-12 4.65,-5.1 11.1,-8.1 6.6,-3 14.7,-3 7.65,0 14.25,3.15 6.6,3.15 10.5,9.75 h 0.3 V 111 Z m -38.4,12.6 q -4.95,0 -8.85,1.8 -3.75,1.8 -6.45,4.95 -2.55,3 -4.05,7.2 -1.35,4.2 -1.35,9 0,4.35 1.35,8.4 1.5,4.05 4.05,7.35 2.7,3.15 6.45,5.1 3.9,1.95 8.7,1.95 4.95,0 8.85,-1.8 4.05,-1.95 6.75,-5.1 2.85,-3.15 4.35,-7.2 1.5,-4.2 1.5,-8.7 0,-4.65 -1.5,-8.85 -1.5,-4.2 -4.35,-7.35 -2.7,-3.15 -6.6,-4.95 -3.9,-1.8 -8.85,-1.8 z m 55,-12.6 h 17.25 v 12 h 0.3 q 3,-6.3 8.4,-10.2 5.4,-3.9 12.6,-3.9 1.05,0 2.25,0.15 1.2,0 2.1,0.3 v 16.5 q -1.8,-0.45 -3.15,-0.6 -1.2,-0.15 -2.4,-0.15 -6.15,0 -9.9,2.25 -3.75,2.25 -5.85,5.4 -2.1,3.15 -2.85,6.45 -0.75,3.3 -0.75,5.25 V 183 h -18 z m 115.30004,72 h -17.1 v -11.55 h -0.3 q -2.4,5.4 -8.4,9.6 -5.85,4.05 -13.95,4.05 -7.05,0 -12.15,-2.4 -4.95,-2.55 -8.25,-6.6 -3.15,-4.05 -4.65,-9.3 -1.5,-5.25 -1.5,-10.8 v -45 h 18 v 39.9 q 0,3.15 0.45,6.6 0.45,3.45 1.95,6.3 1.5,2.7 4.05,4.5 2.7,1.8 7.05,1.8 4.2,0 7.35,-1.65 3.3,-1.8 5.25,-4.5 2.1,-2.7 3.15,-6.15 1.05,-3.6 1.05,-7.2 V 111 h 18 z m 33.85011,-72 v 10.05 h 0.45 q 1.35,-2.1 3.45,-4.2 2.25,-2.25 5.4,-3.9 3.15,-1.8 7.05,-2.85 4.05,-1.2 9,-1.2 7.65,0 14.1,3 6.45,2.85 11.1,7.95 4.65,5.1 7.2,12 2.55,6.9 2.55,14.85 0,7.95 -2.55,15 -2.4,6.9 -7.05,12.15 -4.5,5.1 -11.1,8.25 -6.45,3 -14.7,3 -7.65,0 -14.1,-3.15 -6.3,-3.15 -9.75,-8.55 h -0.3 V 219 h -18 V 111 Z m 42,35.7 q 0,-4.2 -1.35,-8.25 -1.2,-4.2 -3.75,-7.5 -2.55,-3.3 -6.45,-5.25 -3.9,-2.1 -9.15,-2.1 -4.95,0 -8.85,2.1 -3.9,2.1 -6.75,5.4 -2.7,3.3 -4.2,7.5 -1.35,4.2 -1.35,8.4 0,4.2 1.35,8.4 1.5,4.05 4.2,7.35 2.85,3.3 6.75,5.4 3.9,1.95 8.85,1.95 5.25,0 9.15,-2.1 3.9,-2.1 6.45,-5.4 2.55,-3.3 3.75,-7.5 1.35,-4.2 1.35,-8.4 z m 81.3999,-6.9 q 0,-3.45 -1.05,-6.6 -0.9,-3.15 -3,-5.55 -2.1,-2.4 -5.4,-3.75 -3.15,-1.5 -7.5,-1.5 -8.1,0 -13.8,4.95 -5.55,4.8 -6.15,12.45 z m 18,8.1 q 0,1.2 0,2.4 0,1.2 -0.15,2.4 h -54.75 q 0.3,3.9 1.95,7.2 1.8,3.15 4.65,5.55 2.85,2.25 6.45,3.6 3.6,1.35 7.5,1.35 6.75,0 11.4,-2.4 4.65,-2.55 7.65,-6.9 l 12,9.6 q -10.65,14.4 -30.9,14.4 -8.4,0 -15.45,-2.55 -7.05,-2.7 -12.3,-7.5 -5.1,-4.8 -8.1,-11.7 -2.85,-7.05 -2.85,-15.9 0,-8.7 2.85,-15.75 3,-7.2 8.1,-12.15 5.1,-5.1 12,-7.8 7.05,-2.85 15.15,-2.85 7.5,0 13.8,2.55 6.45,2.4 11.1,7.35 4.65,4.8 7.2,12.15 2.7,7.2 2.7,16.95 z M 677.54997,111 h 17.25 v 12 h 0.3 q 3,-6.3 8.4,-10.2 5.4,-3.9 12.6,-3.9 1.05,0 2.25,0.15 1.2,0 2.1,0.3 v 16.5 q -1.8,-0.45 -3.15,-0.6 -1.2,-0.15 -2.4,-0.15 -6.15,0 -9.9,2.25 -3.75,2.25 -5.85,5.4 -2.1,3.15 -2.85,6.45 -0.75,3.3 -0.75,5.25 V 183 h -18 z m 49.90007,-41.4 h 18 V 183 h -18 z"/>
|
||||
<path fill="#171D54" d="M755 111 H767.4 Q771.3 111 772.6 114.8 C776.5 126.2 781.3 137.6 787.6 148.9 Q789.2 151.9 785.8 152.5 C781.9 153.2 777.8 153.4 774 152.9 Q770.2 152.4 768.5 149 C762.9 138 757.9 126.7 753.1 115.3 Q751.3 111 755 111 Z"/>
|
||||
<path fill="#93B4FD" d="M812.5 111 H824.4 Q828.8 111 827.2 115.1 C817.2 140.2 807.3 167 796.4 194.6 C790.1 210.7 783.2 219 767.2 219 C764.2 219 761.5 218.7 759 218.2 Q756.6 217.7 757 214.7 L758 205.1 Q758.3 202.4 761.1 203 C763.1 203.5 765.2 203.9 767 203.9 C774.9 203.9 778 198.7 781.6 189.4 C791.9 162.5 800.1 138.1 808.4 114 Q809.5 111 812.5 111 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Logo, LogoIcon } from '../brand'
|
||||
import { Logo } from '../brand'
|
||||
|
||||
export type AuthShellProps = {
|
||||
title: string
|
||||
@@ -11,11 +11,10 @@ export function AuthShell({ title, subtitle, children }: AuthShellProps) {
|
||||
return (
|
||||
<div className="flex min-h-dvh items-center justify-center bg-background px-4 py-8">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-6 flex items-center justify-center gap-2">
|
||||
<LogoIcon className="size-9" />
|
||||
<Logo className="text-xl" />
|
||||
<div className="mb-6 flex items-center justify-center">
|
||||
<Logo className="h-10" />
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-white p-6 shadow-sm">
|
||||
<div className="rounded-xl border border-border bg-surface p-6 shadow-sm">
|
||||
<h1 className="text-xl font-bold text-primary">{title}</h1>
|
||||
{subtitle ? <p className="mt-1 text-sm text-foreground/60">{subtitle}</p> : null}
|
||||
<div className="mt-5">{children}</div>
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
import { forwardRef, type HTMLAttributes } from 'react'
|
||||
import { forwardRef, type SVGProps } from 'react'
|
||||
import { cn } from '../../lib/utils'
|
||||
|
||||
export type LogoProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
||||
className?: string
|
||||
}
|
||||
export type LogoProps = SVGProps<SVGSVGElement>
|
||||
|
||||
export const Logo = forwardRef<HTMLSpanElement, LogoProps>(
|
||||
export const Logo = forwardRef<SVGSVGElement, LogoProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<span
|
||||
<svg
|
||||
ref={ref}
|
||||
className={cn('font-extrabold text-primary select-none', className)}
|
||||
viewBox="250 42 624 206"
|
||||
role="img"
|
||||
aria-label="Gruperly"
|
||||
className={cn('h-6 w-auto', className)}
|
||||
{...props}
|
||||
>
|
||||
gruperl
|
||||
<span className="text-accent">y</span>
|
||||
</span>
|
||||
<title>Gruperly</title>
|
||||
<path
|
||||
className="fill-brand-night dark:fill-brand-cloud"
|
||||
d="m 358,111 v 65.55 q 0,9.75 -2.4,17.55 -2.4,7.95 -7.5,13.35 -4.95,5.55 -12.6,8.55 -7.65,3 -18.15,3 -4.35,0 -9.3,-0.75 -4.8,-0.75 -9.6,-2.25 -4.65,-1.5 -9,-3.75 -4.35,-2.25 -7.8,-5.1 L 292,193.2 q 5.25,4.95 11.85,7.5 6.6,2.7 13.35,2.7 6.45,0 10.8,-1.95 4.5,-1.8 7.05,-5.1 2.7,-3.3 3.75,-7.8 1.2,-4.5 1.2,-9.9 v -5.25 h -0.3 q -3.9,5.25 -10.05,8.1 -6,2.7 -13.5,2.7 -8.1,0 -14.7,-3 -6.6,-3 -11.25,-8.1 -4.5,-5.1 -7.05,-11.85 -2.55,-6.9 -2.55,-14.55 0,-7.8 2.4,-14.7 2.55,-6.9 7.05,-12 4.65,-5.1 11.1,-8.1 6.6,-3 14.7,-3 7.65,0 14.25,3.15 6.6,3.15 10.5,9.75 h 0.3 V 111 Z m -38.4,12.6 q -4.95,0 -8.85,1.8 -3.75,1.8 -6.45,4.95 -2.55,3 -4.05,7.2 -1.35,4.2 -1.35,9 0,4.35 1.35,8.4 1.5,4.05 4.05,7.35 2.7,3.15 6.45,5.1 3.9,1.95 8.7,1.95 4.95,0 8.85,-1.8 4.05,-1.95 6.75,-5.1 2.85,-3.15 4.35,-7.2 1.5,-4.2 1.5,-8.7 0,-4.65 -1.5,-8.85 -1.5,-4.2 -4.35,-7.35 -2.7,-3.15 -6.6,-4.95 -3.9,-1.8 -8.85,-1.8 z m 55,-12.6 h 17.25 v 12 h 0.3 q 3,-6.3 8.4,-10.2 5.4,-3.9 12.6,-3.9 1.05,0 2.25,0.15 1.2,0 2.1,0.3 v 16.5 q -1.8,-0.45 -3.15,-0.6 -1.2,-0.15 -2.4,-0.15 -6.15,0 -9.9,2.25 -3.75,2.25 -5.85,5.4 -2.1,3.15 -2.85,6.45 -0.75,3.3 -0.75,5.25 V 183 h -18 z m 115.30004,72 h -17.1 v -11.55 h -0.3 q -2.4,5.4 -8.4,9.6 -5.85,4.05 -13.95,4.05 -7.05,0 -12.15,-2.4 -4.95,-2.55 -8.25,-6.6 -3.15,-4.05 -4.65,-9.3 -1.5,-5.25 -1.5,-10.8 v -45 h 18 v 39.9 q 0,3.15 0.45,6.6 0.45,3.45 1.95,6.3 1.5,2.7 4.05,4.5 2.7,1.8 7.05,1.8 4.2,0 7.35,-1.65 3.3,-1.8 5.25,-4.5 2.1,-2.7 3.15,-6.15 1.05,-3.6 1.05,-7.2 V 111 h 18 z m 33.85011,-72 v 10.05 h 0.45 q 1.35,-2.1 3.45,-4.2 2.25,-2.25 5.4,-3.9 3.15,-1.8 7.05,-2.85 4.05,-1.2 9,-1.2 7.65,0 14.1,3 6.45,2.85 11.1,7.95 4.65,5.1 7.2,12 2.55,6.9 2.55,14.85 0,7.95 -2.55,15 -2.4,6.9 -7.05,12.15 -4.5,5.1 -11.1,8.25 -6.45,3 -14.7,3 -7.65,0 -14.1,-3.15 -6.3,-3.15 -9.75,-8.55 h -0.3 V 219 h -18 V 111 Z m 42,35.7 q 0,-4.2 -1.35,-8.25 -1.2,-4.2 -3.75,-7.5 -2.55,-3.3 -6.45,-5.25 -3.9,-2.1 -9.15,-2.1 -4.95,0 -8.85,2.1 -3.9,2.1 -6.75,5.4 -2.7,3.3 -4.2,7.5 -1.35,4.2 -1.35,8.4 0,4.2 1.35,8.4 1.5,4.05 4.2,7.35 2.85,3.3 6.75,5.4 3.9,1.95 8.85,1.95 5.25,0 9.15,-2.1 3.9,-2.1 6.45,-5.4 2.55,-3.3 3.75,-7.5 1.35,-4.2 1.35,-8.4 z m 81.3999,-6.9 q 0,-3.45 -1.05,-6.6 -0.9,-3.15 -3,-5.55 -2.1,-2.4 -5.4,-3.75 -3.15,-1.5 -7.5,-1.5 -8.1,0 -13.8,4.95 -5.55,4.8 -6.15,12.45 z m 18,8.1 q 0,1.2 0,2.4 0,1.2 -0.15,2.4 h -54.75 q 0.3,3.9 1.95,7.2 1.8,3.15 4.65,5.55 2.85,2.25 6.45,3.6 3.6,1.35 7.5,1.35 6.75,0 11.4,-2.4 4.65,-2.55 7.65,-6.9 l 12,9.6 q -10.65,14.4 -30.9,14.4 -8.4,0 -15.45,-2.55 -7.05,-2.7 -12.3,-7.5 -5.1,-4.8 -8.1,-11.7 -2.85,-7.05 -2.85,-15.9 0,-8.7 2.85,-15.75 3,-7.2 8.1,-12.15 5.1,-5.1 12,-7.8 7.05,-2.85 15.15,-2.85 7.5,0 13.8,2.55 6.45,2.4 11.1,7.35 4.65,4.8 7.2,12.15 2.7,7.2 2.7,16.95 z M 677.54997,111 h 17.25 v 12 h 0.3 q 3,-6.3 8.4,-10.2 5.4,-3.9 12.6,-3.9 1.05,0 2.25,0.15 1.2,0 2.1,0.3 v 16.5 q -1.8,-0.45 -3.15,-0.6 -1.2,-0.15 -2.4,-0.15 -6.15,0 -9.9,2.25 -3.75,2.25 -5.85,5.4 -2.1,3.15 -2.85,6.45 -0.75,3.3 -0.75,5.25 V 183 h -18 z m 49.90007,-41.4 h 18 V 183 h -18 z"
|
||||
/>
|
||||
<path
|
||||
className="fill-brand-night dark:fill-brand-cloud"
|
||||
d="M755 111 H767.4 Q771.3 111 772.6 114.8 C776.5 126.2 781.3 137.6 787.6 148.9 Q789.2 151.9 785.8 152.5 C781.9 153.2 777.8 153.4 774 152.9 Q770.2 152.4 768.5 149 C762.9 138 757.9 126.7 753.1 115.3 Q751.3 111 755 111 Z"
|
||||
/>
|
||||
<path
|
||||
className="fill-brand-celeste"
|
||||
d="M812.5 111 H824.4 Q828.8 111 827.2 115.1 C817.2 140.2 807.3 167 796.4 194.6 C790.1 210.7 783.2 219 767.2 219 C764.2 219 761.5 218.7 759 218.2 Q756.6 217.7 757 214.7 L758 205.1 Q758.3 202.4 761.1 203 C763.1 203.5 765.2 203.9 767 203.9 C774.9 203.9 778 198.7 781.6 189.4 C791.9 162.5 800.1 138.1 808.4 114 Q809.5 111 812.5 111 Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Logo.displayName = 'Logo'
|
||||
Logo.displayName = 'Logo'
|
||||
@@ -10,30 +10,24 @@ export const LogoIcon = forwardRef<SVGSVGElement, LogoIconProps>(
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
viewBox="0 0 40 40"
|
||||
fill="none"
|
||||
viewBox="718.5 93 144 144"
|
||||
role="img"
|
||||
aria-label="Gruperly"
|
||||
className={cn('size-8', className)}
|
||||
{...props}
|
||||
>
|
||||
<rect
|
||||
width="40"
|
||||
height="40"
|
||||
rx="10"
|
||||
className="fill-accent-soft"
|
||||
<title>Gruperly</title>
|
||||
<path
|
||||
className="fill-brand-night dark:fill-brand-cloud"
|
||||
d="M755 111 H767.4 Q771.3 111 772.6 114.8 C776.5 126.2 781.3 137.6 787.6 148.9 Q789.2 151.9 785.8 152.5 C781.9 153.2 777.8 153.4 774 152.9 Q770.2 152.4 768.5 149 C762.9 138 757.9 126.7 753.1 115.3 Q751.3 111 755 111 Z"
|
||||
/>
|
||||
<path
|
||||
d="M17 14.5c0-1.4 3-1.4 3 0V27.5c0 3-2.5 6-6.5 6-3.6 0-6-2.1-6-5 0-1.4 3-1.4 3 0 0 1 .6 2 3 2 2 0 3-1.2 3-3V17z"
|
||||
className="fill-accent"
|
||||
/>
|
||||
<path
|
||||
d="M20 15.5c0-1.4 3-1.4 3 0v6c0 3 2 5 5.5 5 1.4 0 3-1.2 3-3 0-1.4 3-1.4 3 0 0 3-2.6 4.6-6.5 4.6-4 0-5-3-5-6.6v-6z"
|
||||
className="fill-accent"
|
||||
className="fill-brand-celeste"
|
||||
d="M812.5 111 H824.4 Q828.8 111 827.2 115.1 C817.2 140.2 807.3 167 796.4 194.6 C790.1 210.7 783.2 219 767.2 219 C764.2 219 761.5 218.7 759 218.2 Q756.6 217.7 757 214.7 L758 205.1 Q758.3 202.4 761.1 203 C763.1 203.5 765.2 203.9 767 203.9 C774.9 203.9 778 198.7 781.6 189.4 C791.9 162.5 800.1 138.1 808.4 114 Q809.5 111 812.5 111 Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
LogoIcon.displayName = 'LogoIcon'
|
||||
LogoIcon.displayName = 'LogoIcon'
|
||||
@@ -6,7 +6,7 @@ export function BottomNav() {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<nav className="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-white/95 backdrop-blur lg:hidden">
|
||||
<nav className="fixed inset-x-0 bottom-0 z-40 border-t border-border bg-surface/95 backdrop-blur lg:hidden">
|
||||
<div className="mx-auto grid max-w-md grid-cols-4">
|
||||
{NAV_ITEMS.map(({ label, to, icon: Icon }) => {
|
||||
const isActive = location.pathname === to
|
||||
@@ -16,7 +16,7 @@ export function BottomNav() {
|
||||
to={to}
|
||||
className={cn(
|
||||
'flex flex-col items-center gap-0.5 py-2.5 text-[11px] font-medium transition-colors',
|
||||
isActive ? 'text-accent' : 'text-foreground/50 hover:text-primary',
|
||||
isActive ? 'text-accent-fg' : 'text-foreground/50 hover:text-primary',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
import { Logo, LogoIcon } from '../brand'
|
||||
import { Moon, Sun } from 'lucide-react'
|
||||
import { useTheme } from '../../context/ThemeProvider'
|
||||
import { Logo } from '../brand'
|
||||
import { Breadcrumb } from './Breadcrumb'
|
||||
import { UserMenu } from './UserMenu'
|
||||
|
||||
function ThemeToggle() {
|
||||
const { isDark, setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme(isDark ? 'light' : 'dark')}
|
||||
aria-label={isDark ? 'Cambiar a tema claro' : 'Cambiar a tema oscuro'}
|
||||
className="flex size-9 items-center justify-center rounded-full border border-border text-foreground/70 transition-colors hover:bg-primary-soft hover:text-primary"
|
||||
>
|
||||
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-40 h-16 w-full border-b border-border bg-background/90 backdrop-blur">
|
||||
<div className="mx-auto flex h-full w-full max-w-7xl items-center justify-between px-4 lg:px-6">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<a href="/" className="flex items-center gap-2 lg:hidden" aria-label="Gruperly inicio">
|
||||
<LogoIcon className="size-7" />
|
||||
<Logo className="text-lg tracking-tight" />
|
||||
<a href="/" className="flex items-center lg:hidden" aria-label="Gruperly inicio">
|
||||
<Logo className="h-8" />
|
||||
</a>
|
||||
<Breadcrumb />
|
||||
</div>
|
||||
<UserMenu />
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeToggle />
|
||||
<UserMenu />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Link, useLocation } from '@tanstack/react-router'
|
||||
import { cn } from '../../lib/utils'
|
||||
import { Logo, LogoIcon } from '../brand'
|
||||
import { Logo } from '../brand'
|
||||
import { NAV_ITEMS } from './nav-items'
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<aside className="sticky top-16 hidden h-[calc(100dvh-4rem)] w-64 shrink-0 flex-col border-r border-border bg-white px-4 py-6 lg:flex">
|
||||
<a href="/" className="mb-8 flex items-center gap-2 px-1" aria-label="Gruperly inicio">
|
||||
<LogoIcon className="size-8" />
|
||||
<Logo className="text-lg tracking-tight" />
|
||||
<aside className="sticky top-16 hidden h-[calc(100dvh-4rem)] w-64 shrink-0 flex-col border-r border-border bg-surface px-4 py-6 lg:flex">
|
||||
<a href="/" className="mb-8 flex items-center px-1" aria-label="Gruperly inicio">
|
||||
<Logo className="h-10" />
|
||||
</a>
|
||||
|
||||
<nav className="flex flex-col gap-1">
|
||||
@@ -23,7 +22,7 @@ export function Sidebar() {
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-accent-soft text-accent'
|
||||
? 'bg-accent-soft text-accent-fg'
|
||||
: 'text-foreground/60 hover:bg-primary-soft hover:text-primary',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -78,15 +78,15 @@ export function UserMenu() {
|
||||
<div role="dialog" aria-modal="true" className="fixed inset-0 z-50 lg:hidden">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 animate-fade-in bg-black/20 backdrop-blur-sm"
|
||||
className="absolute inset-0 animate-fade-in bg-overlay backdrop-blur-sm"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<div className="absolute inset-0 flex animate-fade-in flex-col overflow-y-auto bg-white/90 backdrop-blur">
|
||||
<div className="absolute inset-0 flex animate-fade-in flex-col overflow-y-auto bg-surface/90 backdrop-blur">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
aria-label="Cerrar menú"
|
||||
className="absolute right-4 top-4 z-20 rounded-full bg-white/90 p-2 text-foreground/70 shadow-md backdrop-blur transition-colors hover:bg-white"
|
||||
className="absolute right-4 top-4 z-20 rounded-full bg-surface/90 p-2 text-foreground/70 shadow-md backdrop-blur transition-colors hover:bg-surface"
|
||||
>
|
||||
<X className="size-5" />
|
||||
</button>
|
||||
@@ -105,7 +105,7 @@ export function UserMenu() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1.5 rounded-2xl border border-border/60 bg-white/90 p-2 shadow-xl backdrop-blur">
|
||||
<div className="w-full space-y-1.5 rounded-2xl border border-border/60 bg-surface/90 p-2 shadow-xl backdrop-blur">
|
||||
<Link
|
||||
role="menuitem"
|
||||
to="/profile"
|
||||
@@ -132,7 +132,7 @@ export function UserMenu() {
|
||||
onClick={handleSignOut}
|
||||
className={cn(
|
||||
itemClass,
|
||||
'w-full justify-center border border-border bg-white/90 py-3.5 shadow-sm backdrop-blur hover:bg-primary-soft',
|
||||
'w-full justify-center border border-border bg-surface/90 py-3.5 shadow-sm backdrop-blur hover:bg-primary-soft',
|
||||
)}
|
||||
>
|
||||
<LogOut className="size-5 text-foreground/60" />
|
||||
@@ -147,7 +147,7 @@ export function UserMenu() {
|
||||
: open ? (
|
||||
<div
|
||||
role="menu"
|
||||
className="absolute right-0 top-full z-50 mt-2 w-60 rounded-xl border border-border bg-white p-1.5 shadow-lg"
|
||||
className="absolute right-0 top-full z-50 mt-2 w-60 rounded-xl border border-border bg-surface p-1.5 shadow-lg"
|
||||
>
|
||||
<div className="px-3 py-2">
|
||||
<p className="truncate text-sm font-semibold text-primary">{user?.name ?? 'Usuario'}</p>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ConfirmationStep({ group, providerName }: ConfirmationStepProps)
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="rounded-xl border border-border bg-white p-5">
|
||||
<div className="rounded-xl border border-border bg-surface p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-base font-semibold text-primary">{group.name}</h3>
|
||||
|
||||
@@ -89,8 +89,8 @@ export function FirstGroupStep({ onBack, onCompleted }: FirstGroupStepProps) {
|
||||
className={cn(
|
||||
'h-9 rounded-full border px-3.5 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'border-accent bg-accent text-white'
|
||||
: 'border-border bg-white text-primary hover:bg-primary-soft',
|
||||
? 'border-accent bg-accent text-on-accent'
|
||||
: 'border-border bg-surface text-primary hover:bg-primary-soft',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
@@ -168,7 +168,7 @@ export function FirstGroupStep({ onBack, onCompleted }: FirstGroupStepProps) {
|
||||
'rounded-xl border px-3 py-2.5 text-left transition-colors',
|
||||
isActive
|
||||
? 'border-accent bg-accent-soft'
|
||||
: 'border-border bg-white hover:bg-primary-soft',
|
||||
: 'border-border bg-surface hover:bg-primary-soft',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -46,7 +46,7 @@ export function PaymentStep({ initialResult, onConnected, onBack }: PaymentStepP
|
||||
</header>
|
||||
|
||||
<div className="rounded-xl border border-success/30 bg-success-soft p-5 text-center">
|
||||
<span className="mx-auto flex size-12 items-center justify-center rounded-full bg-white">
|
||||
<span className="mx-auto flex size-12 items-center justify-center rounded-full bg-surface">
|
||||
<CheckCircle2 className="size-6 text-success" />
|
||||
</span>
|
||||
<p className="mt-3 text-sm font-semibold text-success">Cuenta conectada</p>
|
||||
@@ -96,7 +96,7 @@ export function PaymentStep({ initialResult, onConnected, onBack }: PaymentStepP
|
||||
aria-checked={isSelected}
|
||||
onClick={() => setSelected(provider.value)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-xl border bg-white p-4 text-left transition-colors',
|
||||
'flex w-full items-center gap-3 rounded-xl border bg-surface p-4 text-left transition-colors',
|
||||
isSelected
|
||||
? 'border-accent ring-2 ring-accent/20'
|
||||
: 'border-border hover:bg-primary-soft',
|
||||
@@ -123,7 +123,7 @@ export function PaymentStep({ initialResult, onConnected, onBack }: PaymentStepP
|
||||
<span
|
||||
className={cn(
|
||||
'flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors',
|
||||
isSelected ? 'border-accent bg-accent text-white' : 'border-border',
|
||||
isSelected ? 'border-accent bg-accent text-on-accent' : 'border-border',
|
||||
)}
|
||||
>
|
||||
{isSelected ? <Check className="size-3" strokeWidth={3} /> : null}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function Stepper({ steps, current }: StepperProps) {
|
||||
<span
|
||||
className={cn(
|
||||
'flex size-8 items-center justify-center rounded-full border text-sm font-semibold transition-colors',
|
||||
isDone && 'border-accent bg-accent text-white',
|
||||
isDone && 'border-accent bg-accent text-on-accent',
|
||||
isActive && 'border-accent text-accent ring-4 ring-accent/15',
|
||||
!isDone && !isActive && 'border-border text-foreground/40',
|
||||
)}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function WelcomeStep({ name, onNext }: WelcomeStepProps) {
|
||||
{STEPS_TO_SETUP.map(({ icon: Icon, title, description }) => (
|
||||
<li
|
||||
key={title}
|
||||
className="flex items-center gap-3 rounded-xl border border-border bg-white p-4"
|
||||
className="flex items-center gap-3 rounded-xl border border-border bg-surface p-4"
|
||||
>
|
||||
<span className="rounded-lg bg-accent-soft p-2">
|
||||
<Icon className="size-5 text-accent" />
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-accent text-sm font-semibold text-white',
|
||||
'flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-accent text-sm font-semibold text-on-accent',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -10,9 +10,9 @@ export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
}
|
||||
|
||||
const variants: Record<ButtonVariant, string> = {
|
||||
primary: 'bg-accent text-white hover:bg-accent-strong',
|
||||
primary: 'bg-accent text-on-accent hover:bg-accent-strong',
|
||||
ghost: 'bg-transparent text-primary hover:bg-primary-soft',
|
||||
outline: 'border border-border bg-white text-primary hover:bg-primary-soft',
|
||||
outline: 'border border-border bg-surface text-primary hover:bg-primary-soft',
|
||||
}
|
||||
|
||||
const sizes: Record<ButtonSize, string> = {
|
||||
|
||||
@@ -11,7 +11,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'h-10 w-full rounded-xl border border-border bg-white px-3 text-sm text-primary placeholder:text-foreground/40 transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'h-10 w-full rounded-xl border border-border bg-surface px-3 text-sm text-primary placeholder:text-foreground/40 transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
invalid && 'border-danger focus:border-danger focus:ring-danger/30',
|
||||
className,
|
||||
)}
|
||||
|
||||
68
apps/web/src/context/ThemeProvider.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'system'
|
||||
|
||||
type ThemeContextValue = {
|
||||
theme: Theme
|
||||
isDark: boolean
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'theme'
|
||||
|
||||
function readStoredTheme(): Theme {
|
||||
const value = localStorage.getItem(STORAGE_KEY)
|
||||
return value === 'light' || value === 'dark' || value === 'system' ? value : 'system'
|
||||
}
|
||||
|
||||
function systemPrefersDark() {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
|
||||
function resolveDark(theme: Theme) {
|
||||
return theme === 'dark' || (theme === 'system' && systemPrefersDark())
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(readStoredTheme)
|
||||
const [isDark, setIsDark] = useState(() =>
|
||||
document.documentElement.classList.contains('dark'),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
document.documentElement.classList.toggle('dark', resolveDark(theme))
|
||||
setIsDark(document.documentElement.classList.contains('dark'))
|
||||
localStorage.setItem(STORAGE_KEY, theme)
|
||||
}
|
||||
update()
|
||||
|
||||
if (theme !== 'system') return
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
media.addEventListener('change', update)
|
||||
return () => media.removeEventListener('change', update)
|
||||
}, [theme])
|
||||
|
||||
const setTheme = useCallback((next: Theme) => setThemeState(next), [])
|
||||
const value = useMemo(() => ({ theme, isDark, setTheme }), [theme, isDark, setTheme])
|
||||
|
||||
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
if (!context) {
|
||||
throw new Error('useTheme debe usarse dentro de <ThemeProvider>')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -1,28 +1,53 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Colores de marca */
|
||||
--color-background: #f8fafc;
|
||||
--color-primary: #0a2540;
|
||||
--color-primary-soft: #f0f4f9;
|
||||
--color-accent: #1e90ff;
|
||||
--color-accent-strong: #0088ff;
|
||||
--color-accent-soft: #e6f4ff;
|
||||
--color-foreground: #0f172a;
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Badges de estado */
|
||||
--color-success: #10b981;
|
||||
--color-success-soft: #dcfce7;
|
||||
--color-warning: #f59e0b;
|
||||
--color-warning-soft: #fef3c7;
|
||||
--color-danger: #ef4444;
|
||||
--color-danger-soft: #fee2e2;
|
||||
--color-border: #e2e8f0;
|
||||
@theme inline {
|
||||
/* Escala de marca (identidad fija, igual a la landing) */
|
||||
--color-brand-50: var(--brand-50);
|
||||
--color-brand-100: var(--brand-100);
|
||||
--color-brand-200: var(--brand-200);
|
||||
--color-brand-300: var(--brand-300);
|
||||
--color-brand-400: var(--brand-400);
|
||||
--color-brand-500: var(--brand-500);
|
||||
--color-brand-600: var(--brand-600);
|
||||
--color-brand-700: var(--brand-700);
|
||||
--color-brand-800: var(--brand-800);
|
||||
--color-brand-900: var(--brand-900);
|
||||
--color-brand-950: var(--brand-950);
|
||||
|
||||
/* Colores de la gráfica del isotipo/logotipo */
|
||||
--color-brand-night: var(--brand-night);
|
||||
--color-brand-celeste: var(--brand-celeste);
|
||||
--color-brand-cloud: var(--brand-cloud);
|
||||
|
||||
/* Tokens semánticos (conmutan con tema claro/oscuro) */
|
||||
--color-background: var(--background);
|
||||
--color-surface: var(--surface);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-soft: var(--primary-soft);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-strong: var(--accent-strong);
|
||||
--color-accent-soft: var(--accent-soft);
|
||||
--color-accent-fg: var(--accent-fg);
|
||||
--color-border: var(--border);
|
||||
--color-on-accent: var(--on-accent);
|
||||
--color-overlay: var(--overlay);
|
||||
--color-success: var(--success);
|
||||
--color-success-soft: var(--success-soft);
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-soft: var(--warning-soft);
|
||||
--color-danger: var(--danger);
|
||||
--color-danger-soft: var(--danger-soft);
|
||||
|
||||
/* Border radius por defecto */
|
||||
--radius-xl: 0.75rem;
|
||||
|
||||
/* Animaciones */
|
||||
/* Tipografía (igual que la landing) */
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
|
||||
/* Animaciones */
|
||||
--animate-fade-in: fade-in 0.2s ease-out;
|
||||
--animate-step-enter: step-enter 0.35s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
|
||||
@@ -43,17 +68,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
:root {
|
||||
color-scheme: light;
|
||||
|
||||
/* Escala de marca */
|
||||
--brand-50: #eef4ff;
|
||||
--brand-100: #dbe6fe;
|
||||
--brand-200: #bfd3fe;
|
||||
--brand-300: #93b4fd;
|
||||
--brand-400: #608afa;
|
||||
--brand-500: #3b63f5;
|
||||
--brand-600: #2544ea;
|
||||
--brand-700: #1d33d8;
|
||||
--brand-800: #1e2cb0;
|
||||
--brand-900: #1e2b8b;
|
||||
--brand-950: #171d54;
|
||||
--brand-night: #171d54;
|
||||
--brand-celeste: #93b4fd;
|
||||
--brand-cloud: #eef4ff;
|
||||
|
||||
/* Tema claro */
|
||||
--background: #ffffff;
|
||||
--surface: #ffffff;
|
||||
--primary: #0f172a;
|
||||
--primary-soft: #f1f5f9;
|
||||
--foreground: #475569;
|
||||
--accent: #2544ea;
|
||||
--accent-strong: #1d33d8;
|
||||
--accent-soft: #eef4ff;
|
||||
--accent-fg: #2544ea;
|
||||
--border: #e2e8f0;
|
||||
--on-accent: #ffffff;
|
||||
--overlay: rgb(0 0 0 / 0.2);
|
||||
--success: #059669;
|
||||
--success-soft: #ecfdf5;
|
||||
--warning: #b45309;
|
||||
--warning-soft: #fffbeb;
|
||||
--danger: #f43f5e;
|
||||
--danger-soft: #fff1f2;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
/* Tema oscuro (paleta slate/brand de la landing) */
|
||||
--background: #020617;
|
||||
--surface: #0f172a;
|
||||
--primary: #f1f5f9;
|
||||
--primary-soft: #1e293b;
|
||||
--foreground: #94a3b8;
|
||||
--accent: #2544ea;
|
||||
--accent-strong: #1d33d8;
|
||||
--accent-soft: color-mix(in srgb, var(--brand-500) 15%, transparent);
|
||||
--accent-fg: #93b4fd;
|
||||
--border: #1e293b;
|
||||
--on-accent: #ffffff;
|
||||
--overlay: rgb(0 0 0 / 0.4);
|
||||
--success: #34d399;
|
||||
--success-soft: color-mix(in srgb, var(--success) 15%, transparent);
|
||||
--warning: #fcd34d;
|
||||
--warning-soft: color-mix(in srgb, var(--warning) 15%, transparent);
|
||||
--danger: #fb7185;
|
||||
--danger-soft: color-mix(in srgb, var(--danger) 15%, transparent);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-primary antialiased;
|
||||
@apply bg-background text-primary antialiased transition-colors;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { RouterProvider } from '@tanstack/react-router'
|
||||
import { router } from './router'
|
||||
import { AuthProvider } from './context/AuthProvider'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
import './index.css'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -15,9 +16,11 @@ const queryClient = new QueryClient({
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<RouterProvider router={router} />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ export function VerifyEmailPage() {
|
||||
{status === 'success' ? (
|
||||
<Link
|
||||
to="/"
|
||||
className="w-full rounded-xl bg-accent py-2 text-center text-sm font-medium text-white hover:bg-accent-strong"
|
||||
className="w-full rounded-xl bg-accent py-2 text-center text-sm font-medium text-on-accent hover:bg-accent-strong"
|
||||
>
|
||||
Ir a Gruperly
|
||||
</Link>
|
||||
|
||||
@@ -10,7 +10,7 @@ function GroupCard({ group }: { group: GroupDto }) {
|
||||
const hasSchedule = (group.days?.length ?? 0) > 0
|
||||
|
||||
return (
|
||||
<article className="rounded-xl border border-border bg-white p-5">
|
||||
<article className="rounded-xl border border-border bg-surface p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="truncate text-base font-semibold text-primary">{group.name}</h3>
|
||||
@@ -81,7 +81,7 @@ export function GroupsView() {
|
||||
) : null}
|
||||
|
||||
{groupsQuery.isSuccess && groupsQuery.data.data.length === 0 ? (
|
||||
<div className="mt-8 rounded-xl border border-dashed border-border bg-white px-4 py-12 text-center">
|
||||
<div className="mt-8 rounded-xl border border-dashed border-border bg-surface px-4 py-12 text-center">
|
||||
<p className="font-medium text-primary">Todavía no tenés grupos</p>
|
||||
<p className="mt-1 text-sm text-foreground/60">
|
||||
Crea tu primer grupo para empezar a cobrar.
|
||||
|
||||
@@ -63,7 +63,7 @@ export function OnboardingView() {
|
||||
<div className="flex min-h-dvh items-center justify-center px-4">
|
||||
<div className="w-full max-w-sm space-y-4 text-center">
|
||||
<span className="mx-auto flex size-14 items-center justify-center rounded-2xl bg-accent-soft">
|
||||
<LogoIcon className="size-8 text-accent" />
|
||||
<LogoIcon className="size-8" />
|
||||
</span>
|
||||
<h1 className="text-2xl font-bold text-primary">Tu cuenta, lista</h1>
|
||||
<p className="text-sm text-foreground/60">
|
||||
@@ -92,9 +92,8 @@ export function OnboardingView() {
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-dvh w-full max-w-md flex-col px-4 pb-12 pt-8 sm:pt-12">
|
||||
<header className="mb-6 flex items-center justify-center gap-2">
|
||||
<LogoIcon className="size-7" />
|
||||
<Logo className="text-xl" />
|
||||
<header className="mb-6 flex items-center justify-center">
|
||||
<Logo className="h-6" />
|
||||
</header>
|
||||
|
||||
<Stepper steps={STEPS} current={step} />
|
||||
|
||||
@@ -69,7 +69,7 @@ function OrganizationList({ onRefresh }: { onRefresh: () => void }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="divide-y divide-border rounded-xl border border-border bg-white">
|
||||
<ul className="divide-y divide-border rounded-xl border border-border bg-surface">
|
||||
{organizations.map((org) => (
|
||||
<li key={org.id} className="flex flex-wrap items-center gap-3 px-4 py-3">
|
||||
<span className="rounded-lg bg-accent-soft p-2">
|
||||
@@ -148,7 +148,7 @@ export function OrganizationsPage() {
|
||||
<p className="rounded-xl bg-success-soft px-4 py-3 text-sm text-success">{feedback}</p>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-xl border border-border bg-white p-5">
|
||||
<div className="rounded-xl border border-border bg-surface p-5">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<Plus className="size-4 text-accent" />
|
||||
<h2 className="text-base font-semibold text-primary">Crear grupo</h2>
|
||||
@@ -182,7 +182,7 @@ export function OrganizationsPage() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-white p-5">
|
||||
<div className="rounded-xl border border-border bg-surface p-5">
|
||||
<h2 className="mb-4 text-base font-semibold text-primary">Tus grupos</h2>
|
||||
<OrganizationList key={refreshKey} onRefresh={refreshOrganizations} />
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function ProfileView() {
|
||||
<p className="mt-1 text-sm text-foreground/60">Tus datos personales.</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-white p-5">
|
||||
<div className="rounded-xl border border-border bg-surface p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar name={user?.name} src={user?.image ?? undefined} className="size-14 text-lg" />
|
||||
<div className="min-w-0">
|
||||
|
||||
@@ -42,7 +42,7 @@ function PasskeyList({ onRefresh }: { onRefresh: () => void }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="divide-y divide-border rounded-xl border border-border bg-white">
|
||||
<ul className="divide-y divide-border rounded-xl border border-border bg-surface">
|
||||
{passkeys.map((passkey) => (
|
||||
<li key={passkey.id} className="flex items-center gap-3 px-4 py-3">
|
||||
<span className="rounded-lg bg-accent-soft p-2">
|
||||
@@ -126,7 +126,7 @@ export function SecurityPage() {
|
||||
<p className="rounded-xl bg-accent-soft px-4 py-3 text-sm text-accent-strong">{feedback}</p>
|
||||
) : null}
|
||||
|
||||
<div className="rounded-xl border border-border bg-white p-5">
|
||||
<div className="rounded-xl border border-border bg-surface p-5">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<KeyRound className="size-4 text-accent" />
|
||||
<h2 className="text-base font-semibold text-primary">Cambiar contraseña</h2>
|
||||
@@ -165,7 +165,7 @@ export function SecurityPage() {
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-border bg-white p-5">
|
||||
<div className="rounded-xl border border-border bg-surface p-5">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Fingerprint className="size-4 text-accent" />
|
||||
|
||||
@@ -1,7 +1,64 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Building2, Fingerprint } from 'lucide-react'
|
||||
import { Building2, Check, Fingerprint, Monitor, Moon, Sun } from 'lucide-react'
|
||||
import { signOut } from '../lib/auth-client'
|
||||
import { Button } from '../components/ui'
|
||||
import { useTheme, type Theme } from '../context/ThemeProvider'
|
||||
import { cn } from '../lib/utils'
|
||||
|
||||
const THEME_OPTIONS: { value: Theme; label: string; description: string }[] = [
|
||||
{ value: 'light', label: 'Claro', description: 'Interfaz en tonos claros' },
|
||||
{ value: 'dark', label: 'Oscuro', description: 'Interfaz en tonos oscuros' },
|
||||
{ value: 'system', label: 'Sistema', description: 'Sigue la preferencia de tu dispositivo' },
|
||||
]
|
||||
|
||||
const THEME_ICONS: Record<Theme, typeof Sun> = {
|
||||
light: Sun,
|
||||
dark: Moon,
|
||||
system: Monitor,
|
||||
}
|
||||
|
||||
function AppearanceCard() {
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-border rounded-xl border border-border bg-surface">
|
||||
<div className="px-4 py-3">
|
||||
<p className="text-sm font-medium text-primary">Apariencia</p>
|
||||
<p className="text-xs text-foreground/50">Tema claro u oscuro</p>
|
||||
</div>
|
||||
{THEME_OPTIONS.map((option) => {
|
||||
const Icon = THEME_ICONS[option.value]
|
||||
const isSelected = theme === option.value
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setTheme(option.value)}
|
||||
className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-primary-soft"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded-lg p-2',
|
||||
isSelected ? 'bg-accent-soft' : 'bg-primary-soft',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={cn('size-4', isSelected ? 'text-accent-fg' : 'text-foreground/70')}
|
||||
/>
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={cn('text-sm font-medium', isSelected ? 'text-accent-fg' : 'text-primary')}>
|
||||
{option.label}
|
||||
</p>
|
||||
<p className="text-xs text-foreground/50">{option.description}</p>
|
||||
</div>
|
||||
{isSelected ? <Check className="size-4 text-accent-fg" /> : null}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsView() {
|
||||
const handleSignOut = async () => {
|
||||
@@ -18,13 +75,13 @@ export function SettingsView() {
|
||||
<p className="mt-1 text-sm text-foreground/60">Configurá tu cuenta y tus grupos.</p>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border rounded-xl border border-border bg-white">
|
||||
<div className="divide-y divide-border rounded-xl border border-border bg-surface">
|
||||
<Link
|
||||
to="/settings/organizations"
|
||||
className="flex items-center gap-3 px-4 py-3 hover:bg-primary-soft"
|
||||
>
|
||||
<span className="rounded-lg bg-accent-soft p-2">
|
||||
<Building2 className="size-4 text-accent" />
|
||||
<Building2 className="size-4 text-accent-fg" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-primary">Grupos</p>
|
||||
@@ -34,7 +91,7 @@ export function SettingsView() {
|
||||
|
||||
<Link to="/seguridad" className="flex items-center gap-3 px-4 py-3 hover:bg-primary-soft">
|
||||
<span className="rounded-lg bg-accent-soft p-2">
|
||||
<Fingerprint className="size-4 text-accent" />
|
||||
<Fingerprint className="size-4 text-accent-fg" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-primary">Seguridad</p>
|
||||
@@ -43,6 +100,8 @@ export function SettingsView() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<AppearanceCard />
|
||||
|
||||
<Button variant="outline" onClick={handleSignOut}>
|
||||
Cerrar sesión
|
||||
</Button>
|
||||
|
||||