Polish mobile and web experience

This commit is contained in:
2026-08-17 00:18:55 -04:00
parent 6c74436092
commit 9929d7321d
28 changed files with 835 additions and 688 deletions
+56
View File
@@ -0,0 +1,56 @@
const DARK_ACTION_FOREGROUND = "#18181B";
const LIGHT_ACTION_FOREGROUND = "#FFFFFF";
const MIN_TEXT_CONTRAST = 4.5;
function parseHexColor(color: string): [number, number, number] | null {
const value = color.trim().replace(/^#/, "");
const expanded =
value.length === 3
? value
.split("")
.map((character) => character.repeat(2))
.join("")
: value.slice(0, 6);
if (expanded.length !== 6 || !/^[0-9a-f]+$/i.test(expanded)) return null;
return [
Number.parseInt(expanded.slice(0, 2), 16),
Number.parseInt(expanded.slice(2, 4), 16),
Number.parseInt(expanded.slice(4, 6), 16),
];
}
function luminance(color: string): number | null {
const rgb = parseHexColor(color);
if (!rgb) return null;
const channels = rgb.map((channel) => {
const value = channel / 255;
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * channels[0]! + 0.7152 * channels[1]! + 0.0722 * channels[2]!;
}
function contrastRatio(foreground: string, background: string): number | null {
const foregroundLuminance = luminance(foreground);
const backgroundLuminance = luminance(background);
if (foregroundLuminance == null || backgroundLuminance == null) return null;
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
const darker = Math.min(foregroundLuminance, backgroundLuminance);
return (lighter + 0.05) / (darker + 0.05);
}
/** Chooses a readable action label/icon color while preserving a valid requested color. */
export function resolveActionForeground(background: string, requested: string): string {
const requestedContrast = contrastRatio(requested, background);
if (requestedContrast == null || requestedContrast >= MIN_TEXT_CONTRAST) {
return requested;
}
const darkContrast = contrastRatio(DARK_ACTION_FOREGROUND, background) ?? 0;
const lightContrast = contrastRatio(LIGHT_ACTION_FOREGROUND, background) ?? 0;
return darkContrast >= lightContrast ? DARK_ACTION_FOREGROUND : LIGHT_ACTION_FOREGROUND;
}
+35 -37
View File
@@ -11,6 +11,18 @@ const CHUNK_MARKER = "\u0001ba-chunks:";
const SESSION_TOKEN_COOKIE_PART =
/(?:^|;\s*)(?:__Secure-)?[^=]*session_token=([^;]+)/;
const AUTH_COOKIE_DEBUG = process.env.EXPO_PUBLIC_AUTH_COOKIE_DEBUG === "1";
const lastAuthCookieDebugState = new Map<string, string>();
function debugAuthCookie(event: string, storagePrefix: string, details: Record<string, unknown>) {
if (!AUTH_COOKIE_DEBUG) return;
const key = `${event}:${storagePrefix}`;
const state = JSON.stringify(details);
if (lastAuthCookieDebugState.get(key) === state) return;
lastAuthCookieDebugState.set(key, state);
console.info(`[auth-cookie] ${event}`, { storagePrefix, ...details });
}
function readSecureStoreValueSync(key: string): string | null {
const value = SecureStore.getItem(key);
@@ -56,25 +68,19 @@ export function getAuthCookie(
).getCookie?.();
if (fromClient?.trim()) {
const cookie = fromClient.trim();
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] using client cookie", {
storagePrefix,
length: cookie.length,
names: cookieNames(cookie),
});
}
debugAuthCookie("using client cookie", storagePrefix, {
length: cookie.length,
names: cookieNames(cookie),
});
return cookie;
}
const fromPrefix = readStoredCookie(storagePrefix);
if (fromPrefix) {
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] using stored cookie", {
storagePrefix,
length: fromPrefix.length,
names: cookieNames(fromPrefix),
});
}
debugAuthCookie("using stored cookie", storagePrefix, {
length: fromPrefix.length,
names: cookieNames(fromPrefix),
});
return fromPrefix;
}
@@ -82,18 +88,15 @@ export function getAuthCookie(
storagePrefix === GUEST_AUTH_STORAGE_PREFIX
? null
: readStoredCookie(GUEST_AUTH_STORAGE_PREFIX);
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] resolved tRPC cookie", {
storagePrefix,
fallbackPrefix:
fromGuest && storagePrefix !== GUEST_AUTH_STORAGE_PREFIX
? GUEST_AUTH_STORAGE_PREFIX
: null,
hasCookie: Boolean(fromGuest),
length: fromGuest?.length ?? 0,
names: fromGuest ? cookieNames(fromGuest) : [],
});
}
debugAuthCookie("resolved tRPC cookie", storagePrefix, {
fallbackPrefix:
fromGuest && storagePrefix !== GUEST_AUTH_STORAGE_PREFIX
? GUEST_AUTH_STORAGE_PREFIX
: null,
hasCookie: Boolean(fromGuest),
length: fromGuest?.length ?? 0,
names: fromGuest ? cookieNames(fromGuest) : [],
});
return fromGuest;
}
@@ -103,21 +106,16 @@ export function getAuthCookieHeaders(
): Record<string, string> {
const cookie = getAuthCookie(authClient, storagePrefix);
if (!cookie) {
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] no tRPC auth cookie", { storagePrefix });
}
debugAuthCookie("no tRPC auth cookie", storagePrefix, {});
return {};
}
const sessionToken = cookie.match(SESSION_TOKEN_COOKIE_PART)?.[1];
if (AUTH_COOKIE_DEBUG) {
console.info("[auth-cookie] sending tRPC auth headers", {
storagePrefix,
cookieLength: cookie.length,
cookieNames: cookieNames(cookie),
hasSessionTokenHeader: Boolean(sessionToken),
});
}
debugAuthCookie("sending tRPC auth headers", storagePrefix, {
cookieLength: cookie.length,
cookieNames: cookieNames(cookie),
hasSessionTokenHeader: Boolean(sessionToken),
});
return {
cookie,
Cookie: cookie,
+8 -3
View File
@@ -1,7 +1,12 @@
/** Matches web invoice-form default numbering. */
export function generateInvoiceNumber(): string {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
return `INV-${date}-${String(Date.now()).slice(-6)}`;
export function generateInvoiceNumber(now = new Date()): string {
const date = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
].join("");
return `INV-${date}-${String(now.getTime()).slice(-6)}`;
}
export function defaultDueDate(issueDate: Date): Date {
+1 -4
View File
@@ -6,9 +6,6 @@ import { spacing } from "@/constants/theme";
/** Standard UITabBar content height (home indicator is separate). */
const IOS_TAB_BAR_HEIGHT = 49;
/** Trim extra inset so scroll content sits closer to the tab bar. */
const TAB_BAR_PADDING_TRIM = spacing.lg;
/**
* Pixels between the bottom of the safe-area layout frame and the window bottom.
*/
@@ -39,7 +36,7 @@ export function useTabBarScrollPadding(): number {
const tabBar = useNativeTabBarHeight();
const clearance = tabBar + homeIndicator;
return Math.max(spacing.xs, clearance - TAB_BAR_PADDING_TRIM);
return clearance + spacing.sm;
}
/** Bottom offset for floating action buttons above the tab bar. */