Refine admin PWA layout and tamagui usage
This commit is contained in:
@@ -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%)
|
||||||
|
|||||||
@@ -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
BIN
public/fonts/google/archivo-black/ArchivoBlack-400-normal.ttf
Normal file
BIN
public/fonts/google/archivo-black/ArchivoBlack-400-normal.ttf
Normal file
Binary file not shown.
@@ -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');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-400-normal.ttf
Normal file
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-400-normal.ttf
Normal file
Binary file not shown.
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-500-normal.ttf
Normal file
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-500-normal.ttf
Normal file
Binary file not shown.
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-600-normal.ttf
Normal file
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-600-normal.ttf
Normal file
Binary file not shown.
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-700-normal.ttf
Normal file
BIN
public/fonts/google/space-grotesk/SpaceGrotesk-700-normal.ttf
Normal file
Binary file not shown.
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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)',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
235
resources/js/admin/mobile/__tests__/DashboardPage.test.tsx
Normal file
235
resources/js/admin/mobile/__tests__/DashboardPage.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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)',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
217
resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx
Normal file
217
resources/js/admin/mobile/__tests__/EventTasksPage.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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)',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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)',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user