feat(habits): add opt-in partial count carryover with daily reset defaults

This commit is contained in:
syntaxbullet
2026-09-04 09:11:11 +02:00
parent e9cc591576
commit 9a7180ce78
9 changed files with 775 additions and 14 deletions

View File

@@ -17,23 +17,23 @@ export const taskInput = z.object({ name, schedule }).strict();
export const taskPatch = taskInput.partial().refine(v => Object.keys(v).length > 0);
const common = { name, schedule };
export const habitInput = z.discriminatedUnion('method', [
z.object({ ...common, method: z.literal('count'), target: z.number().int().min(1).max(10000), unit: z.string().trim().min(1).max(80).default('steps') }).strict(),
z.object({ ...common, method: z.literal('count'), carryPartialProgress: z.boolean().default(false), target: z.number().int().min(1).max(10000), unit: z.string().trim().min(1).max(80).default('steps') }).strict(),
z.object({ ...common, method: z.literal('manual') }).strict(),
z.object({ ...common, method: z.literal('tasks'), tasks: z.array(taskInput).max(100).default([]) }).strict(),
]);
export type HabitConfig = { name: string; schedule: Schedule; archived: boolean } & (
{ method: 'count'; target: number; unit: string } |
{ method: 'count'; carryPartialProgress: boolean; target: number; unit: string } |
{ method: 'manual' } |
{ method: 'tasks'; tasks: { id: string; name: string; schedule: Schedule }[] }
);
export const habitPatch = z.object({ name: name.optional(), schedule: scheduleSchema.optional(), method: z.enum(['count', 'manual', 'tasks']).optional(), target: z.number().int().min(1).max(10000).optional(), unit: z.string().trim().min(1).max(80).optional(), archived: z.boolean().optional() }).strict().refine(v => Object.keys(v).length > 0);
export const habitPatch = z.object({ carryPartialProgress: z.boolean().optional(), name: name.optional(), schedule: scheduleSchema.optional(), method: z.enum(['count', 'manual', 'tasks']).optional(), target: z.number().int().min(1).max(10000).optional(), unit: z.string().trim().min(1).max(80).optional(), archived: z.boolean().optional() }).strict().refine(v => Object.keys(v).length > 0);
export const progressInput = z.union([z.object({ count: z.number().int().min(0).max(1_000_000_000) }).strict(), z.object({ done: z.boolean() }).strict()]);
export const doneInput = z.object({ done: z.boolean() }).strict();
const color = z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Use a six-digit hex color');
// Product preferences only; layout and callbacks belong to the future UI.
export const calendarSettingsSchema = z.object({
mainColor: color.default('#196127'),
shadeCount: z.number().int().min(2).max(20).default(4),
shadeCount: z.union([z.literal('auto'), z.number().int().min(2).max(20)]).default('auto'),
emptyColor: color.default('#ebedf0'),
notDueColor: color.default('#f5f5f5'),
futureColor: color.default('#dbeafe'),