refactor: improve invoice editor UX and fix visual issues

- Remove clock icons and hour text from calendar month view, show only activity bars
- Fix calendar week view mobile layout (2-column grid instead of vertical stack)
- Update invoice form skeleton to match actual layout structure
- Add client-side validation for empty invoice item descriptions with auto-scroll to error
- Fix hourly rate defaulting logic with proper type guards
- Update invoice details skeleton to match page structure with PageHeader
- Fix hydration error in sidebar (div inside button -> span)
- Improve dashboard chart color consistency (draft status now matches monthly metrics)
- Fix mobile header layout to prevent text squishing (vertical stack on mobile)
- Add IDs to invoice line items for scroll-into-view functionality
This commit is contained in:
2025-12-11 19:57:54 -05:00
parent 39fdf16280
commit 1a3c2e08ce
27 changed files with 1685 additions and 2024 deletions
+18 -105
View File
@@ -1,13 +1,11 @@
import "~/styles/globals.css";
import { type Metadata } from "next";
import { Geist, Geist_Mono, Instrument_Serif } from "next/font/google";
import { Inter, Playfair_Display, Geist_Mono } from "next/font/google";
import { TRPCReactProvider } from "~/trpc/react";
import { Toaster } from "~/components/ui/sonner";
import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider";
import { MotionBackground } from "~/components/layout/motion-background";
import { ThemeProvider } from "~/components/providers/theme-provider";
import { ColorThemeProvider } from "~/components/providers/color-theme-provider";
import { UmamiScript } from "~/components/analytics/umami-script";
@@ -19,9 +17,15 @@ export const metadata: Metadata = {
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
const geistSans = Geist({
const inter = Inter({
subsets: ["latin"],
variable: "--font-geist-sans",
variable: "--font-sans",
display: "swap",
});
const playfair = Playfair_Display({
subsets: ["latin"],
variable: "--font-heading",
display: "swap",
});
@@ -31,13 +35,6 @@ const geistMono = Geist_Mono({
display: "swap",
});
const instrumentSerif = Instrument_Serif({
subsets: ["latin"],
variable: "--font-serif",
display: "swap",
weight: "400",
});
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
@@ -45,105 +42,21 @@ export default function RootLayout({
<html
suppressHydrationWarning
lang="en"
className={`${geistSans.variable} ${geistMono.variable} ${instrumentSerif.variable}`}
className={`${inter.variable} ${playfair.variable} ${geistMono.variable}`}
>
<head>
{/* Inline early theme and animation preference script to avoid FOUC */}
<script
dangerouslySetInnerHTML={{
__html: `(function(){
try {
var root = document.documentElement;
// Mode theme persistence (light/dark/system)
var modeTheme = localStorage.getItem('theme');
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
root.classList.remove('light', 'dark');
if (modeTheme === 'dark' || modeTheme === 'light') {
root.classList.add(modeTheme);
} else {
// Default to system if no preference or 'system'
root.classList.add(systemTheme);
}
// Color theme persistence (custom accent colors)
var customColor = localStorage.getItem('customThemeColor');
var isCustom = localStorage.getItem('isCustomTheme') === 'true';
if (isCustom && customColor) {
try {
var themeData = JSON.parse(customColor);
if (themeData && themeData.colors && themeData.colors.light) {
// Apply saved colors directly
for (var key in themeData.colors.light) {
if (themeData.colors.light.hasOwnProperty(key)) {
root.style.setProperty(key, themeData.colors.light[key]);
}
}
}
} catch (e) {
// Fallback logic omitted for brevity, relying on provider for full recovery
}
} else {
// Apply preset color theme
var colorTheme = localStorage.getItem('color-theme');
if (colorTheme) {
root.classList.add(colorTheme);
} else {
root.classList.add('slate'); // Default
}
}
// Animation preferences script (existing)
var STORAGE_KEY='bv.animation.prefs';
var raw=localStorage.getItem(STORAGE_KEY);
var prefersReduced=false;
var speed=1;
if(raw){
try{
var parsed=JSON.parse(raw);
if(typeof parsed.prefersReducedMotion==='boolean'){
prefersReduced=parsed.prefersReducedMotion;
}else{
prefersReduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
if(typeof parsed.animationSpeedMultiplier==='number'){
speed=parsed.animationSpeedMultiplier;
if(isNaN(speed)||speed<0.25||speed>4)speed=1;
}
}catch(e){}
}else{
prefersReduced=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
if(prefersReduced)root.classList.add('user-reduce-motion');
function apply(fast,normal,slow){
root.style.setProperty('--animation-speed-fast',fast+'s');
root.style.setProperty('--animation-speed-normal',normal+'s');
root.style.setProperty('--animation-speed-slow',slow+'s');
}
if(prefersReduced){
apply(0.01,0.01,0.01);
}else{
var fast=(0.15/speed).toFixed(4);
var normal=(0.30/speed).toFixed(4);
var slow=(0.50/speed).toFixed(4);
apply(fast,normal,slow);
}
} catch(_e) {}
})();`,
}}
/>
</head>
<body className="bg-background text-foreground relative min-h-screen overflow-x-hidden font-sans antialiased">
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none flex items-center justify-center">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
<div className="w-[800px] h-[800px] bg-neutral-400/40 dark:bg-neutral-500/30 rounded-full blur-3xl animate-blob"></div>
</div>
<TRPCReactProvider>
<ThemeProvider>
<ColorThemeProvider>
<AnimationPreferencesProvider>
<MotionBackground />
{children}
<div className="relative z-10">
{children}
</div>
</AnimationPreferencesProvider>
<Toaster />
<UmamiScript />