From 5e3b2ff5d78644b8ef8043fc43d2a4dbc2b0598d Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 12 Aug 2026 20:24:00 -0700 Subject: [PATCH 1/2] RC-T40 Chat Fixes --- global.css | 710 +----------------- global.web.css | 12 + src/app/(app)/chat.tsx | 7 +- src/app/_layout.tsx | 4 +- .../chat/__tests__/chat-utils.test.ts | 27 +- src/components/chat/chat-utils.ts | 2 + .../ui/gluestack-ui-provider/index.tsx | 16 +- src/components/ui/input/index.tsx | 42 +- src/components/ui/select/index.tsx | 33 +- src/hooks/use-signalr-lifecycle.ts | 11 + src/lib/theme-styles.ts | 9 + src/lib/theme-styles.web.ts | 7 + src/models/v4/chat/chatEnums.ts | 2 + src/stores/signalr/signalr-store.ts | 58 ++ theme-tokens.css | 700 +++++++++++++++++ 15 files changed, 924 insertions(+), 716 deletions(-) create mode 100644 global.web.css create mode 100644 src/lib/theme-styles.ts create mode 100644 src/lib/theme-styles.web.ts create mode 100644 theme-tokens.css diff --git a/global.css b/global.css index aeff712..9298e6e 100644 --- a/global.css +++ b/global.css @@ -3,701 +3,15 @@ @import 'tailwindcss/utilities.css'; @import 'nativewind/theme'; -/* Class-driven dark mode (GluestackUIProvider toggles the .dark class on web - and on the native root View). */ -@custom-variant dark (&:where(.dark, .dark *)); - -/* ─── Design tokens (light / dark) ─────────────────────────────── - 1. :root — light defaults (all platforms) - 2. @media dark :root — dark defaults; on native this follows - Appearance.getColorScheme() (the provider - sets an override for explicit modes). - 3. :root.dark / :root.light — web-only class override, beats the media - query within the same layer. */ -@layer theme { - :root { - --color-primary-0: 248 250 252; - --color-primary-50: 241 245 249; - --color-primary-100: 226 232 240; - --color-primary-200: 203 213 225; - --color-primary-300: 148 163 184; - --color-primary-400: 100 116 139; - --color-primary-500: 71 85 105; - --color-primary-600: 51 65 85; - --color-primary-700: 45 55 72; - --color-primary-800: 39 49 66; - --color-primary-900: 30 41 59; - --color-primary-950: 15 23 42; - --color-secondary-0: 254 255 255; - --color-secondary-50: 241 242 242; - --color-secondary-100: 231 232 232; - --color-secondary-200: 219 219 219; - --color-secondary-300: 175 176 176; - --color-secondary-400: 114 115 115; - --color-secondary-500: 94 95 95; - --color-secondary-600: 81 82 82; - --color-secondary-700: 63 64 64; - --color-secondary-800: 39 38 38; - --color-secondary-900: 24 23 23; - --color-secondary-950: 11 12 12; - --color-tertiary-0: 255 250 245; - --color-tertiary-50: 255 242 229; - --color-tertiary-100: 255 233 213; - --color-tertiary-200: 254 209 170; - --color-tertiary-300: 253 180 116; - --color-tertiary-400: 251 157 75; - --color-tertiary-500: 231 129 40; - --color-tertiary-600: 215 117 31; - --color-tertiary-700: 180 98 26; - --color-tertiary-800: 130 73 23; - --color-tertiary-900: 108 61 19; - --color-tertiary-950: 84 49 18; - --color-error-0: 254 233 233; - --color-error-50: 254 226 226; - --color-error-100: 254 202 202; - --color-error-200: 252 165 165; - --color-error-300: 248 113 113; - --color-error-400: 239 68 68; - --color-error-500: 230 53 53; - --color-error-600: 220 38 38; - --color-error-700: 185 28 28; - --color-error-800: 153 27 27; - --color-error-900: 127 29 29; - --color-error-950: 83 19 19; - --color-success-0: 228 255 244; - --color-success-50: 202 255 232; - --color-success-100: 162 241 192; - --color-success-200: 132 211 162; - --color-success-300: 102 181 132; - --color-success-400: 72 151 102; - --color-success-500: 52 131 82; - --color-success-600: 42 121 72; - --color-success-700: 32 111 62; - --color-success-800: 22 101 52; - --color-success-900: 20 83 45; - --color-success-950: 27 50 36; - --color-warning-0: 255 253 251; - --color-warning-50: 255 249 245; - --color-warning-100: 255 231 213; - --color-warning-200: 254 205 170; - --color-warning-300: 253 173 116; - --color-warning-400: 251 149 75; - --color-warning-500: 231 120 40; - --color-warning-600: 215 108 31; - --color-warning-700: 180 90 26; - --color-warning-800: 130 68 23; - --color-warning-900: 108 56 19; - --color-warning-950: 84 45 18; - --color-info-0: 236 248 254; - --color-info-50: 199 235 252; - --color-info-100: 162 221 250; - --color-info-200: 124 207 248; - --color-info-300: 87 194 246; - --color-info-400: 50 180 244; - --color-info-500: 13 166 242; - --color-info-600: 11 141 205; - --color-info-700: 9 115 168; - --color-info-800: 7 90 131; - --color-info-900: 5 64 93; - --color-info-950: 3 38 56; - --color-typography-0: 254 254 255; - --color-typography-50: 245 245 245; - --color-typography-100: 229 229 229; - --color-typography-200: 219 219 220; - --color-typography-300: 212 212 212; - --color-typography-400: 163 163 163; - --color-typography-500: 140 140 140; - --color-typography-600: 115 115 115; - --color-typography-700: 82 82 82; - --color-typography-800: 64 64 64; - --color-typography-900: 38 38 39; - --color-typography-950: 23 23 23; - --color-outline-0: 253 254 254; - --color-outline-50: 243 243 243; - --color-outline-100: 230 230 230; - --color-outline-200: 221 220 219; - --color-outline-300: 211 211 211; - --color-outline-400: 165 163 163; - --color-outline-500: 140 141 141; - --color-outline-600: 115 116 116; - --color-outline-700: 83 82 82; - --color-outline-800: 65 65 65; - --color-outline-900: 39 38 36; - --color-outline-950: 26 23 23; - --color-background-0: 255 255 255; - --color-background-50: 246 246 246; - --color-background-100: 242 241 241; - --color-background-200: 220 219 219; - --color-background-300: 213 212 212; - --color-background-400: 162 163 163; - --color-background-500: 142 142 142; - --color-background-600: 116 116 116; - --color-background-700: 83 82 82; - --color-background-800: 65 64 64; - --color-background-900: 39 38 37; - --color-background-950: 24 23 24; - --color-background-error: 254 241 241; - --color-background-warning: 255 244 235; - --color-background-success: 237 252 242; - --color-background-muted: 247 248 247; - --color-background-info: 235 248 254; - --color-indicator-primary: 55 55 55; - --color-indicator-info: 83 153 236; - --color-indicator-error: 185 28 28; - } - - @media (prefers-color-scheme: dark) { - :root { - --color-primary-0: 15 23 42; - --color-primary-50: 30 41 59; - --color-primary-100: 39 49 66; - --color-primary-200: 45 55 72; - --color-primary-300: 51 65 85; - --color-primary-400: 71 85 105; - --color-primary-500: 100 116 139; - --color-primary-600: 71 85 105; - --color-primary-700: 51 65 85; - --color-primary-800: 45 55 72; - --color-primary-900: 39 49 66; - --color-primary-950: 30 41 59; - --color-secondary-0: 11 12 12; - --color-secondary-50: 24 23 23; - --color-secondary-100: 39 38 38; - --color-secondary-200: 63 64 64; - --color-secondary-300: 81 82 82; - --color-secondary-400: 94 95 95; - --color-secondary-500: 114 115 115; - --color-secondary-600: 175 176 176; - --color-secondary-700: 219 219 219; - --color-secondary-800: 231 232 232; - --color-secondary-900: 241 242 242; - --color-secondary-950: 254 255 255; - --color-tertiary-0: 84 49 18; - --color-tertiary-50: 108 61 19; - --color-tertiary-100: 130 73 23; - --color-tertiary-200: 180 98 26; - --color-tertiary-300: 215 117 31; - --color-tertiary-400: 231 129 40; - --color-tertiary-500: 251 157 75; - --color-tertiary-600: 253 180 116; - --color-tertiary-700: 254 209 170; - --color-tertiary-800: 255 233 213; - --color-tertiary-900: 255 242 229; - --color-tertiary-950: 255 250 245; - --color-error-0: 83 19 19; - --color-error-50: 127 29 29; - --color-error-100: 153 27 27; - --color-error-200: 185 28 28; - --color-error-300: 220 38 38; - --color-error-400: 230 53 53; - --color-error-500: 239 68 68; - --color-error-600: 248 113 113; - --color-error-700: 252 165 165; - --color-error-800: 254 202 202; - --color-error-900: 254 226 226; - --color-error-950: 254 233 233; - --color-success-0: 27 50 36; - --color-success-50: 20 83 45; - --color-success-100: 22 101 52; - --color-success-200: 32 111 62; - --color-success-300: 42 121 72; - --color-success-400: 52 131 82; - --color-success-500: 72 151 102; - --color-success-600: 102 181 132; - --color-success-700: 132 211 162; - --color-success-800: 162 241 192; - --color-success-900: 202 255 232; - --color-success-950: 228 255 244; - --color-warning-0: 84 45 18; - --color-warning-50: 108 56 19; - --color-warning-100: 130 68 23; - --color-warning-200: 180 90 26; - --color-warning-300: 215 108 31; - --color-warning-400: 231 120 40; - --color-warning-500: 251 149 75; - --color-warning-600: 253 173 116; - --color-warning-700: 254 205 170; - --color-warning-800: 255 231 213; - --color-warning-900: 255 249 245; - --color-warning-950: 255 253 251; - --color-info-0: 3 38 56; - --color-info-50: 5 64 93; - --color-info-100: 7 90 131; - --color-info-200: 9 115 168; - --color-info-300: 11 141 205; - --color-info-400: 13 166 242; - --color-info-500: 50 180 244; - --color-info-600: 87 194 246; - --color-info-700: 124 207 248; - --color-info-800: 162 221 250; - --color-info-900: 199 235 252; - --color-info-950: 236 248 254; - --color-typography-0: 23 23 23; - --color-typography-50: 38 38 39; - --color-typography-100: 64 64 64; - --color-typography-200: 82 82 82; - --color-typography-300: 115 115 115; - --color-typography-400: 140 140 140; - --color-typography-500: 163 163 163; - --color-typography-600: 212 212 212; - --color-typography-700: 219 219 220; - --color-typography-800: 229 229 229; - --color-typography-900: 245 245 245; - --color-typography-950: 254 254 255; - --color-outline-0: 26 23 23; - --color-outline-50: 39 38 36; - --color-outline-100: 65 65 65; - --color-outline-200: 83 82 82; - --color-outline-300: 115 116 116; - --color-outline-400: 140 141 141; - --color-outline-500: 165 163 163; - --color-outline-600: 211 211 211; - --color-outline-700: 221 220 219; - --color-outline-800: 230 230 230; - --color-outline-900: 243 243 243; - --color-outline-950: 253 254 254; - --color-background-0: 18 18 18; - --color-background-50: 39 38 37; - --color-background-100: 65 64 64; - --color-background-200: 83 82 82; - --color-background-300: 116 116 116; - --color-background-400: 142 142 142; - --color-background-500: 162 163 163; - --color-background-600: 213 212 212; - --color-background-700: 220 219 219; - --color-background-800: 242 241 241; - --color-background-900: 246 246 246; - --color-background-950: 254 254 254; - --color-background-error: 66 43 43; - --color-background-warning: 65 47 35; - --color-background-success: 28 43 33; - --color-background-muted: 51 51 51; - --color-background-info: 26 40 46; - --color-indicator-primary: 247 247 247; - --color-indicator-info: 161 199 245; - --color-indicator-error: 232 70 69; - } - } - - :root.dark { - --color-primary-0: 15 23 42; - --color-primary-50: 30 41 59; - --color-primary-100: 39 49 66; - --color-primary-200: 45 55 72; - --color-primary-300: 51 65 85; - --color-primary-400: 71 85 105; - --color-primary-500: 100 116 139; - --color-primary-600: 71 85 105; - --color-primary-700: 51 65 85; - --color-primary-800: 45 55 72; - --color-primary-900: 39 49 66; - --color-primary-950: 30 41 59; - --color-secondary-0: 11 12 12; - --color-secondary-50: 24 23 23; - --color-secondary-100: 39 38 38; - --color-secondary-200: 63 64 64; - --color-secondary-300: 81 82 82; - --color-secondary-400: 94 95 95; - --color-secondary-500: 114 115 115; - --color-secondary-600: 175 176 176; - --color-secondary-700: 219 219 219; - --color-secondary-800: 231 232 232; - --color-secondary-900: 241 242 242; - --color-secondary-950: 254 255 255; - --color-tertiary-0: 84 49 18; - --color-tertiary-50: 108 61 19; - --color-tertiary-100: 130 73 23; - --color-tertiary-200: 180 98 26; - --color-tertiary-300: 215 117 31; - --color-tertiary-400: 231 129 40; - --color-tertiary-500: 251 157 75; - --color-tertiary-600: 253 180 116; - --color-tertiary-700: 254 209 170; - --color-tertiary-800: 255 233 213; - --color-tertiary-900: 255 242 229; - --color-tertiary-950: 255 250 245; - --color-error-0: 83 19 19; - --color-error-50: 127 29 29; - --color-error-100: 153 27 27; - --color-error-200: 185 28 28; - --color-error-300: 220 38 38; - --color-error-400: 230 53 53; - --color-error-500: 239 68 68; - --color-error-600: 248 113 113; - --color-error-700: 252 165 165; - --color-error-800: 254 202 202; - --color-error-900: 254 226 226; - --color-error-950: 254 233 233; - --color-success-0: 27 50 36; - --color-success-50: 20 83 45; - --color-success-100: 22 101 52; - --color-success-200: 32 111 62; - --color-success-300: 42 121 72; - --color-success-400: 52 131 82; - --color-success-500: 72 151 102; - --color-success-600: 102 181 132; - --color-success-700: 132 211 162; - --color-success-800: 162 241 192; - --color-success-900: 202 255 232; - --color-success-950: 228 255 244; - --color-warning-0: 84 45 18; - --color-warning-50: 108 56 19; - --color-warning-100: 130 68 23; - --color-warning-200: 180 90 26; - --color-warning-300: 215 108 31; - --color-warning-400: 231 120 40; - --color-warning-500: 251 149 75; - --color-warning-600: 253 173 116; - --color-warning-700: 254 205 170; - --color-warning-800: 255 231 213; - --color-warning-900: 255 249 245; - --color-warning-950: 255 253 251; - --color-info-0: 3 38 56; - --color-info-50: 5 64 93; - --color-info-100: 7 90 131; - --color-info-200: 9 115 168; - --color-info-300: 11 141 205; - --color-info-400: 13 166 242; - --color-info-500: 50 180 244; - --color-info-600: 87 194 246; - --color-info-700: 124 207 248; - --color-info-800: 162 221 250; - --color-info-900: 199 235 252; - --color-info-950: 236 248 254; - --color-typography-0: 23 23 23; - --color-typography-50: 38 38 39; - --color-typography-100: 64 64 64; - --color-typography-200: 82 82 82; - --color-typography-300: 115 115 115; - --color-typography-400: 140 140 140; - --color-typography-500: 163 163 163; - --color-typography-600: 212 212 212; - --color-typography-700: 219 219 220; - --color-typography-800: 229 229 229; - --color-typography-900: 245 245 245; - --color-typography-950: 254 254 255; - --color-outline-0: 26 23 23; - --color-outline-50: 39 38 36; - --color-outline-100: 65 65 65; - --color-outline-200: 83 82 82; - --color-outline-300: 115 116 116; - --color-outline-400: 140 141 141; - --color-outline-500: 165 163 163; - --color-outline-600: 211 211 211; - --color-outline-700: 221 220 219; - --color-outline-800: 230 230 230; - --color-outline-900: 243 243 243; - --color-outline-950: 253 254 254; - --color-background-0: 18 18 18; - --color-background-50: 39 38 37; - --color-background-100: 65 64 64; - --color-background-200: 83 82 82; - --color-background-300: 116 116 116; - --color-background-400: 142 142 142; - --color-background-500: 162 163 163; - --color-background-600: 213 212 212; - --color-background-700: 220 219 219; - --color-background-800: 242 241 241; - --color-background-900: 246 246 246; - --color-background-950: 254 254 254; - --color-background-error: 66 43 43; - --color-background-warning: 65 47 35; - --color-background-success: 28 43 33; - --color-background-muted: 51 51 51; - --color-background-info: 26 40 46; - --color-indicator-primary: 247 247 247; - --color-indicator-info: 161 199 245; - --color-indicator-error: 232 70 69; - } - - :root.light { - --color-primary-0: 248 250 252; - --color-primary-50: 241 245 249; - --color-primary-100: 226 232 240; - --color-primary-200: 203 213 225; - --color-primary-300: 148 163 184; - --color-primary-400: 100 116 139; - --color-primary-500: 71 85 105; - --color-primary-600: 51 65 85; - --color-primary-700: 45 55 72; - --color-primary-800: 39 49 66; - --color-primary-900: 30 41 59; - --color-primary-950: 15 23 42; - --color-secondary-0: 254 255 255; - --color-secondary-50: 241 242 242; - --color-secondary-100: 231 232 232; - --color-secondary-200: 219 219 219; - --color-secondary-300: 175 176 176; - --color-secondary-400: 114 115 115; - --color-secondary-500: 94 95 95; - --color-secondary-600: 81 82 82; - --color-secondary-700: 63 64 64; - --color-secondary-800: 39 38 38; - --color-secondary-900: 24 23 23; - --color-secondary-950: 11 12 12; - --color-tertiary-0: 255 250 245; - --color-tertiary-50: 255 242 229; - --color-tertiary-100: 255 233 213; - --color-tertiary-200: 254 209 170; - --color-tertiary-300: 253 180 116; - --color-tertiary-400: 251 157 75; - --color-tertiary-500: 231 129 40; - --color-tertiary-600: 215 117 31; - --color-tertiary-700: 180 98 26; - --color-tertiary-800: 130 73 23; - --color-tertiary-900: 108 61 19; - --color-tertiary-950: 84 49 18; - --color-error-0: 254 233 233; - --color-error-50: 254 226 226; - --color-error-100: 254 202 202; - --color-error-200: 252 165 165; - --color-error-300: 248 113 113; - --color-error-400: 239 68 68; - --color-error-500: 230 53 53; - --color-error-600: 220 38 38; - --color-error-700: 185 28 28; - --color-error-800: 153 27 27; - --color-error-900: 127 29 29; - --color-error-950: 83 19 19; - --color-success-0: 228 255 244; - --color-success-50: 202 255 232; - --color-success-100: 162 241 192; - --color-success-200: 132 211 162; - --color-success-300: 102 181 132; - --color-success-400: 72 151 102; - --color-success-500: 52 131 82; - --color-success-600: 42 121 72; - --color-success-700: 32 111 62; - --color-success-800: 22 101 52; - --color-success-900: 20 83 45; - --color-success-950: 27 50 36; - --color-warning-0: 255 253 251; - --color-warning-50: 255 249 245; - --color-warning-100: 255 231 213; - --color-warning-200: 254 205 170; - --color-warning-300: 253 173 116; - --color-warning-400: 251 149 75; - --color-warning-500: 231 120 40; - --color-warning-600: 215 108 31; - --color-warning-700: 180 90 26; - --color-warning-800: 130 68 23; - --color-warning-900: 108 56 19; - --color-warning-950: 84 45 18; - --color-info-0: 236 248 254; - --color-info-50: 199 235 252; - --color-info-100: 162 221 250; - --color-info-200: 124 207 248; - --color-info-300: 87 194 246; - --color-info-400: 50 180 244; - --color-info-500: 13 166 242; - --color-info-600: 11 141 205; - --color-info-700: 9 115 168; - --color-info-800: 7 90 131; - --color-info-900: 5 64 93; - --color-info-950: 3 38 56; - --color-typography-0: 254 254 255; - --color-typography-50: 245 245 245; - --color-typography-100: 229 229 229; - --color-typography-200: 219 219 220; - --color-typography-300: 212 212 212; - --color-typography-400: 163 163 163; - --color-typography-500: 140 140 140; - --color-typography-600: 115 115 115; - --color-typography-700: 82 82 82; - --color-typography-800: 64 64 64; - --color-typography-900: 38 38 39; - --color-typography-950: 23 23 23; - --color-outline-0: 253 254 254; - --color-outline-50: 243 243 243; - --color-outline-100: 230 230 230; - --color-outline-200: 221 220 219; - --color-outline-300: 211 211 211; - --color-outline-400: 165 163 163; - --color-outline-500: 140 141 141; - --color-outline-600: 115 116 116; - --color-outline-700: 83 82 82; - --color-outline-800: 65 65 65; - --color-outline-900: 39 38 36; - --color-outline-950: 26 23 23; - --color-background-0: 255 255 255; - --color-background-50: 246 246 246; - --color-background-100: 242 241 241; - --color-background-200: 220 219 219; - --color-background-300: 213 212 212; - --color-background-400: 162 163 163; - --color-background-500: 142 142 142; - --color-background-600: 116 116 116; - --color-background-700: 83 82 82; - --color-background-800: 65 64 64; - --color-background-900: 39 38 37; - --color-background-950: 24 23 24; - --color-background-error: 254 241 241; - --color-background-warning: 255 244 235; - --color-background-success: 237 252 242; - --color-background-muted: 247 248 247; - --color-background-info: 235 248 254; - --color-indicator-primary: 55 55 55; - --color-indicator-info: 83 153 236; - --color-indicator-error: 185 28 28; - } -} - -/* ─── Tailwind v4 utilities for the legacy gluestack v2 token scales ── */ -@theme inline { - --color-primary-0: rgb(var(--color-primary-0)); - --color-primary-50: rgb(var(--color-primary-50)); - --color-primary-100: rgb(var(--color-primary-100)); - --color-primary-200: rgb(var(--color-primary-200)); - --color-primary-300: rgb(var(--color-primary-300)); - --color-primary-400: rgb(var(--color-primary-400)); - --color-primary-500: rgb(var(--color-primary-500)); - --color-primary-600: rgb(var(--color-primary-600)); - --color-primary-700: rgb(var(--color-primary-700)); - --color-primary-800: rgb(var(--color-primary-800)); - --color-primary-900: rgb(var(--color-primary-900)); - --color-primary-950: rgb(var(--color-primary-950)); - --color-secondary-0: rgb(var(--color-secondary-0)); - --color-secondary-50: rgb(var(--color-secondary-50)); - --color-secondary-100: rgb(var(--color-secondary-100)); - --color-secondary-200: rgb(var(--color-secondary-200)); - --color-secondary-300: rgb(var(--color-secondary-300)); - --color-secondary-400: rgb(var(--color-secondary-400)); - --color-secondary-500: rgb(var(--color-secondary-500)); - --color-secondary-600: rgb(var(--color-secondary-600)); - --color-secondary-700: rgb(var(--color-secondary-700)); - --color-secondary-800: rgb(var(--color-secondary-800)); - --color-secondary-900: rgb(var(--color-secondary-900)); - --color-secondary-950: rgb(var(--color-secondary-950)); - --color-tertiary-0: rgb(var(--color-tertiary-0)); - --color-tertiary-50: rgb(var(--color-tertiary-50)); - --color-tertiary-100: rgb(var(--color-tertiary-100)); - --color-tertiary-200: rgb(var(--color-tertiary-200)); - --color-tertiary-300: rgb(var(--color-tertiary-300)); - --color-tertiary-400: rgb(var(--color-tertiary-400)); - --color-tertiary-500: rgb(var(--color-tertiary-500)); - --color-tertiary-600: rgb(var(--color-tertiary-600)); - --color-tertiary-700: rgb(var(--color-tertiary-700)); - --color-tertiary-800: rgb(var(--color-tertiary-800)); - --color-tertiary-900: rgb(var(--color-tertiary-900)); - --color-tertiary-950: rgb(var(--color-tertiary-950)); - --color-error-0: rgb(var(--color-error-0)); - --color-error-50: rgb(var(--color-error-50)); - --color-error-100: rgb(var(--color-error-100)); - --color-error-200: rgb(var(--color-error-200)); - --color-error-300: rgb(var(--color-error-300)); - --color-error-400: rgb(var(--color-error-400)); - --color-error-500: rgb(var(--color-error-500)); - --color-error-600: rgb(var(--color-error-600)); - --color-error-700: rgb(var(--color-error-700)); - --color-error-800: rgb(var(--color-error-800)); - --color-error-900: rgb(var(--color-error-900)); - --color-error-950: rgb(var(--color-error-950)); - --color-success-0: rgb(var(--color-success-0)); - --color-success-50: rgb(var(--color-success-50)); - --color-success-100: rgb(var(--color-success-100)); - --color-success-200: rgb(var(--color-success-200)); - --color-success-300: rgb(var(--color-success-300)); - --color-success-400: rgb(var(--color-success-400)); - --color-success-500: rgb(var(--color-success-500)); - --color-success-600: rgb(var(--color-success-600)); - --color-success-700: rgb(var(--color-success-700)); - --color-success-800: rgb(var(--color-success-800)); - --color-success-900: rgb(var(--color-success-900)); - --color-success-950: rgb(var(--color-success-950)); - --color-warning-0: rgb(var(--color-warning-0)); - --color-warning-50: rgb(var(--color-warning-50)); - --color-warning-100: rgb(var(--color-warning-100)); - --color-warning-200: rgb(var(--color-warning-200)); - --color-warning-300: rgb(var(--color-warning-300)); - --color-warning-400: rgb(var(--color-warning-400)); - --color-warning-500: rgb(var(--color-warning-500)); - --color-warning-600: rgb(var(--color-warning-600)); - --color-warning-700: rgb(var(--color-warning-700)); - --color-warning-800: rgb(var(--color-warning-800)); - --color-warning-900: rgb(var(--color-warning-900)); - --color-warning-950: rgb(var(--color-warning-950)); - --color-info-0: rgb(var(--color-info-0)); - --color-info-50: rgb(var(--color-info-50)); - --color-info-100: rgb(var(--color-info-100)); - --color-info-200: rgb(var(--color-info-200)); - --color-info-300: rgb(var(--color-info-300)); - --color-info-400: rgb(var(--color-info-400)); - --color-info-500: rgb(var(--color-info-500)); - --color-info-600: rgb(var(--color-info-600)); - --color-info-700: rgb(var(--color-info-700)); - --color-info-800: rgb(var(--color-info-800)); - --color-info-900: rgb(var(--color-info-900)); - --color-info-950: rgb(var(--color-info-950)); - --color-typography-0: rgb(var(--color-typography-0)); - --color-typography-50: rgb(var(--color-typography-50)); - --color-typography-100: rgb(var(--color-typography-100)); - --color-typography-200: rgb(var(--color-typography-200)); - --color-typography-300: rgb(var(--color-typography-300)); - --color-typography-400: rgb(var(--color-typography-400)); - --color-typography-500: rgb(var(--color-typography-500)); - --color-typography-600: rgb(var(--color-typography-600)); - --color-typography-700: rgb(var(--color-typography-700)); - --color-typography-800: rgb(var(--color-typography-800)); - --color-typography-900: rgb(var(--color-typography-900)); - --color-typography-950: rgb(var(--color-typography-950)); - --color-outline-0: rgb(var(--color-outline-0)); - --color-outline-50: rgb(var(--color-outline-50)); - --color-outline-100: rgb(var(--color-outline-100)); - --color-outline-200: rgb(var(--color-outline-200)); - --color-outline-300: rgb(var(--color-outline-300)); - --color-outline-400: rgb(var(--color-outline-400)); - --color-outline-500: rgb(var(--color-outline-500)); - --color-outline-600: rgb(var(--color-outline-600)); - --color-outline-700: rgb(var(--color-outline-700)); - --color-outline-800: rgb(var(--color-outline-800)); - --color-outline-900: rgb(var(--color-outline-900)); - --color-outline-950: rgb(var(--color-outline-950)); - --color-background-0: rgb(var(--color-background-0)); - --color-background-50: rgb(var(--color-background-50)); - --color-background-100: rgb(var(--color-background-100)); - --color-background-200: rgb(var(--color-background-200)); - --color-background-300: rgb(var(--color-background-300)); - --color-background-400: rgb(var(--color-background-400)); - --color-background-500: rgb(var(--color-background-500)); - --color-background-600: rgb(var(--color-background-600)); - --color-background-700: rgb(var(--color-background-700)); - --color-background-800: rgb(var(--color-background-800)); - --color-background-900: rgb(var(--color-background-900)); - --color-background-950: rgb(var(--color-background-950)); - --color-background-error: rgb(var(--color-background-error)); - --color-background-warning: rgb(var(--color-background-warning)); - --color-background-success: rgb(var(--color-background-success)); - --color-background-muted: rgb(var(--color-background-muted)); - --color-background-info: rgb(var(--color-background-info)); - --color-indicator-primary: rgb(var(--color-indicator-primary)); - --color-indicator-info: rgb(var(--color-indicator-info)); - --color-indicator-error: rgb(var(--color-indicator-error)); -} - -/* ─── Non-color theme extensions (ported from tailwind.config.js) ── */ -@theme { - --font-roboto: Roboto, sans-serif; - --font-weight-extrablack: 950; - --text-2xs: 10px; - --color-typography-white: #ffffff; - --color-typography-gray: #d4d4d4; - --color-typography-black: #181718; - --color-background-light: #fbfbfb; - --color-background-dark: #181719; - --shadow-hard-1: -2px 2px 8px 0px rgba(38, 38, 38, 0.2); - --shadow-hard-2: 0px 3px 10px 0px rgba(38, 38, 38, 0.2); - --shadow-hard-3: 2px 2px 8px 0px rgba(38, 38, 38, 0.2); - --shadow-hard-4: 0px -3px 10px 0px rgba(38, 38, 38, 0.2); - --shadow-hard-5: 0px 2px 10px 0px rgba(38, 38, 38, 0.1); - --shadow-soft-1: 0px 0px 10px rgba(38, 38, 38, 0.1); - --shadow-soft-2: 0px 0px 20px rgba(38, 38, 38, 0.2); - --shadow-soft-3: 0px 0px 30px rgba(38, 38, 38, 0.1); - --shadow-soft-4: 0px 0px 40px rgba(38, 38, 38, 0.1); -} - -/* Accessible focus styles for keyboard navigation */ -.web-input-accessible:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 2px; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} +@import './theme-tokens.css'; + +/* Native dark mode: the colour-scheme media query, which react-native-css drives from Appearance. + GluestackUIProvider calls Appearance.setColorScheme() for explicit light/dark choices, so this + covers a user-selected theme as well as the system one, and it is the same query the tokens flip on. + + There is deliberately no theme class on native. The provider used to put one on the View wrapping + the whole app, and any className on that wrapper stops every native ScrollView beneath it from + starting a pan: a drag on plain content did nothing app-wide, and only a Pressable handing the + responder off could scroll anything. Keep this selector-free — react-native-css silently drops + rules whose selectors it cannot evaluate, which takes dark mode with them. */ +@custom-variant dark (@media (prefers-color-scheme: dark)); diff --git a/global.web.css b/global.web.css new file mode 100644 index 0000000..cf123fb --- /dev/null +++ b/global.web.css @@ -0,0 +1,12 @@ +@import 'tailwindcss/theme.css' layer(theme); +@import 'tailwindcss/preflight.css' layer(base); +@import 'tailwindcss/utilities.css'; +@import 'nativewind/theme'; + +@import './theme-tokens.css'; + +/* Web dark mode: the .dark class GluestackUIProvider puts on (see index.web.tsx). It is + always present — explicit modes set .dark/.light directly, and system mode sets one from the + media query — so the class alone is authoritative. Matching prefers-color-scheme here as well + would make an explicit Light choice render dark utilities on a dark-themed OS. */ +@custom-variant dark (&:where(.dark, .dark *)); diff --git a/src/app/(app)/chat.tsx b/src/app/(app)/chat.tsx index 067ba38..3c4dafd 100644 --- a/src/app/(app)/chat.tsx +++ b/src/app/(app)/chat.tsx @@ -35,7 +35,12 @@ function ChannelRow({ channel, onPress }: { channel: ChatChannelResultData; onPr ); } - const isIncident = channel.ChannelType === ChatChannelType.Incident || channel.ChannelType === ChatChannelType.IncidentLane || channel.ChannelType === ChatChannelType.IncidentCommand; + const isIncident = + channel.ChannelType === ChatChannelType.Incident || + channel.ChannelType === ChatChannelType.IncidentLane || + channel.ChannelType === ChatChannelType.IncidentCommand || + channel.ChannelType === ChatChannelType.IncidentLeads || + channel.ChannelType === ChatChannelType.IncidentDispatch; const Icon = channel.ChannelType === ChatChannelType.Chatbot ? Sparkles : isIncident ? Network : Users; return ( diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 9d70d37..8ab7c9f 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -1,5 +1,5 @@ -// Import global CSS file -import '../../global.css'; +// Import global CSS (platform-specific entry: global.css on native, global.web.css on web) +import '../lib/theme-styles'; import '../lib/i18n'; import { Env } from '@env'; diff --git a/src/components/chat/__tests__/chat-utils.test.ts b/src/components/chat/__tests__/chat-utils.test.ts index 248d1ce..61fb252 100644 --- a/src/components/chat/__tests__/chat-utils.test.ts +++ b/src/components/chat/__tests__/chat-utils.test.ts @@ -3,7 +3,7 @@ import { type TFunction } from 'i18next'; import { ChatChannelType, type ChatChannelResultData } from '@/models/v4/chat'; -import { copyToClipboard, getChannelDisplayName, getImageMimeType, hasLink, linkifySegments } from '../chat-utils'; +import { copyToClipboard, getChannelDisplayName, getImageMimeType, groupChannels, hasLink, linkifySegments } from '../chat-utils'; jest.mock('expo-clipboard', () => ({ setStringAsync: jest.fn() })); @@ -34,6 +34,31 @@ describe('chat-utils', () => { }); }); + describe('groupChannels', () => { + it('buckets every incident-scoped channel type into the incidents section', () => { + const grouped = groupChannels([ + buildChannel({ ChatChannelId: 'a', ChannelType: ChatChannelType.Incident }), + buildChannel({ ChatChannelId: 'b', ChannelType: ChatChannelType.IncidentLane }), + buildChannel({ ChatChannelId: 'c', ChannelType: ChatChannelType.IncidentCommand }), + buildChannel({ ChatChannelId: 'd', ChannelType: ChatChannelType.IncidentLeads }), + buildChannel({ ChatChannelId: 'e', ChannelType: ChatChannelType.IncidentDispatch }), + ]); + expect(grouped.incidents.map((c) => c.ChatChannelId).sort()).toEqual(['a', 'b', 'c', 'd', 'e']); + expect(grouped.channels).toHaveLength(0); + }); + + it('buckets the unit dispatch line into the channels section', () => { + const grouped = groupChannels([buildChannel({ ChatChannelId: 'ud', ChannelType: ChatChannelType.UnitDispatch })]); + expect(grouped.channels.map((c) => c.ChatChannelId)).toEqual(['ud']); + expect(grouped.incidents).toHaveLength(0); + }); + + it('skips archived channels', () => { + const grouped = groupChannels([buildChannel({ ChatChannelId: 'x', ChannelType: ChatChannelType.Incident, IsArchived: true })]); + expect(grouped.incidents).toHaveLength(0); + }); + }); + describe('getImageMimeType', () => { it('prefers the picker asset mimeType when available', () => { expect(getImageMimeType('file:///photos/photo.jpg', 'image/png')).toBe('image/png'); diff --git a/src/components/chat/chat-utils.ts b/src/components/chat/chat-utils.ts index 3d0476b..f2a939e 100644 --- a/src/components/chat/chat-utils.ts +++ b/src/components/chat/chat-utils.ts @@ -23,6 +23,8 @@ export function groupChannels(channels: ChatChannelResultData[]): GroupedChannel case ChatChannelType.Incident: case ChatChannelType.IncidentLane: case ChatChannelType.IncidentCommand: + case ChatChannelType.IncidentLeads: + case ChatChannelType.IncidentDispatch: grouped.incidents.push(channel); break; case ChatChannelType.Chatbot: diff --git a/src/components/ui/gluestack-ui-provider/index.tsx b/src/components/ui/gluestack-ui-provider/index.tsx index 6b283a4..7ded746 100644 --- a/src/components/ui/gluestack-ui-provider/index.tsx +++ b/src/components/ui/gluestack-ui-provider/index.tsx @@ -2,23 +2,23 @@ import { OverlayProvider } from '@gluestack-ui/core/overlay/creator'; import { ToastProvider } from '@gluestack-ui/core/toast/creator'; import React, { useEffect } from 'react'; -import { Appearance, useColorScheme, View, type ViewProps } from 'react-native'; +import { Appearance, View, type ViewProps } from 'react-native'; export type ModeType = 'light' | 'dark' | 'system'; export function GluestackUIProvider({ mode = 'light', ...props }: { mode?: ModeType; children?: React.ReactNode; style?: ViewProps['style'] }) { - // Tokens (--color-*) flip through the prefers-color-scheme media query, - // which react-native-css drives from Appearance. The className wrapper - // drives the class-based `dark:` variant (see @custom-variant in global.css). - const osScheme = useColorScheme(); - const resolvedScheme: 'light' | 'dark' = mode === 'system' ? (osScheme === 'dark' ? 'dark' : 'light') : mode; - + // Both the tokens (--color-*) and the `dark:` variant flip through the prefers-color-scheme media + // query, which react-native-css drives from Appearance — hence the override below. useEffect(() => { Appearance.setColorScheme(mode === 'system' ? null : mode); }, [mode]); + // This View deliberately carries NO className. It wraps the entire app, and react-native-css wraps + // any classed component in an element of its own; with a class here every native ScrollView beneath + // it stopped responding to a plain drag app-wide — only a Pressable taking the JS responder could + // scroll anything. Style it inline if it ever needs styling, and see the dark variant in global.css. return ( - + {props.children} diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx index a9fa059..9ecd718 100644 --- a/src/components/ui/input/index.tsx +++ b/src/components/ui/input/index.tsx @@ -4,10 +4,44 @@ import { createInput } from '@gluestack-ui/core/input/creator'; import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import { styled } from 'nativewind'; import React from 'react'; -import { Pressable, TextInput, View } from 'react-native'; +import { Platform, Pressable, TextInput, View } from 'react-native'; const SCOPE = 'INPUT'; +/** + * Android field metrics, applied from JS because the class layer cannot express them correctly. + * + * Measured on device, each case a real Input/InputField: + * - the class `h-full` (height: 100%) resolves taller than the fixed-height parent on Android, and + * the parent's overflow-hidden then clips the top of the glyphs; + * - an explicit pixel height matching the parent renders correctly; + * - overriding only the lineHeight, at either the class or the style layer, does not help; + * - lineHeight 0 (what iOS uses) hides Android text completely, and a later `undefined` does not + * clear the value the size class sets. + * + * So Android gets a concrete height plus a lineHeight near the font size, and drops the extra font + * padding. iOS keeps the zero lineHeight that upstream applied through `ios:leading-[0px]`; that class + * is gone from the base style so the value can be chosen per platform here. + */ +const ANDROID_FIELD_METRICS: Record = { + sm: { height: 36, lineHeight: 18 }, + md: { height: 40, lineHeight: 20 }, + lg: { height: 44, lineHeight: 22 }, + xl: { height: 48, lineHeight: 25 }, +}; + +const useTextFieldVerticalFix = (size: string | undefined) => + React.useMemo(() => { + if (Platform.OS === 'ios') { + return { lineHeight: 0 } as const; + } + if (Platform.OS === 'android') { + const metrics = ANDROID_FIELD_METRICS[size ?? 'md'] ?? ANDROID_FIELD_METRICS.md; + return { height: metrics.height, lineHeight: metrics.lineHeight, includeFontPadding: false, textAlignVertical: 'center' } as const; + } + return undefined; + }, [size]); + const StyledUIIcon = styled(UIIcon, { className: 'style' }); const UIInput = createInput({ @@ -60,7 +94,7 @@ const inputSlotStyle = tva({ }); const inputFieldStyle = tva({ - base: 'flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full ios:leading-[0px] web:cursor-text web:data-[disabled=true]:cursor-not-allowed', + base: 'flex-1 text-typography-900 py-0 px-3 placeholder:text-typography-500 h-full web:cursor-text web:data-[disabled=true]:cursor-not-allowed', parentVariants: { variant: { @@ -136,8 +170,9 @@ const InputSlot = React.forwardRef, IInp type IInputFieldProps = React.ComponentProps & VariantProps & { className?: string }; -const InputField = React.forwardRef, IInputFieldProps>(function InputField({ className, ...props }, ref) { +const InputField = React.forwardRef, IInputFieldProps>(function InputField({ className, style, ...props }, ref) { const { variant: parentVariant, size: parentSize } = useStyleContext(SCOPE); + const verticalFix = useTextFieldVerticalFix(parentSize); return ( , II }, class: className, })} + style={[verticalFix, style]} /> ); }); diff --git a/src/components/ui/select/index.tsx b/src/components/ui/select/index.tsx index d8dae42..a909c88 100644 --- a/src/components/ui/select/index.tsx +++ b/src/components/ui/select/index.tsx @@ -7,7 +7,7 @@ import { tva } from '@gluestack-ui/utils/nativewind-utils'; import { useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import { styled } from 'nativewind'; import React from 'react'; -import { Pressable, TextInput, View } from 'react-native'; +import { Platform, Pressable, TextInput, View } from 'react-native'; import { Actionsheet, @@ -66,8 +66,33 @@ const selectTriggerStyle = tva({ }, }); +/** + * Mirrors the Input component: on Android the class `h-full` resolves taller than the fixed-height + * trigger and the trigger's overflow-hidden clips the top of the value, so the field gets a concrete + * height and a lineHeight near the font size instead. iOS keeps the zero lineHeight that + * `ios:leading-[0px]` used to supply. + */ +const ANDROID_FIELD_METRICS: Record = { + sm: { height: 36, lineHeight: 18 }, + md: { height: 40, lineHeight: 20 }, + lg: { height: 44, lineHeight: 22 }, + xl: { height: 48, lineHeight: 25 }, +}; + +const useTextFieldVerticalFix = (size: string | undefined) => + React.useMemo(() => { + if (Platform.OS === 'ios') { + return { lineHeight: 0 } as const; + } + if (Platform.OS === 'android') { + const metrics = ANDROID_FIELD_METRICS[size ?? 'md'] ?? ANDROID_FIELD_METRICS.md; + return { height: metrics.height, lineHeight: metrics.lineHeight, includeFontPadding: false, textAlignVertical: 'center' } as const; + } + return undefined; + }, [size]); + const selectInputStyle = tva({ - base: 'py-auto px-3 placeholder:text-typography-500 web:w-full h-full text-typography-900 pointer-events-none web:outline-none ios:leading-[0px]', + base: 'py-auto px-3 placeholder:text-typography-500 web:w-full h-full text-typography-900 pointer-events-none web:outline-none', parentVariants: { size: { xl: 'text-xl', @@ -146,8 +171,9 @@ const SelectTrigger = React.forwardRef & React.ComponentProps & { className?: string }; -const SelectInput = React.forwardRef, ISelectInputProps>(function SelectInput({ className, ...props }, ref) { +const SelectInput = React.forwardRef, ISelectInputProps>(function SelectInput({ className, style, ...props }, ref) { const { size: parentSize, variant: parentVariant } = useStyleContext(); + const verticalFix = useTextFieldVerticalFix(parentSize); return ( , })} ref={ref} {...props} + style={[verticalFix, style]} /> ); }); diff --git a/src/hooks/use-signalr-lifecycle.ts b/src/hooks/use-signalr-lifecycle.ts index 06d3534..49ade67 100644 --- a/src/hooks/use-signalr-lifecycle.ts +++ b/src/hooks/use-signalr-lifecycle.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { logger } from '@/lib/logging'; +import { useIncidentCommandStore } from '@/stores/incident-command/store'; import { useSignalRStore } from '@/stores/signalr/signalr-store'; import { useAppLifecycle } from './use-app-lifecycle'; @@ -136,6 +137,16 @@ export function useSignalRLifecycle({ isSignedIn, hasInitialized }: UseSignalRLi }); } }); + + // The hubs replay nothing that was pushed while the app was away, so board changes another + // user made during the gap would stay invisible. Reconnecting restores the feed; this + // backfills what it missed. + if (results[0].status === 'fulfilled') { + const openCallId = useIncidentCommandStore.getState().callId; + if (openCallId) { + useIncidentCommandStore.getState().handleIncidentCommandUpdated(openCallId); + } + } } catch (error) { logger.error({ message: 'Unexpected error during SignalR reconnect on app resume', diff --git a/src/lib/theme-styles.ts b/src/lib/theme-styles.ts new file mode 100644 index 0000000..7a80cba --- /dev/null +++ b/src/lib/theme-styles.ts @@ -0,0 +1,9 @@ +/** + * Native stylesheet entry. Pairs with theme-styles.web.ts, which loads global.web.css instead. + * + * The two entries exist because the `dark:` variant cannot be shared: native drives it from the + * colour-scheme media query (it has no theme class, and adding one to the app-wide wrapper View + * breaks native scrolling), while web drives it from the .dark class on . Tokens are shared + * via theme-tokens.css. + */ +import '../../global.css'; diff --git a/src/lib/theme-styles.web.ts b/src/lib/theme-styles.web.ts new file mode 100644 index 0000000..3f1d528 --- /dev/null +++ b/src/lib/theme-styles.web.ts @@ -0,0 +1,7 @@ +/** + * Web stylesheet entry. Pairs with theme-styles.ts, which loads global.css on native. + * + * Web keys the `dark:` variant off the .dark class GluestackUIProvider maintains on , so an + * explicit Light choice stays light on a dark-themed OS. + */ +import '../../global.web.css'; diff --git a/src/models/v4/chat/chatEnums.ts b/src/models/v4/chat/chatEnums.ts index 11916f0..ce9075b 100644 --- a/src/models/v4/chat/chatEnums.ts +++ b/src/models/v4/chat/chatEnums.ts @@ -17,6 +17,8 @@ export enum ChatChannelType { IncidentLeads = 9, /** The incident's line to the dispatch desk: everyone on the incident, plus every authorized dispatcher. */ IncidentDispatch = 10, + /** A unit's standing line to the dispatch desk: the unit identity plus every authorized dispatcher. Department-wide, not call-scoped. */ + UnitDispatch = 11, } /** Message type (ChatMessageResultData.MessageType). */ diff --git a/src/stores/signalr/signalr-store.ts b/src/stores/signalr/signalr-store.ts index 4f5ce2e..bc46e0e 100644 --- a/src/stores/signalr/signalr-store.ts +++ b/src/stores/signalr/signalr-store.ts @@ -219,6 +219,22 @@ let updateHubHandlers: EventHandlers = { onConnected: null, }; +/** + * Lifecycle listeners for the update hub (reconnect/disconnect), kept apart from the method handlers + * so they can be torn down without touching the event subscriptions. + */ +const updateHubLifecycleHandlers: Record void) | null> = {}; + +function unregisterUpdateHubLifecycleHandlers(): void { + Object.keys(updateHubLifecycleHandlers).forEach((event) => { + const handler = updateHubLifecycleHandlers[event]; + if (handler) { + signalRService.off(event, handler); + updateHubLifecycleHandlers[event] = null; + } + }); +} + /** * Helper function to unregister all update hub event handlers */ @@ -526,6 +542,47 @@ export const useSignalRStore = create((set, get) => ({ }; signalRService.on('onConnected', updateHubHandlers.onConnected); + /** + * An automatic reconnect gets a new connection id, so the department group joined above is gone + * with the old one — without re-announcing, the app goes quiet and stops seeing other users' + * board changes until it is backgrounded and resumed. The hub also replays nothing from the + * outage, so the open board is reloaded once the group is rejoined. + */ + unregisterUpdateHubLifecycleHandlers(); + + const updateReconnected = `${SignalRService.HUB_RECONNECTED_EVENT}:${Env.CHANNEL_HUB_NAME}`; + const onUpdateReconnected = () => { + const reconnectDepartmentId = Number(securityStore.getState().rights?.DepartmentId ?? '0'); + if (!Number.isFinite(reconnectDepartmentId)) { + return; + } + signalRService + .invoke(Env.CHANNEL_HUB_NAME, 'connect', reconnectDepartmentId) + .then(() => { + set({ isUpdateHubConnected: true, error: null }); + logger.info({ message: 'Re-announced to update hub after reconnect; reloading incident command', context: { departmentId: reconnectDepartmentId } }); + // Lazy import to avoid circular dependency + const { useIncidentCommandStore } = require('../incident-command/store'); + const openCallId = useIncidentCommandStore.getState().callId; + if (openCallId) { + useIncidentCommandStore.getState().handleIncidentCommandUpdated(openCallId); + } + }) + .catch((error) => { + logger.warn({ message: 'Failed to re-announce to update hub after reconnect', context: { error } }); + }); + }; + updateHubLifecycleHandlers[updateReconnected] = onUpdateReconnected; + signalRService.on(updateReconnected, onUpdateReconnected); + + const updateDisconnected = `${SignalRService.HUB_DISCONNECTED_EVENT}:${Env.CHANNEL_HUB_NAME}`; + const onUpdateDisconnected = () => { + // Clearing the flag is what lets connectUpdateHub rebuild the session later. + set({ isUpdateHubConnected: false }); + }; + updateHubLifecycleHandlers[updateDisconnected] = onUpdateDisconnected; + signalRService.on(updateDisconnected, onUpdateDisconnected); + // Note: Connection state monitoring is now handled internally by the SignalR service // The service properly tracks connection state and will emit events through the registered handlers // We don't need to access internal connection objects anymore @@ -547,6 +604,7 @@ export const useSignalRStore = create((set, get) => ({ try { // Unregister all handlers BEFORE disconnecting to prevent memory leaks unregisterUpdateHubHandlers(); + unregisterUpdateHubLifecycleHandlers(); await signalRService.disconnectFromHub(Env.CHANNEL_HUB_NAME); set({ isUpdateHubConnected: false, lastUpdateMessage: null }); diff --git a/theme-tokens.css b/theme-tokens.css new file mode 100644 index 0000000..29df8da --- /dev/null +++ b/theme-tokens.css @@ -0,0 +1,700 @@ +/* Design tokens and keyframes shared by both platform entry points. + + Imported by global.css (native) and global.web.css (web). Those two exist only because the dark + variant has to differ per platform — see the comments in each. Everything else lives here so the + token set never drifts between them. */ + +/* ─── Design tokens (light / dark) ─────────────────────────────── + 1. :root — light defaults (all platforms) + 2. @media dark :root — dark defaults; on native this follows + Appearance.getColorScheme() (the provider + sets an override for explicit modes). + 3. :root.dark / :root.light — web-only class override, beats the media + query within the same layer. */ +@layer theme { + :root { + --color-primary-0: 248 250 252; + --color-primary-50: 241 245 249; + --color-primary-100: 226 232 240; + --color-primary-200: 203 213 225; + --color-primary-300: 148 163 184; + --color-primary-400: 100 116 139; + --color-primary-500: 71 85 105; + --color-primary-600: 51 65 85; + --color-primary-700: 45 55 72; + --color-primary-800: 39 49 66; + --color-primary-900: 30 41 59; + --color-primary-950: 15 23 42; + --color-secondary-0: 254 255 255; + --color-secondary-50: 241 242 242; + --color-secondary-100: 231 232 232; + --color-secondary-200: 219 219 219; + --color-secondary-300: 175 176 176; + --color-secondary-400: 114 115 115; + --color-secondary-500: 94 95 95; + --color-secondary-600: 81 82 82; + --color-secondary-700: 63 64 64; + --color-secondary-800: 39 38 38; + --color-secondary-900: 24 23 23; + --color-secondary-950: 11 12 12; + --color-tertiary-0: 255 250 245; + --color-tertiary-50: 255 242 229; + --color-tertiary-100: 255 233 213; + --color-tertiary-200: 254 209 170; + --color-tertiary-300: 253 180 116; + --color-tertiary-400: 251 157 75; + --color-tertiary-500: 231 129 40; + --color-tertiary-600: 215 117 31; + --color-tertiary-700: 180 98 26; + --color-tertiary-800: 130 73 23; + --color-tertiary-900: 108 61 19; + --color-tertiary-950: 84 49 18; + --color-error-0: 254 233 233; + --color-error-50: 254 226 226; + --color-error-100: 254 202 202; + --color-error-200: 252 165 165; + --color-error-300: 248 113 113; + --color-error-400: 239 68 68; + --color-error-500: 230 53 53; + --color-error-600: 220 38 38; + --color-error-700: 185 28 28; + --color-error-800: 153 27 27; + --color-error-900: 127 29 29; + --color-error-950: 83 19 19; + --color-success-0: 228 255 244; + --color-success-50: 202 255 232; + --color-success-100: 162 241 192; + --color-success-200: 132 211 162; + --color-success-300: 102 181 132; + --color-success-400: 72 151 102; + --color-success-500: 52 131 82; + --color-success-600: 42 121 72; + --color-success-700: 32 111 62; + --color-success-800: 22 101 52; + --color-success-900: 20 83 45; + --color-success-950: 27 50 36; + --color-warning-0: 255 253 251; + --color-warning-50: 255 249 245; + --color-warning-100: 255 231 213; + --color-warning-200: 254 205 170; + --color-warning-300: 253 173 116; + --color-warning-400: 251 149 75; + --color-warning-500: 231 120 40; + --color-warning-600: 215 108 31; + --color-warning-700: 180 90 26; + --color-warning-800: 130 68 23; + --color-warning-900: 108 56 19; + --color-warning-950: 84 45 18; + --color-info-0: 236 248 254; + --color-info-50: 199 235 252; + --color-info-100: 162 221 250; + --color-info-200: 124 207 248; + --color-info-300: 87 194 246; + --color-info-400: 50 180 244; + --color-info-500: 13 166 242; + --color-info-600: 11 141 205; + --color-info-700: 9 115 168; + --color-info-800: 7 90 131; + --color-info-900: 5 64 93; + --color-info-950: 3 38 56; + --color-typography-0: 254 254 255; + --color-typography-50: 245 245 245; + --color-typography-100: 229 229 229; + --color-typography-200: 219 219 220; + --color-typography-300: 212 212 212; + --color-typography-400: 163 163 163; + --color-typography-500: 140 140 140; + --color-typography-600: 115 115 115; + --color-typography-700: 82 82 82; + --color-typography-800: 64 64 64; + --color-typography-900: 38 38 39; + --color-typography-950: 23 23 23; + --color-outline-0: 253 254 254; + --color-outline-50: 243 243 243; + --color-outline-100: 230 230 230; + --color-outline-200: 221 220 219; + --color-outline-300: 211 211 211; + --color-outline-400: 165 163 163; + --color-outline-500: 140 141 141; + --color-outline-600: 115 116 116; + --color-outline-700: 83 82 82; + --color-outline-800: 65 65 65; + --color-outline-900: 39 38 36; + --color-outline-950: 26 23 23; + --color-background-0: 255 255 255; + --color-background-50: 246 246 246; + --color-background-100: 242 241 241; + --color-background-200: 220 219 219; + --color-background-300: 213 212 212; + --color-background-400: 162 163 163; + --color-background-500: 142 142 142; + --color-background-600: 116 116 116; + --color-background-700: 83 82 82; + --color-background-800: 65 64 64; + --color-background-900: 39 38 37; + --color-background-950: 24 23 24; + --color-background-error: 254 241 241; + --color-background-warning: 255 244 235; + --color-background-success: 237 252 242; + --color-background-muted: 247 248 247; + --color-background-info: 235 248 254; + --color-indicator-primary: 55 55 55; + --color-indicator-info: 83 153 236; + --color-indicator-error: 185 28 28; + } + + @media (prefers-color-scheme: dark) { + :root { + --color-primary-0: 15 23 42; + --color-primary-50: 30 41 59; + --color-primary-100: 39 49 66; + --color-primary-200: 45 55 72; + --color-primary-300: 51 65 85; + --color-primary-400: 71 85 105; + --color-primary-500: 100 116 139; + --color-primary-600: 71 85 105; + --color-primary-700: 51 65 85; + --color-primary-800: 45 55 72; + --color-primary-900: 39 49 66; + --color-primary-950: 30 41 59; + --color-secondary-0: 11 12 12; + --color-secondary-50: 24 23 23; + --color-secondary-100: 39 38 38; + --color-secondary-200: 63 64 64; + --color-secondary-300: 81 82 82; + --color-secondary-400: 94 95 95; + --color-secondary-500: 114 115 115; + --color-secondary-600: 175 176 176; + --color-secondary-700: 219 219 219; + --color-secondary-800: 231 232 232; + --color-secondary-900: 241 242 242; + --color-secondary-950: 254 255 255; + --color-tertiary-0: 84 49 18; + --color-tertiary-50: 108 61 19; + --color-tertiary-100: 130 73 23; + --color-tertiary-200: 180 98 26; + --color-tertiary-300: 215 117 31; + --color-tertiary-400: 231 129 40; + --color-tertiary-500: 251 157 75; + --color-tertiary-600: 253 180 116; + --color-tertiary-700: 254 209 170; + --color-tertiary-800: 255 233 213; + --color-tertiary-900: 255 242 229; + --color-tertiary-950: 255 250 245; + --color-error-0: 83 19 19; + --color-error-50: 127 29 29; + --color-error-100: 153 27 27; + --color-error-200: 185 28 28; + --color-error-300: 220 38 38; + --color-error-400: 230 53 53; + --color-error-500: 239 68 68; + --color-error-600: 248 113 113; + --color-error-700: 252 165 165; + --color-error-800: 254 202 202; + --color-error-900: 254 226 226; + --color-error-950: 254 233 233; + --color-success-0: 27 50 36; + --color-success-50: 20 83 45; + --color-success-100: 22 101 52; + --color-success-200: 32 111 62; + --color-success-300: 42 121 72; + --color-success-400: 52 131 82; + --color-success-500: 72 151 102; + --color-success-600: 102 181 132; + --color-success-700: 132 211 162; + --color-success-800: 162 241 192; + --color-success-900: 202 255 232; + --color-success-950: 228 255 244; + --color-warning-0: 84 45 18; + --color-warning-50: 108 56 19; + --color-warning-100: 130 68 23; + --color-warning-200: 180 90 26; + --color-warning-300: 215 108 31; + --color-warning-400: 231 120 40; + --color-warning-500: 251 149 75; + --color-warning-600: 253 173 116; + --color-warning-700: 254 205 170; + --color-warning-800: 255 231 213; + --color-warning-900: 255 249 245; + --color-warning-950: 255 253 251; + --color-info-0: 3 38 56; + --color-info-50: 5 64 93; + --color-info-100: 7 90 131; + --color-info-200: 9 115 168; + --color-info-300: 11 141 205; + --color-info-400: 13 166 242; + --color-info-500: 50 180 244; + --color-info-600: 87 194 246; + --color-info-700: 124 207 248; + --color-info-800: 162 221 250; + --color-info-900: 199 235 252; + --color-info-950: 236 248 254; + --color-typography-0: 23 23 23; + --color-typography-50: 38 38 39; + --color-typography-100: 64 64 64; + --color-typography-200: 82 82 82; + --color-typography-300: 115 115 115; + --color-typography-400: 140 140 140; + --color-typography-500: 163 163 163; + --color-typography-600: 212 212 212; + --color-typography-700: 219 219 220; + --color-typography-800: 229 229 229; + --color-typography-900: 245 245 245; + --color-typography-950: 254 254 255; + --color-outline-0: 26 23 23; + --color-outline-50: 39 38 36; + --color-outline-100: 65 65 65; + --color-outline-200: 83 82 82; + --color-outline-300: 115 116 116; + --color-outline-400: 140 141 141; + --color-outline-500: 165 163 163; + --color-outline-600: 211 211 211; + --color-outline-700: 221 220 219; + --color-outline-800: 230 230 230; + --color-outline-900: 243 243 243; + --color-outline-950: 253 254 254; + --color-background-0: 18 18 18; + --color-background-50: 39 38 37; + --color-background-100: 65 64 64; + --color-background-200: 83 82 82; + --color-background-300: 116 116 116; + --color-background-400: 142 142 142; + --color-background-500: 162 163 163; + --color-background-600: 213 212 212; + --color-background-700: 220 219 219; + --color-background-800: 242 241 241; + --color-background-900: 246 246 246; + --color-background-950: 254 254 254; + --color-background-error: 66 43 43; + --color-background-warning: 65 47 35; + --color-background-success: 28 43 33; + --color-background-muted: 51 51 51; + --color-background-info: 26 40 46; + --color-indicator-primary: 247 247 247; + --color-indicator-info: 161 199 245; + --color-indicator-error: 232 70 69; + } + } + + :root.dark { + --color-primary-0: 15 23 42; + --color-primary-50: 30 41 59; + --color-primary-100: 39 49 66; + --color-primary-200: 45 55 72; + --color-primary-300: 51 65 85; + --color-primary-400: 71 85 105; + --color-primary-500: 100 116 139; + --color-primary-600: 71 85 105; + --color-primary-700: 51 65 85; + --color-primary-800: 45 55 72; + --color-primary-900: 39 49 66; + --color-primary-950: 30 41 59; + --color-secondary-0: 11 12 12; + --color-secondary-50: 24 23 23; + --color-secondary-100: 39 38 38; + --color-secondary-200: 63 64 64; + --color-secondary-300: 81 82 82; + --color-secondary-400: 94 95 95; + --color-secondary-500: 114 115 115; + --color-secondary-600: 175 176 176; + --color-secondary-700: 219 219 219; + --color-secondary-800: 231 232 232; + --color-secondary-900: 241 242 242; + --color-secondary-950: 254 255 255; + --color-tertiary-0: 84 49 18; + --color-tertiary-50: 108 61 19; + --color-tertiary-100: 130 73 23; + --color-tertiary-200: 180 98 26; + --color-tertiary-300: 215 117 31; + --color-tertiary-400: 231 129 40; + --color-tertiary-500: 251 157 75; + --color-tertiary-600: 253 180 116; + --color-tertiary-700: 254 209 170; + --color-tertiary-800: 255 233 213; + --color-tertiary-900: 255 242 229; + --color-tertiary-950: 255 250 245; + --color-error-0: 83 19 19; + --color-error-50: 127 29 29; + --color-error-100: 153 27 27; + --color-error-200: 185 28 28; + --color-error-300: 220 38 38; + --color-error-400: 230 53 53; + --color-error-500: 239 68 68; + --color-error-600: 248 113 113; + --color-error-700: 252 165 165; + --color-error-800: 254 202 202; + --color-error-900: 254 226 226; + --color-error-950: 254 233 233; + --color-success-0: 27 50 36; + --color-success-50: 20 83 45; + --color-success-100: 22 101 52; + --color-success-200: 32 111 62; + --color-success-300: 42 121 72; + --color-success-400: 52 131 82; + --color-success-500: 72 151 102; + --color-success-600: 102 181 132; + --color-success-700: 132 211 162; + --color-success-800: 162 241 192; + --color-success-900: 202 255 232; + --color-success-950: 228 255 244; + --color-warning-0: 84 45 18; + --color-warning-50: 108 56 19; + --color-warning-100: 130 68 23; + --color-warning-200: 180 90 26; + --color-warning-300: 215 108 31; + --color-warning-400: 231 120 40; + --color-warning-500: 251 149 75; + --color-warning-600: 253 173 116; + --color-warning-700: 254 205 170; + --color-warning-800: 255 231 213; + --color-warning-900: 255 249 245; + --color-warning-950: 255 253 251; + --color-info-0: 3 38 56; + --color-info-50: 5 64 93; + --color-info-100: 7 90 131; + --color-info-200: 9 115 168; + --color-info-300: 11 141 205; + --color-info-400: 13 166 242; + --color-info-500: 50 180 244; + --color-info-600: 87 194 246; + --color-info-700: 124 207 248; + --color-info-800: 162 221 250; + --color-info-900: 199 235 252; + --color-info-950: 236 248 254; + --color-typography-0: 23 23 23; + --color-typography-50: 38 38 39; + --color-typography-100: 64 64 64; + --color-typography-200: 82 82 82; + --color-typography-300: 115 115 115; + --color-typography-400: 140 140 140; + --color-typography-500: 163 163 163; + --color-typography-600: 212 212 212; + --color-typography-700: 219 219 220; + --color-typography-800: 229 229 229; + --color-typography-900: 245 245 245; + --color-typography-950: 254 254 255; + --color-outline-0: 26 23 23; + --color-outline-50: 39 38 36; + --color-outline-100: 65 65 65; + --color-outline-200: 83 82 82; + --color-outline-300: 115 116 116; + --color-outline-400: 140 141 141; + --color-outline-500: 165 163 163; + --color-outline-600: 211 211 211; + --color-outline-700: 221 220 219; + --color-outline-800: 230 230 230; + --color-outline-900: 243 243 243; + --color-outline-950: 253 254 254; + --color-background-0: 18 18 18; + --color-background-50: 39 38 37; + --color-background-100: 65 64 64; + --color-background-200: 83 82 82; + --color-background-300: 116 116 116; + --color-background-400: 142 142 142; + --color-background-500: 162 163 163; + --color-background-600: 213 212 212; + --color-background-700: 220 219 219; + --color-background-800: 242 241 241; + --color-background-900: 246 246 246; + --color-background-950: 254 254 254; + --color-background-error: 66 43 43; + --color-background-warning: 65 47 35; + --color-background-success: 28 43 33; + --color-background-muted: 51 51 51; + --color-background-info: 26 40 46; + --color-indicator-primary: 247 247 247; + --color-indicator-info: 161 199 245; + --color-indicator-error: 232 70 69; + } + + :root.light { + --color-primary-0: 248 250 252; + --color-primary-50: 241 245 249; + --color-primary-100: 226 232 240; + --color-primary-200: 203 213 225; + --color-primary-300: 148 163 184; + --color-primary-400: 100 116 139; + --color-primary-500: 71 85 105; + --color-primary-600: 51 65 85; + --color-primary-700: 45 55 72; + --color-primary-800: 39 49 66; + --color-primary-900: 30 41 59; + --color-primary-950: 15 23 42; + --color-secondary-0: 254 255 255; + --color-secondary-50: 241 242 242; + --color-secondary-100: 231 232 232; + --color-secondary-200: 219 219 219; + --color-secondary-300: 175 176 176; + --color-secondary-400: 114 115 115; + --color-secondary-500: 94 95 95; + --color-secondary-600: 81 82 82; + --color-secondary-700: 63 64 64; + --color-secondary-800: 39 38 38; + --color-secondary-900: 24 23 23; + --color-secondary-950: 11 12 12; + --color-tertiary-0: 255 250 245; + --color-tertiary-50: 255 242 229; + --color-tertiary-100: 255 233 213; + --color-tertiary-200: 254 209 170; + --color-tertiary-300: 253 180 116; + --color-tertiary-400: 251 157 75; + --color-tertiary-500: 231 129 40; + --color-tertiary-600: 215 117 31; + --color-tertiary-700: 180 98 26; + --color-tertiary-800: 130 73 23; + --color-tertiary-900: 108 61 19; + --color-tertiary-950: 84 49 18; + --color-error-0: 254 233 233; + --color-error-50: 254 226 226; + --color-error-100: 254 202 202; + --color-error-200: 252 165 165; + --color-error-300: 248 113 113; + --color-error-400: 239 68 68; + --color-error-500: 230 53 53; + --color-error-600: 220 38 38; + --color-error-700: 185 28 28; + --color-error-800: 153 27 27; + --color-error-900: 127 29 29; + --color-error-950: 83 19 19; + --color-success-0: 228 255 244; + --color-success-50: 202 255 232; + --color-success-100: 162 241 192; + --color-success-200: 132 211 162; + --color-success-300: 102 181 132; + --color-success-400: 72 151 102; + --color-success-500: 52 131 82; + --color-success-600: 42 121 72; + --color-success-700: 32 111 62; + --color-success-800: 22 101 52; + --color-success-900: 20 83 45; + --color-success-950: 27 50 36; + --color-warning-0: 255 253 251; + --color-warning-50: 255 249 245; + --color-warning-100: 255 231 213; + --color-warning-200: 254 205 170; + --color-warning-300: 253 173 116; + --color-warning-400: 251 149 75; + --color-warning-500: 231 120 40; + --color-warning-600: 215 108 31; + --color-warning-700: 180 90 26; + --color-warning-800: 130 68 23; + --color-warning-900: 108 56 19; + --color-warning-950: 84 45 18; + --color-info-0: 236 248 254; + --color-info-50: 199 235 252; + --color-info-100: 162 221 250; + --color-info-200: 124 207 248; + --color-info-300: 87 194 246; + --color-info-400: 50 180 244; + --color-info-500: 13 166 242; + --color-info-600: 11 141 205; + --color-info-700: 9 115 168; + --color-info-800: 7 90 131; + --color-info-900: 5 64 93; + --color-info-950: 3 38 56; + --color-typography-0: 254 254 255; + --color-typography-50: 245 245 245; + --color-typography-100: 229 229 229; + --color-typography-200: 219 219 220; + --color-typography-300: 212 212 212; + --color-typography-400: 163 163 163; + --color-typography-500: 140 140 140; + --color-typography-600: 115 115 115; + --color-typography-700: 82 82 82; + --color-typography-800: 64 64 64; + --color-typography-900: 38 38 39; + --color-typography-950: 23 23 23; + --color-outline-0: 253 254 254; + --color-outline-50: 243 243 243; + --color-outline-100: 230 230 230; + --color-outline-200: 221 220 219; + --color-outline-300: 211 211 211; + --color-outline-400: 165 163 163; + --color-outline-500: 140 141 141; + --color-outline-600: 115 116 116; + --color-outline-700: 83 82 82; + --color-outline-800: 65 65 65; + --color-outline-900: 39 38 36; + --color-outline-950: 26 23 23; + --color-background-0: 255 255 255; + --color-background-50: 246 246 246; + --color-background-100: 242 241 241; + --color-background-200: 220 219 219; + --color-background-300: 213 212 212; + --color-background-400: 162 163 163; + --color-background-500: 142 142 142; + --color-background-600: 116 116 116; + --color-background-700: 83 82 82; + --color-background-800: 65 64 64; + --color-background-900: 39 38 37; + --color-background-950: 24 23 24; + --color-background-error: 254 241 241; + --color-background-warning: 255 244 235; + --color-background-success: 237 252 242; + --color-background-muted: 247 248 247; + --color-background-info: 235 248 254; + --color-indicator-primary: 55 55 55; + --color-indicator-info: 83 153 236; + --color-indicator-error: 185 28 28; + } +} + +/* ─── Tailwind v4 utilities for the legacy gluestack v2 token scales ── */ +@theme inline { + --color-primary-0: rgb(var(--color-primary-0)); + --color-primary-50: rgb(var(--color-primary-50)); + --color-primary-100: rgb(var(--color-primary-100)); + --color-primary-200: rgb(var(--color-primary-200)); + --color-primary-300: rgb(var(--color-primary-300)); + --color-primary-400: rgb(var(--color-primary-400)); + --color-primary-500: rgb(var(--color-primary-500)); + --color-primary-600: rgb(var(--color-primary-600)); + --color-primary-700: rgb(var(--color-primary-700)); + --color-primary-800: rgb(var(--color-primary-800)); + --color-primary-900: rgb(var(--color-primary-900)); + --color-primary-950: rgb(var(--color-primary-950)); + --color-secondary-0: rgb(var(--color-secondary-0)); + --color-secondary-50: rgb(var(--color-secondary-50)); + --color-secondary-100: rgb(var(--color-secondary-100)); + --color-secondary-200: rgb(var(--color-secondary-200)); + --color-secondary-300: rgb(var(--color-secondary-300)); + --color-secondary-400: rgb(var(--color-secondary-400)); + --color-secondary-500: rgb(var(--color-secondary-500)); + --color-secondary-600: rgb(var(--color-secondary-600)); + --color-secondary-700: rgb(var(--color-secondary-700)); + --color-secondary-800: rgb(var(--color-secondary-800)); + --color-secondary-900: rgb(var(--color-secondary-900)); + --color-secondary-950: rgb(var(--color-secondary-950)); + --color-tertiary-0: rgb(var(--color-tertiary-0)); + --color-tertiary-50: rgb(var(--color-tertiary-50)); + --color-tertiary-100: rgb(var(--color-tertiary-100)); + --color-tertiary-200: rgb(var(--color-tertiary-200)); + --color-tertiary-300: rgb(var(--color-tertiary-300)); + --color-tertiary-400: rgb(var(--color-tertiary-400)); + --color-tertiary-500: rgb(var(--color-tertiary-500)); + --color-tertiary-600: rgb(var(--color-tertiary-600)); + --color-tertiary-700: rgb(var(--color-tertiary-700)); + --color-tertiary-800: rgb(var(--color-tertiary-800)); + --color-tertiary-900: rgb(var(--color-tertiary-900)); + --color-tertiary-950: rgb(var(--color-tertiary-950)); + --color-error-0: rgb(var(--color-error-0)); + --color-error-50: rgb(var(--color-error-50)); + --color-error-100: rgb(var(--color-error-100)); + --color-error-200: rgb(var(--color-error-200)); + --color-error-300: rgb(var(--color-error-300)); + --color-error-400: rgb(var(--color-error-400)); + --color-error-500: rgb(var(--color-error-500)); + --color-error-600: rgb(var(--color-error-600)); + --color-error-700: rgb(var(--color-error-700)); + --color-error-800: rgb(var(--color-error-800)); + --color-error-900: rgb(var(--color-error-900)); + --color-error-950: rgb(var(--color-error-950)); + --color-success-0: rgb(var(--color-success-0)); + --color-success-50: rgb(var(--color-success-50)); + --color-success-100: rgb(var(--color-success-100)); + --color-success-200: rgb(var(--color-success-200)); + --color-success-300: rgb(var(--color-success-300)); + --color-success-400: rgb(var(--color-success-400)); + --color-success-500: rgb(var(--color-success-500)); + --color-success-600: rgb(var(--color-success-600)); + --color-success-700: rgb(var(--color-success-700)); + --color-success-800: rgb(var(--color-success-800)); + --color-success-900: rgb(var(--color-success-900)); + --color-success-950: rgb(var(--color-success-950)); + --color-warning-0: rgb(var(--color-warning-0)); + --color-warning-50: rgb(var(--color-warning-50)); + --color-warning-100: rgb(var(--color-warning-100)); + --color-warning-200: rgb(var(--color-warning-200)); + --color-warning-300: rgb(var(--color-warning-300)); + --color-warning-400: rgb(var(--color-warning-400)); + --color-warning-500: rgb(var(--color-warning-500)); + --color-warning-600: rgb(var(--color-warning-600)); + --color-warning-700: rgb(var(--color-warning-700)); + --color-warning-800: rgb(var(--color-warning-800)); + --color-warning-900: rgb(var(--color-warning-900)); + --color-warning-950: rgb(var(--color-warning-950)); + --color-info-0: rgb(var(--color-info-0)); + --color-info-50: rgb(var(--color-info-50)); + --color-info-100: rgb(var(--color-info-100)); + --color-info-200: rgb(var(--color-info-200)); + --color-info-300: rgb(var(--color-info-300)); + --color-info-400: rgb(var(--color-info-400)); + --color-info-500: rgb(var(--color-info-500)); + --color-info-600: rgb(var(--color-info-600)); + --color-info-700: rgb(var(--color-info-700)); + --color-info-800: rgb(var(--color-info-800)); + --color-info-900: rgb(var(--color-info-900)); + --color-info-950: rgb(var(--color-info-950)); + --color-typography-0: rgb(var(--color-typography-0)); + --color-typography-50: rgb(var(--color-typography-50)); + --color-typography-100: rgb(var(--color-typography-100)); + --color-typography-200: rgb(var(--color-typography-200)); + --color-typography-300: rgb(var(--color-typography-300)); + --color-typography-400: rgb(var(--color-typography-400)); + --color-typography-500: rgb(var(--color-typography-500)); + --color-typography-600: rgb(var(--color-typography-600)); + --color-typography-700: rgb(var(--color-typography-700)); + --color-typography-800: rgb(var(--color-typography-800)); + --color-typography-900: rgb(var(--color-typography-900)); + --color-typography-950: rgb(var(--color-typography-950)); + --color-outline-0: rgb(var(--color-outline-0)); + --color-outline-50: rgb(var(--color-outline-50)); + --color-outline-100: rgb(var(--color-outline-100)); + --color-outline-200: rgb(var(--color-outline-200)); + --color-outline-300: rgb(var(--color-outline-300)); + --color-outline-400: rgb(var(--color-outline-400)); + --color-outline-500: rgb(var(--color-outline-500)); + --color-outline-600: rgb(var(--color-outline-600)); + --color-outline-700: rgb(var(--color-outline-700)); + --color-outline-800: rgb(var(--color-outline-800)); + --color-outline-900: rgb(var(--color-outline-900)); + --color-outline-950: rgb(var(--color-outline-950)); + --color-background-0: rgb(var(--color-background-0)); + --color-background-50: rgb(var(--color-background-50)); + --color-background-100: rgb(var(--color-background-100)); + --color-background-200: rgb(var(--color-background-200)); + --color-background-300: rgb(var(--color-background-300)); + --color-background-400: rgb(var(--color-background-400)); + --color-background-500: rgb(var(--color-background-500)); + --color-background-600: rgb(var(--color-background-600)); + --color-background-700: rgb(var(--color-background-700)); + --color-background-800: rgb(var(--color-background-800)); + --color-background-900: rgb(var(--color-background-900)); + --color-background-950: rgb(var(--color-background-950)); + --color-background-error: rgb(var(--color-background-error)); + --color-background-warning: rgb(var(--color-background-warning)); + --color-background-success: rgb(var(--color-background-success)); + --color-background-muted: rgb(var(--color-background-muted)); + --color-background-info: rgb(var(--color-background-info)); + --color-indicator-primary: rgb(var(--color-indicator-primary)); + --color-indicator-info: rgb(var(--color-indicator-info)); + --color-indicator-error: rgb(var(--color-indicator-error)); +} + +/* ─── Non-color theme extensions (ported from tailwind.config.js) ── */ +@theme { + --font-roboto: Roboto, sans-serif; + --font-weight-extrablack: 950; + --text-2xs: 10px; + --color-typography-white: #ffffff; + --color-typography-gray: #d4d4d4; + --color-typography-black: #181718; + --color-background-light: #fbfbfb; + --color-background-dark: #181719; + --shadow-hard-1: -2px 2px 8px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-2: 0px 3px 10px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-3: 2px 2px 8px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-4: 0px -3px 10px 0px rgba(38, 38, 38, 0.2); + --shadow-hard-5: 0px 2px 10px 0px rgba(38, 38, 38, 0.1); + --shadow-soft-1: 0px 0px 10px rgba(38, 38, 38, 0.1); + --shadow-soft-2: 0px 0px 20px rgba(38, 38, 38, 0.2); + --shadow-soft-3: 0px 0px 30px rgba(38, 38, 38, 0.1); + --shadow-soft-4: 0px 0px 40px rgba(38, 38, 38, 0.1); +} + +/* Accessible focus styles for keyboard navigation */ +.web-input-accessible:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} From a09f7ecd7e2b3061321be56d632eb0198a5923ac Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 12 Aug 2026 21:22:16 -0700 Subject: [PATCH 2/2] RC-T39 PR#127 fixes --- gluestack-ui.config.json | 1 - .../ui/gluestack-ui-provider/index.tsx | 4 +- src/components/ui/input/index.tsx | 38 ++----------------- src/components/ui/select/index.tsx | 29 ++------------ src/components/ui/text-field-metrics.ts | 36 ++++++++++++++++++ src/hooks/use-signalr-lifecycle.ts | 4 +- tsconfig.json | 3 +- 7 files changed, 48 insertions(+), 67 deletions(-) create mode 100644 src/components/ui/text-field-metrics.ts diff --git a/gluestack-ui.config.json b/gluestack-ui.config.json index ca842a1..7a9848a 100644 --- a/gluestack-ui.config.json +++ b/gluestack-ui.config.json @@ -1,6 +1,5 @@ { "tailwind": { - "config": "tailwind.config.js", "css": "global.css" }, "app": { diff --git a/src/components/ui/gluestack-ui-provider/index.tsx b/src/components/ui/gluestack-ui-provider/index.tsx index 7ded746..a585733 100644 --- a/src/components/ui/gluestack-ui-provider/index.tsx +++ b/src/components/ui/gluestack-ui-provider/index.tsx @@ -1,7 +1,7 @@ 'use client'; import { OverlayProvider } from '@gluestack-ui/core/overlay/creator'; import { ToastProvider } from '@gluestack-ui/core/toast/creator'; -import React, { useEffect } from 'react'; +import React, { useLayoutEffect } from 'react'; import { Appearance, View, type ViewProps } from 'react-native'; export type ModeType = 'light' | 'dark' | 'system'; @@ -9,7 +9,7 @@ export type ModeType = 'light' | 'dark' | 'system'; export function GluestackUIProvider({ mode = 'light', ...props }: { mode?: ModeType; children?: React.ReactNode; style?: ViewProps['style'] }) { // Both the tokens (--color-*) and the `dark:` variant flip through the prefers-color-scheme media // query, which react-native-css drives from Appearance — hence the override below. - useEffect(() => { + useLayoutEffect(() => { Appearance.setColorScheme(mode === 'system' ? null : mode); }, [mode]); diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx index 9ecd718..5f2157d 100644 --- a/src/components/ui/input/index.tsx +++ b/src/components/ui/input/index.tsx @@ -4,43 +4,11 @@ import { createInput } from '@gluestack-ui/core/input/creator'; import { tva, useStyleContext, type VariantProps, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import { styled } from 'nativewind'; import React from 'react'; -import { Platform, Pressable, TextInput, View } from 'react-native'; +import { Pressable, TextInput, View } from 'react-native'; -const SCOPE = 'INPUT'; +import { useTextFieldVerticalFix } from '../text-field-metrics'; -/** - * Android field metrics, applied from JS because the class layer cannot express them correctly. - * - * Measured on device, each case a real Input/InputField: - * - the class `h-full` (height: 100%) resolves taller than the fixed-height parent on Android, and - * the parent's overflow-hidden then clips the top of the glyphs; - * - an explicit pixel height matching the parent renders correctly; - * - overriding only the lineHeight, at either the class or the style layer, does not help; - * - lineHeight 0 (what iOS uses) hides Android text completely, and a later `undefined` does not - * clear the value the size class sets. - * - * So Android gets a concrete height plus a lineHeight near the font size, and drops the extra font - * padding. iOS keeps the zero lineHeight that upstream applied through `ios:leading-[0px]`; that class - * is gone from the base style so the value can be chosen per platform here. - */ -const ANDROID_FIELD_METRICS: Record = { - sm: { height: 36, lineHeight: 18 }, - md: { height: 40, lineHeight: 20 }, - lg: { height: 44, lineHeight: 22 }, - xl: { height: 48, lineHeight: 25 }, -}; - -const useTextFieldVerticalFix = (size: string | undefined) => - React.useMemo(() => { - if (Platform.OS === 'ios') { - return { lineHeight: 0 } as const; - } - if (Platform.OS === 'android') { - const metrics = ANDROID_FIELD_METRICS[size ?? 'md'] ?? ANDROID_FIELD_METRICS.md; - return { height: metrics.height, lineHeight: metrics.lineHeight, includeFontPadding: false, textAlignVertical: 'center' } as const; - } - return undefined; - }, [size]); +const SCOPE = 'INPUT'; const StyledUIIcon = styled(UIIcon, { className: 'style' }); diff --git a/src/components/ui/select/index.tsx b/src/components/ui/select/index.tsx index a909c88..eee3c38 100644 --- a/src/components/ui/select/index.tsx +++ b/src/components/ui/select/index.tsx @@ -7,8 +7,10 @@ import { tva } from '@gluestack-ui/utils/nativewind-utils'; import { useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'; import { styled } from 'nativewind'; import React from 'react'; -import { Platform, Pressable, TextInput, View } from 'react-native'; +import { Pressable, TextInput, View } from 'react-native'; +// Mirrors the Input component: the trigger's fixed height clips `h-full` text on Android the same way. +import { useTextFieldVerticalFix } from '../text-field-metrics'; import { Actionsheet, ActionsheetBackdrop, @@ -66,31 +68,6 @@ const selectTriggerStyle = tva({ }, }); -/** - * Mirrors the Input component: on Android the class `h-full` resolves taller than the fixed-height - * trigger and the trigger's overflow-hidden clips the top of the value, so the field gets a concrete - * height and a lineHeight near the font size instead. iOS keeps the zero lineHeight that - * `ios:leading-[0px]` used to supply. - */ -const ANDROID_FIELD_METRICS: Record = { - sm: { height: 36, lineHeight: 18 }, - md: { height: 40, lineHeight: 20 }, - lg: { height: 44, lineHeight: 22 }, - xl: { height: 48, lineHeight: 25 }, -}; - -const useTextFieldVerticalFix = (size: string | undefined) => - React.useMemo(() => { - if (Platform.OS === 'ios') { - return { lineHeight: 0 } as const; - } - if (Platform.OS === 'android') { - const metrics = ANDROID_FIELD_METRICS[size ?? 'md'] ?? ANDROID_FIELD_METRICS.md; - return { height: metrics.height, lineHeight: metrics.lineHeight, includeFontPadding: false, textAlignVertical: 'center' } as const; - } - return undefined; - }, [size]); - const selectInputStyle = tva({ base: 'py-auto px-3 placeholder:text-typography-500 web:w-full h-full text-typography-900 pointer-events-none web:outline-none', parentVariants: { diff --git a/src/components/ui/text-field-metrics.ts b/src/components/ui/text-field-metrics.ts new file mode 100644 index 0000000..cd66553 --- /dev/null +++ b/src/components/ui/text-field-metrics.ts @@ -0,0 +1,36 @@ +import React from 'react'; +import { Platform } from 'react-native'; + +/** + * Android field metrics, applied from JS because the class layer cannot express them correctly. + * + * Measured on device, each case a real Input/InputField: + * - the class `h-full` (height: 100%) resolves taller than the fixed-height parent on Android, and + * the parent's overflow-hidden then clips the top of the glyphs; + * - an explicit pixel height matching the parent renders correctly; + * - overriding only the lineHeight, at either the class or the style layer, does not help; + * - lineHeight 0 (what iOS uses) hides Android text completely, and a later `undefined` does not + * clear the value the size class sets. + * + * So Android gets a concrete height plus a lineHeight near the font size, and drops the extra font + * padding. iOS keeps the zero lineHeight that upstream applied through `ios:leading-[0px]`; that class + * is gone from the base style so the value can be chosen per platform here. + */ +const ANDROID_FIELD_METRICS: Record = { + sm: { height: 36, lineHeight: 18 }, + md: { height: 40, lineHeight: 20 }, + lg: { height: 44, lineHeight: 22 }, + xl: { height: 48, lineHeight: 25 }, +}; + +export const useTextFieldVerticalFix = (size: string | undefined) => + React.useMemo(() => { + if (Platform.OS === 'ios') { + return { lineHeight: 0 } as const; + } + if (Platform.OS === 'android') { + const metrics = ANDROID_FIELD_METRICS[size ?? 'md'] ?? ANDROID_FIELD_METRICS.md; + return { height: metrics.height, lineHeight: metrics.lineHeight, includeFontPadding: false, textAlignVertical: 'center' } as const; + } + return undefined; + }, [size]); diff --git a/src/hooks/use-signalr-lifecycle.ts b/src/hooks/use-signalr-lifecycle.ts index 49ade67..84a3994 100644 --- a/src/hooks/use-signalr-lifecycle.ts +++ b/src/hooks/use-signalr-lifecycle.ts @@ -141,7 +141,9 @@ export function useSignalRLifecycle({ isSignedIn, hasInitialized }: UseSignalRLi // The hubs replay nothing that was pushed while the app was away, so board changes another // user made during the gap would stay invisible. Reconnecting restores the feed; this // backfills what it missed. - if (results[0].status === 'fulfilled') { + // connectUpdateHub swallows its own errors, so a fulfilled result alone does not mean the + // hub is up — the store's connected flag is the real signal. + if (results[0].status === 'fulfilled' && useSignalRStore.getState().isUpdateHubConnected) { const openCallId = useIncidentCommandStore.getState().callId; if (openCallId) { useIncidentCommandStore.getState().handleIncidentCommandUpdated(openCallId); diff --git a/tsconfig.json b/tsconfig.json index 84baaca..9a88bfa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,7 @@ "paths": { "@/*": ["./src/*"], "@env": ["./src/lib/env.js"], - "@assets/*": ["./assets/*"], - "tailwind.config": ["./tailwind.config.js"] + "@assets/*": ["./assets/*"] }, "esModuleInterop": true, "checkJs": true,