Refine admin PWA layout and tamagui usage
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-15 22:24:10 +01:00
parent 11018f273d
commit 292c8f0b26
37 changed files with 51503 additions and 21989 deletions

View File

@@ -337,8 +337,8 @@ Tokens are design system values that can be referenced using the `$` prefix.
### Color Tokens ### Color Tokens
- `accent`: #FFB6C1 - `accent`: #3D5AFE
- `accentSoft`: #FFE5EC - `accentSoft`: #E8ECFF
- `blue10Dark`: hsl(209, 100%, 60.6%) - `blue10Dark`: hsl(209, 100%, 60.6%)
- `blue10Light`: hsl(208, 100%, 47.3%) - `blue10Light`: hsl(208, 100%, 47.3%)
- `blue11Dark`: hsl(210, 100%, 66.1%) - `blue11Dark`: hsl(210, 100%, 66.1%)
@@ -363,8 +363,8 @@ Tokens are design system values that can be referenced using the `$` prefix.
- `blue8Light`: hsl(206, 81.9%, 65.3%) - `blue8Light`: hsl(206, 81.9%, 65.3%)
- `blue9Dark`: hsl(206, 100%, 50.0%) - `blue9Dark`: hsl(206, 100%, 50.0%)
- `blue9Light`: hsl(206, 100%, 50.0%) - `blue9Light`: hsl(206, 100%, 50.0%)
- `border`: #F2E4DA - `border`: #F3D6C9
- `danger`: #E04848 - `danger`: #EF4444
- `gray10Dark`: hsl(0, 0%, 49.4%) - `gray10Dark`: hsl(0, 0%, 49.4%)
- `gray10Light`: hsl(0, 0%, 52.3%) - `gray10Light`: hsl(0, 0%, 52.3%)
- `gray11Dark`: hsl(0, 0%, 62.8%) - `gray11Dark`: hsl(0, 0%, 62.8%)
@@ -413,7 +413,7 @@ Tokens are design system values that can be referenced using the `$` prefix.
- `green8Light`: hsl(151, 40.2%, 54.1%) - `green8Light`: hsl(151, 40.2%, 54.1%)
- `green9Dark`: hsl(151, 55.0%, 41.5%) - `green9Dark`: hsl(151, 55.0%, 41.5%)
- `green9Light`: hsl(151, 55.0%, 41.5%) - `green9Light`: hsl(151, 55.0%, 41.5%)
- `muted`: #F4ECE8 - `muted`: #FFF6F0
- `orange10Dark`: hsl(24, 100%, 58.5%) - `orange10Dark`: hsl(24, 100%, 58.5%)
- `orange10Light`: hsl(24, 100%, 46.5%) - `orange10Light`: hsl(24, 100%, 46.5%)
- `orange11Dark`: hsl(24, 100%, 62.2%) - `orange11Dark`: hsl(24, 100%, 62.2%)
@@ -462,7 +462,7 @@ Tokens are design system values that can be referenced using the `$` prefix.
- `pink8Light`: hsl(323, 60.3%, 72.4%) - `pink8Light`: hsl(323, 60.3%, 72.4%)
- `pink9Dark`: hsl(322, 65.0%, 54.5%) - `pink9Dark`: hsl(322, 65.0%, 54.5%)
- `pink9Light`: hsl(322, 65.0%, 54.5%) - `pink9Light`: hsl(322, 65.0%, 54.5%)
- `primary`: #FF5A5F - `primary`: #FF5C5C
- `purple10Dark`: hsl(273, 57.3%, 59.1%) - `purple10Dark`: hsl(273, 57.3%, 59.1%)
- `purple10Light`: hsl(272, 46.8%, 50.3%) - `purple10Light`: hsl(272, 46.8%, 50.3%)
- `purple11Dark`: hsl(275, 80.0%, 71.0%) - `purple11Dark`: hsl(275, 80.0%, 71.0%)
@@ -511,10 +511,10 @@ Tokens are design system values that can be referenced using the `$` prefix.
- `red8Light`: hsl(359, 69.5%, 74.3%) - `red8Light`: hsl(359, 69.5%, 74.3%)
- `red9Dark`: hsl(358, 75.0%, 59.0%) - `red9Dark`: hsl(358, 75.0%, 59.0%)
- `red9Light`: hsl(358, 75.0%, 59.0%) - `red9Light`: hsl(358, 75.0%, 59.0%)
- `success`: #06D6A0 - `success`: #22C55E
- `surface`: #ffffff - `surface`: #ffffff
- `text`: #1F2937 - `text`: #0B132B
- `warning`: #F5C542 - `warning`: #FBBF24
- `yellow10Dark`: hsl(54, 100%, 68.0%) - `yellow10Dark`: hsl(54, 100%, 68.0%)
- `yellow10Light`: hsl(50, 100%, 48.5%) - `yellow10Light`: hsl(50, 100%, 48.5%)
- `yellow11Dark`: hsl(48, 100%, 47.0%) - `yellow11Dark`: hsl(48, 100%, 47.0%)

View File

@@ -4160,16 +4160,16 @@ var tokens3 = {
...tokens2, ...tokens2,
color: { color: {
...tokens2.color, ...tokens2.color,
primary: "#FF5A5F", primary: "#FF5C5C",
accent: "#FFB6C1", accent: "#3D5AFE",
accentSoft: "#FFE5EC", accentSoft: "#E8ECFF",
success: "#06D6A0", success: "#22C55E",
warning: "#F5C542", warning: "#FBBF24",
danger: "#E04848", danger: "#EF4444",
surface: "#ffffff", surface: "#ffffff",
muted: "#F4ECE8", muted: "#FFF6F0",
border: "#F2E4DA", border: "#F3D6C9",
text: "#1F2937" text: "#0B132B"
}, },
radius: { radius: {
...tokens2.radius, ...tokens2.radius,
@@ -4188,53 +4188,53 @@ var themes3 = {
...themes2.light, ...themes2.light,
primary: tokens3.color.primary, primary: tokens3.color.primary,
accent: tokens3.color.accent, accent: tokens3.color.accent,
background: "#FFF8F5", background: "#FFF1E8",
backgroundHover: "#FFF1EC", backgroundHover: "#FFE8DD",
backgroundPress: "#FFE7E0", backgroundPress: "#FFE1D2",
backgroundStrong: tokens3.color.surface, backgroundStrong: tokens3.color.surface,
backgroundTransparent: "rgba(255, 248, 245, 0)", backgroundTransparent: "rgba(255, 241, 232, 0)",
color: tokens3.color.text, color: tokens3.color.text,
colorHover: "#111827", colorHover: "#091024",
colorPress: "#0F172A", colorPress: "#091024",
colorFocus: "#0F172A", colorFocus: "#091024",
borderColor: tokens3.color.border, borderColor: tokens3.color.border,
borderColorHover: "#EAD5C9", borderColorHover: "#EBCABA",
borderColorPress: "#E0C9BC", borderColorPress: "#E1BFAE",
shadowColor: "rgba(31, 41, 55, 0.12)", shadowColor: "rgba(11, 19, 43, 0.16)",
shadowColorPress: "rgba(31, 41, 55, 0.16)", shadowColorPress: "rgba(11, 19, 43, 0.2)",
shadowColorFocus: "rgba(31, 41, 55, 0.18)", shadowColorFocus: "rgba(11, 19, 43, 0.24)",
surface: tokens3.color.surface, surface: tokens3.color.surface,
muted: tokens3.color.muted, muted: tokens3.color.muted,
blue3: tokens3.color.accentSoft, blue3: tokens3.color.accentSoft,
blue6: tokens3.color.accent, blue6: tokens3.color.accent,
blue10: tokens3.color.primary, blue10: tokens3.color.primary,
blue11: "#C2413B" blue11: "#1E36F1"
}, },
dark: { dark: {
...themes2.dark, ...themes2.dark,
primary: tokens3.color.primary, primary: tokens3.color.primary,
accent: tokens3.color.accent, accent: tokens3.color.accent,
background: "#171219", background: "#0B132B",
backgroundHover: "#1F1A23", backgroundHover: "#101A36",
backgroundPress: "#26212B", backgroundPress: "#132142",
backgroundStrong: "#1F1A23", backgroundStrong: "#101A36",
backgroundTransparent: "rgba(23, 18, 25, 0)", backgroundTransparent: "rgba(11, 19, 43, 0)",
color: "#F8F6F2", color: "#F8FAFF",
colorHover: "#FFFFFF", colorHover: "#FFFFFF",
colorPress: "#FDF8F5", colorPress: "#F2F6FF",
colorFocus: "#FFFFFF", colorFocus: "#FFFFFF",
borderColor: "#2C2531", borderColor: "#1F2A4A",
borderColorHover: "#3A3240", borderColorHover: "#29345A",
borderColorPress: "#443C4A", borderColorPress: "#313D67",
shadowColor: "rgba(0, 0, 0, 0.55)", shadowColor: "rgba(0, 0, 0, 0.55)",
shadowColorPress: "rgba(0, 0, 0, 0.65)", shadowColorPress: "rgba(0, 0, 0, 0.65)",
shadowColorFocus: "rgba(0, 0, 0, 0.6)", shadowColorFocus: "rgba(0, 0, 0, 0.6)",
surface: "#1F1A23", surface: "#0F1B36",
muted: "#241E28", muted: "#121F3D",
blue3: "#2B1D23", blue3: "#1B2550",
blue6: "#5A2D34", blue6: "#3D5AFE",
blue10: "#FF7A7F", blue10: "#FF5C5C",
blue11: "#FFB3B6" blue11: "#FF8A8A"
} }
}; };
var sharedWeights = { var sharedWeights = {
@@ -4254,12 +4254,12 @@ var fonts2 = {
}, },
heading: { heading: {
...defaultConfig.fonts.heading, ...defaultConfig.fonts.heading,
family: "Manrope", family: "Archivo Black",
weight: sharedWeights weight: sharedWeights
}, },
display: { display: {
...defaultConfig.fonts.heading, ...defaultConfig.fonts.heading,
family: "Fraunces", family: "Archivo Black",
weight: sharedWeights weight: sharedWeights
} }
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -1,490 +1,106 @@
/* Auto-generated by fonts:sync-google */ /* Auto-generated by fonts:sync-google */
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Manifest Font';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('/fonts/google/roboto/Roboto-400-normal.ttf') format('truetype'); src: url('/fonts/google/manifest-font/regular.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Plus Jakarta Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-400-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-500-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-500-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-600-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-600-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Plus Jakarta Sans';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('/fonts/google/roboto/Roboto-700-normal.ttf') format('truetype'); src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-700-normal.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: 'Plus Jakarta Sans';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/plus-jakarta-sans/PlusJakartaSans-700-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('/fonts/google/open-sans/OpenSans-400-normal.ttf') format('truetype'); src: url('/fonts/google/space-grotesk/SpaceGrotesk-400-normal.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: 'Space Grotesk';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/google/space-grotesk/SpaceGrotesk-500-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/google/space-grotesk/SpaceGrotesk-600-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('/fonts/google/open-sans/OpenSans-700-normal.ttf') format('truetype'); src: url('/fonts/google/space-grotesk/SpaceGrotesk-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/noto-sans-jp/NotoSansJp-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans JP';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/noto-sans-jp/NotoSansJp-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/lato/Lato-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/lato/Lato-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/montserrat/Montserrat-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Montserrat';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/montserrat/Montserrat-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/inter/Inter-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/inter/Inter-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/poppins/Poppins-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/poppins/Poppins-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/material-icons/MaterialIcons-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Roboto Condensed';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/roboto-condensed/RobotoCondensed-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Roboto Condensed';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/roboto-condensed/RobotoCondensed-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/roboto-mono/RobotoMono-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/roboto-mono/RobotoMono-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Arimo';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/arimo/Arimo-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Arimo';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/arimo/Arimo-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Oswald';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/oswald/Oswald-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Oswald';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/oswald/Oswald-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/noto-sans/NotoSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/noto-sans/NotoSans-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/raleway/Raleway-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/raleway/Raleway-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/nunito-sans/NunitoSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/nunito-sans/NunitoSans-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/nunito/Nunito-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/nunito/Nunito-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Playfair Display';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/playfair-display/PlayfairDisplay-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Playfair Display';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/playfair-display/PlayfairDisplay-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/ubuntu/Ubuntu-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/ubuntu/Ubuntu-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Rubik';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/rubik/Rubik-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Rubik';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/rubik/Rubik-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/noto-sans-kr/NotoSansKr-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans KR';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/noto-sans-kr/NotoSansKr-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Roboto Slab';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/roboto-slab/RobotoSlab-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Roboto Slab';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/roboto-slab/RobotoSlab-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/dm-sans/DmSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/dm-sans/DmSans-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Kanit';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/kanit/Kanit-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Kanit';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/kanit/Kanit-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/merriweather/Merriweather-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Merriweather';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/merriweather/Merriweather-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/work-sans/WorkSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/work-sans/WorkSans-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'PT Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/pt-sans/PtSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'PT Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/pt-sans/PtSans-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/material-symbols-outlined/MaterialSymbolsOutlined-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/material-symbols-outlined/MaterialSymbolsOutlined-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Lora';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/lora/Lora-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Lora';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/lora/Lora-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Quicksand';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/quicksand/Quicksand-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Quicksand';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/quicksand/Quicksand-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Mulish';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/mulish/Mulish-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Mulish';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/mulish/Mulish-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans TC';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/noto-sans-tc/NotoSansTc-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Sans TC';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/noto-sans-tc/NotoSansTc-700-normal.ttf') format('truetype');
} }
@font-face { @font-face {
@@ -520,337 +136,9 @@
} }
@font-face { @font-face {
font-family: 'Figtree'; font-family: 'Archivo Black';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('/fonts/google/figtree/Figtree-400-normal.ttf') format('truetype'); src: url('/fonts/google/archivo-black/ArchivoBlack-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Figtree';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/figtree/Figtree-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/inconsolata/Inconsolata-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Inconsolata';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/inconsolata/Inconsolata-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/ibm-plex-sans/IbmPlexSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'IBM Plex Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/ibm-plex-sans/IbmPlexSans-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Fira Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/fira-sans/FiraSans-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Fira Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/fira-sans/FiraSans-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Barlow';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/barlow/Barlow-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Barlow';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/barlow/Barlow-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Outfit';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/outfit/Outfit-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Outfit';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/outfit/Outfit-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/source-sans-3/SourceSans3-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Source Sans 3';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/source-sans-3/SourceSans3-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Bebas Neue';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/bebas-neue/BebasNeue-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Titillium Web';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/titillium-web/TitilliumWeb-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Titillium Web';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/titillium-web/TitilliumWeb-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Karla';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/karla/Karla-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Karla';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/karla/Karla-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Material Icons Outlined';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/material-icons-outlined/MaterialIconsOutlined-400-normal.otf') format('opentype');
}
@font-face {
font-family: 'PT Serif';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/pt-serif/PtSerif-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'PT Serif';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/pt-serif/PtSerif-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Serif';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/noto-serif/NotoSerif-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Noto Serif';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/noto-serif/NotoSerif-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Jost';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/jost/Jost-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Jost';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/jost/Jost-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Prompt';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/prompt/Prompt-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Prompt';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/prompt/Prompt-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Heebo';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/heebo/Heebo-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Heebo';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/heebo/Heebo-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Saira';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/saira/Saira-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Saira';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/saira/Saira-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Archivo';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/archivo/Archivo-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Archivo';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/archivo/Archivo-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-400-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-400-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-500-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-500-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-600-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-600-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-700-normal.ttf') format('truetype');
}
@font-face {
font-family: 'Fraunces';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('/fonts/google/fraunces/Fraunces-700-italic.ttf') format('truetype');
} }

