Files
triggerdotdev--trigger.dev/apps/webapp/app/utils/dashboardPreferences.ts
Katia Bulatova fbd6df33b4 feat(webapp): Themes + contrast settings update (#4206)
Adds System Preferences, Dark and Light themes, gated by the
`hasThemeSwitcher` feature flag (off by default — dark stays the default
theme for everyone).

Old theme is now "Classic"and set as default. 
"System preferences" theme has both Light and Dark modes and uses your
laptop settings to use a correct one.
It has less color accents (specifically less colored text), and they are
the same for both modes, only grayscale values change between them. And
Light/Dark themes can be used separately.

New Contrast setting is available for System Preferences, Dark and Light
themes - it changes the contrast for the whole app. All new visual
Settings live in Account.
2026-08-03 19:29:33 +02:00

93 lines
3.1 KiB
TypeScript

import { z } from "zod";
import { ThemePreference } from "~/utils/themePreference";
/* Schema and pure parsing for the User.dashboardPreferences JSON column.
Kept out of the .server module so tests can exercise the schema without
pulling in the server env graph. */
export const FavoritePage = z.object({
/** Stable id, generated client-side when the page is favorited. */
id: z.string(),
/** App-relative URL including any search params (filters, tabs). */
url: z.string(),
/** Display label shown in the side menu; user-renamable. */
label: z.string(),
/** Key into the favorite page icon registry. */
icon: z.string().optional(),
});
export type FavoritePage = z.infer<typeof FavoritePage>;
export const SideMenuPreferences = z.object({
isCollapsed: z.boolean().default(false),
/** Expanded side menu width in px, set by the resize handle. */
width: z.number().optional(),
// Map for section collapsed states - keys are section identifiers
collapsedSections: z.record(z.string(), z.boolean()).optional(),
/** Organization-specific settings */
organizations: z
.record(
z.string(),
z.object({
orderedItems: z.record(z.string(), z.array(z.string())),
})
)
.optional(),
/** Pages the user favorited, in display order. */
favorites: z.array(FavoritePage).optional(),
/** Custom top-to-bottom order of side menu sections (section ids). */
sectionOrder: z.array(z.string()).optional(),
/** Per-item visibility overrides (item id -> hidden). Items absent fall back to their default. */
hiddenItems: z.record(z.string(), z.boolean()).optional(),
/** Custom item order within a section (section id -> item ids). */
sectionItemOrder: z.record(z.string(), z.array(z.string())).optional(),
});
export type SideMenuPreferences = z.infer<typeof SideMenuPreferences>;
const DashboardPreferences = z.object({
version: z.literal("1"),
/* An unknown value (e.g. written by a newer deploy) degrades to undefined
instead of failing the whole blob and erasing every other setting */
theme: ThemePreference.optional().catch(undefined),
/** Interface contrast for the System themes, 0-100. */
contrast: z.number().int().min(0).max(100).optional().catch(undefined),
currentProjectId: z.string().optional(),
projects: z.record(
z.string(),
z.object({
currentEnvironment: z.object({ id: z.string() }),
})
),
sideMenu: SideMenuPreferences.optional(),
});
export type DashboardPreferences = z.infer<typeof DashboardPreferences>;
/* A function, not a shared constant: the writers mutate through these objects,
so each caller needs its own container */
function defaultPreferences(): DashboardPreferences {
return {
version: "1",
projects: {},
};
}
/** Parses the stored JSON, falling back to defaults on missing or invalid data. */
export function parseDashboardPreferences(
data?: any | null,
onError?: (error: z.ZodError) => void
): DashboardPreferences {
if (!data) {
return defaultPreferences();
}
const result = DashboardPreferences.safeParse(data);
if (!result.success) {
onError?.(result.error);
return defaultPreferences();
}
return result.data;
}