dink-pets-shared 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ import { MONTH_NAMES } from "./constants.js";
3
+ import { DayOfWeek } from "./enums.js";
4
+ // =============================================================================
5
+ // REUSABLE FIELD SCHEMAS
6
+ // =============================================================================
7
+ // Single source of truth for field validation across the app.
8
+ // Use these in both server (tRPC inputs) and mobile (form validation).
9
+ // =============================================================================
10
+ // Text length limits
11
+ export const TINY_TEXT_MAX = 50; // colors, codes, IDs
12
+ export const SHORT_TEXT_MAX = 100; // names, brands, providers
13
+ export const LONG_TEXT_MAX = 2000; // notes, descriptions
14
+ // Numeric limits
15
+ export const MEALS_PER_DAY_MIN = 1;
16
+ export const MEALS_PER_DAY_MAX = 10;
17
+ // Base text schemas
18
+ export const ShortText = z.string().max(SHORT_TEXT_MAX);
19
+ export const TinyText = z.string().max(TINY_TEXT_MAX);
20
+ export const LongText = z.string().max(LONG_TEXT_MAX);
21
+ // Semantic aliases (for readability in inputs)
22
+ export const AmountUnit = TinyText; // "cups", "oz", "mg", etc.
23
+ export const Subtype = TinyText; // "pee", "poop", "breakfast", medication name
24
+ export const PetColor = TinyText; // "black", "golden", "black and white"
25
+ export const Breed = ShortText;
26
+ export const MicrochipId = TinyText;
27
+ export const RabiesTagNumber = TinyText;
28
+ export const InsuranceProvider = ShortText;
29
+ export const InsurancePolicyNumber = ShortText;
30
+ // Activity-specific fields
31
+ export const DURATION_MIN_MIN = 1;
32
+ export const DURATION_MIN_MAX = 1440; // 24 hours in minutes
33
+ export const DurationMin = z
34
+ .number()
35
+ .int()
36
+ .min(DURATION_MIN_MIN)
37
+ .max(DURATION_MIN_MAX);
38
+ export const Amount = z.number().positive();
39
+ // Photo fields
40
+ export const CAPTION_MAX = 250;
41
+ export const Caption = z.string().max(CAPTION_MAX);
42
+ // Health log fields
43
+ export const HealthValue = z.number(); // weight, temperature, etc.
44
+ export const HealthUnit = TinyText; // "lbs", "kg", "°F"
45
+ export const Title = ShortText; // "Annual checkup", "Limping"
46
+ export const VetName = ShortText;
47
+ export const Location = ShortText;
48
+ /**
49
+ * Name field for pets, users, etc.
50
+ *
51
+ * Restricts to safe characters to prevent XSS in contexts like email templates
52
+ * where names are interpolated. Allows unicode letters (\p{L}) for international
53
+ * names (José, 北京, Мария) while blocking HTML-unsafe characters like < >.
54
+ */
55
+ export const Name = ShortText.min(1).regex(/^[\p{L}\p{N}\s\-'\.]+$/u, "Only letters, numbers, spaces, hyphens, apostrophes, periods");
56
+ export const Notes = LongText;
57
+ export const bathroomActivitySubtypes = ["pee", "poo"];
58
+ export const BathroomActivitySubtype = z.enum(bathroomActivitySubtypes);
59
+ /**
60
+ * URL for images (pet photos, avatars, etc.)
61
+ *
62
+ * Restricted to HTTPS only to prevent XSS via javascript:, data:, or file:// URLs
63
+ * if the URL is ever rendered in an <img src> or <a href> context.
64
+ */
65
+ export const ImageUrl = z.url().startsWith("https://");
66
+ export const DateField = z.coerce.date();
67
+ export const MealsPerDay = z
68
+ .number()
69
+ .int()
70
+ .min(MEALS_PER_DAY_MIN)
71
+ .max(MEALS_PER_DAY_MAX);
72
+ export const PAGINATION_LIMIT_MIN = 1;
73
+ export const PAGINATION_LIMIT_MAX = 50;
74
+ export const PAGINATION_LIMIT_DEFAULT = 20;
75
+ export const PaginationLimit = z
76
+ .number()
77
+ .int()
78
+ .min(PAGINATION_LIMIT_MIN)
79
+ .max(PAGINATION_LIMIT_MAX)
80
+ .default(PAGINATION_LIMIT_DEFAULT);
81
+ // Mood scale (1-5)
82
+ export const MOOD_MIN = 1;
83
+ export const MOOD_MAX = 5;
84
+ export const Mood = z.number().int().min(MOOD_MIN).max(MOOD_MAX);
85
+ // =============================================================================
86
+ // REMINDER/SCHEDULE FIELDS
87
+ // =============================================================================
88
+ /**
89
+ * Time of day in HH:MM format (24-hour).
90
+ *
91
+ * Examples: "08:00", "14:30", "23:59"
92
+ * Used for reminder schedules. The mobile app interprets this in the device's timezone.
93
+ */
94
+ export const Time = z
95
+ .string()
96
+ .regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Time must be in HH:MM format (e.g., 08:00)");
97
+ /**
98
+ * Day of month (1-31).
99
+ *
100
+ * Used for monthly reminders. Values 29-31 may not exist in all months;
101
+ * the scheduler should handle this (e.g., Feb 30 becomes Feb 28).
102
+ */
103
+ export const DAY_OF_MONTH_MIN = 1;
104
+ export const DAY_OF_MONTH_MAX = 31;
105
+ export const DayOfMonth = z
106
+ .number()
107
+ .int()
108
+ .min(DAY_OF_MONTH_MIN)
109
+ .max(DAY_OF_MONTH_MAX);
110
+ /**
111
+ * Interval count for recurring reminders.
112
+ *
113
+ * "Every N days/weeks/months" - N is the interval count.
114
+ * Minimum 1 (every day/week/month), no maximum because it depends on user needs and allows flexibility like 45 days.
115
+ */
116
+ export const INTERVAL_COUNT_MIN = 1;
117
+ export const IntervalCount = z.number().int().min(INTERVAL_COUNT_MIN);
118
+ /**
119
+ * Maps DayOfWeek enum values to JavaScript Date.getDay() values (0 = Sunday).
120
+ */
121
+ export const DAY_OF_WEEK_TO_NUMBER = {
122
+ [DayOfWeek.enum.SUN]: 0,
123
+ [DayOfWeek.enum.MON]: 1,
124
+ [DayOfWeek.enum.TUE]: 2,
125
+ [DayOfWeek.enum.WED]: 3,
126
+ [DayOfWeek.enum.THU]: 4,
127
+ [DayOfWeek.enum.FRI]: 5,
128
+ [DayOfWeek.enum.SAT]: 6,
129
+ };
130
+ // =============================================================================
131
+ // PET DATE FIELDS (birthday, adoption date)
132
+ // =============================================================================
133
+ // Stored as separate year/month/day components because owners often don't
134
+ // know exact dates. Year is minimum required, month and day are optional.
135
+ // =============================================================================
136
+ /**
137
+ * Year for pet dates (birthday, adoption).
138
+ *
139
+ * Range: 1990-2100. Pets born before 1990 are extremely rare (35+ years old).
140
+ * Upper bound leaves room for future dates (adoption planned for next year).
141
+ */
142
+ export const YEAR_MIN = 1990;
143
+ export const YEAR_MAX = 2100;
144
+ export const YearInt = z.number().int().min(YEAR_MIN).max(YEAR_MAX);
145
+ /**
146
+ * Month (1-12).
147
+ */
148
+ export const MONTH_MIN = 1;
149
+ export const MONTH_MAX = 12;
150
+ export const MonthInt = z.number().int().min(MONTH_MIN).max(MONTH_MAX);
151
+ /**
152
+ * Format a pet date with variable precision.
153
+ *
154
+ * @example
155
+ * formatPetDate(2020) // "2020"
156
+ * formatPetDate(2020, 3) // "March 2020"
157
+ * formatPetDate(2020, 3, 15) // "March 15, 2020"
158
+ */
159
+ export function formatPetDate(year, month, day) {
160
+ if (!month)
161
+ return String(year);
162
+ const monthName = MONTH_NAMES[month - 1];
163
+ if (!day)
164
+ return `${monthName} ${year}`;
165
+ return `${monthName} ${day}, ${year}`;
166
+ }
@@ -0,0 +1,224 @@
1
+ import { ActivityType } from "./enums.js";
2
+ export declare const palette: {
3
+ readonly white: "#ffffff";
4
+ readonly black: "#000000";
5
+ readonly gray: {
6
+ readonly 100: "#f3f4f6";
7
+ readonly 200: "#e5e7eb";
8
+ readonly 400: "#9ca3af";
9
+ readonly 500: "#6b7280";
10
+ readonly 600: "#4b5563";
11
+ readonly 700: "#374151";
12
+ readonly 800: "#1f2937";
13
+ readonly 900: "#111827";
14
+ };
15
+ readonly purple: {
16
+ readonly 100: "#f3e8ff";
17
+ readonly 300: "#d8b4fe";
18
+ readonly 400: "#a78bfa";
19
+ readonly 500: "#8b5cf6";
20
+ readonly 600: "#7c3aed";
21
+ readonly 900: "#4c1d95";
22
+ };
23
+ readonly pink: {
24
+ readonly 300: "#f9a8d4";
25
+ readonly 500: "#ec4899";
26
+ readonly 900: "#831843";
27
+ };
28
+ readonly orange: {
29
+ readonly 300: "#fdba74";
30
+ readonly 500: "#f97316";
31
+ };
32
+ readonly green: {
33
+ readonly 500: "#22c55e";
34
+ };
35
+ readonly red: {
36
+ readonly 500: "#ef4444";
37
+ };
38
+ readonly yellow: {
39
+ readonly 400: "#facc15";
40
+ readonly 500: "#eab308";
41
+ };
42
+ readonly blue: {
43
+ readonly 500: "#3b82f6";
44
+ };
45
+ readonly amber: {
46
+ readonly 500: "#f59e0b";
47
+ };
48
+ readonly cyan: {
49
+ readonly 500: "#06b6d4";
50
+ };
51
+ readonly teal: {
52
+ readonly 500: "#14b8a6";
53
+ };
54
+ readonly indigo: {
55
+ readonly 500: "#6366f1";
56
+ };
57
+ };
58
+ export declare const semantic: {
59
+ readonly light: {
60
+ readonly background: "#ffffff";
61
+ readonly backgroundSubtle: "#f3f4f6";
62
+ readonly card: "#ffffff";
63
+ readonly gradientFrom: "#a78bfa";
64
+ readonly gradientVia: "#f9a8d4";
65
+ readonly gradientTo: "#fdba74";
66
+ readonly textPrimary: "#1f2937";
67
+ readonly textSecondary: "#6b7280";
68
+ readonly textMuted: "#9ca3af";
69
+ readonly textInverse: "#ffffff";
70
+ readonly textOnGradient: "#ffffff";
71
+ readonly accentPrimary: "#7c3aed";
72
+ readonly accentSecondary: "#ec4899";
73
+ readonly success: "#22c55e";
74
+ readonly destructive: "#ef4444";
75
+ readonly destructiveSubtle: "#fef2f2";
76
+ readonly border: "#e5e7eb";
77
+ readonly borderStrong: "#9ca3af";
78
+ readonly buttonPrimary: "#7c3aed";
79
+ readonly buttonPrimaryText: "#ffffff";
80
+ readonly buttonSecondary: "#ffffff";
81
+ readonly buttonSecondaryText: "#7c3aed";
82
+ readonly pressedOverlay: "rgba(0,0,0,0.1)";
83
+ readonly avatarGradientFrom: "rgba(167, 139, 250, 0.25)";
84
+ readonly avatarGradientTo: "rgba(249, 168, 212, 0.25)";
85
+ };
86
+ readonly dark: {
87
+ readonly background: "#111827";
88
+ readonly backgroundSubtle: "#1f2937";
89
+ readonly card: "#252033";
90
+ readonly gradientFrom: "#111827";
91
+ readonly gradientVia: "#4c1d95";
92
+ readonly gradientTo: "#831843";
93
+ readonly textPrimary: "#e5e7eb";
94
+ readonly textSecondary: "#9ca3af";
95
+ readonly textMuted: "#6b7280";
96
+ readonly textInverse: "#111827";
97
+ readonly textOnGradient: "#e5e7eb";
98
+ readonly accentPrimary: "#a78bfa";
99
+ readonly accentSecondary: "#ec4899";
100
+ readonly success: "#22c55e";
101
+ readonly destructive: "#ef4444";
102
+ readonly destructiveSubtle: "#450a0a";
103
+ readonly border: "#4b5563";
104
+ readonly borderStrong: "#9ca3af";
105
+ readonly buttonPrimary: "#8b5cf6";
106
+ readonly buttonPrimaryText: "#ffffff";
107
+ readonly buttonSecondary: "#1f2937";
108
+ readonly buttonSecondaryText: "#a78bfa";
109
+ readonly pressedOverlay: "rgba(255,255,255,0.1)";
110
+ readonly avatarGradientFrom: "rgba(139, 92, 246, 0.15)";
111
+ readonly avatarGradientTo: "rgba(236, 72, 153, 0.15)";
112
+ };
113
+ };
114
+ export declare const activityTypeColors: Record<ActivityType, string>;
115
+ export declare const appSections: readonly ["achievements", "photos", "schedule"];
116
+ export type AppSection = (typeof appSections)[number];
117
+ export declare const appSectionColors: Record<AppSection, string>;
118
+ export declare const cssVariables: {
119
+ readonly light: {
120
+ readonly "--color-background": "#ffffff";
121
+ readonly "--color-background-subtle": "#f3f4f6";
122
+ readonly "--color-card": "#ffffff";
123
+ readonly "--color-gradient-from": "#a78bfa";
124
+ readonly "--color-gradient-via": "#f9a8d4";
125
+ readonly "--color-gradient-to": "#fdba74";
126
+ readonly "--color-text-primary": "#1f2937";
127
+ readonly "--color-text-secondary": "#6b7280";
128
+ readonly "--color-text-muted": "#9ca3af";
129
+ readonly "--color-text-inverse": "#ffffff";
130
+ readonly "--color-text-on-gradient": "#ffffff";
131
+ readonly "--color-accent-primary": "#7c3aed";
132
+ readonly "--color-accent-secondary": "#ec4899";
133
+ readonly "--color-destructive": "#ef4444";
134
+ readonly "--color-destructive-subtle": "#fef2f2";
135
+ readonly "--color-border": "#e5e7eb";
136
+ readonly "--color-border-strong": "#9ca3af";
137
+ readonly "--color-button-primary": "#7c3aed";
138
+ readonly "--color-button-primary-text": "#ffffff";
139
+ readonly "--color-button-secondary": "#ffffff";
140
+ readonly "--color-button-secondary-text": "#7c3aed";
141
+ };
142
+ readonly dark: {
143
+ readonly "--color-background": "#111827";
144
+ readonly "--color-background-subtle": "#1f2937";
145
+ readonly "--color-card": "#252033";
146
+ readonly "--color-gradient-from": "#111827";
147
+ readonly "--color-gradient-via": "#4c1d95";
148
+ readonly "--color-gradient-to": "#831843";
149
+ readonly "--color-text-primary": "#e5e7eb";
150
+ readonly "--color-text-secondary": "#9ca3af";
151
+ readonly "--color-text-muted": "#6b7280";
152
+ readonly "--color-text-inverse": "#111827";
153
+ readonly "--color-text-on-gradient": "#e5e7eb";
154
+ readonly "--color-accent-primary": "#a78bfa";
155
+ readonly "--color-accent-secondary": "#ec4899";
156
+ readonly "--color-destructive": "#ef4444";
157
+ readonly "--color-destructive-subtle": "#450a0a";
158
+ readonly "--color-border": "#4b5563";
159
+ readonly "--color-border-strong": "#9ca3af";
160
+ readonly "--color-button-primary": "#8b5cf6";
161
+ readonly "--color-button-primary-text": "#ffffff";
162
+ readonly "--color-button-secondary": "#1f2937";
163
+ readonly "--color-button-secondary-text": "#a78bfa";
164
+ };
165
+ };
166
+ export declare const extendColors: {
167
+ readonly "section-photos": string;
168
+ readonly "section-achievements": string;
169
+ readonly "section-schedule": string;
170
+ readonly "activity-WALK": string;
171
+ readonly "activity-MEAL": string;
172
+ readonly "activity-BATHROOM": string;
173
+ readonly "activity-TREAT": string;
174
+ readonly "activity-PLAYTIME": string;
175
+ readonly "activity-MEDICATION": string;
176
+ readonly "activity-VET_APPOINTMENT": string;
177
+ readonly "activity-GROOMING": string;
178
+ readonly "activity-BATH": string;
179
+ readonly "activity-NAIL_TRIM": string;
180
+ readonly background: "var(--color-background)";
181
+ readonly "background-subtle": "var(--color-background-subtle)";
182
+ readonly card: "var(--color-card)";
183
+ readonly "text-primary": "var(--color-text-primary)";
184
+ readonly "text-secondary": "var(--color-text-secondary)";
185
+ readonly "text-muted": "var(--color-text-muted)";
186
+ readonly "text-inverse": "var(--color-text-inverse)";
187
+ readonly "text-on-gradient": "var(--color-text-on-gradient)";
188
+ readonly "accent-primary": "var(--color-accent-primary)";
189
+ readonly "accent-secondary": "var(--color-accent-secondary)";
190
+ readonly destructive: "var(--color-destructive)";
191
+ readonly "destructive-subtle": "var(--color-destructive-subtle)";
192
+ readonly border: "var(--color-border)";
193
+ readonly "border-strong": "var(--color-border-strong)";
194
+ readonly "button-primary": "var(--color-button-primary)";
195
+ readonly "button-primary-text": "var(--color-button-primary-text)";
196
+ readonly "button-secondary": "var(--color-button-secondary)";
197
+ readonly "button-secondary-text": "var(--color-button-secondary-text)";
198
+ };
199
+ export declare const tw: {
200
+ readonly screenCenter: "flex-1 items-center justify-center";
201
+ readonly container: "flex-1 p-6";
202
+ readonly pageCenter: "flex min-h-screen flex-col items-center justify-center";
203
+ readonly pageContent: "flex min-h-screen flex-col items-center justify-center gap-8 p-8";
204
+ readonly storeButton: "rounded-lg bg-accent-primary px-6 py-3 text-text-inverse hover:opacity-90";
205
+ readonly background: "bg-background";
206
+ readonly backgroundSubtle: "bg-background-subtle";
207
+ readonly card: "bg-card rounded-xl";
208
+ readonly cardPadded: "bg-card rounded-xl p-4";
209
+ readonly textPrimary: "text-text-primary";
210
+ readonly textSecondary: "text-text-secondary";
211
+ readonly textMuted: "text-text-muted";
212
+ readonly textInverse: "text-text-inverse";
213
+ readonly textOnGradient: "text-text-on-gradient";
214
+ readonly heading: "text-text-primary text-2xl font-bold";
215
+ readonly subheading: "text-text-primary text-xl font-semibold";
216
+ readonly title: "text-text-primary text-3xl font-bold";
217
+ readonly accentPrimary: "text-accent-primary";
218
+ readonly accentSecondary: "text-accent-secondary";
219
+ readonly bgAccentPrimary: "bg-accent-primary";
220
+ readonly bgAccentSecondary: "bg-accent-secondary";
221
+ readonly buttonPrimary: "bg-button-primary text-button-primary-text px-6 py-3 rounded-full font-semibold";
222
+ readonly buttonSecondary: "bg-button-secondary text-button-secondary-text px-6 py-3 rounded-full font-semibold border border-accent-primary";
223
+ readonly gradientBg: "bg-gradient-to-br from-[var(--color-gradient-from)] via-[var(--color-gradient-via)] to-[var(--color-gradient-to)]";
224
+ };