View File

@@ -1 +1,149 @@
{"fonts":[{"family":"Manifest Font","category":"sans-serif","variants":[{"variant":"regular","weight":400,"style":"normal","url":"\/fonts\/google\/manifest-font\/regular.woff2"}]}]} {
"generated_at": "2026-01-15T21:06:13+01:00",
"source": "google-webfonts",
"count": 5,
"fonts": [
{
"family": "Manifest Font",
"category": "sans-serif",
"variants": [
{
"variant": "regular",
"weight": 400,
"style": "normal",
"url": "/fonts/google/manifest-font/regular.woff2"
}
]
},
{
"family": "Plus Jakarta Sans",
"slug": "plus-jakarta-sans",
"category": "sans-serif",
"variants": [
{
"variant": "regular",
"weight": 400,
"style": "normal",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-400-normal.ttf"
},
{
"variant": "italic",
"weight": 400,
"style": "italic",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-400-italic.ttf"
},
{
"variant": 500,
"weight": 500,
"style": "normal",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-500-normal.ttf"
},
{
"variant": "500italic",
"weight": 500,
"style": "italic",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-500-italic.ttf"
},
{
"variant": 600,
"weight": 600,
"style": "normal",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-600-normal.ttf"
},
{
"variant": "600italic",
"weight": 600,
"style": "italic",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-600-italic.ttf"
},
{
"variant": 700,
"weight": 700,
"style": "normal",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-700-normal.ttf"
},
{
"variant": "700italic",
"weight": 700,
"style": "italic",
"url": "/fonts/google/plus-jakarta-sans/PlusJakartaSans-700-italic.ttf"
}
]
},
{
"family": "Space Grotesk",
"slug": "space-grotesk",
"category": "sans-serif",
"variants": [
{
"variant": "regular",
"weight": 400,
"style": "normal",
"url": "/fonts/google/space-grotesk/SpaceGrotesk-400-normal.ttf"
},
{
"variant": 500,
"weight": 500,
"style": "normal",
"url": "/fonts/google/space-grotesk/SpaceGrotesk-500-normal.ttf"
},
{
"variant": 600,
"weight": 600,
"style": "normal",
"url": "/fonts/google/space-grotesk/SpaceGrotesk-600-normal.ttf"
},
{
"variant": 700,
"weight": 700,
"style": "normal",
"url": "/fonts/google/space-grotesk/SpaceGrotesk-700-normal.ttf"
}
]
},
{
"family": "Manrope",
"slug": "manrope",
"category": "sans-serif",
"variants": [
{
"variant": "regular",
"weight": 400,
"style": "normal",
"url": "/fonts/google/manrope/Manrope-400-normal.ttf"
},
{
"variant": 500,
"weight": 500,
"style": "normal",
"url": "/fonts/google/manrope/Manrope-500-normal.ttf"
},
{
"variant": 600,
"weight": 600,
"style": "normal",
"url": "/fonts/google/manrope/Manrope-600-normal.ttf"
},
{
"variant": 700,
"weight": 700,
"style": "normal",
"url": "/fonts/google/manrope/Manrope-700-normal.ttf"
}
]
},
{
"family": "Archivo Black",
"slug": "archivo-black",
"category": "sans-serif",
"variants": [
{
"variant": "regular",
"weight": 400,
"style": "normal",
"url": "/fonts/google/archivo-black/ArchivoBlack-400-normal.ttf"
}
]
}
]
}

View File

@@ -85,7 +85,7 @@ function AdminApp() {
</div> </div>
)} )}
> >
<div className="bg-[#FFF8F5] font-[Manrope] text-[14px] font-normal leading-[1.6] text-[#1F2937] dark:bg-[#15121A] dark:text-slate-100"> <div className="bg-[#FFF1E8] font-[Manrope] text-[14px] font-normal leading-[1.6] text-[#0B132B] dark:bg-[#0B132B] dark:text-slate-100">
<RouterProvider router={router} /> <RouterProvider router={router} />
</div> </div>
</Suspense> </Suspense>

View File

