diff --git a/src/client/pages/LoginPage.tsx b/src/client/pages/LoginPage.tsx index 939b8fb..3f2ffbc 100644 --- a/src/client/pages/LoginPage.tsx +++ b/src/client/pages/LoginPage.tsx @@ -26,24 +26,41 @@ export function LoginPage() { const errorMessage = loginMutation.error instanceof Error ? loginMutation.error.message : 'Unable to sign in'; return ( -
- -
- +
+ +
+ upwatch
-
-
-

Admin access

-

Sign in to Upwatch

- Enter the admin password to manage your monitors. +
+
+

+ Admin access +

+

+ Sign in to Upwatch +

+ + Enter the admin password to manage your monitors. +
-
-
- + +
+ setPassword(event.target.value)} minLength={8} required + className="transition-[border-color,box-shadow] duration-150 dark:bg-night-input/85 dark:text-gray-50 dark:shadow-[inset_0_1px_2px_rgba(0,0,0,0.4)] dark:hover:border-white/24 dark:focus-visible:border-brand dark:focus-visible:shadow-[0_0_0_3px_rgba(62,207,142,0.2),inset_0_1px_2px_rgba(0,0,0,0.4)]" />
{loginMutation.isError && ( -

+

{errorMessage}

)} -
-

Protected by an encrypted, seven-day session.

+

Protected by an encrypted, seven-day session.

); } diff --git a/src/client/styles.css b/src/client/styles.css index dc6b2bb..2a5a5f9 100644 --- a/src/client/styles.css +++ b/src/client/styles.css @@ -2,7 +2,6 @@ @import 'tailwindcss'; @import 'tw-animate-css'; @import 'shadcn/tailwind.css'; -@import './styles/LoginPage.css'; @import './styles/StatusPage.css'; @import './styles/DashboardPage.css'; @import './styles/MonitorDetailPage.css'; @@ -589,6 +588,72 @@ button { @theme inline { --font-sans: 'DM Sans', 'Helvetica Neue', sans-serif; --font-mono: 'IBM Plex Mono', monospace; + --animate-enter: enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both; + + /* Brand emerald palette */ + --color-brand: #3ecf8e; + --color-brand-deep: #24b47e; + --color-brand-hover: #36c487; + --color-brand-border: #35c586; + --color-brand-light: #4ae09e; + --color-brand-muted: #55816e; + --color-brand-dark-text: #090a0c; + + /* Semantic text & border tokens (auto-adapt in dark mode via CSS variables) */ + --color-ink: var(--ink); + --color-hairline: var(--hairline); + --color-faint: var(--faint); + --color-soft: var(--soft); + --color-night: var(--night); + + /* Neutral text and icon shades */ + --color-ink-label: #353535; + --color-ink-subtle: #626262; + --color-ink-footnote: #858585; + --color-icon-muted: #5f5f5f; + + /* Dark mode surfaces */ + --color-night-root: #090a0c; + --color-night-card: #111317; + --color-night-input: #0b0d0f; + + /* Error alert state */ + --color-error-bg: #fff6f6; + --color-error-border: #efcaca; + --color-error-text: #9f2f2f; + + /* Letter spacing (tracking) */ + --tracking-display: -1.5px; + --tracking-title: -1.2px; + --tracking-brand: -0.6px; + --tracking-overline: 0.08em; + + /* Line height (leading) */ + --leading-title: 1.16; + --leading-overline: 1.4; + --leading-alert: 1.45; + --leading-subtitle: 1.55; + + /* Typography scale (matching DESIGN.md and app UI) */ + --text-2xs: 10px; + --text-footnote: 11px; + --text-micro: 12px; + --text-caption: 13px; + --text-body: 14px; + --text-subtitle: 19px; + --text-brand: 20px; + --text-title-compact: 27px; + --text-title: 30px; + --text-display: 40px; + + /* Custom breakpoints */ + --breakpoint-compact: 440px; + + /* Shadows */ + --shadow-card-elevated: 0 18px 55px rgb(24 74 52 / 0.08), 0 2px 8px rgb(0 0 0 / 0.04); + --shadow-btn-primary: 0 1px 2px rgb(0 0 0 / 0.08), inset 0 1px rgb(255 255 255 / 0.2); + --drop-shadow-brand-glow: 0 0 12px rgba(62, 207, 142, 0.45); + --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); @@ -622,10 +687,11 @@ button { --color-card: var(--card); --color-foreground: var(--foreground); --color-background: var(--background); - --radius-sm: calc(var(--radius) * 0.6); - --radius-md: calc(var(--radius) * 0.8); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) * 1.4); + --radius-xs: 4px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; --radius-2xl: calc(var(--radius) * 1.8); --radius-3xl: calc(var(--radius) * 2.2); --radius-4xl: calc(var(--radius) * 2.6); diff --git a/src/client/styles/LoginPage.css b/src/client/styles/LoginPage.css deleted file mode 100644 index c4abeb0..0000000 --- a/src/client/styles/LoginPage.css +++ /dev/null @@ -1,244 +0,0 @@ -.auth-page { - position: relative; - display: grid; - grid-template-rows: auto 1fr auto; - justify-items: center; - min-height: 100dvh; - padding: 32px 24px 24px; - background: radial-gradient(circle at 50% 38%, rgb(62 207 142 / 0.08), transparent 34%), linear-gradient(#fff, #fcfcfc); - transition: - background 0.25s ease, - color 0.25s ease; -} -.dark .auth-page { - background: - radial-gradient(ellipse 75% 55% at 50% 30%, rgba(62, 207, 142, 0.12), transparent 65%), - radial-gradient(ellipse 40% 30% at 50% 12%, rgba(36, 180, 126, 0.07), transparent 50%), - linear-gradient(180deg, #0c0e10 0%, #070809 100%); -} -.auth-theme-toggle { - position: absolute; - z-index: 1; - top: 28px; - right: 28px; - transition: - color 0.15s ease, - background 0.15s ease, - border-color 0.15s ease; -} -.dark .auth-theme-toggle { - color: #9ca3af; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 8px; - backdrop-filter: blur(8px); -} -.dark .auth-theme-toggle:hover:not(:disabled) { - color: #3ecf8e; - background: rgba(62, 207, 142, 0.1); - border-color: rgba(62, 207, 142, 0.25); -} -.auth-brand { - position: relative; - z-index: 1; - display: inline-flex; - align-items: center; - gap: 8px; - font-size: 20px; - font-weight: 600; - letter-spacing: -0.6px; -} -.dark .auth-brand { - color: #ffffff; -} -.auth-brand-mark { - width: 26px; - height: 26px; - color: var(--primary-deep); -} -.dark .auth-brand-mark { - color: #3ecf8e; - filter: drop-shadow(0 0 12px rgba(62, 207, 142, 0.45)); -} -.auth-card { - position: relative; - z-index: 1; - align-self: center; - width: min(100%, 420px); - padding: 36px; - border: 1px solid var(--hairline); - border-radius: 12px; - background: rgb(255 255 255 / 0.96); - box-shadow: - 0 18px 55px rgb(24 74 52 / 0.08), - 0 2px 8px rgb(0 0 0 / 0.04); - animation: enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both; -} -.dark .auth-card { - background: rgba(17, 19, 23, 0.82); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba(255, 255, 255, 0.08); - box-shadow: - 0 0 0 1px rgba(255, 255, 255, 0.05), - 0 24px 64px -12px rgba(0, 0, 0, 0.8), - 0 0 45px -10px rgba(62, 207, 142, 0.08), - inset 0 1px 0 0 rgba(255, 255, 255, 0.06); -} -.auth-heading p { - margin: 0 0 12px; - font: - 500 10px/1.4 'IBM Plex Mono', - monospace; - letter-spacing: 0.08em; - text-transform: uppercase; - color: #55816e; -} -.dark .auth-heading p { - color: #3ecf8e; -} -.auth-heading h1 { - margin: 0; - font-size: 30px; - font-weight: 500; - line-height: 1.16; - letter-spacing: -1.2px; -} -.dark .auth-heading h1 { - color: #f9fafb; -} -.auth-heading > span { - display: block; - margin-top: 12px; - font-size: 19px; - line-height: 1.55; - color: #626262; -} -.dark .auth-heading > span { - color: #9ca3af; -} -.auth-form { - display: grid; - gap: 20px; - margin-top: 30px; -} -.auth-field { - display: grid; - gap: 8px; -} -.auth-field label { - font-size: 13px; - font-weight: 500; - color: #353535; -} -.dark .auth-field label { - color: #d1d5db; -} -.dark .auth-field input { - background-color: rgba(11, 13, 15, 0.85); - border-color: rgba(255, 255, 255, 0.12); - color: #f9fafb; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4); - transition: - border-color 0.15s ease, - box-shadow 0.15s ease; -} -.dark .auth-field input:hover { - border-color: rgba(255, 255, 255, 0.24); -} -.dark .auth-field input:focus-visible { - border-color: #3ecf8e; - box-shadow: - 0 0 0 3px rgba(62, 207, 142, 0.2), - inset 0 1px 2px rgba(0, 0, 0, 0.4); -} -.auth-error { - margin: -4px 0 0; - padding: 10px 12px; - border: 1px solid #efcaca; - border-radius: 6px; - font-size: 12px; - line-height: 1.45; - color: #9f2f2f; - background: #fff6f6; -} -.dark .auth-error { - background: rgba(239, 68, 68, 0.12); - border-color: rgba(239, 68, 68, 0.28); - color: #fca5a5; -} -.auth-submit { - display: inline-flex; - align-items: center; - justify-content: center; - width: 100%; - min-height: 42px; - padding: 8px 16px; - border: 1px solid #35c586; - border-radius: 6px; - font-size: 14px; - font-weight: 600; - color: var(--ink); - background: var(--primary); - box-shadow: - 0 1px 2px rgb(0 0 0 / 0.08), - inset 0 1px rgb(255 255 255 / 0.2); - cursor: pointer; -} -.auth-submit:hover:not(:disabled) { - background: #36c487; - transform: translateY(-1px); -} -.auth-submit:active:not(:disabled) { - background: var(--primary-deep); - transform: translateY(1px); -} -.auth-submit:disabled { - cursor: wait; - opacity: 0.62; -} -.dark .auth-submit { - background: #3ecf8e; - border-color: #3ecf8e; - color: #090a0c; - font-weight: 600; - box-shadow: - 0 1px 2px rgba(0, 0, 0, 0.3), - 0 4px 14px rgba(62, 207, 142, 0.3), - inset 0 1px 0 rgba(255, 255, 255, 0.35); - transition: all 0.15s ease; -} -.dark .auth-submit:hover:not(:disabled) { - background: #4ae09e; - border-color: #4ae09e; - box-shadow: - 0 4px 20px rgba(62, 207, 142, 0.45), - inset 0 1px 0 rgba(255, 255, 255, 0.4); - transform: translateY(-1px); -} -.dark .auth-submit:active:not(:disabled) { - background: #24b47e; - transform: translateY(1px); -} -.auth-footnote { - position: relative; - z-index: 1; - margin: 0; - font-size: 11px; - color: #858585; -} -.dark .auth-footnote { - color: var(--faint); -} - -@media (max-width: 440px) { - .auth-page { - padding: 24px 16px; - } - .auth-card { - padding: 28px 22px; - } - .auth-heading h1 { - font-size: 27px; - } -} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 160b04b..d861786 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -12,6 +12,10 @@ const buttonVariants = cva( unstyled: null, default: 'bg-oklch(0.205 0 0) text-oklch(0.985 0 0) hover:bg-oklch(0.205 0 0)/80 dark:bg-oklch(0.922 0 0) dark:text-oklch(0.205 0 0) dark:hover:bg-oklch(0.922 0 0)/80', + brand: + 'rounded-sm border-brand-border bg-brand font-semibold text-ink shadow-btn-primary transition-all duration-150 hover:not-disabled:-translate-y-px hover:not-disabled:bg-brand-hover active:not-disabled:translate-y-px active:not-disabled:bg-brand-deep disabled:cursor-wait disabled:opacity-[0.62] dark:border-brand dark:bg-brand dark:text-brand-dark-text dark:shadow-[0_1px_2px_rgba(0,0,0,0.3),0_4px_14px_rgba(62,207,142,0.3),inset_0_1px_0_rgba(255,255,255,0.35)] dark:hover:not-disabled:-translate-y-px dark:hover:not-disabled:border-brand-light dark:hover:not-disabled:bg-brand-light dark:hover:not-disabled:shadow-[0_4px_20px_rgba(62,207,142,0.45),inset_0_1px_0_rgba(255,255,255,0.4)] dark:active:not-disabled:translate-y-px dark:active:not-disabled:bg-brand-deep', + primary: + 'rounded-sm border-brand-border bg-brand font-semibold text-ink shadow-btn-primary transition-all duration-150 hover:not-disabled:-translate-y-px hover:not-disabled:bg-brand-hover active:not-disabled:translate-y-px active:not-disabled:bg-brand-deep disabled:cursor-wait disabled:opacity-[0.62] dark:border-brand dark:bg-brand dark:text-brand-dark-text dark:shadow-[0_1px_2px_rgba(0,0,0,0.3),0_4px_14px_rgba(62,207,142,0.3),inset_0_1px_0_rgba(255,255,255,0.35)] dark:hover:not-disabled:-translate-y-px dark:hover:not-disabled:border-brand-light dark:hover:not-disabled:bg-brand-light dark:hover:not-disabled:shadow-[0_4px_20px_rgba(62,207,142,0.45),inset_0_1px_0_rgba(255,255,255,0.4)] dark:active:not-disabled:translate-y-px dark:active:not-disabled:bg-brand-deep', outline: 'border-oklch(0.922 0 0) bg-oklch(1 0 0) hover:bg-oklch(0.97 0 0) hover:text-oklch(0.145 0 0) aria-expanded:bg-oklch(0.97 0 0) aria-expanded:text-oklch(0.145 0 0) dark:border-oklch(0.922 0 0) dark:bg-oklch(0.922 0 0)/30 dark:hover:bg-oklch(0.922 0 0)/50 dark:border-oklch(1 0 0 / 10%) dark:bg-oklch(0.145 0 0) dark:hover:bg-oklch(0.269 0 0) dark:hover:text-oklch(0.985 0 0) dark:aria-expanded:bg-oklch(0.269 0 0) dark:aria-expanded:text-oklch(0.985 0 0) dark:dark:border-oklch(1 0 0 / 15%) dark:dark:bg-oklch(1 0 0 / 15%)/30 dark:dark:hover:bg-oklch(1 0 0 / 15%)/50', secondary: @@ -27,6 +31,7 @@ const buttonVariants = cva( xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + xl: 'min-h-10.5 gap-2 px-4 py-2 text-body', icon: 'size-8', 'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",