@@ -122,6 +122,10 @@ export default function MobileBrandingPage() {
setLoading(true); setLoading(true);
try { try {
const data = await getEvent(slug); const data = await getEvent(slug);
if (!data) {
setLoading(false);
return;
}
setEvent(data); setEvent(data);
setForm(extractBrandingForm(data.settings ?? {}, BRANDING_FORM_DEFAULTS)); setForm(extractBrandingForm(data.settings ?? {}, BRANDING_FORM_DEFAULTS));
setWatermarkForm(extractWatermark(data)); setWatermarkForm(extractWatermark(data));
@@ -153,7 +157,7 @@ export default function MobileBrandingPage() {
let active = true; let active = true;
getTenantSettings() getTenantSettings()
.then((payload) => { .then((payload) => {
if (!active) return; if (!active || !payload) return;
setTenantBranding(extractBrandingForm(payload.settings ?? {}, BRANDING_FORM_DEFAULTS)); setTenantBranding(extractBrandingForm(payload.settings ?? {}, BRANDING_FORM_DEFAULTS));
}) })
.catch(() => undefined) .catch(() => undefined)
@@ -170,7 +174,7 @@ export default function MobileBrandingPage() {
const previewForm = form.useDefaultBranding && tenantBranding ? tenantBranding : form; const previewForm = form.useDefaultBranding && tenantBranding ? tenantBranding : form;
const previewTitle = event ? renderName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event'); const previewTitle = event ? renderName(event.name) : t('events.placeholders.untitled', 'Unbenanntes Event');
const previewHeadingFont = previewForm.headingFont || 'Fraunces'; const previewHeadingFont = previewForm.headingFont || 'Archivo Black';
const previewBodyFont = previewForm.bodyFont || 'Manrope'; const previewBodyFont = previewForm.bodyFont || 'Manrope';
const previewSurfaceText = getContrastingTextColor(previewForm.surface, '#ffffff', '#0f172a'); const previewSurfaceText = getContrastingTextColor(previewForm.surface, '#ffffff', '#0f172a');
const previewScale = FONT_SIZE_SCALE[previewForm.fontSize] ?? 1; const previewScale = FONT_SIZE_SCALE[previewForm.fontSize] ?? 1;

View File

@@ -3,6 +3,9 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Bell, CalendarDays, Camera, CheckCircle2, ChevronDown, Download, Image as ImageIcon, Layout, ListTodo, MapPin, Megaphone, MessageCircle, Pencil, QrCode, Settings, ShieldCheck, Smartphone, Sparkles, TrendingUp, Tv, Users } from 'lucide-react'; import { Bell, CalendarDays, Camera, CheckCircle2, ChevronDown, Download, Image as ImageIcon, Layout, ListTodo, MapPin, Megaphone, MessageCircle, Pencil, QrCode, Settings, ShieldCheck, Smartphone, Sparkles, TrendingUp, Tv, Users } from 'lucide-react';
import { Card } from '@tamagui/card';
import { Progress } from '@tamagui/progress';
import { XGroup, YGroup } from '@tamagui/group';
import { YStack, XStack } from '@tamagui/stacks'; import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text'; import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite'; import { Pressable } from '@tamagui/react-native-web-lite';
@@ -11,7 +14,7 @@ import { MobileCard, CTAButton, KpiTile, ActionTile, PillBadge, SkeletonCard } f
import { MobileSheet } from './components/Sheet'; import { MobileSheet } from './components/Sheet';
import { adminPath, ADMIN_WELCOME_BASE_PATH } from '../constants'; import { adminPath, ADMIN_WELCOME_BASE_PATH } from '../constants';
import { useEventContext } from '../context/EventContext'; import { useEventContext } from '../context/EventContext';
import { fetchOnboardingStatus, getEventStats, EventStats, TenantEvent, getEvents, getTenantPackagesOverview } from '../api'; import { fetchOnboardingStatus, getEventStats, EventStats, TenantEvent, getEvents, getTenantPackagesOverview, TenantPackageSummary } from '../api';
import { formatEventDate, isBrandingAllowed, resolveEngagementMode, resolveEventDisplayName } from '../lib/events'; import { formatEventDate, isBrandingAllowed, resolveEngagementMode, resolveEventDisplayName } from '../lib/events';
import { useAdminPushSubscription } from './hooks/useAdminPushSubscription'; import { useAdminPushSubscription } from './hooks/useAdminPushSubscription';
import { useDevicePermissions } from './hooks/useDevicePermissions'; import { useDevicePermissions } from './hooks/useDevicePermissions';
@@ -405,9 +408,9 @@ export default function MobileDashboardPage() {
if (!effectiveHasEvents) { if (!effectiveHasEvents) {
return ( return (
<MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}> <MobileShell activeTab="home" title={t('mobileDashboard.title', 'Dashboard')}>
{showPackageSummaryBanner ? ( {showPackageSummaryBanner && activePackage ? (
<PackageSummaryBanner <PackageSummaryBanner
packageName={activePackage?.package_name} activePackage={activePackage}
onOpen={() => setSummaryOpen(true)} onOpen={() => setSummaryOpen(true)}
/> />
) : null} ) : null}
@@ -430,9 +433,9 @@ export default function MobileDashboardPage() {
title={t('mobileDashboard.title', 'Dashboard')} title={t('mobileDashboard.title', 'Dashboard')}
subtitle={t('mobileDashboard.selectEvent', 'Select an event to continue')} subtitle={t('mobileDashboard.selectEvent', 'Select an event to continue')}
> >
{showPackageSummaryBanner ? ( {showPackageSummaryBanner && activePackage ? (
<PackageSummaryBanner <PackageSummaryBanner
packageName={activePackage?.package_name} activePackage={activePackage}
onOpen={() => setSummaryOpen(true)} onOpen={() => setSummaryOpen(true)}
/> />
) : null} ) : null}
@@ -448,9 +451,9 @@ export default function MobileDashboardPage() {
activeTab="home" activeTab="home"
title={t('mobileDashboard.title', 'Dashboard')} title={t('mobileDashboard.title', 'Dashboard')}
> >
{showPackageSummaryBanner ? ( {showPackageSummaryBanner && activePackage ? (
<PackageSummaryBanner <PackageSummaryBanner
packageName={activePackage?.package_name} activePackage={activePackage}
onOpen={() => setSummaryOpen(true)} onOpen={() => setSummaryOpen(true)}
/> />
) : null} ) : null}
@@ -620,18 +623,46 @@ function SummaryRow({ label, value }: { label: string; value: string }) {
} }
function PackageSummaryBanner({ function PackageSummaryBanner({
packageName, activePackage,
onOpen, onOpen,
}: { }: {
packageName?: string | null; activePackage: TenantPackageSummary;
onOpen: () => void; onOpen: () => void;
}) { }) {
const { t } = useTranslation('management'); const { t } = useTranslation('management');
const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme(); const { textStrong, muted, border, surface, accentSoft, primary, surfaceMuted, shadow } = useAdminTheme();
const text = textStrong; const text = textStrong;
const packageName = activePackage.package_name ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary');
const remainingEvents = typeof activePackage.remaining_events === 'number' ? activePackage.remaining_events : null;
const hasLimit = remainingEvents !== null;
const totalEvents = hasLimit ? activePackage.used_events + remainingEvents : null;
const showProgress = hasLimit && (totalEvents ?? 0) > 0;
const usageLabel = hasLimit
? t('mobileDashboard.packageSummary.bannerUsage', '{{used}} of {{total}} events used', {
used: activePackage.used_events,
total: totalEvents ?? 0,
})
: t('mobileDashboard.packageSummary.bannerUnlimited', 'Unlimited events');
const remainingLabel = hasLimit
? t('mobileDashboard.packageSummary.bannerRemaining', '{{count}} remaining', { count: remainingEvents })
: t('mobileDashboard.packageSummary.bannerRemainingUnlimited', 'No limit');
const progressMax = totalEvents ?? 0;
const progressValue = Math.min(activePackage.used_events, progressMax);
return ( return (
<MobileCard space="$2" borderColor={border} backgroundColor={surface}> <Card
className="admin-fade-up"
space="$2"
borderRadius={20}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
shadowColor={shadow}
shadowOpacity={0.16}
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<XStack alignItems="center" justifyContent="space-between" gap="$2"> <XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack alignItems="center" space="$2" flex={1}> <XStack alignItems="center" space="$2" flex={1}>
<XStack <XStack
@@ -650,7 +681,7 @@ function PackageSummaryBanner({
</Text> </Text>
<Text fontSize="$xs" color={muted}> <Text fontSize="$xs" color={muted}>
{t('mobileDashboard.packageSummary.bannerSubtitle', { {t('mobileDashboard.packageSummary.bannerSubtitle', {
name: packageName ?? t('mobileDashboard.packageSummary.fallbackTitle', 'Package summary'), name: packageName,
defaultValue: '{{name}} is active. Review limits & features.', defaultValue: '{{name}} is active. Review limits & features.',
})} })}
</Text> </Text>
@@ -663,7 +694,29 @@ function PackageSummaryBanner({
onPress={onOpen} onPress={onOpen}
/> />
</XStack> </XStack>
</MobileCard> <YStack space="$1.5">
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<Text fontSize="$xs" color={muted}>
{usageLabel}
</Text>
<Text fontSize="$xs" color={muted}>
{remainingLabel}
</Text>
</XStack>
{showProgress ? (
<Progress
value={progressValue}
max={progressMax}
size="$2"
backgroundColor={surfaceMuted}
borderWidth={1}
borderColor={border}
>
<Progress.Indicator backgroundColor={primary} />
</Progress>
) : null}
</YStack>
</Card>
); );
} }
@@ -1112,7 +1165,7 @@ function EventHeaderCard({
onEdit: () => void; onEdit: () => void;
}) { }) {
const { t } = useTranslation('management'); const { t } = useTranslation('management');
const { textStrong, muted, border, surface, accentSoft, primary } = useAdminTheme(); const { textStrong, muted, border, surface, accentSoft, primary, shadow } = useAdminTheme();
if (!event) { if (!event) {
return null; return null;
@@ -1122,7 +1175,19 @@ function EventHeaderCard({
const locationLabel = resolveLocation(event, t); const locationLabel = resolveLocation(event, t);
return ( return (
<MobileCard space="$3" borderColor={border} backgroundColor={surface} position="relative"> <Card
position="relative"
borderRadius={20}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
shadowColor={shadow}
shadowOpacity={0.16}
shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }}
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between" space="$2"> <XStack alignItems="center" justifyContent="space-between" space="$2">
{canSwitch ? ( {canSwitch ? (
<Pressable onPress={onSwitch} aria-label={t('mobileDashboard.pickEvent', 'Select an event')}> <Pressable onPress={onSwitch} aria-label={t('mobileDashboard.pickEvent', 'Select an event')}>
@@ -1155,6 +1220,7 @@ function EventHeaderCard({
{locationLabel} {locationLabel}
</Text> </Text>
</XStack> </XStack>
</YStack>
<Pressable <Pressable
aria-label={t('mobileEvents.edit', 'Edit event')} aria-label={t('mobileEvents.edit', 'Edit event')}
@@ -1174,7 +1240,7 @@ function EventHeaderCard({
> >
<Pencil size={18} color={primary} /> <Pencil size={18} color={primary} />
</Pressable> </Pressable>
</MobileCard> </Card>
); );
} }
@@ -1188,7 +1254,7 @@ function EventManagementGrid({
onNavigate: (path: string) => void; onNavigate: (path: string) => void;
}) { }) {
const { t } = useTranslation('management'); const { t } = useTranslation('management');
const { textStrong } = useAdminTheme(); const { textStrong, border, surface, surfaceMuted, shadow } = useAdminTheme();
const slug = event?.slug ?? null; const slug = event?.slug ?? null;
const brandingAllowed = isBrandingAllowed(event ?? null); const brandingAllowed = isBrandingAllowed(event ?? null);
@@ -1287,25 +1353,67 @@ function EventManagementGrid({
}); });
} }
const rows: typeof tiles[] = [];
tiles.forEach((tile, index) => {
const rowIndex = Math.floor(index / 2);
if (!rows[rowIndex]) {
rows[rowIndex] = [];
}
rows[rowIndex].push(tile);
});
return ( return (
<YStack space="$2"> <Card
<Text fontSize="$sm" fontWeight="800" color={textStrong}> borderRadius={22}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
shadowColor={shadow}
shadowOpacity={0.14}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between">
<XStack
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={textStrong}>
{t('events.detail.managementTitle', 'Event management')} {t('events.detail.managementTitle', 'Event management')}
</Text> </Text>
<XStack flexWrap="wrap" space="$2"> </XStack>
{tiles.map((tile, index) => ( </XStack>
<YGroup space="$2">
{rows.map((row, rowIndex) => (
<YGroup.Item key={`row-${rowIndex}`}>
<XGroup space="$2">
{row.map((tile, index) => (
<XGroup.Item key={tile.label}>
<ActionTile <ActionTile
key={tile.label}
icon={tile.icon} icon={tile.icon}
label={tile.label} label={tile.label}
color={tile.color} color={tile.color}
onPress={tile.onPress} onPress={tile.onPress}
disabled={tile.disabled} disabled={tile.disabled}
delayMs={index * ADMIN_MOTION.tileStaggerMs} variant="cluster"
delayMs={(rowIndex * 2 + index) * ADMIN_MOTION.tileStaggerMs}
/> />
</XGroup.Item>
))} ))}
</XStack> {row.length === 1 ? <XStack flex={1} /> : null}
</XGroup>
</YGroup.Item>
))}
</YGroup>
</YStack> </YStack>
</Card>
); );
} }
@@ -1323,7 +1431,7 @@ function KpiStrip({
tasksEnabled: boolean; tasksEnabled: boolean;
}) { }) {
const { t } = useTranslation('management'); const { t } = useTranslation('management');
const { textStrong, muted } = useAdminTheme(); const { textStrong, muted, border, surface, surfaceMuted, shadow } = useAdminTheme();
const text = textStrong; const text = textStrong;
if (!event) return null; if (!event) return null;
@@ -1349,10 +1457,36 @@ function KpiStrip({
} }
return ( return (
<YStack space="$2"> <Card
<Text fontSize="$sm" fontWeight="800" color={text}> borderRadius={22}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
shadowColor={shadow}
shadowOpacity={0.14}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between" gap="$2">
<XStack
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('mobileDashboard.kpiTitle', 'Key performance indicators')} {t('mobileDashboard.kpiTitle', 'Key performance indicators')}
</Text> </Text>
</XStack>
<Text fontSize="$xs" color={muted}>
{formatEventDate(event.event_date, locale) ?? ''}
</Text>
</XStack>
{loading ? ( {loading ? (
<XStack space="$2" flexWrap="wrap"> <XStack space="$2" flexWrap="wrap">
{Array.from({ length: 3 }).map((_, idx) => ( {Array.from({ length: 3 }).map((_, idx) => (
@@ -1366,16 +1500,14 @@ function KpiStrip({
))} ))}
</XStack> </XStack>
)} )}
<Text fontSize="$xs" color={muted}>
{formatEventDate(event.event_date, locale) ?? ''}
</Text>
</YStack> </YStack>
</Card>
); );
} }
function AlertsAndHints({ event, stats, tasksEnabled }: { event: TenantEvent | null; stats: EventStats | null | undefined; tasksEnabled: boolean }) { function AlertsAndHints({ event, stats, tasksEnabled }: { event: TenantEvent | null; stats: EventStats | null | undefined; tasksEnabled: boolean }) {
const { t } = useTranslation('management'); const { t } = useTranslation('management');
const { textStrong, warningBg, warningBorder, warningText } = useAdminTheme(); const { textStrong, warningBg, warningBorder, warningText, border, surface, surfaceMuted, shadow } = useAdminTheme();
const text = textStrong; const text = textStrong;
if (!event) return null; if (!event) return null;
@@ -1392,17 +1524,50 @@ function AlertsAndHints({ event, stats, tasksEnabled }: { event: TenantEvent | n
} }
return ( return (
<YStack space="$1.5"> <Card
<Text fontSize="$sm" fontWeight="800" color={text}> borderRadius={22}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
shadowColor={shadow}
shadowOpacity={0.14}
shadowRadius={14}
shadowOffset={{ width: 0, height: 8 }}
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between">
<XStack
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('mobileDashboard.alertsTitle', 'Alerts')} {t('mobileDashboard.alertsTitle', 'Alerts')}
</Text> </Text>
</XStack>
</XStack>
<YStack space="$2">
{alerts.map((alert) => ( {alerts.map((alert) => (
<MobileCard key={alert} backgroundColor={warningBg} borderColor={warningBorder} space="$2"> <XStack
key={alert}
padding="$2.5"
borderRadius={16}
borderWidth={1}
borderColor={warningBorder}
backgroundColor={warningBg}
>
<Text fontSize="$sm" color={warningText}> <Text fontSize="$sm" color={warningText}>
{alert} {alert}
</Text> </Text>
</MobileCard> </XStack>
))} ))}
</YStack> </YStack>
</YStack>
</Card>
); );
} }

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react'; import { RefreshCcw, Plus, Pencil, Trash2, ChevronDown, ChevronRight } from 'lucide-react';
import { Card } from '@tamagui/card';
import { YStack, XStack } from '@tamagui/stacks'; import { YStack, XStack } from '@tamagui/stacks';
import { YGroup } from '@tamagui/group'; import { YGroup } from '@tamagui/group';
import { SizableText as Text } from '@tamagui/text'; import { SizableText as Text } from '@tamagui/text';
@@ -9,8 +10,9 @@ import { ListItem } from '@tamagui/list-item';
import { Pressable } from '@tamagui/react-native-web-lite'; import { Pressable } from '@tamagui/react-native-web-lite';
import { Button } from '@tamagui/button'; import { Button } from '@tamagui/button';
import { AlertDialog } from '@tamagui/alert-dialog'; import { AlertDialog } from '@tamagui/alert-dialog';
import { ScrollView } from '@tamagui/scroll-view';
import { MobileShell, HeaderActionButton } from './components/MobileShell'; import { MobileShell, HeaderActionButton } from './components/MobileShell';
import { MobileCard, CTAButton, SkeletonCard, PillBadge, FloatingActionButton } from './components/Primitives'; import { MobileCard, CTAButton, SkeletonCard, FloatingActionButton } from './components/Primitives';
import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls'; import { MobileField, MobileInput, MobileSelect, MobileTextArea } from './components/FormControls';
import { import {
getEvent, getEvent,
@@ -43,82 +45,183 @@ import { RadioGroup } from '@tamagui/radio-group';
import { useBackNavigation } from './hooks/useBackNavigation'; import { useBackNavigation } from './hooks/useBackNavigation';
import { buildTaskSummary } from './lib/taskSummary'; import { buildTaskSummary } from './lib/taskSummary';
import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts'; import { buildTaskSectionCounts, type TaskSectionKey } from './lib/taskSectionCounts';
import { useAdminTheme } from './theme'; import { withAlpha } from './components/colors';
import { ADMIN_ACTION_COLORS, useAdminTheme } from './theme';
function TaskSummaryCard({ function TaskSummaryCard({ summary }: { summary: ReturnType<typeof buildTaskSummary> }) {
summary,
text,
muted,
border,
surfaceMuted,
}: {
summary: ReturnType<typeof buildTaskSummary>;
text: string;
muted: string;
border: string;
surfaceMuted: string;
}) {
const { t } = useTranslation('management'); const { t } = useTranslation('management');
const { textStrong, muted, border, surface, surfaceMuted, shadow } = useAdminTheme();
const total = summary.assigned + summary.library + summary.collections + summary.emotions;
const segments = [
{
key: 'assigned',
label: t('events.tasks.summary.assigned', 'Assigned'),
value: summary.assigned,
color: ADMIN_ACTION_COLORS.tasks,
},
{
key: 'library',
label: t('events.tasks.summary.library', 'Library'),
value: summary.library,
color: ADMIN_ACTION_COLORS.qr,
},
{
key: 'collections',
label: t('events.tasks.summary.collections', 'Collections'),
value: summary.collections,
color: ADMIN_ACTION_COLORS.settings,
},
{
key: 'emotions',
label: t('events.tasks.summary.emotions', 'Emotions'),
value: summary.emotions,
color: ADMIN_ACTION_COLORS.branding,
},
];
return ( return (
<MobileCard space="$2" borderColor={border}> <Card
<XStack alignItems="center" justifyContent="space-between" space="$2"> borderRadius={22}
<SummaryItem borderWidth={2}
label={t('events.tasks.summary.assigned', 'Assigned')} borderColor={border}
value={summary.assigned} backgroundColor={surface}
text={text} padding="$3"
muted={muted} shadowColor={shadow}
surfaceMuted={surfaceMuted} shadowOpacity={0.14}
/> shadowRadius={14}
<SummaryItem shadowOffset={{ width: 0, height: 8 }}
label={t('events.tasks.summary.library', 'Library')} >
value={summary.library} <YStack space="$2.5">
text={text} <XStack alignItems="center" justifyContent="space-between">
muted={muted} <XStack
surfaceMuted={surfaceMuted} alignItems="center"
/> paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={textStrong}>
{t('events.tasks.summary.title', 'Task overview')}
</Text>
</XStack> </XStack>
<XStack alignItems="center" justifyContent="space-between" space="$2">
<SummaryItem
label={t('events.tasks.summary.collections', 'Collections')}
value={summary.collections}
text={text}
muted={muted}
surfaceMuted={surfaceMuted}
/>
<SummaryItem
label={t('events.tasks.summary.emotions', 'Emotions')}
value={summary.emotions}
text={text}
muted={muted}
surfaceMuted={surfaceMuted}
/>
</XStack> </XStack>
</MobileCard>
<XStack alignItems="baseline" space="$2">
<Text fontSize="$xl" fontWeight="900" color={textStrong}>
{total}
</Text>
<Text fontSize="$xs" color={muted}>
{t('events.tasks.summary.total', 'Tasks total')}
</Text>
</XStack>
<XStack
height={10}
borderRadius={999}
overflow="hidden"
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
{total > 0 ? (
segments.map((segment) =>
segment.value > 0 ? (
<XStack
key={segment.key}
flex={segment.value}
backgroundColor={withAlpha(segment.color, 0.55)}
/>
) : null,
)
) : (
<XStack flex={1} backgroundColor={withAlpha(border, 0.4)} />
)}
</XStack>
<XStack flexWrap="wrap" space="$2">
{segments.map((segment) => (
<SummaryLegendItem key={segment.key} label={segment.label} value={segment.value} color={segment.color} />
))}
</XStack>
</YStack>
</Card>
); );
} }
function SummaryItem({ function SummaryLegendItem({
label, label,
value, value,
text, color,
muted,
surfaceMuted,
}: { }: {
label: string; label: string;
value: number; value: number;
text: string; color: string;
muted: string;
surfaceMuted: string;
}) { }) {
const { textStrong } = useAdminTheme();
return ( return (
<YStack flex={1} padding="$2" borderRadius={12} backgroundColor={surfaceMuted} space="$1"> <XStack
<Text fontSize={11} color={muted}> alignItems="center"
space="$1.5"
paddingHorizontal="$2.5"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={withAlpha(color, 0.35)}
backgroundColor={withAlpha(color, 0.12)}
>
<XStack width={8} height={8} borderRadius={999} backgroundColor={color} />
<Text fontSize={11} fontWeight="700" color={textStrong}>
{label} {label}
</Text> </Text>
<Text fontSize={16} fontWeight="800" color={text}> <Text fontSize={11} fontWeight="700" color={textStrong}>
{value} {value}
</Text> </Text>
</YStack> </XStack>
);
}
function QuickNavChip({
label,
count,
onPress,
}: {
label: string;
count: number;
onPress: () => void;
}) {
const { textStrong, border, surface, surfaceMuted } = useAdminTheme();
return (
<Pressable onPress={onPress}>
<XStack
alignItems="center"
space="$1.5"
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surface}
style={{ minHeight: 36 }}
>
<Text fontSize="$xs" fontWeight="700" color={textStrong}>
{label}
</Text>
<XStack
paddingHorizontal="$2"
paddingVertical="$0.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize={10} fontWeight="800" color={textStrong}>
{count}
</Text>
</XStack>
</XStack>
</Pressable>
); );
} }
@@ -491,45 +594,82 @@ export default function MobileEventTasksPage() {
) : null} ) : null}
{!loading ? ( {!loading ? (
<TaskSummaryCard <TaskSummaryCard summary={summary} />
summary={summary}
text={text}
muted={muted}
border={border}
surfaceMuted={surfaceMuted}
/>
) : null} ) : null}
{!loading ? ( {!loading ? (
<YStack space="$2"> <Card
<Text fontSize={12} fontWeight="700" color={muted}> borderRadius={22}
borderWidth={2}
borderColor={border}
backgroundColor={surface}
padding="$3"
>
<YStack space="$2.5">
<XStack alignItems="center" justifyContent="space-between">
<XStack
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$1.5"
borderRadius={999}
borderWidth={1}
borderColor={border}
backgroundColor={surfaceMuted}
>
<Text fontSize="$xs" fontWeight="800" color={text}>
{t('events.tasks.quickNav', 'Quick jump')} {t('events.tasks.quickNav', 'Quick jump')}
</Text> </Text>
<XStack space="$2" flexWrap="wrap"> </XStack>
</XStack>
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<XStack space="$2" paddingVertical="$1">
{sectionCounts.map((section) => ( {sectionCounts.map((section) => (
<Button <QuickNavChip
key={section.key} key={section.key}
unstyled label={t(`events.tasks.sections.${section.key}`, section.key)}
count={section.count}
onPress={() => handleQuickNav(section.key)} onPress={() => handleQuickNav(section.key)}
/>
))}
</XStack>
</ScrollView>
<XStack alignItems="center" space="$2">
<XStack flex={1}>
<MobileInput
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('events.tasks.search', 'Search tasks')}
compact
/>
</XStack>
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
<XStack
alignItems="center"
space="$1.5"
paddingVertical="$2"
paddingHorizontal="$3"
borderRadius={14} borderRadius={14}
borderWidth={1} borderWidth={1}
borderColor={border} borderColor={border}
backgroundColor={surface} backgroundColor={surface}
paddingVertical="$2"
paddingHorizontal="$3"
pressStyle={{ backgroundColor: surfaceMuted }}
style={{ flexGrow: 1 }}
> >
<XStack alignItems="center" justifyContent="center" space="$1.5"> <Text fontSize={11} fontWeight="700" color={text}>
<Text fontSize="$xs" fontWeight="700" color={text}> {t('events.tasks.emotionFilterShort', 'Emotion')}
{t(`events.tasks.sections.${section.key}`, section.key)}
</Text> </Text>
<PillBadge tone="muted">{section.count}</PillBadge> <Text fontSize={11} color={muted}>
{emotionFilter
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
: t('events.tasks.allEmotions', 'All')}
</Text>
<ChevronDown size={14} color={muted} />
</XStack> </XStack>
</Button> </Pressable>
))}
</XStack> </XStack>
</YStack> </YStack>
</Card>
) : null} ) : null}
{loading ? ( {loading ? (
@@ -631,33 +771,6 @@ export default function MobileEventTasksPage() {
) : ( ) : (
<YStack space="$2"> <YStack space="$2">
<div ref={assignedRef} /> <div ref={assignedRef} />
<YStack space="$2">
<MobileInput
type="search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder={t('events.tasks.search', 'Search tasks')}
compact
/>
<Pressable onPress={() => setShowEmotionFilterSheet(true)}>
<MobileCard borderColor={border} backgroundColor={surface} space="$2">
<XStack alignItems="center" justifyContent="space-between">
<YStack>
<Text fontSize={12} fontWeight="700" color={text}>
{t('events.tasks.emotionFilter', 'Emotion filter')}
</Text>
<Text fontSize={11} color={muted}>
{emotionFilter
? emotions.find((e) => String(e.id) === emotionFilter)?.name ?? t('events.tasks.customEmotion', 'Custom emotion')
: t('events.tasks.allEmotions', 'All')}
</Text>
</YStack>
<ChevronDown size={16} color={muted} />
</XStack>
</MobileCard>
</Pressable>
</YStack>
<Text fontSize="$sm" color={muted}> <Text fontSize="$sm" color={muted}>
{t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })} {t('events.tasks.count', '{{count}} Tasks', { count: filteredTasks.length })}
</Text> </Text>

View File

@@ -59,7 +59,7 @@ const BACKGROUND_PRESETS = [
}, },
]; ];
const DEFAULT_BODY_FONT = 'Manrope'; const DEFAULT_BODY_FONT = 'Manrope';
const DEFAULT_DISPLAY_FONT = 'Fraunces'; const DEFAULT_DISPLAY_FONT = 'Archivo Black';
export default function MobileQrLayoutCustomizePage() { export default function MobileQrLayoutCustomizePage() {
const { slug: slugParam, tokenId: tokenParam } = useParams<{ slug?: string; tokenId?: string }>(); const { slug: slugParam, tokenId: tokenParam } = useParams<{ slug?: string; tokenId?: string }>();
@@ -1206,7 +1206,7 @@ function LayoutControls({
const [openColorSlot, setOpenColorSlot] = React.useState<string | null>(null); const [openColorSlot, setOpenColorSlot] = React.useState<string | null>(null);
const { border, surface, surfaceMuted, text, textStrong, muted, overlay, shadow, danger } = useAdminTheme(); const { border, surface, surfaceMuted, text, textStrong, muted, overlay, shadow, danger } = useAdminTheme();
const fontOptions = React.useMemo(() => { const fontOptions = React.useMemo(() => {
const preset = ['Fraunces', 'Manrope', 'Inter', 'Roboto', 'Lora']; const preset = ['Archivo Black', 'Manrope', 'Inter', 'Roboto', 'Lora'];
const tenant = tenantFonts.map((font) => font.family); const tenant = tenantFonts.map((font) => font.family);
return Array.from(new Set([...tenant, ...preset])); return Array.from(new Set([...tenant, ...preset]));
}, [tenantFonts]); }, [tenantFonts]);

View File

@@ -65,6 +65,11 @@ vi.mock('../theme', () => ({
backdrop: '#0f172a', backdrop: '#0f172a',
dangerBg: '#fee2e2', dangerBg: '#fee2e2',
dangerText: '#b91c1c', dangerText: '#b91c1c',
glassSurface: 'rgba(255,255,255,0.8)',
glassSurfaceStrong: 'rgba(255,255,255,0.9)',
glassBorder: 'rgba(226,232,240,0.7)',
glassShadow: 'rgba(15,23,42,0.14)',
appBackground: 'linear-gradient(180deg, #f7fafc, #eef3f7)',
}), }),
})); }));

View File

@@ -0,0 +1,235 @@
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
const fixtures = vi.hoisted(() => ({
event: {
id: 1,
name: 'Demo Wedding',
slug: 'demo-event',
event_date: '2026-02-19',
status: 'published' as const,
settings: { location: 'Berlin' },
tasks_count: 4,
photo_count: 12,
active_invites_count: 3,
total_invites_count: 5,
},
activePackage: {
id: 1,
package_id: 1,
package_name: 'Standard',
package_type: 'standard',
included_package_slug: null,
active: true,
used_events: 2,
remaining_events: 3,
price: null,
currency: null,
purchased_at: null,
expires_at: null,
package_limits: null,
branding_allowed: true,
watermark_allowed: true,
features: [],
},
}));
const navigateMock = vi.fn();
vi.mock('react-router-dom', () => ({
useNavigate: () => navigateMock,
useLocation: () => ({ search: '', pathname: '/event-admin/mobile/dashboard' }),
useParams: () => ({ slug: fixtures.event.slug }),
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, fallback?: string | Record<string, unknown>, options?: Record<string, unknown>) => {
let text = key;
let resolvedOptions = options;
if (typeof fallback === 'string') {
text = fallback;
} else if (fallback && typeof fallback === 'object') {
resolvedOptions = fallback;
if (typeof fallback.defaultValue === 'string') {
text = fallback.defaultValue;
}
}
if (!resolvedOptions || typeof text !== 'string') {
return text;
}
return text.replace(/\{\{(\w+)\}\}/g, (_, token) => String(resolvedOptions?.[token] ?? ''));
},
i18n: {
language: 'de',
},
}),
initReactI18next: {
type: '3rdParty',
init: () => undefined,
},
}));
vi.mock('@tanstack/react-query', () => ({
useQuery: ({ queryKey }: { queryKey: unknown }) => {
const keyParts = Array.isArray(queryKey) ? queryKey : [queryKey];
const key = keyParts.map(String).join(':');
if (key.includes('packages-overview')) {
return { data: { packages: [], activePackage: fixtures.activePackage }, isLoading: false, isError: false };
}
if (key.includes('onboarding') && key.includes('status')) {
return { data: { steps: { summary_seen_package_id: fixtures.activePackage.id } }, isLoading: false };
}
if (key.includes('dashboard') && key.includes('events')) {
return { data: [fixtures.event], isLoading: false };
}
if (key.includes('dashboard') && key.includes('stats')) {
return { data: null, isLoading: false };
}
return { data: null, isLoading: false, isError: false };
},
}));
vi.mock('../../context/EventContext', () => ({
useEventContext: () => ({
events: [fixtures.event],
activeEvent: fixtures.event,
hasEvents: true,
hasMultipleEvents: false,
isLoading: false,
selectEvent: vi.fn(),
}),
}));
vi.mock('../../auth/context', () => ({
useAuth: () => ({ status: 'unauthenticated' }),
}));
vi.mock('../hooks/useInstallPrompt', () => ({
useInstallPrompt: () => ({ isInstalled: true, canInstall: false, isIos: false, promptInstall: vi.fn() }),
}));
vi.mock('../hooks/useAdminPushSubscription', () => ({
useAdminPushSubscription: () => ({ supported: true, subscribed: true, permission: 'granted', loading: false, enable: vi.fn() }),
}));
vi.mock('../hooks/useDevicePermissions', () => ({
useDevicePermissions: () => ({ storage: 'unavailable', loading: false, requestPersistentStorage: vi.fn() }),
}));
vi.mock('../lib/mobileTour', () => ({
getTourSeen: () => true,
resolveTourStepKeys: () => [],
setTourSeen: vi.fn(),
}));
vi.mock('../components/MobileShell', () => ({
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('../components/Sheet', () => ({
MobileSheet: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('../components/Primitives', () => ({
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CTAButton: ({ label }: { label: string }) => <button type="button">{label}</button>,
KpiTile: ({ label, value }: { label: string; value: string | number }) => (
<div>
<span>{label}</span>
<span>{value}</span>
</div>
),
ActionTile: ({ label }: { label: string }) => <div>{label}</div>,
PillBadge: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
SkeletonCard: () => <div>Loading...</div>,
}));
vi.mock('@tamagui/card', () => ({
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/progress', () => ({
Progress: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Indicator: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/group', () => ({
XGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
YGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/stacks', () => ({
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress}>
{children}
</button>
),
}));
vi.mock('../theme', () => ({
ADMIN_ACTION_COLORS: {
settings: '#10b981',
tasks: '#f59e0b',
qr: '#6366f1',
images: '#ec4899',
liveShow: '#f97316',
liveShowSettings: '#38bdf8',
guests: '#22c55e',
guestMessages: '#f97316',
branding: '#0ea5e9',
photobooth: '#f43f5e',
recap: '#94a3b8',
analytics: '#22c55e',
},
ADMIN_MOTION: {
tileStaggerMs: 0,
},
useAdminTheme: () => ({
textStrong: '#0f172a',
muted: '#64748b',
border: '#e2e8f0',
surface: '#ffffff',
accentSoft: '#eef2ff',
primary: '#ff5a5f',
surfaceMuted: '#f8fafc',
shadow: 'rgba(15,23,42,0.12)',
}),
}));
vi.mock('../../lib/events', () => ({
resolveEventDisplayName: () => fixtures.event.name,
resolveEngagementMode: () => 'full',
isBrandingAllowed: () => true,
formatEventDate: () => '19. Feb. 2026',
}));
vi.mock('../eventDate', () => ({
isPastEvent: () => false,
}));
import MobileDashboardPage from '../DashboardPage';
describe('MobileDashboardPage', () => {
it('shows package usage progress when a limit is available', () => {
render(<MobileDashboardPage />);
expect(screen.getByText('2 of 5 events used')).toBeInTheDocument();
expect(screen.getByText('3 remaining')).toBeInTheDocument();
});
});

View File

@@ -79,6 +79,11 @@ vi.mock('../theme', () => ({
border: '#e5e7eb', border: '#e5e7eb',
surface: '#ffffff', surface: '#ffffff',
primary: '#ff5a5f', primary: '#ff5a5f',
glassSurface: 'rgba(255,255,255,0.8)',
glassSurfaceStrong: 'rgba(255,255,255,0.9)',
glassBorder: 'rgba(229,231,235,0.7)',
glassShadow: 'rgba(15,23,42,0.14)',
appBackground: 'linear-gradient(180deg, #f7fafc, #eef3f7)',
}), }),
})); }));

View File

@@ -0,0 +1,217 @@
import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
const fixtures = vi.hoisted(() => ({
event: {
id: 1,
name: 'Demo Event',
slug: 'demo-event',
event_date: '2026-02-19',
event_type_id: null,
event_type: null,
status: 'published',
settings: {},
},
assignedTasks: [
{ id: 1, title: 'Task A', description: 'Desc', emotion: null, event_type_id: null },
{ id: 2, title: 'Task B', description: '', emotion: null, event_type_id: null },
],
libraryTasks: [
{ id: 3, title: 'Task C', description: '', emotion: null, event_type_id: null },
],
collections: [{ id: 1, name: 'Starter Pack', description: '' }],
emotions: [{ id: 1, name: 'Joy', color: '#ff6b6b' }],
}));
const navigateMock = vi.fn();
const backMock = vi.fn();
vi.mock('react-router-dom', () => ({
useNavigate: () => navigateMock,
useParams: () => ({ slug: fixtures.event.slug }),
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, fallback?: string | Record<string, unknown>) => {
if (typeof fallback === 'string') {
return fallback;
}
if (fallback && typeof fallback === 'object' && typeof fallback.defaultValue === 'string') {
return fallback.defaultValue;
}
return key;
},
}),
}));
vi.mock('../hooks/useBackNavigation', () => ({
useBackNavigation: () => backMock,
}));
vi.mock('../../context/EventContext', () => ({
useEventContext: () => ({
activeEvent: fixtures.event,
selectEvent: vi.fn(),
}),
}));
vi.mock('../../api', () => ({
getEvent: vi.fn().mockResolvedValue(fixtures.event),
getEvents: vi.fn().mockResolvedValue([fixtures.event]),
getEventTasks: vi.fn().mockResolvedValue({ data: fixtures.assignedTasks }),
getTasks: vi.fn().mockResolvedValue({ data: fixtures.libraryTasks }),
getTaskCollections: vi.fn().mockResolvedValue({ data: fixtures.collections }),
getEmotions: vi.fn().mockResolvedValue(fixtures.emotions),
assignTasksToEvent: vi.fn(),
updateTask: vi.fn(),
importTaskCollection: vi.fn(),
createTask: vi.fn(),
detachTasksFromEvent: vi.fn(),
createEmotion: vi.fn(),
updateEmotion: vi.fn(),
deleteEmotion: vi.fn(),
}));
vi.mock('react-hot-toast', () => ({
default: {
error: vi.fn(),
success: vi.fn(),
},
}));
vi.mock('@tamagui/card', () => ({
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/scroll-view', () => ({
ScrollView: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/group', () => ({
YGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('@tamagui/stacks', () => ({
YStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
XStack: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('@tamagui/text', () => ({
SizableText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
vi.mock('@tamagui/list-item', () => ({
ListItem: ({ title, subTitle, iconAfter }: { title?: React.ReactNode; subTitle?: React.ReactNode; iconAfter?: React.ReactNode }) => (
<div>
{title}
{subTitle}
{iconAfter}
</div>
),
}));
vi.mock('@tamagui/react-native-web-lite', () => ({
Pressable: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress}>
{children}
</button>
),
}));
vi.mock('@tamagui/button', () => ({
Button: ({ children, onPress }: { children: React.ReactNode; onPress?: () => void }) => (
<button type="button" onClick={onPress}>
{children}
</button>
),
}));
vi.mock('@tamagui/radio-group', () => ({
RadioGroup: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Item: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Indicator: () => <div />,
}),
}));
vi.mock('@tamagui/alert-dialog', () => ({
AlertDialog: Object.assign(({ children }: { children: React.ReactNode }) => <div>{children}</div>, {
Portal: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Overlay: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
Content: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Title: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Description: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Cancel: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Action: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}),
}));
vi.mock('../components/MobileShell', () => ({
MobileShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
HeaderActionButton: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('../components/Primitives', () => ({
MobileCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
CTAButton: ({ label, onPress }: { label: string; onPress?: () => void }) => (
<button type="button" onClick={onPress}>
{label}
</button>
),
SkeletonCard: () => <div>Loading...</div>,
FloatingActionButton: ({ label }: { label: string }) => <div>{label}</div>,
}));
vi.mock('../components/FormControls', () => ({
MobileField: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
MobileInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
MobileSelect: ({ children }: { children: React.ReactNode }) => <select>{children}</select>,
MobileTextArea: (props: React.TextareaHTMLAttributes<HTMLTextAreaElement>) => <textarea {...props} />,
}));
vi.mock('../components/Sheet', () => ({
MobileSheet: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}));
vi.mock('../components/Tag', () => ({
Tag: ({ label }: { label: string }) => <span>{label}</span>,
}));
vi.mock('../theme', () => ({
ADMIN_ACTION_COLORS: {
settings: '#0ea5e9',
tasks: '#f59e0b',
qr: '#6366f1',
branding: '#22c55e',
},
useAdminTheme: () => ({
textStrong: '#111827',
muted: '#6b7280',
subtle: '#94a3b8',
border: '#e5e7eb',
primary: '#ff5a5f',
danger: '#dc2626',
surface: '#ffffff',
surfaceMuted: '#f9fafb',
dangerBg: '#fee2e2',
dangerText: '#b91c1c',
overlay: 'rgba(15,23,42,0.5)',
shadow: 'rgba(15,23,42,0.12)',
}),
}));
import MobileEventTasksPage from '../EventTasksPage';
describe('MobileEventTasksPage', () => {
it('renders the task overview summary and quick jump chips', async () => {
render(<MobileEventTasksPage />);
expect(await screen.findByText('Task overview')).toBeInTheDocument();
expect(screen.getByText('Tasks total')).toBeInTheDocument();
expect(screen.getByText('Quick jump')).toBeInTheDocument();
expect(screen.getByText('Assigned')).toBeInTheDocument();
});
});

View File

@@ -69,6 +69,11 @@ vi.mock('../theme', () => ({
surface: '#ffffff', surface: '#ffffff',
primary: '#ff5a5f', primary: '#ff5a5f',
accentSoft: '#fde7ea', accentSoft: '#fde7ea',
glassSurface: 'rgba(255,255,255,0.8)',
glassSurfaceStrong: 'rgba(255,255,255,0.9)',
glassBorder: 'rgba(229,231,235,0.7)',
glassShadow: 'rgba(15,23,42,0.14)',
appBackground: 'linear-gradient(180deg, #f7fafc, #eef3f7)',
}), }),
})); }));

View File

@@ -110,6 +110,11 @@ vi.mock('../theme', () => ({
primary: '#ff5a5f', primary: '#ff5a5f',
danger: '#b91c1c', danger: '#b91c1c',
accentSoft: '#ffe5ec', accentSoft: '#ffe5ec',
glassSurface: 'rgba(255,255,255,0.8)',
glassSurfaceStrong: 'rgba(255,255,255,0.9)',
glassBorder: 'rgba(229,231,235,0.7)',
glassShadow: 'rgba(15,23,42,0.14)',
appBackground: 'linear-gradient(180deg, #f7fafc, #eef3f7)',
}), }),
})); }));

View File

@@ -16,9 +16,11 @@ export type NavKey = 'home' | 'tasks' | 'uploads' | 'profile';
export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate: (key: NavKey) => void }) { export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate: (key: NavKey) => void }) {
const { t } = useTranslation('mobile'); const { t } = useTranslation('mobile');
const location = useLocation(); const location = useLocation();
const { surface, border, primary, accentSoft, muted, subtle, shadow } = useAdminTheme(); const { surface, border, primary, accent, muted, subtle, shadow, glassSurfaceStrong, glassBorder, glassShadow } = useAdminTheme();
const surfaceColor = surface; const surfaceColor = glassSurfaceStrong ?? surface;
const navSurface = withAlpha(surfaceColor, 0.92); const navSurface = glassSurfaceStrong ?? withAlpha(surfaceColor, 0.92);
const navBorder = glassBorder ?? border;
const navShadow = glassShadow ?? shadow;
const [pressedKey, setPressedKey] = React.useState<NavKey | null>(null); const [pressedKey, setPressedKey] = React.useState<NavKey | null>(null);
const isDeepHome = active === 'home' && location.pathname !== adminPath('/mobile/dashboard'); const isDeepHome = active === 'home' && location.pathname !== adminPath('/mobile/dashboard');
@@ -38,14 +40,14 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
right={0} right={0}
backgroundColor={navSurface} backgroundColor={navSurface}
borderTopWidth={1} borderTopWidth={1}
borderColor={border} borderColor={navBorder}
paddingVertical="$2" paddingVertical="$2"
paddingHorizontal="$4" paddingHorizontal="$4"
zIndex={50} zIndex={50}
shadowColor={shadow} shadowColor={navShadow}
shadowOpacity={0.08} shadowOpacity={0.12}
shadowRadius={12} shadowRadius={16}
shadowOffset={{ width: 0, height: -4 }} shadowOffset={{ width: 0, height: -6 }}
// allow for safe-area inset on modern phones // allow for safe-area inset on modern phones
style={{ style={{
paddingBottom: 'max(env(safe-area-inset-bottom, 0px), 8px)', paddingBottom: 'max(env(safe-area-inset-bottom, 0px), 8px)',
@@ -58,6 +60,8 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
const activeState = item.key === active; const activeState = item.key === active;
const isPressed = pressedKey === item.key; const isPressed = pressedKey === item.key;
const IconCmp = item.icon; const IconCmp = item.icon;
const activeBg = primary;
const activeShadow = withAlpha(primary, 0.4);
return ( return (
<Pressable <Pressable
key={item.key} key={item.key}
@@ -78,9 +82,10 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
paddingHorizontal="$3" paddingHorizontal="$3"
paddingVertical="$2" paddingVertical="$2"
borderRadius={12} borderRadius={12}
backgroundColor={activeState ? accentSoft : 'transparent'} backgroundColor={activeState ? activeBg : 'transparent'}
gap="$1" gap="$1"
style={{ style={{
boxShadow: activeState ? `0 10px 22px ${activeShadow}` : undefined,
transform: isPressed ? 'scale(0.96)' : 'scale(1)', transform: isPressed ? 'scale(0.96)' : 'scale(1)',
opacity: isPressed ? 0.9 : 1, opacity: isPressed ? 0.9 : 1,
transition: 'transform 140ms ease, background-color 140ms ease, opacity 140ms ease', transition: 'transform 140ms ease, background-color 140ms ease, opacity 140ms ease',
@@ -93,17 +98,17 @@ export function BottomNav({ active, onNavigate }: { active: NavKey; onNavigate:
width={28} width={28}
height={3} height={3}
borderRadius={999} borderRadius={999}
backgroundColor={primary} backgroundColor={accent}
/> />
) : null} ) : null}
<YStack width={ICON_SIZE} height={ICON_SIZE} alignItems="center" justifyContent="center" shrink={0}> <YStack width={ICON_SIZE} height={ICON_SIZE} alignItems="center" justifyContent="center" shrink={0}>
<IconCmp size={ICON_SIZE} color={activeState ? primary : subtle} /> <IconCmp size={ICON_SIZE} color={activeState ? 'white' : subtle} />
</YStack> </YStack>
<Text <Text
fontSize="$xs" fontSize="$xs"
fontWeight="700" fontWeight="700"
fontFamily="$body" fontFamily="$body"
color={activeState ? primary : muted} color={activeState ? 'white' : muted}
textAlign="center" textAlign="center"
flexShrink={1} flexShrink={1}
> >

View File

@@ -37,13 +37,32 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
const { t } = useTranslation('mobile'); const { t } = useTranslation('mobile');
const { count: notificationCount } = useNotificationsBadge(); const { count: notificationCount } = useNotificationsBadge();
const online = useOnlineStatus(); const online = useOnlineStatus();
const { background, surface, border, text, muted, warningBg, warningText, primary, danger, shadow } = useAdminTheme(); const {
background,
surface,
border,
text,
muted,
warningBg,
warningText,
primary,
danger,
shadow,
glassSurfaceStrong,
glassBorder,
glassShadow,
appBackground,
} = useAdminTheme();
const backgroundColor = background; const backgroundColor = background;
const surfaceColor = surface; const surfaceColor = surface;
const borderColor = border; const borderColor = border;
const textColor = text; const textColor = text;
const mutedText = muted; const mutedText = muted;
const headerSurface = withAlpha(surfaceColor, 0.94); const headerSurface = glassSurfaceStrong ?? withAlpha(surfaceColor, 0.94);
const headerBorder = glassBorder ?? borderColor;
const actionSurface = glassSurfaceStrong ?? surfaceColor;
const actionBorder = glassBorder ?? borderColor;
const actionShadow = glassShadow ?? shadow;
const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]); const [fallbackEvents, setFallbackEvents] = React.useState<TenantEvent[]>([]);
const [loadingEvents, setLoadingEvents] = React.useState(false); const [loadingEvents, setLoadingEvents] = React.useState(false);
const [attemptedFetch, setAttemptedFetch] = React.useState(false); const [attemptedFetch, setAttemptedFetch] = React.useState(false);
@@ -152,10 +171,17 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
width={34} width={34}
height={34} height={34}
borderRadius={12} borderRadius={12}
backgroundColor={surfaceColor} backgroundColor={actionSurface}
borderWidth={1}
borderColor={actionBorder}
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
position="relative" position="relative"
style={{
boxShadow: `0 10px 18px ${actionShadow}`,
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
}}
> >
<Bell size={16} color={textColor} /> <Bell size={16} color={textColor} />
{notificationCount > 0 ? ( {notificationCount > 0 ? (
@@ -190,6 +216,7 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
backgroundColor={primary} backgroundColor={primary}
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
style={{ boxShadow: `0 10px 18px ${withAlpha(primary, 0.32)}` }}
> >
<QrCode size={16} color="white" /> <QrCode size={16} color="white" />
</XStack> </XStack>
@@ -200,18 +227,23 @@ export function MobileShell({ title, subtitle, children, activeTab, onBack, head
); );
return ( return (
<YStack backgroundColor={backgroundColor} minHeight="100vh" alignItems="center"> <YStack
backgroundColor={backgroundColor}
minHeight="100vh"
alignItems="center"
style={{ background: appBackground }}
>
<YStack <YStack
backgroundColor={headerSurface} backgroundColor={headerSurface}
borderBottomWidth={1} borderBottomWidth={1}
borderColor={borderColor} borderColor={headerBorder}
paddingHorizontal="$4" paddingHorizontal="$4"
paddingTop="$4" paddingTop="$4"
paddingBottom="$3" paddingBottom="$3"
shadowColor={shadow} shadowColor={actionShadow}
shadowOpacity={0.06} shadowOpacity={0.08}
shadowRadius={10} shadowRadius={14}
shadowOffset={{ width: 0, height: 4 }} shadowOffset={{ width: 0, height: 8 }}
width="100%" width="100%"
maxWidth={800} maxWidth={800}
position="sticky" position="sticky"

View File

@@ -2,27 +2,32 @@ import React from 'react';
import { YStack, XStack } from '@tamagui/stacks'; import { YStack, XStack } from '@tamagui/stacks';
import { SizableText as Text } from '@tamagui/text'; import { SizableText as Text } from '@tamagui/text';
import { Pressable } from '@tamagui/react-native-web-lite'; import { Pressable } from '@tamagui/react-native-web-lite';
import { useAdminTheme } from '../theme'; import { ADMIN_GRADIENTS, useAdminTheme } from '../theme';
import { withAlpha } from './colors';
export function MobileCard({ export function MobileCard({
children, children,
className, className,
style,
...rest ...rest
}: React.ComponentProps<typeof YStack>) { }: React.ComponentProps<typeof YStack>) {
const { surface, border, shadow } = useAdminTheme(); const { surface, border, shadow, glassSurface, glassBorder, glassShadow } = useAdminTheme();
return ( return (
<YStack <YStack
className={['admin-fade-up', className].filter(Boolean).join(' ')} className={['admin-fade-up', className].filter(Boolean).join(' ')}
backgroundColor={surface} backgroundColor={glassSurface ?? surface}
borderRadius={18} borderRadius={20}
borderWidth={1} borderWidth={2}
borderColor={border} borderColor={glassBorder ?? border}
shadowColor={shadow} shadowColor={glassShadow ?? shadow}
shadowOpacity={0.08} shadowOpacity={0.16}
shadowRadius={14} shadowRadius={16}
shadowOffset={{ width: 0, height: 10 }} shadowOffset={{ width: 0, height: 10 }}
padding="$3.5" padding="$3.5"
space="$2" space="$2"
style={{
...style,
}}
{...rest} {...rest}
> >
{children} {children}
@@ -99,13 +104,18 @@ export function CTAButton({
iconLeft?: React.ReactNode; iconLeft?: React.ReactNode;
iconRight?: React.ReactNode; iconRight?: React.ReactNode;
}) { }) {
const { primary, surface, border, text, danger } = useAdminTheme(); const { primary, surface, border, text, danger, glassSurfaceStrong } = useAdminTheme();
const isPrimary = tone === 'primary'; const isPrimary = tone === 'primary';
const isDanger = tone === 'danger'; const isDanger = tone === 'danger';
const isDisabled = disabled || loading; const isDisabled = disabled || loading;
const backgroundColor = isDanger ? danger : isPrimary ? primary : surface; const backgroundColor = isDanger ? danger : isPrimary ? primary : glassSurfaceStrong ?? surface;
const borderColor = isPrimary || isDanger ? 'transparent' : border; const borderColor = isPrimary || isDanger ? 'transparent' : border;
const labelColor = isPrimary || isDanger ? 'white' : text; const labelColor = isPrimary || isDanger ? 'white' : text;
const primaryStyle = isPrimary
? {
boxShadow: `0 18px 28px ${withAlpha(primary, 0.4)}`,
}
: undefined;
return ( return (
<Pressable <Pressable
onPress={isDisabled ? undefined : onPress} onPress={isDisabled ? undefined : onPress}
@@ -119,13 +129,14 @@ export function CTAButton({
> >
<XStack <XStack
height={52} height={52}
borderRadius={16} borderRadius={18}
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
borderWidth={isPrimary || isDanger ? 0 : 1} borderWidth={isPrimary || isDanger ? 0 : 2}
borderColor={borderColor} borderColor={borderColor}
space="$2" space="$2"
style={primaryStyle}
> >
{iconLeft} {iconLeft}
<Text fontSize="$sm" fontWeight="800" color={labelColor}> <Text fontSize="$sm" fontWeight="800" color={labelColor}>
@@ -183,6 +194,7 @@ export function ActionTile({
color, color,
onPress, onPress,
disabled = false, disabled = false,
variant = 'grid',
delayMs = 0, delayMs = 0,
}: { }: {
icon: React.ComponentType<{ size?: number; color?: string }>; icon: React.ComponentType<{ size?: number; color?: string }>;
@@ -190,49 +202,56 @@ export function ActionTile({
color: string; color: string;
onPress?: () => void; onPress?: () => void;
disabled?: boolean; disabled?: boolean;
variant?: 'grid' | 'cluster';
delayMs?: number; delayMs?: number;
}) { }) {
const { textStrong } = useAdminTheme(); const { textStrong, glassSurface } = useAdminTheme();
const backgroundColor = `${color}18`; const isCluster = variant === 'cluster';
const borderColor = `${color}40`; const backgroundColor = withAlpha(color, 0.12);
const shadowColor = `${color}2b`; const borderColor = withAlpha(color, 0.4);
const iconShadow = `${color}55`; const shadowColor = withAlpha(color, 0.35);
const iconShadow = withAlpha(color, 0.5);
const tileStyle = { const tileStyle = {
...(delayMs ? { animationDelay: `${delayMs}ms` } : {}), ...(delayMs ? { animationDelay: `${delayMs}ms` } : {}),
backgroundImage: `linear-gradient(135deg, ${backgroundColor}, ${color}0f)`, backgroundImage: `linear-gradient(135deg, ${backgroundColor}, ${withAlpha(color, 0.08)})`,
boxShadow: `0 10px 24px ${shadowColor}`, boxShadow: isCluster ? `0 12px 18px ${shadowColor}` : `0 20px 30px ${shadowColor}`,
}; };
return ( return (
<Pressable <Pressable
onPress={disabled ? undefined : onPress} onPress={disabled ? undefined : onPress}
style={{ width: '48%', marginBottom: 12, opacity: disabled ? 0.5 : 1 }} style={{
width: isCluster ? '100%' : '48%',
flex: isCluster ? 1 : undefined,
marginBottom: isCluster ? 0 : 12,
opacity: disabled ? 0.5 : 1,
}}
disabled={disabled} disabled={disabled}
> >
<YStack <YStack
className="admin-fade-up" className="admin-fade-up"
style={tileStyle} style={tileStyle}
borderRadius={16} borderRadius={isCluster ? 14 : 16}
padding="$3" padding="$3"
space="$2.5" space="$2.5"
backgroundColor={backgroundColor} backgroundColor={glassSurface ?? backgroundColor}
borderWidth={1} borderWidth={2}
borderColor={borderColor} borderColor={borderColor}
minHeight={110} minHeight={120}
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
> >
<XStack <XStack
width={36} width={44}
height={36} height={44}
borderRadius={12} borderRadius={14}
backgroundColor={color} backgroundColor={color}
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
style={{ boxShadow: `0 6px 14px ${iconShadow}` }} style={{ boxShadow: `0 6px 14px ${iconShadow}` }}
> >
<IconCmp size={16} color="white" /> <IconCmp size={18} color="white" />
</XStack> </XStack>
<Text fontSize="$sm" fontWeight="700" color={textStrong} textAlign="center"> <Text fontSize="$sm" fontWeight="800" color={textStrong} textAlign="center">
{label} {label}
</Text> </Text>
</YStack> </YStack>

View File

@@ -17,11 +17,12 @@ type MobileScaffoldProps = {
export function MobileScaffold({ title, onBack, rightSlot, children, footer }: MobileScaffoldProps) { export function MobileScaffold({ title, onBack, rightSlot, children, footer }: MobileScaffoldProps) {
const { t } = useTranslation('mobile'); const { t } = useTranslation('mobile');
const { background, surface, border, text, primary } = useAdminTheme(); const { background, surface, border, text, primary, glassSurfaceStrong, glassBorder, appBackground } = useAdminTheme();
const headerSurface = withAlpha(surface, 0.94); const headerSurface = glassSurfaceStrong ?? withAlpha(surface, 0.94);
const headerBorder = glassBorder ?? border;
return ( return (
<YStack backgroundColor={background} minHeight="100vh"> <YStack backgroundColor={background} minHeight="100vh" style={{ background: appBackground }}>
<XStack <XStack
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
@@ -30,7 +31,7 @@ export function MobileScaffold({ title, onBack, rightSlot, children, footer }: M
paddingBottom="$3" paddingBottom="$3"
backgroundColor={headerSurface} backgroundColor={headerSurface}
borderBottomWidth={1} borderBottomWidth={1}
borderColor={border} borderColor={headerBorder}
position="sticky" position="sticky"
top={0} top={0}
zIndex={60} zIndex={60}

View File

@@ -92,6 +92,11 @@ vi.mock('../../theme', () => ({
primary: '#FF5A5F', primary: '#FF5A5F',
danger: '#b91c1c', danger: '#b91c1c',
shadow: 'rgba(0,0,0,0.12)', shadow: 'rgba(0,0,0,0.12)',
glassSurface: 'rgba(255,255,255,0.8)',
glassSurfaceStrong: 'rgba(255,255,255,0.9)',
glassBorder: 'rgba(229,231,235,0.7)',
glassShadow: 'rgba(15,23,42,0.14)',
appBackground: 'linear-gradient(180deg, #f7fafc, #eef3f7)',
}), }),
})); }));

View File

@@ -182,7 +182,7 @@ const DEFAULT_PRESET: LayoutPreset = [
height: 220, height: 220,
fontSize: 110, fontSize: 110,
align: 'center', align: 'center',
fontFamily: 'Fraunces', fontFamily: 'Archivo Black',
lineHeight: 1.3, lineHeight: 1.3,
}, },
{ id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 800) / 2, y: 580, width: 800, height: 120, fontSize: 42, align: 'center', fontFamily: 'Manrope', lineHeight: 1.4 }, { id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 800) / 2, y: 580, width: 800, height: 120, fontSize: 42, align: 'center', fontFamily: 'Manrope', lineHeight: 1.4 },
@@ -202,7 +202,7 @@ const evergreenVowsPreset: LayoutPreset = [
height: 200, height: 200,
fontSize: 95, fontSize: 95,
align: 'left', align: 'left',
fontFamily: 'Fraunces', fontFamily: 'Archivo Black',
lineHeight: 1.3, lineHeight: 1.3,
}, },
{ {
@@ -251,7 +251,7 @@ const midnightGalaPreset: LayoutPreset = [
height: 220, height: 220,
fontSize: 105, fontSize: 105,
align: 'center', align: 'center',
fontFamily: 'Fraunces', fontFamily: 'Archivo Black',
lineHeight: 1.3, lineHeight: 1.3,
}, },
{ id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 900) / 2, y: 480, width: 900, height: 120, fontSize: 40, align: 'center', fontFamily: 'Manrope', lineHeight: 1.4 }, { id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 900) / 2, y: 480, width: 900, height: 120, fontSize: 40, align: 'center', fontFamily: 'Manrope', lineHeight: 1.4 },
@@ -269,7 +269,7 @@ const midnightGalaPreset: LayoutPreset = [
const gardenBrunchPreset: LayoutPreset = [ const gardenBrunchPreset: LayoutPreset = [
// Verspielt, asymmetrisch, aber ausbalanciert // Verspielt, asymmetrisch, aber ausbalanciert
{ id: 'headline', type: 'headline', x: 120, y: 240, width: 900, height: 200, fontSize: 90, align: 'left', fontFamily: 'Fraunces', lineHeight: 1.3 }, { id: 'headline', type: 'headline', x: 120, y: 240, width: 900, height: 200, fontSize: 90, align: 'left', fontFamily: 'Archivo Black', lineHeight: 1.3 },
{ id: 'subtitle', type: 'subtitle', x: 120, y: 450, width: 700, height: 120, fontSize: 40, align: 'left', fontFamily: 'Manrope', lineHeight: 1.4 }, { id: 'subtitle', type: 'subtitle', x: 120, y: 450, width: 700, height: 120, fontSize: 40, align: 'left', fontFamily: 'Manrope', lineHeight: 1.4 },
{ {
id: 'qr', id: 'qr',
@@ -305,7 +305,7 @@ const sparklerSoireePreset: LayoutPreset = [
height: 220, height: 220,
fontSize: 100, fontSize: 100,
align: 'center', align: 'center',
fontFamily: 'Fraunces', fontFamily: 'Archivo Black',
lineHeight: 1.3, lineHeight: 1.3,
}, },
{ id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 800) / 2, y: 480, width: 800, height: 120, fontSize: 40, align: 'center', fontFamily: 'Manrope' }, { id: 'subtitle', type: 'subtitle', x: (c) => (c.canvasWidth - 800) / 2, y: 480, width: 800, height: 120, fontSize: 40, align: 'center', fontFamily: 'Manrope' },
@@ -333,7 +333,7 @@ const confettiBashPreset: LayoutPreset = [
height: 220, height: 220,
fontSize: 110, fontSize: 110,
align: 'center', align: 'center',
fontFamily: 'Fraunces', fontFamily: 'Archivo Black',
lineHeight: 1.3, lineHeight: 1.3,
}, },
{ {
@@ -394,7 +394,7 @@ const balancedModernPreset: LayoutPreset = [
height: 380, height: 380,
fontSize: 100, fontSize: 100,
align: 'left', align: 'left',
fontFamily: 'Fraunces', fontFamily: 'Archivo Black',
lineHeight: 1.3, lineHeight: 1.3,
}, },
{ {

View File

@@ -1,59 +1,120 @@
import { useTheme } from '@tamagui/core'; import { useTheme } from '@tamagui/core';
export const ADMIN_COLORS = { export const ADMIN_COLORS = {
primary: '#FF5A5F', primary: '#FF5C5C',
primaryStrong: '#C2413B', primaryStrong: '#E63B57',
accent: '#FFF8F5', accent: '#3D5AFE',
accentSoft: '#FFF1EB', accentSoft: '#E8ECFF',
accentWarm: '#FFE4DA', accentWarm: '#FFE3D6',
warning: '#F5C542', warning: '#FBBF24',
success: '#06D6A0', success: '#22C55E',
danger: '#B91C1C', danger: '#EF4444',
text: '#1F2937', text: '#0B132B',
textMuted: '#6B7280', textMuted: '#54606E',
textSubtle: '#94A3B8', textSubtle: '#8C99A8',
border: '#F2E4DA', border: '#F3D6C9',
surface: '#FFFFFF', surface: '#FFFFFF',
surfaceMuted: '#FFFDFB', surfaceMuted: '#FFF6F0',
backdrop: '#0F172A', backdrop: '#0B132B',
}; };
export const ADMIN_ACTION_COLORS = { export const ADMIN_ACTION_COLORS = {
settings: '#14B8A6', settings: '#00C2A8',
tasks: '#F59E0B', tasks: '#FFC857',
qr: '#3B82F6', qr: '#3D5AFE',
images: '#8B5CF6', images: '#FF7AB6',
liveShow: '#EC4899', liveShow: '#FF6D00',
liveShowSettings: '#0EA5E9', liveShowSettings: '#00B0FF',
guests: '#10B981', guests: '#22C55E',
guestMessages: '#F97316', guestMessages: '#FF8A00',
branding: '#6366F1', branding: '#00B4D8',
photobooth: '#E11D48', photobooth: '#FF3D71',
recap: '#64748B', recap: '#94A3B8',
packages: ADMIN_COLORS.primary, packages: ADMIN_COLORS.primary,
analytics: '#22C55E', analytics: '#22C55E',
invites: ADMIN_COLORS.primaryStrong, invites: '#3D5AFE',
}; };
export const ADMIN_GRADIENTS = { export const ADMIN_GRADIENTS = {
primaryCta: `linear-gradient(135deg, ${ADMIN_COLORS.primary}, #FF8A8E, ${ADMIN_COLORS.accent})`, primaryCta: `linear-gradient(135deg, ${ADMIN_COLORS.primary}, #FF9A5C, ${ADMIN_COLORS.accent})`,
softCard: `linear-gradient(135deg, ${ADMIN_COLORS.accentSoft}, ${ADMIN_COLORS.accentWarm})`, softCard: 'linear-gradient(145deg, rgba(255,255,255,0.98), rgba(255,245,238,0.92))',
loginBackground: 'linear-gradient(135deg, #0b1020, #0f172a, #0b1020)', loginBackground: 'linear-gradient(135deg, #0b132b, #162040, #0b132b)',
appBackground:
'radial-gradient(circle at 15% 0%, rgba(255, 92, 92, 0.22), transparent 50%), radial-gradient(circle at 85% 10%, rgba(61, 90, 254, 0.2), transparent 55%), linear-gradient(180deg, #fff4ee 0%, #ffefe4 100%)',
appBackgroundDark:
'radial-gradient(circle at 15% 0%, rgba(255, 92, 92, 0.18), transparent 55%), radial-gradient(circle at 85% 10%, rgba(61, 90, 254, 0.18), transparent 55%), linear-gradient(180deg, #0b132b 0%, #0e1a33 100%)',
}; };
export const ADMIN_MOTION = { export const ADMIN_MOTION = {
tileStaggerMs: 40, tileStaggerMs: 40,
}; };
type Rgb = { r: number; g: number; b: number };
function parseRgb(color: string): Rgb | null {
const trimmed = color.trim();
if (trimmed.startsWith('#')) {
const hex = trimmed.slice(1);
const normalized = hex.length === 3 ? hex.split('').map((ch) => ch + ch).join('') : hex;
if (normalized.length === 6) {
return {
r: Number.parseInt(normalized.slice(0, 2), 16),
g: Number.parseInt(normalized.slice(2, 4), 16),
b: Number.parseInt(normalized.slice(4, 6), 16),
};
}
}
const rgb = trimmed.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/i);
if (rgb) {
return { r: Number(rgb[1]), g: Number(rgb[2]), b: Number(rgb[3]) };
}
const rgba = trimmed.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*([0-9.]+)\)$/i);
if (rgba) {
return { r: Number(rgba[1]), g: Number(rgba[2]), b: Number(rgba[3]) };
}
return null;
}
function withAlpha(color: string, alpha: number): string {
const rgb = parseRgb(color);
if (! rgb) {
return color;
}
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
}
function isDarkColor(color: string): boolean {
const rgb = parseRgb(color);
if (! rgb) {
return false;
}
const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
return luminance < 0.5;
}
export function useAdminTheme() { export function useAdminTheme() {
const theme = useTheme(); const theme = useTheme();
const background = String(theme.background?.val ?? '#FFF8F5');
const surface = String(theme.surface?.val ?? ADMIN_COLORS.surface);
const border = String(theme.borderColor?.val ?? ADMIN_COLORS.border);
const isDark = isDarkColor(background);
const glassSurface = withAlpha(surface, isDark ? 0.96 : 0.98);
const glassSurfaceStrong = withAlpha(surface, isDark ? 1 : 1);
const glassBorder = withAlpha(border, isDark ? 0.85 : 0.95);
const glassShadow = isDark ? 'rgba(0, 0, 0, 0.55)' : 'rgba(11, 19, 43, 0.18)';
const appBackground = isDark ? ADMIN_GRADIENTS.appBackgroundDark : ADMIN_GRADIENTS.appBackground;
return { return {
theme, theme,
background: String(theme.background?.val ?? '#FFF8F5'), background,
surface: String(theme.surface?.val ?? ADMIN_COLORS.surface), surface,
surfaceMuted: String(theme.gray2?.val ?? ADMIN_COLORS.surfaceMuted), surfaceMuted: String(theme.gray2?.val ?? ADMIN_COLORS.surfaceMuted),
border: String(theme.borderColor?.val ?? ADMIN_COLORS.border), border,
text: String(theme.color?.val ?? ADMIN_COLORS.text), text: String(theme.color?.val ?? ADMIN_COLORS.text),
textStrong: String(theme.color12?.val ?? theme.color?.val ?? ADMIN_COLORS.text), textStrong: String(theme.color12?.val ?? theme.color?.val ?? ADMIN_COLORS.text),
muted: String(theme.gray?.val ?? ADMIN_COLORS.textMuted), muted: String(theme.gray?.val ?? ADMIN_COLORS.textMuted),
@@ -73,7 +134,12 @@ export function useAdminTheme() {
infoText: String(theme.blue10?.val ?? ADMIN_COLORS.primaryStrong), infoText: String(theme.blue10?.val ?? ADMIN_COLORS.primaryStrong),
danger: String(theme.red10?.val ?? ADMIN_COLORS.danger), danger: String(theme.red10?.val ?? ADMIN_COLORS.danger),
backdrop: String(theme.gray12?.val ?? ADMIN_COLORS.backdrop), backdrop: String(theme.gray12?.val ?? ADMIN_COLORS.backdrop),
overlay: String(theme.gray12?.val ?? 'rgba(15, 23, 42, 0.6)'), overlay: withAlpha(String(theme.gray12?.val ?? ADMIN_COLORS.backdrop), 0.6),
shadow: String(theme.shadowColor?.val ?? 'rgba(31, 41, 55, 0.12)'), shadow: String(theme.shadowColor?.val ?? 'rgba(15, 23, 42, 0.12)'),
glassSurface,
glassSurfaceStrong,
glassBorder,
glassShadow,
appBackground,
}; };
} }

View File

@@ -8,16 +8,16 @@ const tokens = {
...baseTokens, ...baseTokens,
color: { color: {
...baseTokens.color, ...baseTokens.color,
primary: '#FF5A5F', primary: '#FF5C5C',
accent: '#FFB6C1', accent: '#3D5AFE',
accentSoft: '#FFE5EC', accentSoft: '#E8ECFF',
success: '#06D6A0', success: '#22C55E',
warning: '#F5C542', warning: '#FBBF24',
danger: '#E04848', danger: '#EF4444',
surface: '#ffffff', surface: '#ffffff',
muted: '#F4ECE8', muted: '#FFF6F0',
border: '#F2E4DA', border: '#F3D6C9',
text: '#1F2937', text: '#0B132B',
}, },
radius: { radius: {
...baseTokens.radius, ...baseTokens.radius,
@@ -37,53 +37,53 @@ const themes = {
...baseThemes.light, ...baseThemes.light,
primary: tokens.color.primary, primary: tokens.color.primary,
accent: tokens.color.accent, accent: tokens.color.accent,
background: '#FFF8F5', background: '#FFF1E8',
backgroundHover: '#FFF1EC', backgroundHover: '#FFE8DD',
backgroundPress: '#FFE7E0', backgroundPress: '#FFE1D2',
backgroundStrong: tokens.color.surface, backgroundStrong: tokens.color.surface,
backgroundTransparent: 'rgba(255, 248, 245, 0)', backgroundTransparent: 'rgba(255, 241, 232, 0)',
color: tokens.color.text, color: tokens.color.text,
colorHover: '#111827', colorHover: '#091024',
colorPress: '#0F172A', colorPress: '#091024',
colorFocus: '#0F172A', colorFocus: '#091024',
borderColor: tokens.color.border, borderColor: tokens.color.border,
borderColorHover: '#EAD5C9', borderColorHover: '#EBCABA',
borderColorPress: '#E0C9BC', borderColorPress: '#E1BFAE',
shadowColor: 'rgba(31, 41, 55, 0.12)', shadowColor: 'rgba(11, 19, 43, 0.16)',
shadowColorPress: 'rgba(31, 41, 55, 0.16)', shadowColorPress: 'rgba(11, 19, 43, 0.2)',
shadowColorFocus: 'rgba(31, 41, 55, 0.18)', shadowColorFocus: 'rgba(11, 19, 43, 0.24)',
surface: tokens.color.surface, surface: tokens.color.surface,
muted: tokens.color.muted, muted: tokens.color.muted,
blue3: tokens.color.accentSoft, blue3: tokens.color.accentSoft,
blue6: tokens.color.accent, blue6: tokens.color.accent,
blue10: tokens.color.primary, blue10: tokens.color.primary,
blue11: '#C2413B', blue11: '#1E36F1',
}, },
dark: { dark: {
...baseThemes.dark, ...baseThemes.dark,
primary: tokens.color.primary, primary: tokens.color.primary,
accent: tokens.color.accent, accent: tokens.color.accent,
background: '#171219', background: '#0B132B',
backgroundHover: '#1F1A23', backgroundHover: '#101A36',
backgroundPress: '#26212B', backgroundPress: '#132142',
backgroundStrong: '#1F1A23', backgroundStrong: '#101A36',
backgroundTransparent: 'rgba(23, 18, 25, 0)', backgroundTransparent: 'rgba(11, 19, 43, 0)',
color: '#F8F6F2', color: '#F8FAFF',
colorHover: '#FFFFFF', colorHover: '#FFFFFF',
colorPress: '#FDF8F5', colorPress: '#F2F6FF',
colorFocus: '#FFFFFF', colorFocus: '#FFFFFF',
borderColor: '#2C2531', borderColor: '#1F2A4A',
borderColorHover: '#3A3240', borderColorHover: '#29345A',
borderColorPress: '#443C4A', borderColorPress: '#313D67',
shadowColor: 'rgba(0, 0, 0, 0.55)', shadowColor: 'rgba(0, 0, 0, 0.55)',
shadowColorPress: 'rgba(0, 0, 0, 0.65)', shadowColorPress: 'rgba(0, 0, 0, 0.65)',
shadowColorFocus: 'rgba(0, 0, 0, 0.6)', shadowColorFocus: 'rgba(0, 0, 0, 0.6)',
surface: '#1F1A23', surface: '#0F1B36',
muted: '#241E28', muted: '#121F3D',
blue3: '#2B1D23', blue3: '#1B2550',
blue6: '#5A2D34', blue6: '#3D5AFE',
blue10: '#FF7A7F', blue10: '#FF5C5C',
blue11: '#FFB3B6', blue11: '#FF8A8A',
}, },
}; };
@@ -105,12 +105,12 @@ const fonts = {
}, },
heading: { heading: {
...defaultConfig.fonts.heading, ...defaultConfig.fonts.heading,
family: 'Manrope', family: 'Archivo Black',
weight: sharedWeights, weight: sharedWeights,
}, },
display: { display: {
...defaultConfig.fonts.heading, ...defaultConfig.fonts.heading,
family: 'Fraunces', family: 'Archivo Black',
weight: sharedWeights, weight: sharedWeights,
}, },
}; };