| author | |
| committer | |
| log | 018d7f2775daabd2ef07898af29586915a0b7f67 |
| tree | bd856f872c5a262273a9f4a07d64fb4114db4bd9 |
| parent | 9257bd860a44ce2a324c917ca66a1a81e73607bd |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>111 files changed, 3636 insertions(+), 1221 deletions(-)
apps/mobile/generated-uniwind-default-theme-variables.json created+136| ... | ... | @@ -0,0 +1,136 @@ |
| 1 | { | |
| 2 | "light": { | |
| 3 | "--color-screen": "#f2f2f7", | |
| 4 | "--color-sheet": "rgba(242, 242, 247, 0.98)", | |
| 5 | "--color-sheet-solid": "#f2f2f7", | |
| 6 | "--color-card": "#ffffff", | |
| 7 | "--color-card-alt": "#f5f5f5", | |
| 8 | "--color-card-translucent": "rgba(255, 255, 255, 0.8)", | |
| 9 | "--color-foreground": "#262626", | |
| 10 | "--color-foreground-secondary": "#525252", | |
| 11 | "--color-foreground-muted": "#737373", | |
| 12 | "--color-foreground-tertiary": "#8e8e93", | |
| 13 | "--color-border": "rgba(0, 0, 0, 0.08)", | |
| 14 | "--color-border-subtle": "rgba(0, 0, 0, 0.06)", | |
| 15 | "--color-separator": "rgba(0, 0, 0, 0.04)", | |
| 16 | "--color-subtle": "rgba(0, 0, 0, 0.04)", | |
| 17 | "--color-subtle-strong": "rgba(0, 0, 0, 0.08)", | |
| 18 | "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)", | |
| 19 | "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)", | |
| 20 | "--color-inline-skill-foreground": "#a21caf", | |
| 21 | "--color-primary": "#262626", | |
| 22 | "--color-primary-foreground": "#ffffff", | |
| 23 | "--color-primary-shadow": "#000000", | |
| 24 | "--color-secondary": "#ffffff", | |
| 25 | "--color-secondary-foreground": "#262626", | |
| 26 | "--color-secondary-border": "rgba(0, 0, 0, 0.08)", | |
| 27 | "--color-switch-active-track": "#34c759", | |
| 28 | "--color-switch-active-thumb": "#ffffff", | |
| 29 | "--color-switch-inactive-track": "rgba(0, 0, 0, 0.08)", | |
| 30 | "--color-switch-inactive-thumb": "#8e8e93", | |
| 31 | "--color-danger": "#fef2f2", | |
| 32 | "--color-danger-border": "rgba(239, 68, 68, 0.12)", | |
| 33 | "--color-danger-foreground": "#dc2626", | |
| 34 | "--color-input": "#ffffff", | |
| 35 | "--color-input-border": "rgba(0, 0, 0, 0.1)", | |
| 36 | "--color-sidebar-search": "rgba(118, 118, 128, 0.12)", | |
| 37 | "--color-placeholder": "#737373", | |
| 38 | "--color-icon": "#262626", | |
| 39 | "--color-icon-muted": "#525252", | |
| 40 | "--color-icon-subtle": "#a3a3a3", | |
| 41 | "--color-header": "rgba(255, 255, 255, 0.97)", | |
| 42 | "--color-header-border": "rgba(0, 0, 0, 0.06)", | |
| 43 | "--color-glass-surface": "rgba(255, 255, 255, 0.72)", | |
| 44 | "--color-glass-tint": "rgba(255, 255, 255, 0.18)", | |
| 45 | "--color-status-bar": "#f2f2f7", | |
| 46 | "--color-md-body": "#111111", | |
| 47 | "--color-md-strong": "#000000", | |
| 48 | "--color-md-link": "#2563eb", | |
| 49 | "--color-md-blockquote-border": "rgba(0, 0, 0, 0.08)", | |
| 50 | "--color-md-blockquote-bg": "rgba(0, 0, 0, 0.02)", | |
| 51 | "--color-md-code-bg": "rgba(0, 0, 0, 0.04)", | |
| 52 | "--color-md-code-text": "#262626", | |
| 53 | "--color-md-user-code-bg": "rgba(255, 255, 255, 0.22)", | |
| 54 | "--color-md-user-code-text": "#ffffff", | |
| 55 | "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.16)", | |
| 56 | "--color-md-user-fence-text": "#ffffff", | |
| 57 | "--color-md-hr": "rgba(0, 0, 0, 0.08)", | |
| 58 | "--color-user-bubble": "#007aff", | |
| 59 | "--color-user-bubble-foreground": "#ffffff", | |
| 60 | "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)", | |
| 61 | "--color-user-bubble-skill-foreground": "#f0abfc", | |
| 62 | "--color-backdrop": "rgba(0, 0, 0, 0.22)", | |
| 63 | "--color-drawer": "rgba(255, 255, 255, 0.99)", | |
| 64 | "--color-drawer-shadow": "rgba(0, 0, 0, 0.12)", | |
| 65 | "--color-dot-separator": "rgba(0, 0, 0, 0.2)", | |
| 66 | "--color-wordmark": "#262626", | |
| 67 | "--color-chevron": "rgba(0, 0, 0, 0.2)" | |
| 68 | }, | |
| 69 | "dark": { | |
| 70 | "--color-screen": "#0a0a0a", | |
| 71 | "--color-sheet": "rgba(14, 14, 14, 0.98)", | |
| 72 | "--color-sheet-solid": "#0e0e0e", | |
| 73 | "--color-card": "#171717", | |
| 74 | "--color-card-alt": "#1c1c1c", | |
| 75 | "--color-card-translucent": "rgba(17, 17, 17, 0.8)", | |
| 76 | "--color-foreground": "#f5f5f5", | |
| 77 | "--color-foreground-secondary": "#a3a3a3", | |
| 78 | "--color-foreground-muted": "#8e8e93", | |
| 79 | "--color-foreground-tertiary": "#636366", | |
| 80 | "--color-border": "rgba(255, 255, 255, 0.06)", | |
| 81 | "--color-border-subtle": "rgba(255, 255, 255, 0.04)", | |
| 82 | "--color-separator": "rgba(255, 255, 255, 0.03)", | |
| 83 | "--color-subtle": "rgba(255, 255, 255, 0.04)", | |
| 84 | "--color-subtle-strong": "rgba(255, 255, 255, 0.08)", | |
| 85 | "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)", | |
| 86 | "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)", | |
| 87 | "--color-inline-skill-foreground": "#f0abfc", | |
| 88 | "--color-primary": "#f5f5f5", | |
| 89 | "--color-primary-foreground": "#0a0a0a", | |
| 90 | "--color-primary-shadow": "#000000", | |
| 91 | "--color-secondary": "rgba(255, 255, 255, 0.04)", | |
| 92 | "--color-secondary-foreground": "#f5f5f5", | |
| 93 | "--color-secondary-border": "rgba(255, 255, 255, 0.06)", | |
| 94 | "--color-switch-active-track": "#30d158", | |
| 95 | "--color-switch-active-thumb": "#ffffff", | |
| 96 | "--color-switch-inactive-track": "rgba(255, 255, 255, 0.06)", | |
| 97 | "--color-switch-inactive-thumb": "#8e8e93", | |
| 98 | "--color-danger": "rgba(239, 68, 68, 0.14)", | |
| 99 | "--color-danger-border": "rgba(248, 113, 113, 0.18)", | |
| 100 | "--color-danger-foreground": "#fca5a5", | |
| 101 | "--color-input": "#141414", | |
| 102 | "--color-input-border": "rgba(255, 255, 255, 0.08)", | |
| 103 | "--color-sidebar-search": "rgba(118, 118, 128, 0.24)", | |
| 104 | "--color-placeholder": "#8e8e93", | |
| 105 | "--color-icon": "#f5f5f5", | |
| 106 | "--color-icon-muted": "#a3a3a3", | |
| 107 | "--color-icon-subtle": "#8e8e93", | |
| 108 | "--color-header": "rgba(10, 10, 10, 0.97)", | |
| 109 | "--color-header-border": "rgba(255, 255, 255, 0.06)", | |
| 110 | "--color-glass-surface": "rgba(23, 23, 23, 0.78)", | |
| 111 | "--color-glass-tint": "rgba(23, 23, 23, 0.24)", | |
| 112 | "--color-status-bar": "#0a0a0a", | |
| 113 | "--color-md-body": "#e5e5e5", | |
| 114 | "--color-md-strong": "#f5f5f5", | |
| 115 | "--color-md-link": "#60a5fa", | |
| 116 | "--color-md-blockquote-border": "rgba(255, 255, 255, 0.1)", | |
| 117 | "--color-md-blockquote-bg": "rgba(255, 255, 255, 0.03)", | |
| 118 | "--color-md-code-bg": "rgba(255, 255, 255, 0.06)", | |
| 119 | "--color-md-code-text": "#e5e5e5", | |
| 120 | "--color-md-user-code-bg": "rgba(255, 255, 255, 0.18)", | |
| 121 | "--color-md-user-code-text": "#ffffff", | |
| 122 | "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.28)", | |
| 123 | "--color-md-user-fence-text": "#ffffff", | |
| 124 | "--color-md-hr": "rgba(255, 255, 255, 0.08)", | |
| 125 | "--color-user-bubble": "#0a84ff", | |
| 126 | "--color-user-bubble-foreground": "#ffffff", | |
| 127 | "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)", | |
| 128 | "--color-user-bubble-skill-foreground": "#f0abfc", | |
| 129 | "--color-backdrop": "rgba(0, 0, 0, 0.48)", | |
| 130 | "--color-drawer": "rgba(14, 14, 14, 0.99)", | |
| 131 | "--color-drawer-shadow": "rgba(0, 0, 0, 0.32)", | |
| 132 | "--color-dot-separator": "rgba(255, 255, 255, 0.2)", | |
| 133 | "--color-wordmark": "#f5f5f5", | |
| 134 | "--color-chevron": "rgba(255, 255, 255, 0.2)" | |
| 135 | } | |
| 136 | } |
apps/mobile/generated-uniwind-theme-names.json created+12| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | [ | |
| 2 | "t3-chat-light", | |
| 3 | "t3-chat-dark", | |
| 4 | "grove-light", | |
| 5 | "grove-dark", | |
| 6 | "ocean-light", | |
| 7 | "ocean-dark", | |
| 8 | "ember-light", | |
| 9 | "ember-dark", | |
| 10 | "iris-light", | |
| 11 | "iris-dark" | |
| 12 | ] |
apps/mobile/generated-uniwind-themes.css created+1374| ... | ... | @@ -0,0 +1,1374 @@ |
| 1 | /* Generated by scripts/generate-uniwind-themes.mts. Do not edit manually. */ | |
| 2 | @layer theme { | |
| 3 | :root { | |
| 4 | @variant light { | |
| 5 | --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277); | |
| 6 | --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746); | |
| 7 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%); | |
| 8 | --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998); | |
| 9 | --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998); | |
| 10 | --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201); | |
| 11 | --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604); | |
| 12 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%); | |
| 13 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%); | |
| 14 | --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815); | |
| 15 | --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881); | |
| 16 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%); | |
| 17 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%); | |
| 18 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%); | |
| 19 | --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225); | |
| 20 | --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612); | |
| 21 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%); | |
| 22 | --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966); | |
| 23 | --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023); | |
| 24 | --color-adaptive-neutral-100-900: oklch(97% 0 0); | |
| 25 | --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0); | |
| 26 | --color-adaptive-neutral-200-800: oklch(92.2% 0 0); | |
| 27 | --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%); | |
| 28 | --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0); | |
| 29 | --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0); | |
| 30 | --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%); | |
| 31 | --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%); | |
| 32 | --color-adaptive-neutral-400-500: oklch(70.8% 0 0); | |
| 33 | --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%); | |
| 34 | --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%); | |
| 35 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%); | |
| 36 | --color-adaptive-neutral-500-400: oklch(55.6% 0 0); | |
| 37 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 38 | --color-adaptive-neutral-600-300: oklch(43.9% 0 0); | |
| 39 | --color-adaptive-neutral-600-400: oklch(43.9% 0 0); | |
| 40 | --color-adaptive-neutral-950-50: oklch(14.5% 0 0); | |
| 41 | --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38); | |
| 42 | --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334); | |
| 43 | --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%); | |
| 44 | --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518); | |
| 45 | --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58); | |
| 46 | --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%); | |
| 47 | --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%); | |
| 48 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%); | |
| 49 | --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439); | |
| 50 | --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585); | |
| 51 | --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935); | |
| 52 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%); | |
| 53 | --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966); | |
| 54 | --color-adaptive-sky-700-300: oklch(50% 0.134 242.749); | |
| 55 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%); | |
| 56 | --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009); | |
| 57 | --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581); | |
| 58 | --color-adaptive-white-neutral-950-a70: #fff; | |
| 59 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%); | |
| 60 | --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938); | |
| 61 | --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786); | |
| 62 | } | |
| 63 | ||
| 64 | @variant dark { | |
| 65 | --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%); | |
| 66 | --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%); | |
| 67 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%); | |
| 68 | --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605); | |
| 69 | --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429); | |
| 70 | --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746); | |
| 71 | --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%); | |
| 72 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%); | |
| 73 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%); | |
| 74 | --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624); | |
| 75 | --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624); | |
| 76 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%); | |
| 77 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%); | |
| 78 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%); | |
| 79 | --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223); | |
| 80 | --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978); | |
| 81 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%); | |
| 82 | --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713); | |
| 83 | --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713); | |
| 84 | --color-adaptive-neutral-100-900: oklch(20.5% 0 0); | |
| 85 | --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%); | |
| 86 | --color-adaptive-neutral-200-800: oklch(26.9% 0 0); | |
| 87 | --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%); | |
| 88 | --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%); | |
| 89 | --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%); | |
| 90 | --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%); | |
| 91 | --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%); | |
| 92 | --color-adaptive-neutral-400-500: oklch(55.6% 0 0); | |
| 93 | --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%); | |
| 94 | --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%); | |
| 95 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%); | |
| 96 | --color-adaptive-neutral-500-400: oklch(70.8% 0 0); | |
| 97 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 98 | --color-adaptive-neutral-600-300: oklch(87% 0 0); | |
| 99 | --color-adaptive-neutral-600-400: oklch(70.8% 0 0); | |
| 100 | --color-adaptive-neutral-950-50: oklch(98.5% 0 0); | |
| 101 | --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%); | |
| 102 | --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899); | |
| 103 | --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%); | |
| 104 | --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571); | |
| 105 | --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%); | |
| 106 | --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%); | |
| 107 | --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%); | |
| 108 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%); | |
| 109 | --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428); | |
| 110 | --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428); | |
| 111 | --color-adaptive-rose-700-300: oklch(81% 0.117 11.638); | |
| 112 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%); | |
| 113 | --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661); | |
| 114 | --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318); | |
| 115 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%); | |
| 116 | --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541); | |
| 117 | --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571); | |
| 118 | --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%); | |
| 119 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%); | |
| 120 | --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067); | |
| 121 | --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286); | |
| 122 | } | |
| 123 | ||
| 124 | @variant t3-chat-light { | |
| 125 | --color-screen: #fdf7fd; | |
| 126 | --color-sheet: rgba(253, 247, 253, 0.98); | |
| 127 | --color-sheet-solid: #fdf7fd; | |
| 128 | --color-card: #fdfafd; | |
| 129 | --color-card-alt: #faf3fb; | |
| 130 | --color-card-translucent: rgba(253, 250, 253, 0.8); | |
| 131 | --color-foreground: #501854; | |
| 132 | --color-foreground-secondary: #ac1668; | |
| 133 | --color-foreground-muted: #8d1255; | |
| 134 | --color-foreground-tertiary: #ac1668; | |
| 135 | --color-border: #eee1ed; | |
| 136 | --color-border-subtle: rgba(238, 225, 237, 0.7); | |
| 137 | --color-separator: rgba(238, 225, 237, 0.55); | |
| 138 | --color-subtle: #eaa7cb; | |
| 139 | --color-subtle-strong: #f1c4e6; | |
| 140 | --color-inline-skill-background: #f3e6f5; | |
| 141 | --color-inline-skill-border: rgba(219, 39, 119, 0.42); | |
| 142 | --color-inline-skill-foreground: #454554; | |
| 143 | --color-primary: #db2777; | |
| 144 | --color-primary-foreground: #ffffff; | |
| 145 | --color-primary-shadow: #000000; | |
| 146 | --color-secondary: #f1c4e6; | |
| 147 | --color-secondary-foreground: #77347c; | |
| 148 | --color-secondary-border: #eee1ed; | |
| 149 | --color-switch-active-track: #db2777; | |
| 150 | --color-switch-active-thumb: #ffffff; | |
| 151 | --color-switch-inactive-track: #f1c4e6; | |
| 152 | --color-switch-inactive-thumb: #8d1255; | |
| 153 | --color-danger: #fde4f1; | |
| 154 | --color-danger-border: rgba(247, 8, 108, 0.32); | |
| 155 | --color-danger-foreground: #9d174d; | |
| 156 | --color-input: #fdfafd; | |
| 157 | --color-input-border: #e7c1dc; | |
| 158 | --color-sidebar-search: #f8f8f7; | |
| 159 | --color-placeholder: #8b5f90; | |
| 160 | --color-icon: #501854; | |
| 161 | --color-icon-muted: #ac1668; | |
| 162 | --color-icon-subtle: #ac1668; | |
| 163 | --color-header: rgba(253, 247, 253, 0.97); | |
| 164 | --color-header-border: #efbdeb; | |
| 165 | --color-glass-surface: rgba(255, 255, 255, 0.74); | |
| 166 | --color-glass-tint: rgba(255, 255, 255, 0.22); | |
| 167 | --color-status-bar: #fdf7fd; | |
| 168 | --color-md-body: #501854; | |
| 169 | --color-md-strong: #501854; | |
| 170 | --color-md-link: #db2777; | |
| 171 | --color-md-blockquote-border: #eee1ed; | |
| 172 | --color-md-blockquote-bg: #eaa7cb; | |
| 173 | --color-md-code-bg: #f5ecf9; | |
| 174 | --color-md-code-text: #673c8b; | |
| 175 | --color-md-user-code-bg: rgba(73, 44, 97, 0.18); | |
| 176 | --color-md-user-code-text: #492c61; | |
| 177 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.16); | |
| 178 | --color-md-user-fence-text: #492c61; | |
| 179 | --color-md-hr: #eee1ed; | |
| 180 | --color-user-bubble: #f7def2; | |
| 181 | --color-user-bubble-foreground: #492c61; | |
| 182 | --color-user-bubble-foreground-muted: rgba(73, 44, 97, 0.78); | |
| 183 | --color-user-bubble-skill-foreground: #c12269; | |
| 184 | --color-backdrop: rgba(0, 0, 0, 0.22); | |
| 185 | --color-drawer: rgba(242, 225, 244, 0.99); | |
| 186 | --color-drawer-shadow: rgba(0, 0, 0, 0.12); | |
| 187 | --color-dot-separator: rgba(172, 22, 104, 0.35); | |
| 188 | --color-wordmark: #501854; | |
| 189 | --color-chevron: rgba(172, 22, 104, 0.42); | |
| 190 | --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277); | |
| 191 | --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746); | |
| 192 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%); | |
| 193 | --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998); | |
| 194 | --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998); | |
| 195 | --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201); | |
| 196 | --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604); | |
| 197 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%); | |
| 198 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%); | |
| 199 | --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815); | |
| 200 | --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881); | |
| 201 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%); | |
| 202 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%); | |
| 203 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%); | |
| 204 | --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225); | |
| 205 | --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612); | |
| 206 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%); | |
| 207 | --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966); | |
| 208 | --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023); | |
| 209 | --color-adaptive-neutral-100-900: oklch(97% 0 0); | |
| 210 | --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0); | |
| 211 | --color-adaptive-neutral-200-800: oklch(92.2% 0 0); | |
| 212 | --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%); | |
| 213 | --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0); | |
| 214 | --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0); | |
| 215 | --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%); | |
| 216 | --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%); | |
| 217 | --color-adaptive-neutral-400-500: oklch(70.8% 0 0); | |
| 218 | --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%); | |
| 219 | --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%); | |
| 220 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%); | |
| 221 | --color-adaptive-neutral-500-400: oklch(55.6% 0 0); | |
| 222 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 223 | --color-adaptive-neutral-600-300: oklch(43.9% 0 0); | |
| 224 | --color-adaptive-neutral-600-400: oklch(43.9% 0 0); | |
| 225 | --color-adaptive-neutral-950-50: oklch(14.5% 0 0); | |
| 226 | --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38); | |
| 227 | --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334); | |
| 228 | --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%); | |
| 229 | --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518); | |
| 230 | --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58); | |
| 231 | --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%); | |
| 232 | --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%); | |
| 233 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%); | |
| 234 | --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439); | |
| 235 | --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585); | |
| 236 | --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935); | |
| 237 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%); | |
| 238 | --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966); | |
| 239 | --color-adaptive-sky-700-300: oklch(50% 0.134 242.749); | |
| 240 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%); | |
| 241 | --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009); | |
| 242 | --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581); | |
| 243 | --color-adaptive-white-neutral-950-a70: #fff; | |
| 244 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%); | |
| 245 | --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938); | |
| 246 | --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786); | |
| 247 | } | |
| 248 | ||
| 249 | @variant t3-chat-dark { | |
| 250 | --color-screen: #1f1a24; | |
| 251 | --color-sheet: rgba(31, 26, 36, 0.98); | |
| 252 | --color-sheet-solid: #1f1a24; | |
| 253 | --color-card: #2c2631; | |
| 254 | --color-card-alt: #29232d; | |
| 255 | --color-card-translucent: rgba(44, 38, 49, 0.8); | |
| 256 | --color-foreground: #f9f8fb; | |
| 257 | --color-foreground-secondary: #e7d0dd; | |
| 258 | --color-foreground-muted: #e7d0dd; | |
| 259 | --color-foreground-tertiary: #e7d0dd; | |
| 260 | --color-border: #27242c; | |
| 261 | --color-border-subtle: rgba(39, 36, 44, 0.7); | |
| 262 | --color-separator: rgba(39, 36, 44, 0.55); | |
| 263 | --color-subtle: #423a45; | |
| 264 | --color-subtle-strong: #362d3d; | |
| 265 | --color-inline-skill-background: #463753; | |
| 266 | --color-inline-skill-border: rgba(163, 0, 76, 0.42); | |
| 267 | --color-inline-skill-foreground: #f8f1f5; | |
| 268 | --color-primary: #a3004c; | |
| 269 | --color-primary-foreground: #fbd0e8; | |
| 270 | --color-primary-shadow: #000000; | |
| 271 | --color-secondary: #362d3d; | |
| 272 | --color-secondary-foreground: #d4c7e1; | |
| 273 | --color-secondary-border: #27242c; | |
| 274 | --color-switch-active-track: #a3004c; | |
| 275 | --color-switch-active-thumb: #fbd0e8; | |
| 276 | --color-switch-inactive-track: #362d3d; | |
| 277 | --color-switch-inactive-thumb: #e7d0dd; | |
| 278 | --color-danger: #331a2b; | |
| 279 | --color-danger-border: rgba(157, 23, 77, 0.32); | |
| 280 | --color-danger-foreground: #fbd0e8; | |
| 281 | --color-input: #2c2631; | |
| 282 | --color-input-border: #302029; | |
| 283 | --color-sidebar-search: #261922; | |
| 284 | --color-placeholder: #968d9f; | |
| 285 | --color-icon: #f9f8fb; | |
| 286 | --color-icon-muted: #d4c7e1; | |
| 287 | --color-icon-subtle: #e7d0dd; | |
| 288 | --color-header: rgba(31, 26, 36, 0.97); | |
| 289 | --color-header-border: #27242c; | |
| 290 | --color-glass-surface: rgba(16, 10, 14, 0.74); | |
| 291 | --color-glass-tint: rgba(16, 10, 14, 0.22); | |
| 292 | --color-status-bar: #1f1a24; | |
| 293 | --color-md-body: #f9f8fb; | |
| 294 | --color-md-strong: #f9f8fb; | |
| 295 | --color-md-link: #a3004c; | |
| 296 | --color-md-blockquote-border: #27242c; | |
| 297 | --color-md-blockquote-bg: #423a45; | |
| 298 | --color-md-code-bg: #1f1a24; | |
| 299 | --color-md-code-text: #d8c3ef; | |
| 300 | --color-md-user-code-bg: rgba(242, 235, 250, 0.18); | |
| 301 | --color-md-user-code-text: #f2ebfa; | |
| 302 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.28); | |
| 303 | --color-md-user-fence-text: #f2ebfa; | |
| 304 | --color-md-hr: #27242c; | |
| 305 | --color-user-bubble: #2b2431; | |
| 306 | --color-user-bubble-foreground: #f2ebfa; | |
| 307 | --color-user-bubble-foreground-muted: rgba(242, 235, 250, 0.78); | |
| 308 | --color-user-bubble-skill-foreground: #cb709a; | |
| 309 | --color-backdrop: rgba(0, 0, 0, 0.48); | |
| 310 | --color-drawer: rgba(23, 16, 24, 0.99); | |
| 311 | --color-drawer-shadow: rgba(0, 0, 0, 0.32); | |
| 312 | --color-dot-separator: rgba(231, 208, 221, 0.35); | |
| 313 | --color-wordmark: #f9f8fb; | |
| 314 | --color-chevron: rgba(231, 208, 221, 0.42); | |
| 315 | --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%); | |
| 316 | --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%); | |
| 317 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%); | |
| 318 | --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605); | |
| 319 | --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429); | |
| 320 | --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746); | |
| 321 | --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%); | |
| 322 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%); | |
| 323 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%); | |
| 324 | --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624); | |
| 325 | --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624); | |
| 326 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%); | |
| 327 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%); | |
| 328 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%); | |
| 329 | --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223); | |
| 330 | --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978); | |
| 331 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%); | |
| 332 | --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713); | |
| 333 | --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713); | |
| 334 | --color-adaptive-neutral-100-900: oklch(20.5% 0 0); | |
| 335 | --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%); | |
| 336 | --color-adaptive-neutral-200-800: oklch(26.9% 0 0); | |
| 337 | --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%); | |
| 338 | --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%); | |
| 339 | --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%); | |
| 340 | --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%); | |
| 341 | --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%); | |
| 342 | --color-adaptive-neutral-400-500: oklch(55.6% 0 0); | |
| 343 | --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%); | |
| 344 | --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%); | |
| 345 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%); | |
| 346 | --color-adaptive-neutral-500-400: oklch(70.8% 0 0); | |
| 347 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 348 | --color-adaptive-neutral-600-300: oklch(87% 0 0); | |
| 349 | --color-adaptive-neutral-600-400: oklch(70.8% 0 0); | |
| 350 | --color-adaptive-neutral-950-50: oklch(98.5% 0 0); | |
| 351 | --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%); | |
| 352 | --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899); | |
| 353 | --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%); | |
| 354 | --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571); | |
| 355 | --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%); | |
| 356 | --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%); | |
| 357 | --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%); | |
| 358 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%); | |
| 359 | --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428); | |
| 360 | --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428); | |
| 361 | --color-adaptive-rose-700-300: oklch(81% 0.117 11.638); | |
| 362 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%); | |
| 363 | --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661); | |
| 364 | --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318); | |
| 365 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%); | |
| 366 | --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541); | |
| 367 | --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571); | |
| 368 | --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%); | |
| 369 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%); | |
| 370 | --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067); | |
| 371 | --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286); | |
| 372 | } | |
| 373 | ||
| 374 | @variant grove-light { | |
| 375 | --color-screen: #f3f7f4; | |
| 376 | --color-sheet: rgba(243, 247, 244, 0.98); | |
| 377 | --color-sheet-solid: #f3f7f4; | |
| 378 | --color-card: #ecefed; | |
| 379 | --color-card-alt: #f3f7f4; | |
| 380 | --color-card-translucent: rgba(236, 239, 237, 0.8); | |
| 381 | --color-foreground: #241523; | |
| 382 | --color-foreground-secondary: #746c73; | |
| 383 | --color-foreground-muted: #6e696f; | |
| 384 | --color-foreground-tertiary: #746c73; | |
| 385 | --color-border: #cbd5d1; | |
| 386 | --color-border-subtle: rgba(203, 213, 209, 0.7); | |
| 387 | --color-separator: rgba(203, 213, 209, 0.55); | |
| 388 | --color-subtle: #e6f0ea; | |
| 389 | --color-subtle-strong: #e2ede7; | |
| 390 | --color-inline-skill-background: #d5e6dd; | |
| 391 | --color-inline-skill-border: rgba(27, 125, 80, 0.42); | |
| 392 | --color-inline-skill-foreground: #241523; | |
| 393 | --color-primary: #1b7d50; | |
| 394 | --color-primary-foreground: #fffaff; | |
| 395 | --color-primary-shadow: #000000; | |
| 396 | --color-secondary: #e2ede7; | |
| 397 | --color-secondary-foreground: #241523; | |
| 398 | --color-secondary-border: #cbd5d1; | |
| 399 | --color-switch-active-track: #1b7d50; | |
| 400 | --color-switch-active-thumb: #fffaff; | |
| 401 | --color-switch-inactive-track: #e2ede7; | |
| 402 | --color-switch-inactive-thumb: #6e696f; | |
| 403 | --color-danger: #f4e7e5; | |
| 404 | --color-danger-border: rgba(251, 44, 54, 0.32); | |
| 405 | --color-danger-foreground: #c10007; | |
| 406 | --color-input: #ecefed; | |
| 407 | --color-input-border: #becbc5; | |
| 408 | --color-sidebar-search: #d3dcd8; | |
| 409 | --color-placeholder: #716971; | |
| 410 | --color-icon: #241523; | |
| 411 | --color-icon-muted: #746c73; | |
| 412 | --color-icon-subtle: #746c73; | |
| 413 | --color-header: rgba(243, 247, 244, 0.97); | |
| 414 | --color-header-border: #d5e6dd; | |
| 415 | --color-glass-surface: rgba(231, 233, 232, 0.74); | |
| 416 | --color-glass-tint: rgba(231, 233, 232, 0.22); | |
| 417 | --color-status-bar: #f3f7f4; | |
| 418 | --color-md-body: #241523; | |
| 419 | --color-md-strong: #241523; | |
| 420 | --color-md-link: #1b7d50; | |
| 421 | --color-md-blockquote-border: #cbd5d1; | |
| 422 | --color-md-blockquote-bg: #e6f0ea; | |
| 423 | --color-md-code-bg: #eef1ef; | |
| 424 | --color-md-code-text: #241523; | |
| 425 | --color-md-user-code-bg: rgba(36, 21, 35, 0.18); | |
| 426 | --color-md-user-code-text: #241523; | |
| 427 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.16); | |
| 428 | --color-md-user-fence-text: #241523; | |
| 429 | --color-md-hr: #cbd5d1; | |
| 430 | --color-user-bubble: #cce1d7; | |
| 431 | --color-user-bubble-foreground: #241523; | |
| 432 | --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78); | |
| 433 | --color-user-bubble-skill-foreground: #815a0e; | |
| 434 | --color-backdrop: rgba(0, 0, 0, 0.22); | |
| 435 | --color-drawer: rgba(226, 237, 231, 0.99); | |
| 436 | --color-drawer-shadow: rgba(0, 0, 0, 0.12); | |
| 437 | --color-dot-separator: rgba(116, 108, 115, 0.35); | |
| 438 | --color-wordmark: #241523; | |
| 439 | --color-chevron: rgba(116, 108, 115, 0.42); | |
| 440 | --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277); | |
| 441 | --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746); | |
| 442 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%); | |
| 443 | --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998); | |
| 444 | --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998); | |
| 445 | --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201); | |
| 446 | --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604); | |
| 447 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%); | |
| 448 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%); | |
| 449 | --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815); | |
| 450 | --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881); | |
| 451 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%); | |
| 452 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%); | |
| 453 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%); | |
| 454 | --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225); | |
| 455 | --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612); | |
| 456 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%); | |
| 457 | --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966); | |
| 458 | --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023); | |
| 459 | --color-adaptive-neutral-100-900: oklch(97% 0 0); | |
| 460 | --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0); | |
| 461 | --color-adaptive-neutral-200-800: oklch(92.2% 0 0); | |
| 462 | --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%); | |
| 463 | --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0); | |
| 464 | --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0); | |
| 465 | --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%); | |
| 466 | --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%); | |
| 467 | --color-adaptive-neutral-400-500: oklch(70.8% 0 0); | |
| 468 | --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%); | |
| 469 | --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%); | |
| 470 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%); | |
| 471 | --color-adaptive-neutral-500-400: oklch(55.6% 0 0); | |
| 472 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 473 | --color-adaptive-neutral-600-300: oklch(43.9% 0 0); | |
| 474 | --color-adaptive-neutral-600-400: oklch(43.9% 0 0); | |
| 475 | --color-adaptive-neutral-950-50: oklch(14.5% 0 0); | |
| 476 | --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38); | |
| 477 | --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334); | |
| 478 | --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%); | |
| 479 | --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518); | |
| 480 | --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58); | |
| 481 | --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%); | |
| 482 | --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%); | |
| 483 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%); | |
| 484 | --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439); | |
| 485 | --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585); | |
| 486 | --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935); | |
| 487 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%); | |
| 488 | --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966); | |
| 489 | --color-adaptive-sky-700-300: oklch(50% 0.134 242.749); | |
| 490 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%); | |
| 491 | --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009); | |
| 492 | --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581); | |
| 493 | --color-adaptive-white-neutral-950-a70: #fff; | |
| 494 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%); | |
| 495 | --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938); | |
| 496 | --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786); | |
| 497 | } | |
| 498 | ||
| 499 | @variant grove-dark { | |
| 500 | --color-screen: #1b2821; | |
| 501 | --color-sheet: rgba(27, 40, 33, 0.98); | |
| 502 | --color-sheet-solid: #1b2821; | |
| 503 | --color-card: #36413c; | |
| 504 | --color-card-alt: #1b2821; | |
| 505 | --color-card-translucent: rgba(54, 65, 60, 0.8); | |
| 506 | --color-foreground: #fffaff; | |
| 507 | --color-foreground-secondary: #919595; | |
| 508 | --color-foreground-muted: #9da5a2; | |
| 509 | --color-foreground-tertiary: #919595; | |
| 510 | --color-border: #415f4f; | |
| 511 | --color-border-subtle: rgba(65, 95, 79, 0.7); | |
| 512 | --color-separator: rgba(65, 95, 79, 0.55); | |
| 513 | --color-subtle: #253e31; | |
| 514 | --color-subtle-strong: #2a4b39; | |
| 515 | --color-inline-skill-background: #325c46; | |
| 516 | --color-inline-skill-border: rgba(105, 214, 154, 0.42); | |
| 517 | --color-inline-skill-foreground: #fffaff; | |
| 518 | --color-primary: #69d69a; | |
| 519 | --color-primary-foreground: #241523; | |
| 520 | --color-primary-shadow: #000000; | |
| 521 | --color-secondary: #2a4b39; | |
| 522 | --color-secondary-foreground: #fffaff; | |
| 523 | --color-secondary-border: #415f4f; | |
| 524 | --color-switch-active-track: #69d69a; | |
| 525 | --color-switch-active-thumb: #241523; | |
| 526 | --color-switch-inactive-track: #2a4b39; | |
| 527 | --color-switch-inactive-thumb: #9da5a2; | |
| 528 | --color-danger: #3f2c28; | |
| 529 | --color-danger-border: rgba(251, 65, 74, 0.32); | |
| 530 | --color-danger-foreground: #ff6668; | |
| 531 | --color-input: #36413c; | |
| 532 | --color-input-border: #4f725f; | |
| 533 | --color-sidebar-search: #45554d; | |
| 534 | --color-placeholder: #a9abab; | |
| 535 | --color-icon: #fffaff; | |
| 536 | --color-icon-muted: #919595; | |
| 537 | --color-icon-subtle: #919595; | |
| 538 | --color-header: rgba(27, 40, 33, 0.97); | |
| 539 | --color-header-border: #36654c; | |
| 540 | --color-glass-surface: rgba(68, 77, 73, 0.74); | |
| 541 | --color-glass-tint: rgba(68, 77, 73, 0.22); | |
| 542 | --color-status-bar: #1b2821; | |
| 543 | --color-md-body: #fffaff; | |
| 544 | --color-md-strong: #fffaff; | |
| 545 | --color-md-link: #69d69a; | |
| 546 | --color-md-blockquote-border: #415f4f; | |
| 547 | --color-md-blockquote-bg: #253e31; | |
| 548 | --color-md-code-bg: #28342e; | |
| 549 | --color-md-code-text: #fffaff; | |
| 550 | --color-md-user-code-bg: rgba(255, 250, 255, 0.18); | |
| 551 | --color-md-user-code-text: #fffaff; | |
| 552 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.28); | |
| 553 | --color-md-user-fence-text: #fffaff; | |
| 554 | --color-md-hr: #415f4f; | |
| 555 | --color-user-bubble: #37664d; | |
| 556 | --color-user-bubble-foreground: #fffaff; | |
| 557 | --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78); | |
| 558 | --color-user-bubble-skill-foreground: #eed295; | |
| 559 | --color-backdrop: rgba(0, 0, 0, 0.48); | |
| 560 | --color-drawer: rgba(33, 54, 43, 0.99); | |
| 561 | --color-drawer-shadow: rgba(0, 0, 0, 0.32); | |
| 562 | --color-dot-separator: rgba(145, 149, 149, 0.35); | |
| 563 | --color-wordmark: #fffaff; | |
| 564 | --color-chevron: rgba(145, 149, 149, 0.42); | |
| 565 | --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%); | |
| 566 | --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%); | |
| 567 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%); | |
| 568 | --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605); | |
| 569 | --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429); | |
| 570 | --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746); | |
| 571 | --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%); | |
| 572 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%); | |
| 573 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%); | |
| 574 | --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624); | |
| 575 | --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624); | |
| 576 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%); | |
| 577 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%); | |
| 578 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%); | |
| 579 | --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223); | |
| 580 | --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978); | |
| 581 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%); | |
| 582 | --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713); | |
| 583 | --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713); | |
| 584 | --color-adaptive-neutral-100-900: oklch(20.5% 0 0); | |
| 585 | --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%); | |
| 586 | --color-adaptive-neutral-200-800: oklch(26.9% 0 0); | |
| 587 | --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%); | |
| 588 | --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%); | |
| 589 | --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%); | |
| 590 | --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%); | |
| 591 | --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%); | |
| 592 | --color-adaptive-neutral-400-500: oklch(55.6% 0 0); | |
| 593 | --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%); | |
| 594 | --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%); | |
| 595 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%); | |
| 596 | --color-adaptive-neutral-500-400: oklch(70.8% 0 0); | |
| 597 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 598 | --color-adaptive-neutral-600-300: oklch(87% 0 0); | |
| 599 | --color-adaptive-neutral-600-400: oklch(70.8% 0 0); | |
| 600 | --color-adaptive-neutral-950-50: oklch(98.5% 0 0); | |
| 601 | --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%); | |
| 602 | --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899); | |
| 603 | --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%); | |
| 604 | --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571); | |
| 605 | --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%); | |
| 606 | --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%); | |
| 607 | --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%); | |
| 608 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%); | |
| 609 | --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428); | |
| 610 | --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428); | |
| 611 | --color-adaptive-rose-700-300: oklch(81% 0.117 11.638); | |
| 612 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%); | |
| 613 | --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661); | |
| 614 | --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318); | |
| 615 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%); | |
| 616 | --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541); | |
| 617 | --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571); | |
| 618 | --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%); | |
| 619 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%); | |
| 620 | --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067); | |
| 621 | --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286); | |
| 622 | } | |
| 623 | ||
| 624 | @variant ocean-light { | |
| 625 | --color-screen: #f5f7f8; | |
| 626 | --color-sheet: rgba(245, 247, 248, 0.98); | |
| 627 | --color-sheet-solid: #f5f7f8; | |
| 628 | --color-card: #edeff1; | |
| 629 | --color-card-alt: #f5f7f8; | |
| 630 | --color-card-translucent: rgba(237, 239, 241, 0.8); | |
| 631 | --color-foreground: #241523; | |
| 632 | --color-foreground-secondary: #746c75; | |
| 633 | --color-foreground-muted: #6f6873; | |
| 634 | --color-foreground-tertiary: #746c75; | |
| 635 | --color-border: #cdd4dc; | |
| 636 | --color-border-subtle: rgba(205, 212, 220, 0.7); | |
| 637 | --color-separator: rgba(205, 212, 220, 0.55); | |
| 638 | --color-subtle: #e8eff4; | |
| 639 | --color-subtle-strong: #e4ecf2; | |
| 640 | --color-inline-skill-background: #d8e4ee; | |
| 641 | --color-inline-skill-border: rgba(38, 114, 175, 0.42); | |
| 642 | --color-inline-skill-foreground: #241523; | |
| 643 | --color-primary: #2672af; | |
| 644 | --color-primary-foreground: #fffaff; | |
| 645 | --color-primary-shadow: #000000; | |
| 646 | --color-secondary: #e4ecf2; | |
| 647 | --color-secondary-foreground: #241523; | |
| 648 | --color-secondary-border: #cdd4dc; | |
| 649 | --color-switch-active-track: #2672af; | |
| 650 | --color-switch-active-thumb: #fffaff; | |
| 651 | --color-switch-inactive-track: #e4ecf2; | |
| 652 | --color-switch-inactive-thumb: #6f6873; | |
| 653 | --color-danger: #f5e6e9; | |
| 654 | --color-danger-border: rgba(251, 44, 54, 0.32); | |
| 655 | --color-danger-foreground: #c10007; | |
| 656 | --color-input: #edeff1; | |
| 657 | --color-input-border: #c0c9d4; | |
| 658 | --color-sidebar-search: #d5dbe2; | |
| 659 | --color-placeholder: #716972; | |
| 660 | --color-icon: #241523; | |
| 661 | --color-icon-muted: #746c75; | |
| 662 | --color-icon-subtle: #746c75; | |
| 663 | --color-header: rgba(245, 247, 248, 0.97); | |
| 664 | --color-header-border: #d8e4ee; | |
| 665 | --color-glass-surface: rgba(232, 233, 235, 0.74); | |
| 666 | --color-glass-tint: rgba(232, 233, 235, 0.22); | |
| 667 | --color-status-bar: #f5f7f8; | |
| 668 | --color-md-body: #241523; | |
| 669 | --color-md-strong: #241523; | |
| 670 | --color-md-link: #2672af; | |
| 671 | --color-md-blockquote-border: #cdd4dc; | |
| 672 | --color-md-blockquote-bg: #e8eff4; | |
| 673 | --color-md-code-bg: #f0f1f3; | |
| 674 | --color-md-code-text: #241523; | |
| 675 | --color-md-user-code-bg: rgba(36, 21, 35, 0.18); | |
| 676 | --color-md-user-code-text: #241523; | |
| 677 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.16); | |
| 678 | --color-md-user-fence-text: #241523; | |
| 679 | --color-md-hr: #cdd4dc; | |
| 680 | --color-user-bubble: #d0dfeb; | |
| 681 | --color-user-bubble-foreground: #241523; | |
| 682 | --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78); | |
| 683 | --color-user-bubble-skill-foreground: #0a6c72; | |
| 684 | --color-backdrop: rgba(0, 0, 0, 0.22); | |
| 685 | --color-drawer: rgba(228, 236, 242, 0.99); | |
| 686 | --color-drawer-shadow: rgba(0, 0, 0, 0.12); | |
| 687 | --color-dot-separator: rgba(116, 108, 117, 0.35); | |
| 688 | --color-wordmark: #241523; | |
| 689 | --color-chevron: rgba(116, 108, 117, 0.42); | |
| 690 | --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277); | |
| 691 | --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746); | |
| 692 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%); | |
| 693 | --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998); | |
| 694 | --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998); | |
| 695 | --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201); | |
| 696 | --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604); | |
| 697 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%); | |
| 698 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%); | |
| 699 | --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815); | |
| 700 | --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881); | |
| 701 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%); | |
| 702 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%); | |
| 703 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%); | |
| 704 | --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225); | |
| 705 | --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612); | |
| 706 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%); | |
| 707 | --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966); | |
| 708 | --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023); | |
| 709 | --color-adaptive-neutral-100-900: oklch(97% 0 0); | |
| 710 | --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0); | |
| 711 | --color-adaptive-neutral-200-800: oklch(92.2% 0 0); | |
| 712 | --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%); | |
| 713 | --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0); | |
| 714 | --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0); | |
| 715 | --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%); | |
| 716 | --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%); | |
| 717 | --color-adaptive-neutral-400-500: oklch(70.8% 0 0); | |
| 718 | --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%); | |
| 719 | --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%); | |
| 720 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%); | |
| 721 | --color-adaptive-neutral-500-400: oklch(55.6% 0 0); | |
| 722 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 723 | --color-adaptive-neutral-600-300: oklch(43.9% 0 0); | |
| 724 | --color-adaptive-neutral-600-400: oklch(43.9% 0 0); | |
| 725 | --color-adaptive-neutral-950-50: oklch(14.5% 0 0); | |
| 726 | --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38); | |
| 727 | --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334); | |
| 728 | --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%); | |
| 729 | --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518); | |
| 730 | --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58); | |
| 731 | --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%); | |
| 732 | --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%); | |
| 733 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%); | |
| 734 | --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439); | |
| 735 | --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585); | |
| 736 | --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935); | |
| 737 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%); | |
| 738 | --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966); | |
| 739 | --color-adaptive-sky-700-300: oklch(50% 0.134 242.749); | |
| 740 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%); | |
| 741 | --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009); | |
| 742 | --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581); | |
| 743 | --color-adaptive-white-neutral-950-a70: #fff; | |
| 744 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%); | |
| 745 | --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938); | |
| 746 | --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786); | |
| 747 | } | |
| 748 | ||
| 749 | @variant ocean-dark { | |
| 750 | --color-screen: #17212b; | |
| 751 | --color-sheet: rgba(23, 33, 43, 0.98); | |
| 752 | --color-sheet-solid: #17212b; | |
| 753 | --color-card: #333b45; | |
| 754 | --color-card-alt: #17212b; | |
| 755 | --color-card-translucent: rgba(51, 59, 69, 0.8); | |
| 756 | --color-foreground: #fffaff; | |
| 757 | --color-foreground-secondary: #8d8f97; | |
| 758 | --color-foreground-muted: #969ca6; | |
| 759 | --color-foreground-tertiary: #8d8f97; | |
| 760 | --color-border: #405567; | |
| 761 | --color-border-subtle: rgba(64, 85, 103, 0.7); | |
| 762 | --color-separator: rgba(64, 85, 103, 0.55); | |
| 763 | --color-subtle: #233544; | |
| 764 | --color-subtle-strong: #293f52; | |
| 765 | --color-inline-skill-background: #324e66; | |
| 766 | --color-inline-skill-border: rgba(112, 185, 238, 0.42); | |
| 767 | --color-inline-skill-foreground: #fffaff; | |
| 768 | --color-primary: #70b9ee; | |
| 769 | --color-primary-foreground: #241523; | |
| 770 | --color-primary-shadow: #000000; | |
| 771 | --color-secondary: #293f52; | |
| 772 | --color-secondary-foreground: #fffaff; | |
| 773 | --color-secondary-border: #405567; | |
| 774 | --color-switch-active-track: #70b9ee; | |
| 775 | --color-switch-active-thumb: #241523; | |
| 776 | --color-switch-inactive-track: #293f52; | |
| 777 | --color-switch-inactive-thumb: #969ca6; | |
| 778 | --color-danger: #3c2630; | |
| 779 | --color-danger-border: rgba(251, 65, 74, 0.32); | |
| 780 | --color-danger-foreground: #ff6467; | |
| 781 | --color-input: #333b45; | |
| 782 | --color-input-border: #4f677b; | |
| 783 | --color-sidebar-search: #424e5a; | |
| 784 | --color-placeholder: #a4a4ac; | |
| 785 | --color-icon: #fffaff; | |
| 786 | --color-icon-muted: #8d8f97; | |
| 787 | --color-icon-subtle: #8d8f97; | |
| 788 | --color-header: rgba(23, 33, 43, 0.97); | |
| 789 | --color-header-border: #36566f; | |
| 790 | --color-glass-surface: rgba(65, 72, 81, 0.74); | |
| 791 | --color-glass-tint: rgba(65, 72, 81, 0.22); | |
| 792 | --color-status-bar: #17212b; | |
| 793 | --color-md-body: #fffaff; | |
| 794 | --color-md-strong: #fffaff; | |
| 795 | --color-md-link: #70b9ee; | |
| 796 | --color-md-blockquote-border: #405567; | |
| 797 | --color-md-blockquote-bg: #233544; | |
| 798 | --color-md-code-bg: #252e38; | |
| 799 | --color-md-code-text: #fffaff; | |
| 800 | --color-md-user-code-bg: rgba(255, 250, 255, 0.18); | |
| 801 | --color-md-user-code-text: #fffaff; | |
| 802 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.28); | |
| 803 | --color-md-user-fence-text: #fffaff; | |
| 804 | --color-md-hr: #405567; | |
| 805 | --color-user-bubble: #375871; | |
| 806 | --color-user-bubble-foreground: #fffaff; | |
| 807 | --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78); | |
| 808 | --color-user-bubble-skill-foreground: #75d8dd; | |
| 809 | --color-backdrop: rgba(0, 0, 0, 0.48); | |
| 810 | --color-drawer: rgba(30, 45, 59, 0.99); | |
| 811 | --color-drawer-shadow: rgba(0, 0, 0, 0.32); | |
| 812 | --color-dot-separator: rgba(141, 143, 151, 0.35); | |
| 813 | --color-wordmark: #fffaff; | |
| 814 | --color-chevron: rgba(141, 143, 151, 0.42); | |
| 815 | --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%); | |
| 816 | --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%); | |
| 817 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%); | |
| 818 | --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605); | |
| 819 | --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429); | |
| 820 | --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746); | |
| 821 | --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%); | |
| 822 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%); | |
| 823 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%); | |
| 824 | --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624); | |
| 825 | --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624); | |
| 826 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%); | |
| 827 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%); | |
| 828 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%); | |
| 829 | --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223); | |
| 830 | --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978); | |
| 831 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%); | |
| 832 | --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713); | |
| 833 | --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713); | |
| 834 | --color-adaptive-neutral-100-900: oklch(20.5% 0 0); | |
| 835 | --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%); | |
| 836 | --color-adaptive-neutral-200-800: oklch(26.9% 0 0); | |
| 837 | --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%); | |
| 838 | --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%); | |
| 839 | --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%); | |
| 840 | --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%); | |
| 841 | --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%); | |
| 842 | --color-adaptive-neutral-400-500: oklch(55.6% 0 0); | |
| 843 | --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%); | |
| 844 | --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%); | |
| 845 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%); | |
| 846 | --color-adaptive-neutral-500-400: oklch(70.8% 0 0); | |
| 847 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 848 | --color-adaptive-neutral-600-300: oklch(87% 0 0); | |
| 849 | --color-adaptive-neutral-600-400: oklch(70.8% 0 0); | |
| 850 | --color-adaptive-neutral-950-50: oklch(98.5% 0 0); | |
| 851 | --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%); | |
| 852 | --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899); | |
| 853 | --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%); | |
| 854 | --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571); | |
| 855 | --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%); | |
| 856 | --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%); | |
| 857 | --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%); | |
| 858 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%); | |
| 859 | --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428); | |
| 860 | --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428); | |
| 861 | --color-adaptive-rose-700-300: oklch(81% 0.117 11.638); | |
| 862 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%); | |
| 863 | --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661); | |
| 864 | --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318); | |
| 865 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%); | |
| 866 | --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541); | |
| 867 | --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571); | |
| 868 | --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%); | |
| 869 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%); | |
| 870 | --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067); | |
| 871 | --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286); | |
| 872 | } | |
| 873 | ||
| 874 | @variant ember-light { | |
| 875 | --color-screen: #f9f7f5; | |
| 876 | --color-sheet: rgba(249, 247, 245, 0.98); | |
| 877 | --color-sheet-solid: #f9f7f5; | |
| 878 | --color-card: #f1efee; | |
| 879 | --color-card-alt: #f9f7f5; | |
| 880 | --color-card-translucent: rgba(241, 239, 238, 0.8); | |
| 881 | --color-foreground: #241523; | |
| 882 | --color-foreground-secondary: #766c74; | |
| 883 | --color-foreground-muted: #74686f; | |
| 884 | --color-foreground-tertiary: #766c74; | |
| 885 | --color-border: #ddd2ce; | |
| 886 | --color-border-subtle: rgba(221, 210, 206, 0.7); | |
| 887 | --color-separator: rgba(221, 210, 206, 0.55); | |
| 888 | --color-subtle: #f4ede9; | |
| 889 | --color-subtle-strong: #f3eae5; | |
| 890 | --color-inline-skill-background: #eee0d9; | |
| 891 | --color-inline-skill-border: rgba(174, 85, 42, 0.42); | |
| 892 | --color-inline-skill-foreground: #241523; | |
| 893 | --color-primary: #ae552a; | |
| 894 | --color-primary-foreground: #fffaff; | |
| 895 | --color-primary-shadow: #000000; | |
| 896 | --color-secondary: #f3eae5; | |
| 897 | --color-secondary-foreground: #241523; | |
| 898 | --color-secondary-border: #ddd2ce; | |
| 899 | --color-switch-active-track: #ae552a; | |
| 900 | --color-switch-active-thumb: #fffaff; | |
| 901 | --color-switch-inactive-track: #f3eae5; | |
| 902 | --color-switch-inactive-thumb: #74686f; | |
| 903 | --color-danger: #f9e7e6; | |
| 904 | --color-danger-border: rgba(251, 44, 54, 0.32); | |
| 905 | --color-danger-foreground: #c10007; | |
| 906 | --color-input: #f1efee; | |
| 907 | --color-input-border: #d4c6c1; | |
| 908 | --color-sidebar-search: #e2d9d6; | |
| 909 | --color-placeholder: #736971; | |
| 910 | --color-icon: #241523; | |
| 911 | --color-icon-muted: #766c74; | |
| 912 | --color-icon-subtle: #766c74; | |
| 913 | --color-header: rgba(249, 247, 245, 0.97); | |
| 914 | --color-header-border: #eee0d9; | |
| 915 | --color-glass-surface: rgba(236, 233, 233, 0.74); | |
| 916 | --color-glass-tint: rgba(236, 233, 233, 0.22); | |
| 917 | --color-status-bar: #f9f7f5; | |
| 918 | --color-md-body: #241523; | |
| 919 | --color-md-strong: #241523; | |
| 920 | --color-md-link: #ae552a; | |
| 921 | --color-md-blockquote-border: #ddd2ce; | |
| 922 | --color-md-blockquote-bg: #f4ede9; | |
| 923 | --color-md-code-bg: #f3f1f0; | |
| 924 | --color-md-code-text: #241523; | |
| 925 | --color-md-user-code-bg: rgba(36, 21, 35, 0.18); | |
| 926 | --color-md-user-code-text: #241523; | |
| 927 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.16); | |
| 928 | --color-md-user-fence-text: #241523; | |
| 929 | --color-md-hr: #ddd2ce; | |
| 930 | --color-user-bubble: #ebdad1; | |
| 931 | --color-user-bubble-foreground: #241523; | |
| 932 | --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78); | |
| 933 | --color-user-bubble-skill-foreground: #b13535; | |
| 934 | --color-backdrop: rgba(0, 0, 0, 0.22); | |
| 935 | --color-drawer: rgba(243, 234, 229, 0.99); | |
| 936 | --color-drawer-shadow: rgba(0, 0, 0, 0.12); | |
| 937 | --color-dot-separator: rgba(118, 108, 116, 0.35); | |
| 938 | --color-wordmark: #241523; | |
| 939 | --color-chevron: rgba(118, 108, 116, 0.42); | |
| 940 | --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277); | |
| 941 | --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746); | |
| 942 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%); | |
| 943 | --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998); | |
| 944 | --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998); | |
| 945 | --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201); | |
| 946 | --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604); | |
| 947 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%); | |
| 948 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%); | |
| 949 | --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815); | |
| 950 | --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881); | |
| 951 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%); | |
| 952 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%); | |
| 953 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%); | |
| 954 | --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225); | |
| 955 | --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612); | |
| 956 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%); | |
| 957 | --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966); | |
| 958 | --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023); | |
| 959 | --color-adaptive-neutral-100-900: oklch(97% 0 0); | |
| 960 | --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0); | |
| 961 | --color-adaptive-neutral-200-800: oklch(92.2% 0 0); | |
| 962 | --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%); | |
| 963 | --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0); | |
| 964 | --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0); | |
| 965 | --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%); | |
| 966 | --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%); | |
| 967 | --color-adaptive-neutral-400-500: oklch(70.8% 0 0); | |
| 968 | --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%); | |
| 969 | --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%); | |
| 970 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%); | |
| 971 | --color-adaptive-neutral-500-400: oklch(55.6% 0 0); | |
| 972 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 973 | --color-adaptive-neutral-600-300: oklch(43.9% 0 0); | |
| 974 | --color-adaptive-neutral-600-400: oklch(43.9% 0 0); | |
| 975 | --color-adaptive-neutral-950-50: oklch(14.5% 0 0); | |
| 976 | --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38); | |
| 977 | --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334); | |
| 978 | --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%); | |
| 979 | --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518); | |
| 980 | --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58); | |
| 981 | --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%); | |
| 982 | --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%); | |
| 983 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%); | |
| 984 | --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439); | |
| 985 | --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585); | |
| 986 | --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935); | |
| 987 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%); | |
| 988 | --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966); | |
| 989 | --color-adaptive-sky-700-300: oklch(50% 0.134 242.749); | |
| 990 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%); | |
| 991 | --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009); | |
| 992 | --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581); | |
| 993 | --color-adaptive-white-neutral-950-a70: #fff; | |
| 994 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%); | |
| 995 | --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938); | |
| 996 | --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786); | |
| 997 | } | |
| 998 | ||
| 999 | @variant ember-dark { | |
| 1000 | --color-screen: #291e1a; | |
| 1001 | --color-sheet: rgba(41, 30, 26, 0.98); | |
| 1002 | --color-sheet-solid: #291e1a; | |
| 1003 | --color-card: #433835; | |
| 1004 | --color-card-alt: #291e1a; | |
| 1005 | --color-card-translucent: rgba(67, 56, 53, 0.8); | |
| 1006 | --color-foreground: #fffaff; | |
| 1007 | --color-foreground-secondary: #968e8f; | |
| 1008 | --color-foreground-muted: #a59996; | |
| 1009 | --color-foreground-tertiary: #968e8f; | |
| 1010 | --color-border: #664c3f; | |
| 1011 | --color-border-subtle: rgba(102, 76, 63, 0.7); | |
| 1012 | --color-separator: rgba(102, 76, 63, 0.55); | |
| 1013 | --color-subtle: #432e23; | |
| 1014 | --color-subtle-strong: #513728; | |
| 1015 | --color-inline-skill-background: #644330; | |
| 1016 | --color-inline-skill-border: rgba(240, 154, 100, 0.42); | |
| 1017 | --color-inline-skill-foreground: #fffaff; | |
| 1018 | --color-primary: #f09a64; | |
| 1019 | --color-primary-foreground: #241523; | |
| 1020 | --color-primary-shadow: #000000; | |
| 1021 | --color-secondary: #513728; | |
| 1022 | --color-secondary-foreground: #fffaff; | |
| 1023 | --color-secondary-border: #664c3f; | |
| 1024 | --color-switch-active-track: #f09a64; | |
| 1025 | --color-switch-active-thumb: #241523; | |
| 1026 | --color-switch-inactive-track: #513728; | |
| 1027 | --color-switch-inactive-thumb: #a59996; | |
| 1028 | --color-danger: #4a2321; | |
| 1029 | --color-danger-border: rgba(251, 65, 74, 0.32); | |
| 1030 | --color-danger-foreground: #ff6467; | |
| 1031 | --color-input: #433835; | |
| 1032 | --color-input-border: #7a5d4d; | |
| 1033 | --color-sidebar-search: #584943; | |
| 1034 | --color-placeholder: #aba3a5; | |
| 1035 | --color-icon: #fffaff; | |
| 1036 | --color-icon-muted: #968e8f; | |
| 1037 | --color-icon-subtle: #968e8f; | |
| 1038 | --color-header: rgba(41, 30, 26, 0.97); | |
| 1039 | --color-header-border: #6e4934; | |
| 1040 | --color-glass-surface: rgba(79, 69, 67, 0.74); | |
| 1041 | --color-glass-tint: rgba(79, 69, 67, 0.22); | |
| 1042 | --color-status-bar: #291e1a; | |
| 1043 | --color-md-body: #fffaff; | |
| 1044 | --color-md-strong: #fffaff; | |
| 1045 | --color-md-link: #f09a64; | |
| 1046 | --color-md-blockquote-border: #664c3f; | |
| 1047 | --color-md-blockquote-bg: #432e23; | |
| 1048 | --color-md-code-bg: #362b27; | |
| 1049 | --color-md-code-text: #fffaff; | |
| 1050 | --color-md-user-code-bg: rgba(255, 250, 255, 0.18); | |
| 1051 | --color-md-user-code-text: #fffaff; | |
| 1052 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.28); | |
| 1053 | --color-md-user-fence-text: #fffaff; | |
| 1054 | --color-md-hr: #664c3f; | |
| 1055 | --color-user-bubble: #704b34; | |
| 1056 | --color-user-bubble-foreground: #fffaff; | |
| 1057 | --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78); | |
| 1058 | --color-user-bubble-skill-foreground: #fab6ad; | |
| 1059 | --color-backdrop: rgba(0, 0, 0, 0.48); | |
| 1060 | --color-drawer: rgba(57, 40, 31, 0.99); | |
| 1061 | --color-drawer-shadow: rgba(0, 0, 0, 0.32); | |
| 1062 | --color-dot-separator: rgba(150, 142, 143, 0.35); | |
| 1063 | --color-wordmark: #fffaff; | |
| 1064 | --color-chevron: rgba(150, 142, 143, 0.42); | |
| 1065 | --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%); | |
| 1066 | --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%); | |
| 1067 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%); | |
| 1068 | --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605); | |
| 1069 | --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429); | |
| 1070 | --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746); | |
| 1071 | --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%); | |
| 1072 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%); | |
| 1073 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%); | |
| 1074 | --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624); | |
| 1075 | --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624); | |
| 1076 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%); | |
| 1077 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%); | |
| 1078 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%); | |
| 1079 | --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223); | |
| 1080 | --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978); | |
| 1081 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%); | |
| 1082 | --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713); | |
| 1083 | --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713); | |
| 1084 | --color-adaptive-neutral-100-900: oklch(20.5% 0 0); | |
| 1085 | --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%); | |
| 1086 | --color-adaptive-neutral-200-800: oklch(26.9% 0 0); | |
| 1087 | --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%); | |
| 1088 | --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%); | |
| 1089 | --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%); | |
| 1090 | --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%); | |
| 1091 | --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%); | |
| 1092 | --color-adaptive-neutral-400-500: oklch(55.6% 0 0); | |
| 1093 | --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%); | |
| 1094 | --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%); | |
| 1095 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%); | |
| 1096 | --color-adaptive-neutral-500-400: oklch(70.8% 0 0); | |
| 1097 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 1098 | --color-adaptive-neutral-600-300: oklch(87% 0 0); | |
| 1099 | --color-adaptive-neutral-600-400: oklch(70.8% 0 0); | |
| 1100 | --color-adaptive-neutral-950-50: oklch(98.5% 0 0); | |
| 1101 | --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%); | |
| 1102 | --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899); | |
| 1103 | --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%); | |
| 1104 | --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571); | |
| 1105 | --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%); | |
| 1106 | --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%); | |
| 1107 | --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%); | |
| 1108 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%); | |
| 1109 | --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428); | |
| 1110 | --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428); | |
| 1111 | --color-adaptive-rose-700-300: oklch(81% 0.117 11.638); | |
| 1112 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%); | |
| 1113 | --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661); | |
| 1114 | --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318); | |
| 1115 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%); | |
| 1116 | --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541); | |
| 1117 | --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571); | |
| 1118 | --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%); | |
| 1119 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%); | |
| 1120 | --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067); | |
| 1121 | --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286); | |
| 1122 | } | |
| 1123 | ||
| 1124 | @variant iris-light { | |
| 1125 | --color-screen: #f8f7f9; | |
| 1126 | --color-sheet: rgba(248, 247, 249, 0.98); | |
| 1127 | --color-sheet-solid: #f8f7f9; | |
| 1128 | --color-card: #f0eff2; | |
| 1129 | --color-card-alt: #f8f7f9; | |
| 1130 | --color-card-translucent: rgba(240, 239, 242, 0.8); | |
| 1131 | --color-foreground: #241523; | |
| 1132 | --color-foreground-secondary: #766c76; | |
| 1133 | --color-foreground-muted: #726874; | |
| 1134 | --color-foreground-tertiary: #766c76; | |
| 1135 | --color-border: #d6d1de; | |
| 1136 | --color-border-subtle: rgba(214, 209, 222, 0.7); | |
| 1137 | --color-separator: rgba(214, 209, 222, 0.55); | |
| 1138 | --color-subtle: #f0edf6; | |
| 1139 | --color-subtle-strong: #edeaf4; | |
| 1140 | --color-inline-skill-background: #e5e0f0; | |
| 1141 | --color-inline-skill-border: rgba(114, 83, 185, 0.42); | |
| 1142 | --color-inline-skill-foreground: #241523; | |
| 1143 | --color-primary: #7253b9; | |
| 1144 | --color-primary-foreground: #fffaff; | |
| 1145 | --color-primary-shadow: #000000; | |
| 1146 | --color-secondary: #edeaf4; | |
| 1147 | --color-secondary-foreground: #241523; | |
| 1148 | --color-secondary-border: #d6d1de; | |
| 1149 | --color-switch-active-track: #7253b9; | |
| 1150 | --color-switch-active-thumb: #fffaff; | |
| 1151 | --color-switch-inactive-track: #edeaf4; | |
| 1152 | --color-switch-inactive-thumb: #726874; | |
| 1153 | --color-danger: #f8e6ea; | |
| 1154 | --color-danger-border: rgba(251, 44, 54, 0.32); | |
| 1155 | --color-danger-foreground: #c10007; | |
| 1156 | --color-input: #f0eff2; | |
| 1157 | --color-input-border: #ccc5d6; | |
| 1158 | --color-sidebar-search: #ddd9e3; | |
| 1159 | --color-placeholder: #736973; | |
| 1160 | --color-icon: #241523; | |
| 1161 | --color-icon-muted: #766c76; | |
| 1162 | --color-icon-subtle: #766c76; | |
| 1163 | --color-header: rgba(248, 247, 249, 0.97); | |
| 1164 | --color-header-border: #e5e0f0; | |
| 1165 | --color-glass-surface: rgba(235, 233, 237, 0.74); | |
| 1166 | --color-glass-tint: rgba(235, 233, 237, 0.22); | |
| 1167 | --color-status-bar: #f8f7f9; | |
| 1168 | --color-md-body: #241523; | |
| 1169 | --color-md-strong: #241523; | |
| 1170 | --color-md-link: #7253b9; | |
| 1171 | --color-md-blockquote-border: #d6d1de; | |
| 1172 | --color-md-blockquote-bg: #f0edf6; | |
| 1173 | --color-md-code-bg: #f2f1f4; | |
| 1174 | --color-md-code-text: #241523; | |
| 1175 | --color-md-user-code-bg: rgba(36, 21, 35, 0.18); | |
| 1176 | --color-md-user-code-text: #241523; | |
| 1177 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.16); | |
| 1178 | --color-md-user-fence-text: #241523; | |
| 1179 | --color-md-hr: #d6d1de; | |
| 1180 | --color-user-bubble: #e0d9ee; | |
| 1181 | --color-user-bubble-foreground: #241523; | |
| 1182 | --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78); | |
| 1183 | --color-user-bubble-skill-foreground: #a82c87; | |
| 1184 | --color-backdrop: rgba(0, 0, 0, 0.22); | |
| 1185 | --color-drawer: rgba(237, 234, 244, 0.99); | |
| 1186 | --color-drawer-shadow: rgba(0, 0, 0, 0.12); | |
| 1187 | --color-dot-separator: rgba(118, 108, 118, 0.35); | |
| 1188 | --color-wordmark: #241523; | |
| 1189 | --color-chevron: rgba(118, 108, 118, 0.42); | |
| 1190 | --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277); | |
| 1191 | --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746); | |
| 1192 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%); | |
| 1193 | --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998); | |
| 1194 | --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998); | |
| 1195 | --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201); | |
| 1196 | --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604); | |
| 1197 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%); | |
| 1198 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%); | |
| 1199 | --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815); | |
| 1200 | --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881); | |
| 1201 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%); | |
| 1202 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%); | |
| 1203 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%); | |
| 1204 | --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225); | |
| 1205 | --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612); | |
| 1206 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%); | |
| 1207 | --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966); | |
| 1208 | --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023); | |
| 1209 | --color-adaptive-neutral-100-900: oklch(97% 0 0); | |
| 1210 | --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0); | |
| 1211 | --color-adaptive-neutral-200-800: oklch(92.2% 0 0); | |
| 1212 | --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%); | |
| 1213 | --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0); | |
| 1214 | --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0); | |
| 1215 | --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%); | |
| 1216 | --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%); | |
| 1217 | --color-adaptive-neutral-400-500: oklch(70.8% 0 0); | |
| 1218 | --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%); | |
| 1219 | --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%); | |
| 1220 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%); | |
| 1221 | --color-adaptive-neutral-500-400: oklch(55.6% 0 0); | |
| 1222 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 1223 | --color-adaptive-neutral-600-300: oklch(43.9% 0 0); | |
| 1224 | --color-adaptive-neutral-600-400: oklch(43.9% 0 0); | |
| 1225 | --color-adaptive-neutral-950-50: oklch(14.5% 0 0); | |
| 1226 | --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38); | |
| 1227 | --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334); | |
| 1228 | --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%); | |
| 1229 | --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518); | |
| 1230 | --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58); | |
| 1231 | --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%); | |
| 1232 | --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%); | |
| 1233 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%); | |
| 1234 | --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439); | |
| 1235 | --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585); | |
| 1236 | --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935); | |
| 1237 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%); | |
| 1238 | --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966); | |
| 1239 | --color-adaptive-sky-700-300: oklch(50% 0.134 242.749); | |
| 1240 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%); | |
| 1241 | --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009); | |
| 1242 | --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581); | |
| 1243 | --color-adaptive-white-neutral-950-a70: #fff; | |
| 1244 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%); | |
| 1245 | --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938); | |
| 1246 | --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786); | |
| 1247 | } | |
| 1248 | ||
| 1249 | @variant iris-dark { | |
| 1250 | --color-screen: #1d1929; | |
| 1251 | --color-sheet: rgba(29, 25, 41, 0.98); | |
| 1252 | --color-sheet-solid: #1d1929; | |
| 1253 | --color-card: #383443; | |
| 1254 | --color-card-alt: #1d1929; | |
| 1255 | --color-card-translucent: rgba(56, 52, 67, 0.8); | |
| 1256 | --color-foreground: #fffaff; | |
| 1257 | --color-foreground-secondary: #8e8a95; | |
| 1258 | --color-foreground-muted: #9690a1; | |
| 1259 | --color-foreground-tertiary: #8e8a95; | |
| 1260 | --color-border: #4d4366; | |
| 1261 | --color-border-subtle: rgba(77, 67, 102, 0.7); | |
| 1262 | --color-separator: rgba(77, 67, 102, 0.55); | |
| 1263 | --color-subtle: #2d2643; | |
| 1264 | --color-subtle-strong: #362d51; | |
| 1265 | --color-inline-skill-background: #433765; | |
| 1266 | --color-inline-skill-border: rgba(157, 125, 242, 0.42); | |
| 1267 | --color-inline-skill-foreground: #fffaff; | |
| 1268 | --color-primary: #9d7df2; | |
| 1269 | --color-primary-foreground: #241523; | |
| 1270 | --color-primary-shadow: #000000; | |
| 1271 | --color-secondary: #362d51; | |
| 1272 | --color-secondary-foreground: #fffaff; | |
| 1273 | --color-secondary-border: #4d4366; | |
| 1274 | --color-switch-active-track: #9d7df2; | |
| 1275 | --color-switch-active-thumb: #241523; | |
| 1276 | --color-switch-inactive-track: #362d51; | |
| 1277 | --color-switch-inactive-thumb: #9690a1; | |
| 1278 | --color-danger: #40202e; | |
| 1279 | --color-danger-border: rgba(251, 65, 74, 0.32); | |
| 1280 | --color-danger-foreground: #ff6467; | |
| 1281 | --color-input: #383443; | |
| 1282 | --color-input-border: #5d527b; | |
| 1283 | --color-sidebar-search: #494459; | |
| 1284 | --color-placeholder: #a29ea8; | |
| 1285 | --color-icon: #fffaff; | |
| 1286 | --color-icon-muted: #8e8a95; | |
| 1287 | --color-icon-subtle: #8e8a95; | |
| 1288 | --color-header: rgba(29, 25, 41, 0.97); | |
| 1289 | --color-header-border: #4a3c70; | |
| 1290 | --color-glass-surface: rgba(69, 66, 80, 0.74); | |
| 1291 | --color-glass-tint: rgba(69, 66, 80, 0.22); | |
| 1292 | --color-status-bar: #1d1929; | |
| 1293 | --color-md-body: #fffaff; | |
| 1294 | --color-md-strong: #fffaff; | |
| 1295 | --color-md-link: #9d7df2; | |
| 1296 | --color-md-blockquote-border: #4d4366; | |
| 1297 | --color-md-blockquote-bg: #2d2643; | |
| 1298 | --color-md-code-bg: #2a2736; | |
| 1299 | --color-md-code-text: #fffaff; | |
| 1300 | --color-md-user-code-bg: rgba(255, 250, 255, 0.18); | |
| 1301 | --color-md-user-code-text: #fffaff; | |
| 1302 | --color-md-user-fence-bg: rgba(0, 0, 0, 0.28); | |
| 1303 | --color-md-user-fence-text: #fffaff; | |
| 1304 | --color-md-hr: #4d4366; | |
| 1305 | --color-user-bubble: #4b3d72; | |
| 1306 | --color-user-bubble-foreground: #fffaff; | |
| 1307 | --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78); | |
| 1308 | --color-user-bubble-skill-foreground: #f099d8; | |
| 1309 | --color-backdrop: rgba(0, 0, 0, 0.48); | |
| 1310 | --color-drawer: rgba(39, 33, 57, 0.99); | |
| 1311 | --color-drawer-shadow: rgba(0, 0, 0, 0.32); | |
| 1312 | --color-dot-separator: rgba(142, 138, 149, 0.35); | |
| 1313 | --color-wordmark: #fffaff; | |
| 1314 | --color-chevron: rgba(142, 138, 149, 0.42); | |
| 1315 | --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%); | |
| 1316 | --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%); | |
| 1317 | --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%); | |
| 1318 | --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605); | |
| 1319 | --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429); | |
| 1320 | --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746); | |
| 1321 | --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%); | |
| 1322 | --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%); | |
| 1323 | --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%); | |
| 1324 | --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624); | |
| 1325 | --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624); | |
| 1326 | --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%); | |
| 1327 | --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%); | |
| 1328 | --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%); | |
| 1329 | --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223); | |
| 1330 | --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978); | |
| 1331 | --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%); | |
| 1332 | --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713); | |
| 1333 | --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713); | |
| 1334 | --color-adaptive-neutral-100-900: oklch(20.5% 0 0); | |
| 1335 | --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%); | |
| 1336 | --color-adaptive-neutral-200-800: oklch(26.9% 0 0); | |
| 1337 | --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%); | |
| 1338 | --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%); | |
| 1339 | --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%); | |
| 1340 | --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%); | |
| 1341 | --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%); | |
| 1342 | --color-adaptive-neutral-400-500: oklch(55.6% 0 0); | |
| 1343 | --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%); | |
| 1344 | --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%); | |
| 1345 | --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%); | |
| 1346 | --color-adaptive-neutral-500-400: oklch(70.8% 0 0); | |
| 1347 | --color-adaptive-neutral-500-500: oklch(55.6% 0 0); | |
| 1348 | --color-adaptive-neutral-600-300: oklch(87% 0 0); | |
| 1349 | --color-adaptive-neutral-600-400: oklch(70.8% 0 0); | |
| 1350 | --color-adaptive-neutral-950-50: oklch(98.5% 0 0); | |
| 1351 | --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%); | |
| 1352 | --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899); | |
| 1353 | --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%); | |
| 1354 | --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571); | |
| 1355 | --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%); | |
| 1356 | --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%); | |
| 1357 | --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%); | |
| 1358 | --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%); | |
| 1359 | --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428); | |
| 1360 | --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428); | |
| 1361 | --color-adaptive-rose-700-300: oklch(81% 0.117 11.638); | |
| 1362 | --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%); | |
| 1363 | --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661); | |
| 1364 | --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318); | |
| 1365 | --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%); | |
| 1366 | --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541); | |
| 1367 | --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571); | |
| 1368 | --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%); | |
| 1369 | --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%); | |
| 1370 | --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067); | |
| 1371 | --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286); | |
| 1372 | } | |
| 1373 | } | |
| 1374 | } |
apps/mobile/global.css+1| ... | ... | @@ -1,5 +1,6 @@ |
| 1 | 1 | @import "tailwindcss"; |
| 2 | 2 | @import "uniwind"; |
| 3 | @import "./generated-uniwind-themes.css"; | |
| 3 | 4 | |
| 4 | 5 | /* ─── Theme tokens ──────────────────────────────────────────────────── */ |
| 5 | 6 | @layer theme { |
apps/mobile/metro.config.js+2| ... | ... | @@ -2,6 +2,7 @@ const fs = require("node:fs"); |
| 2 | 2 | const path = require("node:path"); |
| 3 | 3 | const { getDefaultConfig } = require("expo/metro-config"); |
| 4 | 4 | const { withUniwindConfig } = require("uniwind/metro"); |
| 5 | const extraThemes = require("./generated-uniwind-theme-names.json"); | |
| 5 | 6 | |
| 6 | 7 | /** @type {import("expo/metro-config").MetroConfig} */ |
| 7 | 8 | const config = getDefaultConfig(__dirname); |
| ... | ... | @@ -50,5 +51,6 @@ config.resolver = { |
| 50 | 51 | |
| 51 | 52 | module.exports = withUniwindConfig(config, { |
| 52 | 53 | cssEntryFile: "./global.css", |
| 54 | extraThemes, | |
| 53 | 55 | polyfills: { rem: 14 }, |
| 54 | 56 | }); |
apps/mobile/package.json+1| ... | ... | @@ -39,6 +39,7 @@ |
| 39 | 39 | "config:prod": "APP_VARIANT=production expo config", |
| 40 | 40 | "profile:android:hermes": "mkdir -p profiles/review && react-native profile-hermes profiles/review", |
| 41 | 41 | "sync:pierre-icons": "node modules/t3-markdown-text/scripts/sync-pierre-file-icons.mjs", |
| 42 | "generate": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate-uniwind-themes.mts", | |
| 42 | 43 | "test": "vp test run", |
| 43 | 44 | "typecheck": "tsc --noEmit" |
| 44 | 45 | }, |
apps/mobile/scripts/generate-uniwind-themes.mts created+262| ... | ... | @@ -0,0 +1,262 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | ||
| 3 | import * as NodeFS from "node:fs"; | |
| 4 | import * as NodePath from "node:path"; | |
| 5 | import tailwindColors from "tailwindcss/colors"; | |
| 6 | import { BUILT_IN_THEME_IDS, type BuiltInThemeId } from "@t3tools/shared/themePalettes"; | |
| 7 | ||
| 8 | import { | |
| 9 | getMobileThemeVariables, | |
| 10 | MOBILE_THEME_VARIABLE_NAMES, | |
| 11 | type MobileThemeAppearance, | |
| 12 | type MobileThemeVariables, | |
| 13 | } from "../src/lib/mobileTheme.ts"; | |
| 14 | ||
| 15 | const APPEARANCES = ["light", "dark"] as const; | |
| 16 | const GLOBAL_CSS_PATH = NodePath.resolve(import.meta.dirname, "../global.css"); | |
| 17 | const GENERATED_CSS_PATH = NodePath.resolve(import.meta.dirname, "../generated-uniwind-themes.css"); | |
| 18 | const GENERATED_NAMES_PATH = NodePath.resolve( | |
| 19 | import.meta.dirname, | |
| 20 | "../generated-uniwind-theme-names.json", | |
| 21 | ); | |
| 22 | const GENERATED_DEFAULT_VARIABLES_PATH = NodePath.resolve( | |
| 23 | import.meta.dirname, | |
| 24 | "../generated-uniwind-default-theme-variables.json", | |
| 25 | ); | |
| 26 | ||
| 27 | type TailwindColorFamily = keyof typeof tailwindColors; | |
| 28 | type TailwindColorShade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950; | |
| 29 | ||
| 30 | const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity = 1): string => { | |
| 31 | const familyColors = tailwindColors[family]; | |
| 32 | const value = | |
| 33 | typeof familyColors === "string" | |
| 34 | ? shade === undefined | |
| 35 | ? familyColors | |
| 36 | : undefined | |
| 37 | : shade === undefined | |
| 38 | ? undefined | |
| 39 | : familyColors[String(shade) as keyof typeof familyColors]; | |
| 40 | if (value === undefined) { | |
| 41 | throw new Error(`Unknown Tailwind color ${family}${shade === undefined ? "" : `-${shade}`}.`); | |
| 42 | } | |
| 43 | if (opacity === 1) return value; | |
| 44 | ||
| 45 | const percentage = Number((opacity * 100).toFixed(4)); | |
| 46 | const oklch = /^oklch\((.*)\)$/.exec(value); | |
| 47 | if (oklch) return `oklch(${oklch[1]} / ${percentage}%)`; | |
| 48 | if (value === "#fff") return `rgb(255 255 255 / ${percentage}%)`; | |
| 49 | if (value === "#000") return `rgb(0 0 0 / ${percentage}%)`; | |
| 50 | return `color-mix(in srgb, ${value} ${percentage}%, transparent)`; | |
| 51 | }; | |
| 52 | ||
| 53 | // These replace the remaining dark:* utility pairs. A registered palette theme is | |
| 54 | // neither literally `light` nor `dark`, so appearance-sensitive values must also be | |
| 55 | // represented as semantic variables for custom themes. | |
| 56 | const ADAPTIVE_COLORS = { | |
| 57 | "--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)], | |
| 58 | "--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)], | |
| 59 | "--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)], | |
| 60 | "--color-adaptive-amber-700-300": [color("amber", 700), color("amber", 300)], | |
| 61 | "--color-adaptive-amber-700-400": [color("amber", 700), color("amber", 400)], | |
| 62 | "--color-adaptive-amber-800-200": [color("amber", 800), color("amber", 200)], | |
| 63 | "--color-adaptive-blue-50-blue-400-a14": [color("blue", 50), color("blue", 400, 0.14)], | |
| 64 | "--color-adaptive-blue-300-a50-blue-400-a28": [color("blue", 300, 0.5), color("blue", 400, 0.28)], | |
| 65 | "--color-adaptive-blue-500-a20-blue-400-a15": [color("blue", 500, 0.2), color("blue", 400, 0.15)], | |
| 66 | "--color-adaptive-blue-500-400": [color("blue", 500), color("blue", 400)], | |
| 67 | "--color-adaptive-blue-600-400": [color("blue", 600), color("blue", 400)], | |
| 68 | "--color-adaptive-black-a10-a25": [ | |
| 69 | color("black", undefined, 0.1), | |
| 70 | color("black", undefined, 0.25), | |
| 71 | ], | |
| 72 | "--color-adaptive-black-a15-a35": [ | |
| 73 | color("black", undefined, 0.15), | |
| 74 | color("black", undefined, 0.35), | |
| 75 | ], | |
| 76 | "--color-adaptive-emerald-500-a12-a16": [ | |
| 77 | color("emerald", 500, 0.12), | |
| 78 | color("emerald", 500, 0.16), | |
| 79 | ], | |
| 80 | "--color-adaptive-emerald-600-400": [color("emerald", 600), color("emerald", 400)], | |
| 81 | "--color-adaptive-emerald-700-300": [color("emerald", 700), color("emerald", 300)], | |
| 82 | "--color-adaptive-indigo-500-a12-a16": [color("indigo", 500, 0.12), color("indigo", 500, 0.16)], | |
| 83 | "--color-adaptive-indigo-600-300": [color("indigo", 600), color("indigo", 300)], | |
| 84 | "--color-adaptive-indigo-700-300": [color("indigo", 700), color("indigo", 300)], | |
| 85 | "--color-adaptive-neutral-100-900": [color("neutral", 100), color("neutral", 900)], | |
| 86 | "--color-adaptive-neutral-200-700-a60": [color("neutral", 200), color("neutral", 700, 0.6)], | |
| 87 | "--color-adaptive-neutral-200-800": [color("neutral", 200), color("neutral", 800)], | |
| 88 | "--color-adaptive-neutral-200-a70-white-a8": [ | |
| 89 | color("neutral", 200, 0.7), | |
| 90 | color("white", undefined, 0.08), | |
| 91 | ], | |
| 92 | "--color-adaptive-neutral-200-white-a6": [color("neutral", 200), color("white", undefined, 0.06)], | |
| 93 | "--color-adaptive-neutral-200-white-a8": [color("neutral", 200), color("white", undefined, 0.08)], | |
| 94 | "--color-adaptive-neutral-200-a80-white-a8": [ | |
| 95 | color("neutral", 200, 0.8), | |
| 96 | color("white", undefined, 0.08), | |
| 97 | ], | |
| 98 | "--color-adaptive-neutral-300-a60-white-a12": [ | |
| 99 | color("neutral", 300, 0.6), | |
| 100 | color("white", undefined, 0.12), | |
| 101 | ], | |
| 102 | "--color-adaptive-neutral-400-500": [color("neutral", 400), color("neutral", 500)], | |
| 103 | "--color-adaptive-neutral-400-a60-500-a60": [ | |
| 104 | color("neutral", 400, 0.6), | |
| 105 | color("neutral", 500, 0.6), | |
| 106 | ], | |
| 107 | "--color-adaptive-neutral-400-a80-500-a80": [ | |
| 108 | color("neutral", 400, 0.8), | |
| 109 | color("neutral", 500, 0.8), | |
| 110 | ], | |
| 111 | "--color-adaptive-neutral-500-a10-a16": [color("neutral", 500, 0.1), color("neutral", 500, 0.16)], | |
| 112 | "--color-adaptive-neutral-500-400": [color("neutral", 500), color("neutral", 400)], | |
| 113 | "--color-adaptive-neutral-500-500": [color("neutral", 500), color("neutral", 500)], | |
| 114 | "--color-adaptive-neutral-600-300": [color("neutral", 600), color("neutral", 300)], | |
| 115 | "--color-adaptive-neutral-600-400": [color("neutral", 600), color("neutral", 400)], | |
| 116 | "--color-adaptive-neutral-950-50": [color("neutral", 950), color("neutral", 50)], | |
| 117 | "--color-adaptive-red-50-950-a80": [color("red", 50), color("red", 950, 0.8)], | |
| 118 | "--color-adaptive-red-200-800": [color("red", 200), color("red", 800)], | |
| 119 | "--color-adaptive-red-600-a80-400-a80": [color("red", 600, 0.8), color("red", 400, 0.8)], | |
| 120 | "--color-adaptive-red-700-300": [color("red", 700), color("red", 300)], | |
| 121 | "--color-adaptive-rose-100-500-a18": [color("rose", 100), color("rose", 500, 0.18)], | |
| 122 | "--color-adaptive-rose-100-a80-500-a12": [color("rose", 100, 0.8), color("rose", 500, 0.12)], | |
| 123 | "--color-adaptive-rose-300-a70-400-a28": [color("rose", 300, 0.7), color("rose", 400, 0.28)], | |
| 124 | "--color-adaptive-rose-500-a12-a16": [color("rose", 500, 0.12), color("rose", 500, 0.16)], | |
| 125 | "--color-adaptive-rose-500-400": [color("rose", 500), color("rose", 400)], | |
| 126 | "--color-adaptive-rose-600-400": [color("rose", 600), color("rose", 400)], | |
| 127 | "--color-adaptive-rose-700-300": [color("rose", 700), color("rose", 300)], | |
| 128 | "--color-adaptive-sky-500-a12-a16": [color("sky", 500, 0.12), color("sky", 500, 0.16)], | |
| 129 | "--color-adaptive-sky-600-400": [color("sky", 600), color("sky", 400)], | |
| 130 | "--color-adaptive-sky-700-300": [color("sky", 700), color("sky", 300)], | |
| 131 | "--color-adaptive-violet-500-a12-a16": [color("violet", 500, 0.12), color("violet", 500, 0.16)], | |
| 132 | "--color-adaptive-violet-600-400": [color("violet", 600), color("violet", 400)], | |
| 133 | "--color-adaptive-violet-700-300": [color("violet", 700), color("violet", 300)], | |
| 134 | "--color-adaptive-white-neutral-950-a70": [color("white"), color("neutral", 950, 0.7)], | |
| 135 | "--color-adaptive-zinc-500-a12-a16": [color("zinc", 500, 0.12), color("zinc", 500, 0.16)], | |
| 136 | "--color-adaptive-zinc-500-400": [color("zinc", 500), color("zinc", 400)], | |
| 137 | "--color-adaptive-zinc-600-300": [color("zinc", 600), color("zinc", 300)], | |
| 138 | }; | |
| 139 | ||
| 140 | export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) => | |
| 141 | APPEARANCES.map((appearance) => `${themeId}-${appearance}`), | |
| 142 | ); | |
| 143 | ||
| 144 | const adaptiveVariablesFor = (appearance: MobileThemeAppearance) => | |
| 145 | Object.fromEntries( | |
| 146 | Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [ | |
| 147 | name, | |
| 148 | values[appearance === "light" ? 0 : 1], | |
| 149 | ]), | |
| 150 | ); | |
| 151 | ||
| 152 | const variablesFor = (themeId: BuiltInThemeId, appearance: MobileThemeAppearance) => ({ | |
| 153 | ...getMobileThemeVariables(themeId, appearance), | |
| 154 | ...adaptiveVariablesFor(appearance), | |
| 155 | }); | |
| 156 | ||
| 157 | const renderVariant = (name: string, variables: Readonly<Record<string, string>>) => { | |
| 158 | const declarations = Object.entries(variables) | |
| 159 | .map(([variable, value]) => ` ${variable}: ${value};`) | |
| 160 | .join("\n"); | |
| 161 | return ` @variant ${name} {\n${declarations}\n }`; | |
| 162 | }; | |
| 163 | ||
| 164 | export const renderUniwindThemesCSS = () => { | |
| 165 | const variants = [ | |
| 166 | renderVariant("light", adaptiveVariablesFor("light")), | |
| 167 | renderVariant("dark", adaptiveVariablesFor("dark")), | |
| 168 | ...BUILT_IN_THEME_IDS.flatMap((themeId) => | |
| 169 | APPEARANCES.map((appearance) => | |
| 170 | renderVariant(`${themeId}-${appearance}`, variablesFor(themeId, appearance)), | |
| 171 | ), | |
| 172 | ), | |
| 173 | ]; | |
| 174 | return [ | |
| 175 | "/* Generated by scripts/generate-uniwind-themes.mts. Do not edit manually. */", | |
| 176 | "@layer theme {", | |
| 177 | " :root {", | |
| 178 | variants.join("\n\n"), | |
| 179 | " }", | |
| 180 | "}", | |
| 181 | "", | |
| 182 | ].join("\n"); | |
| 183 | }; | |
| 184 | ||
| 185 | const readVariantBody = (css: string, appearance: MobileThemeAppearance): string => { | |
| 186 | const marker = `@variant ${appearance} {`; | |
| 187 | const markerIndex = css.indexOf(marker); | |
| 188 | if (markerIndex === -1) throw new Error(`Could not find ${marker} in global.css.`); | |
| 189 | ||
| 190 | const openingBraceIndex = css.indexOf("{", markerIndex); | |
| 191 | let depth = 0; | |
| 192 | for (let index = openingBraceIndex; index < css.length; index += 1) { | |
| 193 | if (css[index] === "{") depth += 1; | |
| 194 | if (css[index] !== "}") continue; | |
| 195 | depth -= 1; | |
| 196 | if (depth === 0) return css.slice(openingBraceIndex + 1, index); | |
| 197 | } | |
| 198 | throw new Error(`Could not find the end of ${marker} in global.css.`); | |
| 199 | }; | |
| 200 | ||
| 201 | export const readDefaultThemeVariables = (css: string) => | |
| 202 | Object.fromEntries( | |
| 203 | APPEARANCES.map((appearance) => { | |
| 204 | const body = readVariantBody(css, appearance); | |
| 205 | const variables = Object.fromEntries( | |
| 206 | MOBILE_THEME_VARIABLE_NAMES.map((name) => { | |
| 207 | const match = new RegExp(`^\\s*${name}:\\s*([^;]+);`, "mu").exec(body); | |
| 208 | if (!match?.[1]) { | |
| 209 | throw new Error(`Default ${appearance} theme is missing ${name}.`); | |
| 210 | } | |
| 211 | return [name, match[1].trim()]; | |
| 212 | }), | |
| 213 | ) as MobileThemeVariables; | |
| 214 | return [appearance, variables]; | |
| 215 | }), | |
| 216 | ) as Readonly<Record<MobileThemeAppearance, MobileThemeVariables>>; | |
| 217 | ||
| 218 | export const renderDefaultThemeVariablesJSON = (css: string) => | |
| 219 | `${JSON.stringify(readDefaultThemeVariables(css), null, 2)}\n`; | |
| 220 | ||
| 221 | export const getGeneratedUniwindThemeOutputs = (): ReadonlyArray< | |
| 222 | readonly [filename: string, contents: string] | |
| 223 | > => [ | |
| 224 | [GENERATED_CSS_PATH, renderUniwindThemesCSS()], | |
| 225 | [GENERATED_NAMES_PATH, `${JSON.stringify(customThemeNames, null, 2)}\n`], | |
| 226 | [ | |
| 227 | GENERATED_DEFAULT_VARIABLES_PATH, | |
| 228 | renderDefaultThemeVariablesJSON(NodeFS.readFileSync(GLOBAL_CSS_PATH, "utf8")), | |
| 229 | ], | |
| 230 | ]; | |
| 231 | ||
| 232 | const writeFileAtomically = (filename: string, contents: string) => { | |
| 233 | const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null; | |
| 234 | if (current === contents) return; | |
| 235 | ||
| 236 | const temporaryFilename = `${filename}.${process.pid}.tmp`; | |
| 237 | try { | |
| 238 | NodeFS.writeFileSync(temporaryFilename, contents); | |
| 239 | NodeFS.renameSync(temporaryFilename, filename); | |
| 240 | } finally { | |
| 241 | if (NodeFS.existsSync(temporaryFilename)) NodeFS.unlinkSync(temporaryFilename); | |
| 242 | } | |
| 243 | }; | |
| 244 | ||
| 245 | if (import.meta.main) { | |
| 246 | const checkOnly = process.argv.includes("--check"); | |
| 247 | for (const [filename, contents] of getGeneratedUniwindThemeOutputs()) { | |
| 248 | if (checkOnly) { | |
| 249 | const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null; | |
| 250 | if (current !== contents) { | |
| 251 | console.error( | |
| 252 | `${NodePath.relative(process.cwd(), filename)} is stale. Run vp run --filter @t3tools/mobile generate.`, | |
| 253 | ); | |
| 254 | process.exitCode = 1; | |
| 255 | } | |
| 256 | continue; | |
| 257 | } | |
| 258 | // Metro watches the generated CSS. Replacing a complete temporary file keeps | |
| 259 | // Tailwind from compiling a partially rewritten theme file. | |
| 260 | writeFileAtomically(filename, contents); | |
| 261 | } | |
| 262 | } |
apps/mobile/scripts/generate-uniwind-themes.test.ts created+55| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | import * as NodeFS from "node:fs"; | |
| 2 | import * as NodePath from "node:path"; | |
| 3 | import { describe, expect, it } from "vite-plus/test"; | |
| 4 | ||
| 5 | import { | |
| 6 | customThemeNames, | |
| 7 | getGeneratedUniwindThemeOutputs, | |
| 8 | readDefaultThemeVariables, | |
| 9 | renderUniwindThemesCSS, | |
| 10 | } from "./generate-uniwind-themes.mts"; | |
| 11 | ||
| 12 | describe("generate mobile Uniwind themes", () => { | |
| 13 | it("keeps the committed outputs current", () => { | |
| 14 | const staleOutputs = getGeneratedUniwindThemeOutputs() | |
| 15 | .filter( | |
| 16 | ([filename, contents]) => | |
| 17 | !NodeFS.existsSync(filename) || NodeFS.readFileSync(filename, "utf8") !== contents, | |
| 18 | ) | |
| 19 | .map(([filename]) => NodePath.relative(import.meta.dirname, filename)); | |
| 20 | ||
| 21 | expect( | |
| 22 | staleOutputs, | |
| 23 | "Run `vp run --filter @t3tools/mobile generate` and commit the generated outputs.", | |
| 24 | ).toEqual([]); | |
| 25 | }); | |
| 26 | ||
| 27 | it("registers every custom palette for both appearances", () => { | |
| 28 | expect(customThemeNames).toEqual([ | |
| 29 | "t3-chat-light", | |
| 30 | "t3-chat-dark", | |
| 31 | "grove-light", | |
| 32 | "grove-dark", | |
| 33 | "ocean-light", | |
| 34 | "ocean-dark", | |
| 35 | "ember-light", | |
| 36 | "ember-dark", | |
| 37 | "iris-light", | |
| 38 | "iris-dark", | |
| 39 | ]); | |
| 40 | ||
| 41 | const stylesheet = renderUniwindThemesCSS(); | |
| 42 | for (const themeName of customThemeNames) { | |
| 43 | expect(stylesheet.match(new RegExp(`@variant ${themeName} \\{`, "gu"))).toHaveLength(1); | |
| 44 | } | |
| 45 | }); | |
| 46 | ||
| 47 | it("generates the default runtime bridge from the authored CSS", () => { | |
| 48 | const css = NodeFS.readFileSync(NodePath.resolve(import.meta.dirname, "../global.css"), "utf8"); | |
| 49 | const variables = readDefaultThemeVariables(css); | |
| 50 | ||
| 51 | expect(variables.light["--color-screen"]).toBe("#f2f2f7"); | |
| 52 | expect(variables.dark["--color-screen"]).toBe("#0a0a0a"); | |
| 53 | expect(Object.keys(variables.light)).toEqual(Object.keys(variables.dark)); | |
| 54 | }); | |
| 55 | }); |
apps/mobile/src/App.tsx+1-4| ... | ... | @@ -21,7 +21,6 @@ import { RootStack } from "./Stack"; |
| 21 | 21 | import { appAtomRegistry } from "./state/atom-registry"; |
| 22 | 22 | import { OverlayPortalHost } from "./components/OverlayPortal"; |
| 23 | 23 | import { appBlurTargetRef } from "./lib/appBlurTarget"; |
| 24 | import { useThemeColor } from "./lib/useThemeColor"; | |
| 25 | 24 | import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme"; |
| 26 | 25 | |
| 27 | 26 | import "../global.css"; |
| ... | ... | @@ -72,8 +71,7 @@ export default function App() { |
| 72 | 71 | |
| 73 | 72 | function AppContent() { |
| 74 | 73 | const { themeAppearance } = useAppearancePreferences(); |
| 75 | const statusBarBg = useThemeColor("--color-status-bar"); | |
| 76 | const navigationTheme = useMobileNavigationTheme(themeAppearance); | |
| 74 | const navigationTheme = useMobileNavigationTheme(); | |
| 77 | 75 | |
| 78 | 76 | return ( |
| 79 | 77 | <> |
| ... | ... | @@ -83,7 +81,6 @@ function AppContent() { |
| 83 | 81 | <SafeAreaProvider> |
| 84 | 82 | <StatusBar |
| 85 | 83 | barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"} |
| 86 | backgroundColor={statusBarBg} | |
| 87 | 84 | translucent |
| 88 | 85 | /> |
| 89 | 86 | {/* The navigation theme drives the NATIVE header appearance: native-stack |
apps/mobile/src/components/AndroidAnchoredMenu.tsx+6-11| ... | ... | @@ -9,7 +9,6 @@ import Animated, { FadeIn } from "react-native-reanimated"; |
| 9 | 9 | |
| 10 | 10 | import { appBlurTargetRef } from "../lib/appBlurTarget"; |
| 11 | 11 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 12 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 13 | 12 | import { cn } from "../lib/cn"; |
| 14 | 13 | import { type AppSymbolName, SymbolView } from "./AppSymbol"; |
| 15 | 14 | import { AppText as Text } from "./AppText"; |
| ... | ... | @@ -84,11 +83,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { |
| 84 | 83 | const isDarkMode = themeAppearance === "dark"; |
| 85 | 84 | const keyboardVisible = useKeyboardState((state) => state.isVisible); |
| 86 | 85 | const keyboardHeight = useKeyboardState((state) => state.height); |
| 87 | const rippleColor = useThemeColor("--color-subtle"); | |
| 88 | const iconColor = useThemeColor("--color-icon"); | |
| 89 | const iconSubtleColor = useThemeColor("--color-icon-subtle"); | |
| 90 | const dangerColor = useThemeColor("--color-danger-foreground"); | |
| 91 | ||
| 92 | 86 | const close = useCallback(() => { |
| 93 | 87 | setAnchor(null); |
| 94 | 88 | setPath([]); |
| ... | ... | @@ -279,10 +273,9 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { |
| 279 | 273 | return ( |
| 280 | 274 | <Pressable |
| 281 | 275 | key={action.id ?? `${index}-${action.title}`} |
| 282 | android_ripple={{ color: rippleColor }} | |
| 283 | 276 | disabled={disabled} |
| 284 | 277 | className={cn( |
| 285 | "min-h-11 flex-row items-center gap-2.5 px-3.5 py-2.5", | |
| 278 | "min-h-11 flex-row items-center gap-2.5 px-3.5 py-2.5 active:bg-subtle", | |
| 286 | 279 | disabled && "opacity-45", |
| 287 | 280 | )} |
| 288 | 281 | onPress={() => onPressItem(action)} |
| ... | ... | @@ -307,21 +300,23 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) { |
| 307 | 300 | <SymbolView |
| 308 | 301 | name="chevron.right" |
| 309 | 302 | size={13} |
| 310 | tintColor={iconSubtleColor} | |
| 303 | tintColorClassName={"accent-icon-subtle"} | |
| 311 | 304 | type="monochrome" |
| 312 | 305 | /> |
| 313 | 306 | ) : action.state === "on" ? ( |
| 314 | 307 | <SymbolView |
| 315 | 308 | name="checkmark" |
| 316 | 309 | size={15} |
| 317 | tintColor={iconColor} | |
| 310 | tintColorClassName={"accent-icon"} | |
| 318 | 311 | type="monochrome" |
| 319 | 312 | /> |
| 320 | 313 | ) : action.image ? ( |
| 321 | 314 | <SymbolView |
| 322 | 315 | name={action.image as AppSymbolName} |
| 323 | 316 | size={15} |
| 324 | tintColor={destructive ? dangerColor : iconColor} | |
| 317 | tintColorClassName={ | |
| 318 | destructive ? "accent-danger-foreground" : "accent-icon" | |
| 319 | } | |
| 325 | 320 | type="monochrome" |
| 326 | 321 | /> |
| 327 | 322 | ) : null} |
apps/mobile/src/components/AndroidScreenHeader.tsx+2-7| ... | ... | @@ -5,7 +5,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 5 | 5 | import { SymbolView, type AppSymbolName } from "./AppSymbol"; |
| 6 | 6 | import { AppText as Text } from "./AppText"; |
| 7 | 7 | import { cn } from "../lib/cn"; |
| 8 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 9 | 8 | |
| 10 | 9 | export interface AndroidHeaderAction { |
| 11 | 10 | readonly accessibilityLabel: string; |
| ... | ... | @@ -20,9 +19,6 @@ export function AndroidHeaderIconButton(props: { |
| 20 | 19 | readonly onPress?: () => void; |
| 21 | 20 | readonly disabled?: boolean; |
| 22 | 21 | }) { |
| 23 | const foregroundColor = useThemeColor("--color-foreground"); | |
| 24 | const disabledColor = useThemeColor("--color-icon-subtle"); | |
| 25 | ||
| 26 | 22 | return ( |
| 27 | 23 | <Pressable |
| 28 | 24 | accessibilityLabel={props.accessibilityLabel} |
| ... | ... | @@ -38,7 +34,7 @@ export function AndroidHeaderIconButton(props: { |
| 38 | 34 | <SymbolView |
| 39 | 35 | name={props.icon} |
| 40 | 36 | size={20} |
| 41 | tintColor={props.disabled ? disabledColor : foregroundColor} | |
| 37 | tintColorClassName={props.disabled ? "accent-icon-subtle" : "accent-foreground"} | |
| 42 | 38 | type="monochrome" |
| 43 | 39 | /> |
| 44 | 40 | </Pressable> |
| ... | ... | @@ -54,7 +50,6 @@ export function AndroidScreenHeader(props: { |
| 54 | 50 | readonly embedded?: boolean; |
| 55 | 51 | }) { |
| 56 | 52 | const insets = useSafeAreaInsets(); |
| 57 | const foregroundColor = useThemeColor("--color-foreground"); | |
| 58 | 53 | |
| 59 | 54 | return ( |
| 60 | 55 | <View |
| ... | ... | @@ -75,7 +70,7 @@ export function AndroidScreenHeader(props: { |
| 75 | 70 | <SymbolView |
| 76 | 71 | name="chevron.left" |
| 77 | 72 | size={24} |
| 78 | tintColor={foregroundColor} | |
| 73 | tintColorClassName={"accent-foreground"} | |
| 79 | 74 | type="monochrome" |
| 80 | 75 | /> |
| 81 | 76 | </Pressable> |
apps/mobile/src/components/AppSymbol.tsx+10-1| ... | ... | @@ -81,6 +81,7 @@ import { |
| 81 | 81 | } from "@tabler/icons-react-native"; |
| 82 | 82 | import { Platform } from "react-native"; |
| 83 | 83 | import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols"; |
| 84 | import { withUniwind } from "uniwind"; | |
| 84 | 85 | |
| 85 | 86 | const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = { |
| 86 | 87 | "arrow.branch": IconGitBranch, |
| ... | ... | @@ -190,7 +191,7 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = { |
| 190 | 191 | export type { SFSymbol } from "expo-symbols"; |
| 191 | 192 | export type AppSymbolName = SymbolViewProps["name"]; |
| 192 | 193 | |
| 193 | export function SymbolView(props: SymbolViewProps) { | |
| 194 | function AppSymbolView(props: SymbolViewProps) { | |
| 194 | 195 | if (Platform.OS !== "android") { |
| 195 | 196 | return <ExpoSymbolView {...props} />; |
| 196 | 197 | } |
| ... | ... | @@ -216,3 +217,11 @@ export function SymbolView(props: SymbolViewProps) { |
| 216 | 217 | /> |
| 217 | 218 | ); |
| 218 | 219 | } |
| 220 | ||
| 221 | /** | |
| 222 | * expo-symbols and the Android Tabler fallback both expose tint as a native | |
| 223 | * prop rather than a React Native style. Keep that third-party boundary here | |
| 224 | * so callers can use Uniwind's `tintColorClassName` instead of subscribing to | |
| 225 | * theme variables in every parent component. | |
| 226 | */ | |
| 227 | export const SymbolView = withUniwind(AppSymbolView); |
apps/mobile/src/components/CompactBrandTitle.tsx+6-32| ... | ... | @@ -9,7 +9,6 @@ import { AppText as Text } from "./AppText"; |
| 9 | 9 | import { T3Wordmark } from "./T3Wordmark"; |
| 10 | 10 | import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics"; |
| 11 | 11 | import { resolveMobileStageLabel } from "../lib/mobileBranding"; |
| 12 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 13 | 12 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass"; |
| 14 | 13 | |
| 15 | 14 | // Native leading items inherit different UIKit margins than title views. |
| ... | ... | @@ -37,9 +36,6 @@ export function CompactBrandTitle( |
| 37 | 36 | readonly nativeLeadingItem?: boolean; |
| 38 | 37 | } = {}, |
| 39 | 38 | ) { |
| 40 | const iconColor = useThemeColor("--color-icon"); | |
| 41 | const mutedColor = useThemeColor("--color-foreground-muted"); | |
| 42 | const subtleColor = useThemeColor("--color-subtle"); | |
| 43 | 39 | const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); |
| 44 | 40 | const titleOffset = brandTitleOffset(props.nativeLeadingItem === true); |
| 45 | 41 | |
| ... | ... | @@ -49,42 +45,20 @@ export function CompactBrandTitle( |
| 49 | 45 | accessibilityLabel="T3 Code, Threads" |
| 50 | 46 | accessible |
| 51 | 47 | role="heading" |
| 52 | style={{ | |
| 53 | alignItems: "center", | |
| 54 | flexDirection: "row", | |
| 55 | gap: 6, | |
| 56 | marginLeft: titleOffset, | |
| 57 | }} | |
| 48 | className="flex-row items-center gap-1.5" | |
| 49 | style={{ marginLeft: titleOffset }} | |
| 58 | 50 | > |
| 59 | <T3Wordmark color={iconColor} height={15} /> | |
| 51 | <T3Wordmark colorClassName="accent-icon" height={15} /> | |
| 60 | 52 | <Text |
| 61 | 53 | allowFontScaling={props.allowFontScaling} |
| 62 | style={{ | |
| 63 | color: mutedColor, | |
| 64 | fontFamily: "DMSans-Medium", | |
| 65 | fontSize: 21, | |
| 66 | letterSpacing: -0.5, | |
| 67 | }} | |
| 54 | className="font-t3-medium text-[21px] tracking-[-0.5px] text-foreground-muted" | |
| 68 | 55 | > |
| 69 | 56 | Code |
| 70 | 57 | </Text> |
| 71 | <View | |
| 72 | style={{ | |
| 73 | backgroundColor: subtleColor, | |
| 74 | borderRadius: 999, | |
| 75 | paddingHorizontal: 6, | |
| 76 | paddingVertical: 2, | |
| 77 | }} | |
| 78 | > | |
| 58 | <View className="rounded-full bg-subtle px-1.5 py-0.5"> | |
| 79 | 59 | <Text |
| 80 | 60 | allowFontScaling={props.allowFontScaling} |
| 81 | style={{ | |
| 82 | color: mutedColor, | |
| 83 | fontFamily: "DMSans-Bold", | |
| 84 | fontSize: 9, | |
| 85 | letterSpacing: 0.9, | |
| 86 | textTransform: "uppercase", | |
| 87 | }} | |
| 61 | className="font-t3-bold text-[9px] tracking-[0.9px] text-foreground-muted uppercase" | |
| 88 | 62 | > |
| 89 | 63 | {stageLabel} |
| 90 | 64 | </Text> |
apps/mobile/src/components/ComposerAttachmentStrip.tsx+1-3| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | import { SymbolView } from "../components/AppSymbol"; |
| 2 | 2 | import { Image, Pressable, ScrollView, View } from "react-native"; |
| 3 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 4 | 3 | |
| 5 | 4 | import type { DraftComposerImageAttachment } from "../lib/composerImages"; |
| 6 | 5 | |
| ... | ... | @@ -24,7 +23,6 @@ export interface ComposerAttachmentStripProps { |
| 24 | 23 | * buttons. Used by both the thread composer and the new-task draft screen. |
| 25 | 24 | */ |
| 26 | 25 | export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) { |
| 27 | const subtleBg = useThemeColor("--color-subtle"); | |
| 28 | 26 | const size = props.imageSize ?? 72; |
| 29 | 27 | const radius = props.imageBorderRadius ?? 16; |
| 30 | 28 | const removeButtonPlacement = props.removeButtonPlacement ?? "overlay"; |
| ... | ... | @@ -56,11 +54,11 @@ export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) { |
| 56 | 54 | > |
| 57 | 55 | <Image |
| 58 | 56 | source={{ uri: image.previewUri }} |
| 57 | className="bg-subtle" | |
| 59 | 58 | style={{ |
| 60 | 59 | width: size, |
| 61 | 60 | height: size, |
| 62 | 61 | borderRadius: radius, |
| 63 | backgroundColor: subtleBg, | |
| 64 | 62 | }} |
| 65 | 63 | resizeMode="cover" |
| 66 | 64 | /> |
apps/mobile/src/components/ComposerToolbar.tsx+65-54| ... | ... | @@ -11,9 +11,6 @@ import { |
| 11 | 11 | type ViewStyle, |
| 12 | 12 | } from "react-native"; |
| 13 | 13 | |
| 14 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 15 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | |
| 16 | import { themeColorWithAlpha } from "../lib/mobileTheme"; | |
| 17 | 14 | import { cn } from "../lib/cn"; |
| 18 | 15 | import { AppText as Text } from "./AppText"; |
| 19 | 16 | import { SymbolView } from "./AppSymbol"; |
| ... | ... | @@ -42,10 +39,6 @@ export function ComposerInlineControl(props: { |
| 42 | 39 | readonly chevronDirection?: "down" | "right"; |
| 43 | 40 | readonly showChevron?: boolean; |
| 44 | 41 | }) { |
| 45 | const iconColor = useThemeColor( | |
| 46 | props.emphasized || props.selected ? "--color-icon" : "--color-icon-muted", | |
| 47 | ); | |
| 48 | ||
| 49 | 42 | return ( |
| 50 | 43 | <Pressable |
| 51 | 44 | accessibilityLabel={props.accessibilityLabel ?? props.label} |
| ... | ... | @@ -62,7 +55,14 @@ export function ComposerInlineControl(props: { |
| 62 | 55 | {props.iconNode ? ( |
| 63 | 56 | <View className="size-4 shrink-0 items-center justify-center">{props.iconNode}</View> |
| 64 | 57 | ) : props.icon ? ( |
| 65 | <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" /> | |
| 58 | <SymbolView | |
| 59 | name={props.icon} | |
| 60 | size={16} | |
| 61 | tintColorClassName={ | |
| 62 | props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted" | |
| 63 | } | |
| 64 | type="monochrome" | |
| 65 | /> | |
| 66 | 66 | ) : null} |
| 67 | 67 | <Text |
| 68 | 68 | className={cn( |
| ... | ... | @@ -77,7 +77,9 @@ export function ComposerInlineControl(props: { |
| 77 | 77 | <SymbolView |
| 78 | 78 | name={props.chevronDirection === "right" ? "chevron.right" : "chevron.down"} |
| 79 | 79 | size={10} |
| 80 | tintColor={iconColor} | |
| 80 | tintColorClassName={ | |
| 81 | props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted" | |
| 82 | } | |
| 81 | 83 | type="monochrome" |
| 82 | 84 | /> |
| 83 | 85 | )} |
| ... | ... | @@ -111,8 +113,12 @@ export function ComposerToolbarRow(props: { |
| 111 | 113 | |
| 112 | 114 | export function ComposerToolbarScroller(props: { |
| 113 | 115 | readonly children: ReactNode; |
| 114 | readonly fadeOpaque: string; | |
| 115 | readonly fadeTransparent: string; | |
| 116 | /** Only for non-Uniwind surfaces such as the native terminal palette. */ | |
| 117 | readonly fadeOpaque?: string; | |
| 118 | /** Only for non-Uniwind surfaces such as the native terminal palette. */ | |
| 119 | readonly fadeTransparent?: string; | |
| 120 | /** Semantic Uniwind surface behind the toolbar. Defaults to card. */ | |
| 121 | readonly fadeSurface?: "card" | "sheet"; | |
| 116 | 122 | readonly contentPaddingRight?: number; |
| 117 | 123 | }) { |
| 118 | 124 | const [metrics, setMetrics] = useState({ |
| ... | ... | @@ -170,27 +176,37 @@ export function ComposerToolbarScroller(props: { |
| 170 | 176 | </ScrollView> |
| 171 | 177 | {scrollEdges.showLeftFade ? ( |
| 172 | 178 | <View |
| 179 | className={cn( | |
| 180 | "absolute inset-y-0 left-0", | |
| 181 | props.fadeOpaque === undefined && "bg-linear-to-r to-transparent", | |
| 182 | props.fadeOpaque === undefined && | |
| 183 | (props.fadeSurface === "sheet" ? "from-sheet" : "from-card"), | |
| 184 | )} | |
| 173 | 185 | pointerEvents="none" |
| 174 | 186 | style={{ |
| 175 | bottom: 0, | |
| 176 | experimental_backgroundImage: `linear-gradient(to right, ${props.fadeOpaque} 0%, ${props.fadeTransparent} 100%)`, | |
| 177 | left: 0, | |
| 178 | position: "absolute", | |
| 179 | top: 0, | |
| 180 | 187 | width: COMPOSER_TOOLBAR_FADE_WIDTH, |
| 188 | experimental_backgroundImage: | |
| 189 | props.fadeOpaque === undefined | |
| 190 | ? undefined | |
| 191 | : `linear-gradient(to right, ${props.fadeOpaque} 0%, ${props.fadeTransparent ?? "transparent"} 100%)`, | |
| 181 | 192 | }} |
| 182 | 193 | /> |
| 183 | 194 | ) : null} |
| 184 | 195 | {scrollEdges.showRightFade ? ( |
| 185 | 196 | <View |
| 197 | className={cn( | |
| 198 | "absolute inset-y-0 right-0", | |
| 199 | props.fadeOpaque === undefined && "bg-linear-to-r from-transparent", | |
| 200 | props.fadeOpaque === undefined && | |
| 201 | (props.fadeSurface === "sheet" ? "to-sheet" : "to-card"), | |
| 202 | )} | |
| 186 | 203 | pointerEvents="none" |
| 187 | 204 | style={{ |
| 188 | bottom: 0, | |
| 189 | experimental_backgroundImage: `linear-gradient(to right, ${props.fadeTransparent} 0%, ${props.fadeOpaque} 100%)`, | |
| 190 | position: "absolute", | |
| 191 | right: 0, | |
| 192 | top: 0, | |
| 193 | 205 | width: COMPOSER_TOOLBAR_FADE_WIDTH, |
| 206 | experimental_backgroundImage: | |
| 207 | props.fadeOpaque === undefined | |
| 208 | ? undefined | |
| 209 | : `linear-gradient(to right, ${props.fadeTransparent ?? "transparent"} 0%, ${props.fadeOpaque} 100%)`, | |
| 194 | 210 | }} |
| 195 | 211 | /> |
| 196 | 212 | ) : null} |
| ... | ... | @@ -214,30 +230,16 @@ export function ComposerToolbarButton(props: { |
| 214 | 230 | readonly className?: string; |
| 215 | 231 | readonly style?: StyleProp<ViewStyle>; |
| 216 | 232 | }) { |
| 217 | const { themeAppearance } = useAppearancePreferences(); | |
| 218 | const isDarkMode = themeAppearance === "dark"; | |
| 219 | const iconColor = useThemeColor("--color-icon"); | |
| 220 | const iconSubtle = useThemeColor("--color-icon-subtle"); | |
| 221 | const primaryFg = useThemeColor("--color-primary-foreground"); | |
| 222 | const dangerFg = useThemeColor("--color-danger-foreground"); | |
| 223 | 233 | const variant = props.variant ?? "default"; |
| 224 | 234 | const isCircle = !props.label && props.showChevron === false; |
| 225 | const defaultBorderColor = useThemeColor("--color-border-subtle"); | |
| 226 | const activeBorderColor = useThemeColor("--color-border"); | |
| 227 | const filledBorderColor = | |
| 228 | variant === "danger" | |
| 229 | ? themeColorWithAlpha(String(dangerFg), 0.14) | |
| 230 | : props.disabled | |
| 231 | ? defaultBorderColor | |
| 232 | : themeColorWithAlpha(String(primaryFg), 0.18); | |
| 233 | const iconTintColor = | |
| 235 | const iconTintClassName = | |
| 234 | 236 | variant === "primary" |
| 235 | 237 | ? props.disabled |
| 236 | ? iconSubtle | |
| 237 | : primaryFg | |
| 238 | ? "accent-icon-subtle" | |
| 239 | : "accent-primary-foreground" | |
| 238 | 240 | : variant === "danger" |
| 239 | ? dangerFg | |
| 240 | : iconColor; | |
| 241 | ? "accent-danger-foreground" | |
| 242 | : "accent-icon"; | |
| 241 | 243 | |
| 242 | 244 | return ( |
| 243 | 245 | <Pressable |
| ... | ... | @@ -250,7 +252,7 @@ export function ComposerToolbarButton(props: { |
| 250 | 252 | // so callers can lift it with max-w-full — flex-filling pills in the |
| 251 | 253 | // thread composer stretch to the row's edge. The numeric maxWidth |
| 252 | 254 | // prop still wins via the inline style below. |
| 253 | "h-11 max-w-[172px] flex-row items-center justify-center rounded-full active:opacity-70", | |
| 255 | "h-11 max-w-[172px] flex-row items-center justify-center rounded-full border shadow-lg shadow-adaptive-black-a10-a25 active:opacity-70", | |
| 254 | 256 | isCircle ? "w-11" : "gap-2 px-3.5", |
| 255 | 257 | variant === "primary" |
| 256 | 258 | ? props.disabled |
| ... | ... | @@ -261,24 +263,23 @@ export function ComposerToolbarButton(props: { |
| 261 | 263 | : props.active |
| 262 | 264 | ? "bg-subtle-strong" |
| 263 | 265 | : "bg-subtle", |
| 266 | variant === "default" | |
| 267 | ? props.active | |
| 268 | ? "border-border" | |
| 269 | : "border-border-subtle" | |
| 270 | : variant === "danger" | |
| 271 | ? "border-danger-border" | |
| 272 | : props.disabled | |
| 273 | ? "border-border-subtle" | |
| 274 | : "border-primary-foreground/20", | |
| 264 | 275 | props.className, |
| 265 | 276 | )} |
| 266 | 277 | style={({ pressed }) => [ |
| 267 | 278 | { |
| 268 | borderColor: | |
| 269 | variant === "default" | |
| 270 | ? props.active | |
| 271 | ? activeBorderColor | |
| 272 | : defaultBorderColor | |
| 273 | : filledBorderColor, | |
| 274 | borderWidth: 1, | |
| 275 | 279 | maxWidth: props.maxWidth, |
| 276 | 280 | minWidth: props.minWidth, |
| 277 | 281 | opacity: props.disabled ? 0.55 : pressed ? 0.72 : 1, |
| 278 | shadowColor: "#000", | |
| 279 | shadowOffset: { width: 0, height: isDarkMode ? 3 : 2 }, | |
| 280 | shadowOpacity: props.disabled ? 0 : isDarkMode ? 0.24 : 0.08, | |
| 281 | shadowRadius: isDarkMode ? 10 : 8, | |
| 282 | shadowOpacity: props.disabled ? 0 : 1, | |
| 282 | 283 | }, |
| 283 | 284 | props.style, |
| 284 | 285 | ]} |
| ... | ... | @@ -286,7 +287,12 @@ export function ComposerToolbarButton(props: { |
| 286 | 287 | {props.iconNode ? ( |
| 287 | 288 | <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View> |
| 288 | 289 | ) : props.icon ? ( |
| 289 | <SymbolView name={props.icon} size={16} tintColor={iconTintColor} type="monochrome" /> | |
| 290 | <SymbolView | |
| 291 | name={props.icon} | |
| 292 | size={16} | |
| 293 | tintColorClassName={iconTintClassName} | |
| 294 | type="monochrome" | |
| 295 | /> | |
| 290 | 296 | ) : null} |
| 291 | 297 | {props.label ? ( |
| 292 | 298 | <Text |
| ... | ... | @@ -306,7 +312,12 @@ export function ComposerToolbarButton(props: { |
| 306 | 312 | </Text> |
| 307 | 313 | ) : null} |
| 308 | 314 | {props.showChevron === false ? null : ( |
| 309 | <SymbolView name="chevron.down" size={11} tintColor={iconTintColor} type="monochrome" /> | |
| 315 | <SymbolView | |
| 316 | name="chevron.down" | |
| 317 | size={11} | |
| 318 | tintColorClassName={iconTintClassName} | |
| 319 | type="monochrome" | |
| 320 | /> | |
| 310 | 321 | )} |
| 311 | 322 | </Pressable> |
| 312 | 323 | ); |
apps/mobile/src/components/ConfirmDialogHost.tsx+2-7| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import { useCallback, useEffect, useState } from "react"; |
| 2 | 2 | import { Modal, Pressable, View } from "react-native"; |
| 3 | 3 | |
| 4 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 5 | 4 | import { cn } from "../lib/cn"; |
| 6 | 5 | import { AppText } from "./AppText"; |
| 7 | 6 | |
| ... | ... | @@ -35,8 +34,6 @@ export function showConfirmDialog(request: ConfirmDialogRequest): void { |
| 35 | 34 | */ |
| 36 | 35 | export function ConfirmDialogHost() { |
| 37 | 36 | const [request, setRequest] = useState<ConfirmDialogRequest | null>(null); |
| 38 | const pressedOverlay = useThemeColor("--color-subtle"); | |
| 39 | ||
| 40 | 37 | useEffect(() => { |
| 41 | 38 | presentRequest = setRequest; |
| 42 | 39 | return () => { |
| ... | ... | @@ -76,8 +73,7 @@ export function ConfirmDialogHost() { |
| 76 | 73 | <View className="overflow-hidden rounded-full"> |
| 77 | 74 | <Pressable |
| 78 | 75 | accessibilityRole="button" |
| 79 | className="min-h-10 items-center justify-center px-4" | |
| 80 | android_ripple={{ color: pressedOverlay }} | |
| 76 | className="min-h-10 items-center justify-center px-4 active:bg-subtle" | |
| 81 | 77 | onPress={handleCancel} |
| 82 | 78 | > |
| 83 | 79 | <AppText className="text-base font-t3-medium"> |
| ... | ... | @@ -88,8 +84,7 @@ export function ConfirmDialogHost() { |
| 88 | 84 | <View className="overflow-hidden rounded-full"> |
| 89 | 85 | <Pressable |
| 90 | 86 | accessibilityRole="button" |
| 91 | className="min-h-10 items-center justify-center px-4" | |
| 92 | android_ripple={{ color: pressedOverlay }} | |
| 87 | className="min-h-10 items-center justify-center px-4 active:bg-subtle" | |
| 93 | 88 | onPress={handleConfirm} |
| 94 | 89 | > |
| 95 | 90 | <AppText |
apps/mobile/src/components/ControlPill.tsx+11-11| ... | ... | @@ -9,7 +9,6 @@ import { |
| 9 | 9 | useRef, |
| 10 | 10 | } from "react"; |
| 11 | 11 | import { Platform, Pressable, View } from "react-native"; |
| 12 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 13 | 12 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 14 | 13 | |
| 15 | 14 | import { cn } from "../lib/cn"; |
| ... | ... | @@ -49,18 +48,14 @@ export function ControlPill(props: { |
| 49 | 48 | props.onPress?.(); |
| 50 | 49 | }; |
| 51 | 50 | |
| 52 | const iconColor = useThemeColor("--color-icon"); | |
| 53 | const iconSubtle = useThemeColor("--color-icon-subtle"); | |
| 54 | const primaryFg = useThemeColor("--color-primary-foreground"); | |
| 55 | const dangerFg = useThemeColor("--color-danger-foreground"); | |
| 56 | const iconTintColor = | |
| 51 | const iconTintClassName = | |
| 57 | 52 | variant === "primary" |
| 58 | 53 | ? props.disabled |
| 59 | ? iconSubtle | |
| 60 | : primaryFg | |
| 54 | ? "accent-icon-subtle" | |
| 55 | : "accent-primary-foreground" | |
| 61 | 56 | : variant === "danger" |
| 62 | ? dangerFg | |
| 63 | : iconColor; | |
| 57 | ? "accent-danger-foreground" | |
| 58 | : "accent-icon"; | |
| 64 | 59 | |
| 65 | 60 | const isCircle = |
| 66 | 61 | variant === "circle" || variant === "danger" || (variant === "primary" && !props.label); |
| ... | ... | @@ -101,7 +96,12 @@ export function ControlPill(props: { |
| 101 | 96 | {props.iconNode ? ( |
| 102 | 97 | <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View> |
| 103 | 98 | ) : props.icon ? ( |
| 104 | <SymbolView name={props.icon} size={16} tintColor={iconTintColor} type="monochrome" /> | |
| 99 | <SymbolView | |
| 100 | name={props.icon} | |
| 101 | size={16} | |
| 102 | tintColorClassName={iconTintClassName} | |
| 103 | type="monochrome" | |
| 104 | /> | |
| 105 | 105 | ) : null} |
| 106 | 106 | {props.label ? <Text className={labelClassName}>{props.label}</Text> : null} |
| 107 | 107 | </Pressable> |
apps/mobile/src/components/ErrorBanner.tsx+2-4| ... | ... | @@ -3,10 +3,8 @@ import { View } from "react-native"; |
| 3 | 3 | import { AppText as Text } from "./AppText"; |
| 4 | 4 | export function ErrorBanner(props: { readonly message: string }) { |
| 5 | 5 | return ( |
| 6 | <View className="rounded-2xl border border-rose-300/70 bg-rose-100/80 px-3.5 py-3 dark:border-rose-400/28 dark:bg-rose-500/12"> | |
| 7 | <Text className="font-t3-medium text-sm text-rose-700 dark:text-rose-300"> | |
| 8 | {props.message} | |
| 9 | </Text> | |
| 6 | <View className="rounded-2xl border border-adaptive-rose-300-a70-400-a28 bg-adaptive-rose-100-a80-500-a12 px-3.5 py-3"> | |
| 7 | <Text className="font-t3-medium text-sm text-adaptive-rose-700-300">{props.message}</Text> | |
| 10 | 8 | </View> |
| 11 | 9 | ); |
| 12 | 10 | } |
apps/mobile/src/components/GlassSafeAreaView.tsx+1-12| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import type { ReactNode } from "react"; |
| 2 | 2 | import { View, type StyleProp, type ViewStyle } from "react-native"; |
| 3 | 3 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 4 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 5 | 4 | |
| 6 | 5 | import { GlassSurface } from "./GlassSurface"; |
| 7 | 6 | |
| ... | ... | @@ -19,23 +18,13 @@ export function GlassSafeAreaView({ |
| 19 | 18 | style, |
| 20 | 19 | }: GlassSafeAreaViewProps) { |
| 21 | 20 | const insets = useSafeAreaInsets(); |
| 22 | const headerColor = useThemeColor("--color-header"); | |
| 23 | const headerBorderColor = useThemeColor("--color-header-border"); | |
| 24 | const glassTint = useThemeColor("--color-glass-tint"); | |
| 25 | 21 | const headerPaddingTop = insets.top + 16; |
| 26 | const surfaceStyle = { | |
| 27 | borderRadius: 0, | |
| 28 | backgroundColor: headerColor, | |
| 29 | borderBottomWidth: 1, | |
| 30 | borderBottomColor: headerBorderColor, | |
| 31 | } as const; | |
| 32 | 22 | |
| 33 | 23 | return ( |
| 34 | <View style={[surfaceStyle, style]}> | |
| 24 | <View className="border-b border-header-border bg-header" style={style}> | |
| 35 | 25 | <GlassSurface |
| 36 | 26 | chrome="none" |
| 37 | 27 | glassEffectStyle="regular" |
| 38 | tintColor={glassTint} | |
| 39 | 28 | style={{ borderRadius: 0, backgroundColor: "transparent" }} |
| 40 | 29 | > |
| 41 | 30 | <View |
apps/mobile/src/components/GlassSurface.tsx+37-15| ... | ... | @@ -2,22 +2,29 @@ import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect"; |
| 2 | 2 | import type { ReactNode } from "react"; |
| 3 | 3 | import { |
| 4 | 4 | Platform, |
| 5 | useColorScheme, | |
| 5 | 6 | View, |
| 6 | 7 | type ColorValue, |
| 7 | 8 | type StyleProp, |
| 8 | 9 | type ViewProps, |
| 9 | 10 | type ViewStyle, |
| 10 | 11 | } from "react-native"; |
| 11 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 12 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | |
| 12 | import { withUniwind } from "uniwind"; | |
| 13 | 13 | |
| 14 | interface GlassSurfaceProps extends Omit<ViewProps, "className"> { | |
| 14 | import { cn } from "../lib/cn"; | |
| 15 | ||
| 16 | const ThemedGlassView = withUniwind(GlassView); | |
| 17 | ||
| 18 | interface GlassSurfaceProps extends ViewProps { | |
| 15 | 19 | readonly children: ReactNode; |
| 16 | 20 | readonly glassEffectStyle?: "clear" | "regular" | "none"; |
| 17 | 21 | readonly tintColor?: ColorValue; |
| 22 | readonly tintColorClassName?: string; | |
| 18 | 23 | readonly chrome?: "default" | "none"; |
| 19 | 24 | /** Styling used only when native Liquid Glass is unavailable. */ |
| 20 | 25 | readonly fallbackStyle?: StyleProp<ViewStyle>; |
| 26 | /** Uniwind styling used only when native Liquid Glass is unavailable. */ | |
| 27 | readonly fallbackClassName?: string; | |
| 21 | 28 | } |
| 22 | 29 | |
| 23 | 30 | export function GlassSurface({ |
| ... | ... | @@ -25,22 +32,18 @@ export function GlassSurface({ |
| 25 | 32 | glassEffectStyle = "regular", |
| 26 | 33 | chrome = "default", |
| 27 | 34 | tintColor, |
| 35 | tintColorClassName, | |
| 28 | 36 | fallbackStyle, |
| 37 | fallbackClassName, | |
| 38 | className, | |
| 29 | 39 | style, |
| 30 | 40 | ...props |
| 31 | 41 | }: GlassSurfaceProps) { |
| 32 | const { themeAppearance } = useAppearancePreferences(); | |
| 33 | const isDarkMode = themeAppearance === "dark"; | |
| 34 | const borderColor = useThemeColor("--color-border"); | |
| 35 | const glassSurface = useThemeColor("--color-glass-surface"); | |
| 36 | const glassTint = useThemeColor("--color-glass-tint"); | |
| 42 | const isDarkMode = useColorScheme() === "dark"; | |
| 37 | 43 | const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable(); |
| 38 | 44 | const surfaceStyle: ViewStyle = { |
| 39 | 45 | borderRadius: 32, |
| 40 | 46 | overflow: "hidden", |
| 41 | borderWidth: chrome === "none" ? 0 : 1, | |
| 42 | borderColor: chrome === "none" ? "transparent" : borderColor, | |
| 43 | backgroundColor: chrome === "none" ? "transparent" : glassSurface, | |
| 44 | 47 | shadowColor: chrome === "none" ? "transparent" : "#000000", |
| 45 | 48 | shadowOpacity: chrome === "none" ? 0 : isDarkMode ? 0.22 : 0.08, |
| 46 | 49 | shadowRadius: chrome === "none" ? 0 : 28, |
| ... | ... | @@ -59,20 +62,39 @@ export function GlassSurface({ |
| 59 | 62 | |
| 60 | 63 | if (supportsGlass) { |
| 61 | 64 | return ( |
| 62 | <GlassView | |
| 65 | <ThemedGlassView | |
| 63 | 66 | {...props} |
| 67 | className={cn( | |
| 68 | chrome === "none" | |
| 69 | ? "border-0 border-transparent bg-transparent" | |
| 70 | : "border border-border bg-glass-surface", | |
| 71 | className, | |
| 72 | )} | |
| 64 | 73 | glassEffectStyle={glassEffectStyle} |
| 65 | tintColor={String(tintColor ?? glassTint)} | |
| 74 | tintColor={tintColor === undefined ? undefined : String(tintColor)} | |
| 75 | tintColorClassName={ | |
| 76 | tintColorClassName ?? (tintColor === undefined ? "accent-glass-tint" : undefined) | |
| 77 | } | |
| 66 | 78 | colorScheme={isDarkMode ? "dark" : "light"} |
| 67 | 79 | style={[surfaceStyle, style]} |
| 68 | 80 | > |
| 69 | 81 | {children} |
| 70 | </GlassView> | |
| 82 | </ThemedGlassView> | |
| 71 | 83 | ); |
| 72 | 84 | } |
| 73 | 85 | |
| 74 | 86 | return ( |
| 75 | <View {...props} style={[surfaceStyle, fallbackStyle, style]}> | |
| 87 | <View | |
| 88 | {...props} | |
| 89 | className={cn( | |
| 90 | chrome === "none" | |
| 91 | ? "border-0 border-transparent bg-transparent" | |
| 92 | : "border border-border bg-glass-surface", | |
| 93 | fallbackClassName, | |
| 94 | className, | |
| 95 | )} | |
| 96 | style={[surfaceStyle, fallbackStyle, style]} | |
| 97 | > | |
| 76 | 98 | {children} |
| 77 | 99 | </View> |
| 78 | 100 | ); |
apps/mobile/src/components/LoadingScreen.tsx+1-7| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | import { ActivityIndicator, StatusBar, View } from "react-native"; |
| 2 | 2 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 3 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 4 | 3 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; |
| 5 | 4 | |
| 6 | 5 | import { AppText as Text } from "./AppText"; |
| ... | ... | @@ -11,17 +10,12 @@ export function LoadingScreen(props: { |
| 11 | 10 | readonly messagePlacement?: "above-spinner" | "below-spinner"; |
| 12 | 11 | }) { |
| 13 | 12 | const { themeAppearance: colorScheme } = useAppearancePreferences(); |
| 14 | const screenBg = useThemeColor("--color-screen"); | |
| 15 | 13 | const insets = useSafeAreaInsets(); |
| 16 | 14 | const messagePlacement = props.messagePlacement ?? "below-spinner"; |
| 17 | 15 | |
| 18 | 16 | return ( |
| 19 | 17 | <View className="flex-1 bg-screen" style={{ paddingTop: insets.top }}> |
| 20 | <StatusBar | |
| 21 | barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} | |
| 22 | backgroundColor={screenBg as string} | |
| 23 | translucent | |
| 24 | /> | |
| 18 | <StatusBar barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} translucent /> | |
| 25 | 19 | <View className="flex-1 items-center justify-center gap-5 px-6"> |
| 26 | 20 | <BrandMark compact /> |
| 27 | 21 | {messagePlacement === "above-spinner" ? ( |
apps/mobile/src/components/PierreEntryIcon.tsx+8-3| ... | ... | @@ -3,7 +3,6 @@ import { Image, type ImageStyle, type StyleProp } from "react-native"; |
| 3 | 3 | |
| 4 | 4 | import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; |
| 5 | 5 | import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; |
| 6 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 7 | 6 | |
| 8 | 7 | export function PierreEntryIcon(props: { |
| 9 | 8 | readonly path: string; |
| ... | ... | @@ -12,9 +11,15 @@ export function PierreEntryIcon(props: { |
| 12 | 11 | readonly style?: StyleProp<ImageStyle>; |
| 13 | 12 | }) { |
| 14 | 13 | const size = props.size ?? 16; |
| 15 | const folderColor = useThemeColor("--color-icon-subtle"); | |
| 16 | 14 | if (props.kind === "directory") { |
| 17 | return <SymbolView name="folder" size={size} tintColor={folderColor} type="monochrome" />; | |
| 15 | return ( | |
| 16 | <SymbolView | |
| 17 | name="folder" | |
| 18 | size={size} | |
| 19 | tintColorClassName={"accent-icon-subtle"} | |
| 20 | type="monochrome" | |
| 21 | /> | |
| 22 | ); | |
| 18 | 23 | } |
| 19 | 24 | |
| 20 | 25 | return ( |
apps/mobile/src/components/ProjectFavicon.tsx+1-3| ... | ... | @@ -7,7 +7,6 @@ import { |
| 7 | 7 | getProjectFaviconCacheKey, |
| 8 | 8 | isProjectFaviconFallbackUrl, |
| 9 | 9 | } from "@t3tools/shared/projectFavicon"; |
| 10 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 11 | 10 | import { useAssetUrl } from "../state/assets"; |
| 12 | 11 | import { |
| 13 | 12 | beginProjectFaviconRequest, |
| ... | ... | @@ -62,7 +61,6 @@ function ProjectFaviconImage(props: { |
| 62 | 61 | readonly projectTitle: string; |
| 63 | 62 | readonly size: number; |
| 64 | 63 | }) { |
| 65 | const iconMuted = useThemeColor("--color-icon-subtle"); | |
| 66 | 64 | const faviconRequest = useMemo( |
| 67 | 65 | () => createProjectFaviconRequest(props.cacheKey, props.faviconUrl), |
| 68 | 66 | [props.cacheKey, props.faviconUrl], |
| ... | ... | @@ -97,7 +95,7 @@ function ProjectFaviconImage(props: { |
| 97 | 95 | <SymbolView |
| 98 | 96 | name={{ ios: "folder.fill", android: props.open ? "folder_open" : "folder" }} |
| 99 | 97 | size={props.size * 0.78} |
| 100 | tintColor={iconMuted} | |
| 98 | tintColorClassName={"accent-icon-subtle"} | |
| 101 | 99 | type="monochrome" |
| 102 | 100 | /> |
| 103 | 101 | ) : null} |
apps/mobile/src/components/SourceControlIcon.tsx+14-3| ... | ... | @@ -1,4 +1,7 @@ |
| 1 | 1 | import Svg, { Defs, LinearGradient, Path, Stop } from "react-native-svg"; |
| 2 | import { withUniwind } from "uniwind"; | |
| 3 | ||
| 4 | const ThemedSvg = withUniwind(Svg); | |
| 2 | 5 | |
| 3 | 6 | export type SourceControlIconKind = "github" | "gitlab" | "bitbucket" | "azure-devops"; |
| 4 | 7 | |
| ... | ... | @@ -6,20 +9,28 @@ export function SourceControlIcon(props: { |
| 6 | 9 | readonly kind: SourceControlIconKind; |
| 7 | 10 | readonly size?: number; |
| 8 | 11 | readonly color?: string; |
| 12 | readonly colorClassName?: string; | |
| 9 | 13 | }) { |
| 10 | 14 | const size = props.size ?? 18; |
| 11 | 15 | |
| 12 | 16 | switch (props.kind) { |
| 13 | 17 | case "github": |
| 14 | 18 | return ( |
| 15 | <Svg width={size} height={size} viewBox="0 0 16 16" fill="none"> | |
| 19 | <ThemedSvg | |
| 20 | width={size} | |
| 21 | height={size} | |
| 22 | viewBox="0 0 16 16" | |
| 23 | color={props.color} | |
| 24 | colorClassName={props.colorClassName} | |
| 25 | fill="none" | |
| 26 | > | |
| 16 | 27 | <Path |
| 17 | 28 | fillRule="evenodd" |
| 18 | 29 | clipRule="evenodd" |
| 19 | 30 | d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.68 7.68 0 0 1 8.02 3.86c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z" |
| 20 | fill={props.color ?? "#24292F"} | |
| 31 | fill="currentColor" | |
| 21 | 32 | /> |
| 22 | </Svg> | |
| 33 | </ThemedSvg> | |
| 23 | 34 | ); |
| 24 | 35 | case "gitlab": |
| 25 | 36 | return ( |
apps/mobile/src/components/T3Wordmark.tsx+12-3| ... | ... | @@ -1,11 +1,18 @@ |
| 1 | 1 | import type { ColorValue } from "react-native"; |
| 2 | 2 | import Svg, { Path } from "react-native-svg"; |
| 3 | import { withUniwind } from "uniwind"; | |
| 4 | ||
| 5 | const ThemedPath = withUniwind(Path); | |
| 3 | 6 | |
| 4 | 7 | /** |
| 5 | 8 | * The "T3" brand mark, matching the desktop sidebar's T3Wordmark SVG |
| 6 | 9 | * (apps/web Sidebar.tsx). Width derives from the viewBox aspect ratio. |
| 7 | 10 | */ |
| 8 | export function T3Wordmark(props: { readonly height: number; readonly color: ColorValue }) { | |
| 11 | export function T3Wordmark(props: { | |
| 12 | readonly height: number; | |
| 13 | readonly color?: ColorValue; | |
| 14 | readonly colorClassName?: string; | |
| 15 | }) { | |
| 9 | 16 | const aspectRatio = 94.3941 / 56.96; |
| 10 | 17 | return ( |
| 11 | 18 | <Svg |
| ... | ... | @@ -14,9 +21,11 @@ export function T3Wordmark(props: { readonly height: number; readonly color: Col |
| 14 | 21 | width={props.height * aspectRatio} |
| 15 | 22 | viewBox="15.5309 37 94.3941 56.96" |
| 16 | 23 | > |
| 17 | <Path | |
| 24 | <ThemedPath | |
| 18 | 25 | d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z" |
| 19 | fill={props.color} | |
| 26 | color={props.color} | |
| 27 | colorClassName={props.colorClassName} | |
| 28 | fill="currentColor" | |
| 20 | 29 | /> |
| 21 | 30 | </Svg> |
| 22 | 31 | ); |
apps/mobile/src/components/ThemedSwitch.tsx+9-11| ... | ... | @@ -1,21 +1,19 @@ |
| 1 | 1 | import { Platform, Switch, type SwitchProps } from "react-native"; |
| 2 | 2 | |
| 3 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 4 | ||
| 5 | 3 | export function ThemedSwitch(props: SwitchProps) { |
| 6 | const activeTrack = String(useThemeColor("--color-switch-active-track")); | |
| 7 | const inactiveTrack = String(useThemeColor("--color-switch-inactive-track")); | |
| 8 | const activeThumb = String(useThemeColor("--color-switch-active-thumb")); | |
| 9 | const inactiveThumb = String(useThemeColor("--color-switch-inactive-thumb")); | |
| 10 | ||
| 11 | 4 | return ( |
| 12 | 5 | <Switch |
| 13 | 6 | {...props} |
| 14 | ios_backgroundColor={inactiveTrack} | |
| 15 | thumbColor={ | |
| 16 | Platform.OS === "android" ? (props.value ? activeThumb : inactiveThumb) : undefined | |
| 7 | ios_backgroundColorClassName="accent-switch-inactive-track" | |
| 8 | thumbColorClassName={ | |
| 9 | Platform.OS === "android" | |
| 10 | ? props.value | |
| 11 | ? "accent-switch-active-thumb" | |
| 12 | : "accent-switch-inactive-thumb" | |
| 13 | : undefined | |
| 17 | 14 | } |
| 18 | trackColor={{ false: inactiveTrack, true: activeTrack }} | |
| 15 | trackColorOffClassName="accent-switch-inactive-track" | |
| 16 | trackColorOnClassName="accent-switch-active-track" | |
| 19 | 17 | /> |
| 20 | 18 | ); |
| 21 | 19 | } |
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+16-20| ... | ... | @@ -27,7 +27,7 @@ import { EmptyState } from "../../components/EmptyState"; |
| 27 | 27 | import { ProjectFavicon } from "../../components/ProjectFavicon"; |
| 28 | 28 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 29 | 29 | import { relativeTime } from "../../lib/time"; |
| 30 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 30 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 31 | 31 | import { ThreadSwipeable } from "../home/thread-swipe-actions"; |
| 32 | 32 | import { |
| 33 | 33 | createNativeMailSearchToolbarItem, |
| ... | ... | @@ -70,8 +70,6 @@ function ArchivedThreadsHeader(props: { |
| 70 | 70 | const navigation = useNavigation(); |
| 71 | 71 | const insets = useSafeAreaInsets(); |
| 72 | 72 | const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest"; |
| 73 | const searchIconColor = useThemeColor("--color-icon"); | |
| 74 | const searchTextColor = useThemeColor("--color-foreground"); | |
| 75 | 73 | const usesNativeChrome = Platform.OS === "ios"; |
| 76 | 74 | const usesCompactMailToolbar = |
| 77 | 75 | Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; |
| ... | ... | @@ -154,7 +152,7 @@ function ArchivedThreadsHeader(props: { |
| 154 | 152 | <SymbolView |
| 155 | 153 | name="chevron.left" |
| 156 | 154 | size={24} |
| 157 | tintColor={searchTextColor} | |
| 155 | tintColorClassName={"accent-foreground"} | |
| 158 | 156 | type="monochrome" |
| 159 | 157 | /> |
| 160 | 158 | </Pressable> |
| ... | ... | @@ -162,7 +160,7 @@ function ArchivedThreadsHeader(props: { |
| 162 | 160 | <SymbolView |
| 163 | 161 | name="magnifyingglass" |
| 164 | 162 | size={17} |
| 165 | tintColor={searchIconColor} | |
| 163 | tintColorClassName={"accent-icon"} | |
| 166 | 164 | type="monochrome" |
| 167 | 165 | /> |
| 168 | 166 | <TextInput |
| ... | ... | @@ -192,7 +190,7 @@ function ArchivedThreadsHeader(props: { |
| 192 | 190 | : "line.3.horizontal.decrease.circle" |
| 193 | 191 | } |
| 194 | 192 | size={16} |
| 195 | tintColor={searchIconColor} | |
| 193 | tintColorClassName={"accent-icon"} | |
| 196 | 194 | type="monochrome" |
| 197 | 195 | /> |
| 198 | 196 | </Pressable> |
| ... | ... | @@ -402,9 +400,7 @@ function ArchivedThreadRow(props: { |
| 402 | 400 | readonly thread: EnvironmentThreadShell; |
| 403 | 401 | }) { |
| 404 | 402 | const { width: windowWidth } = useWindowDimensions(); |
| 405 | const cardColor = useThemeColor("--color-card"); | |
| 406 | const iconColor = useThemeColor("--color-icon-subtle"); | |
| 407 | const separatorColor = useThemeColor("--color-separator"); | |
| 403 | const cardColor = useUniwindTheme()["--color-card"]; | |
| 408 | 404 | const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt); |
| 409 | 405 | const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string => |
| 410 | 406 | Boolean(part), |
| ... | ... | @@ -436,14 +432,15 @@ function ArchivedThreadRow(props: { |
| 436 | 432 | > |
| 437 | 433 | {() => ( |
| 438 | 434 | <View |
| 439 | className="flex-row items-center gap-3 bg-card px-4 py-3" | |
| 440 | style={{ | |
| 441 | borderBottomColor: separatorColor, | |
| 442 | borderBottomWidth: props.isLast ? 0 : 1, | |
| 443 | }} | |
| 435 | className={`flex-row items-center gap-3 bg-card px-4 py-3 ${props.isLast ? "" : "border-b border-separator"}`} | |
| 444 | 436 | > |
| 445 | 437 | <View className="h-[34px] w-[34px] items-center justify-center rounded-[11px] bg-subtle"> |
| 446 | <SymbolView name="archivebox.fill" size={15} tintColor={iconColor} type="monochrome" /> | |
| 438 | <SymbolView | |
| 439 | name="archivebox.fill" | |
| 440 | size={15} | |
| 441 | tintColorClassName={"accent-icon-subtle"} | |
| 442 | type="monochrome" | |
| 443 | /> | |
| 447 | 444 | </View> |
| 448 | 445 | |
| 449 | 446 | <View className="min-w-0 flex-1 gap-1"> |
| ... | ... | @@ -463,7 +460,7 @@ function ArchivedThreadRow(props: { |
| 463 | 460 | <SymbolView |
| 464 | 461 | name="arrow.triangle.branch" |
| 465 | 462 | size={10} |
| 466 | tintColor={iconColor} | |
| 463 | tintColorClassName={"accent-icon-subtle"} | |
| 467 | 464 | type="monochrome" |
| 468 | 465 | /> |
| 469 | 466 | <Text |
| ... | ... | @@ -513,7 +510,6 @@ export function ArchivedThreadsScreen(props: { |
| 513 | 510 | const { onDeleteThread, onUnarchiveThread } = props; |
| 514 | 511 | const openSwipeableRef = useRef<SwipeableMethods | null>(null); |
| 515 | 512 | const archiveScrollGesture = useMemo(() => Gesture.Native(), []); |
| 516 | const refreshTint = useThemeColor("--color-icon"); | |
| 517 | 513 | const environmentLabelsById = useMemo( |
| 518 | 514 | () => |
| 519 | 515 | new Map( |
| ... | ... | @@ -594,7 +590,7 @@ export function ArchivedThreadsScreen(props: { |
| 594 | 590 | if (isInitialLoad) { |
| 595 | 591 | return ( |
| 596 | 592 | <View className="items-center py-16"> |
| 597 | <ActivityIndicator color={refreshTint} /> | |
| 593 | <ActivityIndicator colorClassName={"accent-icon"} /> | |
| 598 | 594 | <Text className="mt-3 text-sm text-foreground-muted">Loading archive...</Text> |
| 599 | 595 | </View> |
| 600 | 596 | ); |
| ... | ... | @@ -610,7 +606,7 @@ export function ArchivedThreadsScreen(props: { |
| 610 | 606 | title={isFiltered ? "No matching threads" : "No archived threads"} |
| 611 | 607 | /> |
| 612 | 608 | ); |
| 613 | }, [isFiltered, isInitialLoad, refreshTint]); | |
| 609 | }, [isFiltered, isInitialLoad]); | |
| 614 | 610 | |
| 615 | 611 | return ( |
| 616 | 612 | <View className="flex-1 bg-sheet"> |
| ... | ... | @@ -649,7 +645,7 @@ export function ArchivedThreadsScreen(props: { |
| 649 | 645 | <RefreshControl |
| 650 | 646 | onRefresh={props.onRefresh} |
| 651 | 647 | refreshing={props.isLoading && !isInitialLoad} |
| 652 | tintColor={String(refreshTint)} | |
| 648 | tintColorClassName={String("accent-icon")} | |
| 653 | 649 | /> |
| 654 | 650 | } |
| 655 | 651 | renderItem={renderListItem} |
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+11-11| ... | ... | @@ -18,7 +18,6 @@ import { AppText as Text } from "../../components/AppText"; |
| 18 | 18 | import { ThemedSwitch } from "../../components/ThemedSwitch"; |
| 19 | 19 | import { cn } from "../../lib/cn"; |
| 20 | 20 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; |
| 21 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 22 | 21 | import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types"; |
| 23 | 22 | import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation"; |
| 24 | 23 | import { hasCloudPublicConfig } from "../cloud/publicConfig"; |
| ... | ... | @@ -78,7 +77,6 @@ function CloudEnvironmentRowsContent( |
| 78 | 77 | props: CloudEnvironmentRowsProps & { readonly discoveryAvailable?: boolean }, |
| 79 | 78 | ) { |
| 80 | 79 | const controller = useConnectionController(); |
| 81 | const iconColor = useThemeColor("--color-icon"); | |
| 82 | 80 | const discoveryAvailable = props.discoveryAvailable ?? true; |
| 83 | 81 | const availableCloudEnvironments = discoveryAvailable |
| 84 | 82 | ? (props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments) |
| ... | ... | @@ -118,12 +116,12 @@ function CloudEnvironmentRowsContent( |
| 118 | 116 | className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50" |
| 119 | 117 | > |
| 120 | 118 | {controller.relayDiscovery.isRefreshing ? ( |
| 121 | <ActivityIndicator color={iconColor} size="small" /> | |
| 119 | <ActivityIndicator colorClassName={"accent-icon"} size="small" /> | |
| 122 | 120 | ) : ( |
| 123 | 121 | <SymbolView |
| 124 | 122 | name="arrow.clockwise" |
| 125 | 123 | size={14} |
| 126 | tintColor={iconColor} | |
| 124 | tintColorClassName={"accent-icon"} | |
| 127 | 125 | type="monochrome" |
| 128 | 126 | /> |
| 129 | 127 | )} |
| ... | ... | @@ -158,7 +156,7 @@ function CloudEnvironmentRowsContent( |
| 158 | 156 | </View> |
| 159 | 157 | ) : controller.relayDiscovery.isRefreshing ? ( |
| 160 | 158 | <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card p-6"> |
| 161 | <ActivityIndicator color={iconColor} /> | |
| 159 | <ActivityIndicator colorClassName={"accent-icon"} /> | |
| 162 | 160 | <Text className="text-center text-sm leading-normal text-foreground-muted"> |
| 163 | 161 | Loading linked cloud environments. |
| 164 | 162 | </Text> |
| ... | ... | @@ -275,7 +273,6 @@ function CloudEnvironmentRowShell(props: { |
| 275 | 273 | readonly statusText?: string; |
| 276 | 274 | readonly value: boolean; |
| 277 | 275 | }) { |
| 278 | const chevron = useThemeColor("--color-chevron"); | |
| 279 | 276 | const isRetrying = |
| 280 | 277 | props.connectionState === "connecting" || props.connectionState === "reconnecting"; |
| 281 | 278 | const shouldPulse = isRetrying; |
| ... | ... | @@ -287,7 +284,7 @@ function CloudEnvironmentRowShell(props: { |
| 287 | 284 | traceId: props.connectionErrorTraceId, |
| 288 | 285 | }); |
| 289 | 286 | const statusClassName = props.connectionError |
| 290 | ? "text-rose-500 dark:text-rose-400" | |
| 287 | ? "text-adaptive-rose-500-400" | |
| 291 | 288 | : "text-foreground-muted"; |
| 292 | 289 | const [errorMeasurement, setErrorMeasurement] = useState<{ |
| 293 | 290 | readonly text: string; |
| ... | ... | @@ -377,7 +374,7 @@ function CloudEnvironmentRowShell(props: { |
| 377 | 374 | <SymbolView |
| 378 | 375 | name="chevron.down" |
| 379 | 376 | size={10} |
| 380 | tintColor={chevron} | |
| 377 | tintColorClassName={"accent-chevron"} | |
| 381 | 378 | type="monochrome" |
| 382 | 379 | style={{ |
| 383 | 380 | marginTop: 3, |
| ... | ... | @@ -397,8 +394,6 @@ function CloudEnvironmentRowShell(props: { |
| 397 | 394 | } |
| 398 | 395 | |
| 399 | 396 | function CopyTraceIdButton(props: { readonly traceId: string }) { |
| 400 | const iconColor = useThemeColor("--color-icon"); | |
| 401 | ||
| 402 | 397 | return ( |
| 403 | 398 | <Pressable |
| 404 | 399 | accessibilityRole="button" |
| ... | ... | @@ -407,7 +402,12 @@ function CopyTraceIdButton(props: { readonly traceId: string }) { |
| 407 | 402 | }} |
| 408 | 403 | className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70" |
| 409 | 404 | > |
| 410 | <SymbolView name="doc.on.doc" size={12} tintColor={iconColor} type="monochrome" /> | |
| 405 | <SymbolView | |
| 406 | name="doc.on.doc" | |
| 407 | size={12} | |
| 408 | tintColorClassName={"accent-icon"} | |
| 409 | type="monochrome" | |
| 410 | /> | |
| 411 | 411 | <Text className="text-xs font-t3-bold text-foreground">Copy trace ID</Text> |
| 412 | 412 | </Pressable> |
| 413 | 413 | ); |
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx+15-10| ... | ... | @@ -7,7 +7,6 @@ import { AsyncResult } from "effect/unstable/reactivity"; |
| 7 | 7 | import { useCallback, useState } from "react"; |
| 8 | 8 | import { Alert, Pressable, View } from "react-native"; |
| 9 | 9 | import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated"; |
| 10 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 11 | 10 | |
| 12 | 11 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; |
| 13 | 12 | import { cn } from "../../lib/cn"; |
| ... | ... | @@ -36,10 +35,6 @@ export function ConnectionEnvironmentRow(props: { |
| 36 | 35 | }) { |
| 37 | 36 | const [label, setLabel] = useState(props.environment.environmentLabel); |
| 38 | 37 | const [url, setUrl] = useState(props.environment.displayUrl); |
| 39 | ||
| 40 | const mutedColor = useThemeColor("--color-icon-subtle"); | |
| 41 | const primaryFg = useThemeColor("--color-primary-foreground"); | |
| 42 | const dangerFg = useThemeColor("--color-danger-foreground"); | |
| 43 | 38 | const statusLabel = connectionStatusLabel(props.environment); |
| 44 | 39 | const statusTraceId = props.environment.connectionErrorTraceId; |
| 45 | 40 | const hasConnectionFailure = props.environment.connectionError !== null; |
| ... | ... | @@ -85,7 +80,7 @@ export function ConnectionEnvironmentRow(props: { |
| 85 | 80 | <Text |
| 86 | 81 | className={cn( |
| 87 | 82 | "text-xs", |
| 88 | hasConnectionFailure ? "text-rose-500 dark:text-rose-400" : "text-foreground-muted", | |
| 83 | hasConnectionFailure ? "text-adaptive-rose-500-400" : "text-foreground-muted", | |
| 89 | 84 | )} |
| 90 | 85 | numberOfLines={props.expanded ? undefined : 1} |
| 91 | 86 | selectable={props.expanded} |
| ... | ... | @@ -117,7 +112,7 @@ export function ConnectionEnvironmentRow(props: { |
| 117 | 112 | <SymbolView |
| 118 | 113 | name="chevron.down" |
| 119 | 114 | size={12} |
| 120 | tintColor={mutedColor} | |
| 115 | tintColorClassName={"accent-icon-subtle"} | |
| 121 | 116 | type="monochrome" |
| 122 | 117 | style={{ |
| 123 | 118 | transform: [{ rotate: props.expanded ? "180deg" : "0deg" }], |
| ... | ... | @@ -174,7 +169,12 @@ export function ConnectionEnvironmentRow(props: { |
| 174 | 169 | className="min-h-[42px] flex-1 flex-row items-center justify-center gap-1.5 rounded-[14px] bg-primary px-3.5 py-2.5 active:opacity-70" |
| 175 | 170 | onPress={handleSave} |
| 176 | 171 | > |
| 177 | <SymbolView name="checkmark" size={13} tintColor={primaryFg} type="monochrome" /> | |
| 172 | <SymbolView | |
| 173 | name="checkmark" | |
| 174 | size={13} | |
| 175 | tintColorClassName={"accent-primary-foreground"} | |
| 176 | type="monochrome" | |
| 177 | /> | |
| 178 | 178 | <Text className="text-xs font-t3-bold tracking-[0.8px] uppercase text-primary-foreground"> |
| 179 | 179 | Save |
| 180 | 180 | </Text> |
| ... | ... | @@ -188,7 +188,7 @@ export function ConnectionEnvironmentRow(props: { |
| 188 | 188 | <SymbolView |
| 189 | 189 | name="arrow.clockwise" |
| 190 | 190 | size={14} |
| 191 | tintColor={mutedColor} | |
| 191 | tintColorClassName={"accent-icon-subtle"} | |
| 192 | 192 | type="monochrome" |
| 193 | 193 | /> |
| 194 | 194 | </Pressable> |
| ... | ... | @@ -197,7 +197,12 @@ export function ConnectionEnvironmentRow(props: { |
| 197 | 197 | className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-danger-border bg-danger active:opacity-70" |
| 198 | 198 | onPress={() => props.onRemove(props.environment.environmentId)} |
| 199 | 199 | > |
| 200 | <SymbolView name="trash" size={14} tintColor={dangerFg} type="monochrome" /> | |
| 200 | <SymbolView | |
| 201 | name="trash" | |
| 202 | size={14} | |
| 203 | tintColorClassName={"accent-danger-foreground"} | |
| 204 | type="monochrome" | |
| 205 | /> | |
| 201 | 206 | </Pressable> |
| 202 | 207 | </View> |
| 203 | 208 | </Animated.View> |
apps/mobile/src/features/connection/ConnectionSheetButton.tsx+7-7| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | import { SymbolView } from "../../components/AppSymbol"; |
| 2 | 2 | import { Platform, Pressable } from "react-native"; |
| 3 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 4 | 3 | |
| 5 | 4 | import { AppText as Text } from "../../components/AppText"; |
| 6 | 5 | import { cn } from "../../lib/cn"; |
| ... | ... | @@ -37,11 +36,12 @@ export function ConnectionSheetButton(props: { |
| 37 | 36 | }) { |
| 38 | 37 | const tone = props.tone ?? "secondary"; |
| 39 | 38 | |
| 40 | const primaryFg = useThemeColor("--color-primary-foreground"); | |
| 41 | const dangerFg = useThemeColor("--color-danger-foreground"); | |
| 42 | const secondaryFg = useThemeColor("--color-secondary-foreground"); | |
| 43 | ||
| 44 | const textColor = tone === "primary" ? primaryFg : tone === "danger" ? dangerFg : secondaryFg; | |
| 39 | const textColorClassName = | |
| 40 | tone === "primary" | |
| 41 | ? "accent-primary-foreground" | |
| 42 | : tone === "danger" | |
| 43 | ? "accent-danger-foreground" | |
| 44 | : "accent-secondary-foreground"; | |
| 45 | 45 | |
| 46 | 46 | const primaryShadow = |
| 47 | 47 | tone === "primary" |
| ... | ... | @@ -79,7 +79,7 @@ export function ConnectionSheetButton(props: { |
| 79 | 79 | <SymbolView |
| 80 | 80 | name={props.icon} |
| 81 | 81 | size={props.compact ? 13 : 14} |
| 82 | tintColor={textColor} | |
| 82 | tintColorClassName={textColorClassName} | |
| 83 | 83 | type="monochrome" |
| 84 | 84 | /> |
| 85 | 85 | <Text |
apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx+2-2| ... | ... | @@ -5,7 +5,7 @@ import { AsyncResult } from "effect/unstable/reactivity"; |
| 5 | 5 | import { useCallback, useEffect, useRef, useState } from "react"; |
| 6 | 6 | import { Alert, Linking, Platform, ScrollView, View } from "react-native"; |
| 7 | 7 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 8 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 8 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 9 | 9 | |
| 10 | 10 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 11 | 11 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; |
| ... | ... | @@ -47,7 +47,7 @@ export function ConnectionsNewRouteScreen({ |
| 47 | 47 | const [scannerLocked, setScannerLocked] = useState(false); |
| 48 | 48 | const attemptedAutoConnectRef = useRef<string | null>(null); |
| 49 | 49 | |
| 50 | const headerIconColor = useThemeColor("--color-icon"); | |
| 50 | const headerIconColor = useUniwindTheme()["--color-icon"]; | |
| 51 | 51 | |
| 52 | 52 | const connectDisabled = isSubmitting || hostInput.trim().length === 0; |
| 53 | 53 |
apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx+1-5| ... | ... | @@ -5,7 +5,6 @@ import type { EnvironmentId } from "@t3tools/contracts"; |
| 5 | 5 | import { useCallback, useState } from "react"; |
| 6 | 6 | import { Platform, ScrollView, View } from "react-native"; |
| 7 | 7 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 8 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 9 | 8 | |
| 10 | 9 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 11 | 10 | import { AppText as Text } from "../../components/AppText"; |
| ... | ... | @@ -24,9 +23,6 @@ export function ConnectionsRouteScreen() { |
| 24 | 23 | const insets = useSafeAreaInsets(); |
| 25 | 24 | const hasEnvironments = connectedEnvironments.length > 0; |
| 26 | 25 | const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null); |
| 27 | ||
| 28 | const accentColor = useThemeColor("--color-icon-muted"); | |
| 29 | ||
| 30 | 26 | const handleToggle = useCallback((environmentId: EnvironmentId) => { |
| 31 | 27 | setExpandedId((prev) => (prev === environmentId ? null : environmentId)); |
| 32 | 28 | }, []); |
| ... | ... | @@ -89,7 +85,7 @@ export function ConnectionsRouteScreen() { |
| 89 | 85 | <SymbolView |
| 90 | 86 | name="point.3.connected.trianglepath.dotted" |
| 91 | 87 | size={20} |
| 92 | tintColor={accentColor} | |
| 88 | tintColorClassName={"accent-icon-muted"} | |
| 93 | 89 | type="monochrome" |
| 94 | 90 | /> |
| 95 | 91 | </View> |
apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx+2-4| ... | ... | @@ -7,7 +7,6 @@ import { ActivityIndicator, Pressable, View } from "react-native"; |
| 7 | 7 | |
| 8 | 8 | import { AppText as Text } from "../../components/AppText"; |
| 9 | 9 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; |
| 10 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 11 | 10 | |
| 12 | 11 | function noticeTitle(phase: EnvironmentConnectionPhase, environmentLabel: string): string { |
| 13 | 12 | switch (phase) { |
| ... | ... | @@ -55,7 +54,6 @@ export function EnvironmentConnectionNotice(props: { |
| 55 | 54 | readonly resourceName: string; |
| 56 | 55 | readonly onRetry: () => void; |
| 57 | 56 | }) { |
| 58 | const iconColor = String(useThemeColor("--color-icon-muted")); | |
| 59 | 57 | const isRetrying = |
| 60 | 58 | props.connection.phase === "connecting" || props.connection.phase === "reconnecting"; |
| 61 | 59 | |
| ... | ... | @@ -63,12 +61,12 @@ export function EnvironmentConnectionNotice(props: { |
| 63 | 61 | <View className="flex-1 items-center justify-center px-8"> |
| 64 | 62 | <View className="max-w-[320px] items-center gap-3"> |
| 65 | 63 | {isRetrying ? ( |
| 66 | <ActivityIndicator size="small" color={iconColor} /> | |
| 64 | <ActivityIndicator size="small" colorClassName={"accent-icon-muted"} /> | |
| 67 | 65 | ) : ( |
| 68 | 66 | <SymbolView |
| 69 | 67 | name={props.connection.phase === "offline" ? "wifi.slash" : "bolt.horizontal.circle"} |
| 70 | 68 | size={24} |
| 71 | tintColor={iconColor} | |
| 69 | tintColorClassName={"accent-icon-muted"} | |
| 72 | 70 | type="monochrome" |
| 73 | 71 | /> |
| 74 | 72 | )} |
apps/mobile/src/features/connection/connectionTone.ts+12-12| ... | ... | @@ -6,38 +6,38 @@ export function connectionTone(state: RemoteClientConnectionState): StatusTone { |
| 6 | 6 | case "connected": |
| 7 | 7 | return { |
| 8 | 8 | label: "Connected", |
| 9 | pillClassName: "bg-emerald-500/12 dark:bg-emerald-500/16", | |
| 10 | textClassName: "text-emerald-700 dark:text-emerald-300", | |
| 9 | pillClassName: "bg-adaptive-emerald-500-a12-a16", | |
| 10 | textClassName: "text-adaptive-emerald-700-300", | |
| 11 | 11 | }; |
| 12 | 12 | case "reconnecting": |
| 13 | 13 | return { |
| 14 | 14 | label: "Reconnecting", |
| 15 | pillClassName: "bg-amber-500/12 dark:bg-amber-500/16", | |
| 16 | textClassName: "text-amber-700 dark:text-amber-300", | |
| 15 | pillClassName: "bg-adaptive-amber-500-a12-a16", | |
| 16 | textClassName: "text-adaptive-amber-700-300", | |
| 17 | 17 | }; |
| 18 | 18 | case "connecting": |
| 19 | 19 | return { |
| 20 | 20 | label: "Connecting", |
| 21 | pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", | |
| 22 | textClassName: "text-sky-700 dark:text-sky-300", | |
| 21 | pillClassName: "bg-adaptive-sky-500-a12-a16", | |
| 22 | textClassName: "text-adaptive-sky-700-300", | |
| 23 | 23 | }; |
| 24 | 24 | case "error": |
| 25 | 25 | return { |
| 26 | 26 | label: "Connection failed", |
| 27 | pillClassName: "bg-rose-500/12 dark:bg-rose-500/16", | |
| 28 | textClassName: "text-rose-700 dark:text-rose-300", | |
| 27 | pillClassName: "bg-adaptive-rose-500-a12-a16", | |
| 28 | textClassName: "text-adaptive-rose-700-300", | |
| 29 | 29 | }; |
| 30 | 30 | case "offline": |
| 31 | 31 | return { |
| 32 | 32 | label: "Offline", |
| 33 | pillClassName: "bg-rose-500/12 dark:bg-rose-500/16", | |
| 34 | textClassName: "text-rose-700 dark:text-rose-300", | |
| 33 | pillClassName: "bg-adaptive-rose-500-a12-a16", | |
| 34 | textClassName: "text-adaptive-rose-700-300", | |
| 35 | 35 | }; |
| 36 | 36 | case "available": |
| 37 | 37 | return { |
| 38 | 38 | label: "Available", |
| 39 | pillClassName: "bg-neutral-500/10 dark:bg-neutral-500/16", | |
| 40 | textClassName: "text-neutral-600 dark:text-neutral-300", | |
| 39 | pillClassName: "bg-adaptive-neutral-500-a10-a16", | |
| 40 | textClassName: "text-adaptive-neutral-600-300", | |
| 41 | 41 | }; |
| 42 | 42 | } |
| 43 | 43 | } |
apps/mobile/src/features/files/FileMarkdownPreview.tsx+10-9| ... | ... | @@ -13,7 +13,7 @@ import { |
| 13 | 13 | resolveMarkdownFontSizes, |
| 14 | 14 | resolveNativeMarkdownTypography, |
| 15 | 15 | } from "../../lib/appearancePreferences"; |
| 16 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 16 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 17 | 17 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 18 | 18 | import { |
| 19 | 19 | hasNativeSelectableMarkdownText, |
| ... | ... | @@ -38,14 +38,15 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { |
| 38 | 38 | () => resolveNativeMarkdownTypography(appearance.baseFontSize), |
| 39 | 39 | [appearance.baseFontSize], |
| 40 | 40 | ); |
| 41 | const body = String(useThemeColor("--color-md-body")); | |
| 42 | const strong = String(useThemeColor("--color-md-strong")); | |
| 43 | const link = String(useThemeColor("--color-md-link")); | |
| 44 | const blockquoteBorder = String(useThemeColor("--color-md-blockquote-border")); | |
| 45 | const blockquoteBackground = String(useThemeColor("--color-md-blockquote-bg")); | |
| 46 | const codeBackground = String(useThemeColor("--color-md-code-bg")); | |
| 47 | const codeText = String(useThemeColor("--color-md-code-text")); | |
| 48 | const horizontalRule = String(useThemeColor("--color-md-hr")); | |
| 41 | const theme = useUniwindTheme(); | |
| 42 | const body = theme["--color-md-body"]; | |
| 43 | const strong = theme["--color-md-strong"]; | |
| 44 | const link = theme["--color-md-link"]; | |
| 45 | const blockquoteBorder = theme["--color-md-blockquote-border"]; | |
| 46 | const blockquoteBackground = theme["--color-md-blockquote-bg"]; | |
| 47 | const codeBackground = theme["--color-md-code-bg"]; | |
| 48 | const codeText = theme["--color-md-code-text"]; | |
| 49 | const horizontalRule = theme["--color-md-hr"]; | |
| 49 | 50 | const regularFontFamily = useFontFamily("regular"); |
| 50 | 51 | const mediumFontFamily = useFontFamily("medium"); |
| 51 | 52 | const boldFontFamily = useFontFamily("bold"); |
apps/mobile/src/features/files/FileTreeBrowser.tsx+2-6| ... | ... | @@ -7,7 +7,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 7 | 7 | import { AppText as Text } from "../../components/AppText"; |
| 8 | 8 | import { PierreEntryIcon } from "../../components/PierreEntryIcon"; |
| 9 | 9 | import { cn } from "../../lib/cn"; |
| 10 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 11 | 10 | import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics"; |
| 12 | 11 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 13 | 12 | import { |
| ... | ... | @@ -46,7 +45,6 @@ const FileTreeRow = memo(function FileTreeRow(props: { |
| 46 | 45 | readonly item: VisibleFileTreeNode; |
| 47 | 46 | readonly selected: boolean; |
| 48 | 47 | readonly expanded: boolean; |
| 49 | readonly iconColor: string; | |
| 50 | 48 | readonly onPressDirectory: (path: string) => void; |
| 51 | 49 | readonly onPreviewFile?: (path: string) => void; |
| 52 | 50 | readonly onPressFile: (path: string) => void; |
| ... | ... | @@ -79,7 +77,7 @@ const FileTreeRow = memo(function FileTreeRow(props: { |
| 79 | 77 | <SymbolView |
| 80 | 78 | name={props.expanded ? "chevron.down" : "chevron.right"} |
| 81 | 79 | size={12} |
| 82 | tintColor={props.iconColor} | |
| 80 | tintColorClassName="accent-icon-muted" | |
| 83 | 81 | type="monochrome" |
| 84 | 82 | /> |
| 85 | 83 | ) : ( |
| ... | ... | @@ -125,7 +123,6 @@ export function FileTreeBrowser(props: { |
| 125 | 123 | // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the |
| 126 | 124 | // observed adjustedContentInset bottom (~102) seen in the native trace. |
| 127 | 125 | const headerInset = NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + IOS_NAV_BAR_HEIGHT : 0; |
| 128 | const iconColor = String(useThemeColor("--color-icon-muted")); | |
| 129 | 126 | const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props; |
| 130 | 127 | const controlledSelectedPathRef = useRef(controlledSelectedPath); |
| 131 | 128 | const pendingSelectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| ... | ... | @@ -216,13 +213,12 @@ export function FileTreeBrowser(props: { |
| 216 | 213 | item={item} |
| 217 | 214 | selected={item.node.kind === "file" && item.node.path === selectedPath} |
| 218 | 215 | expanded={expandedPaths.has(item.node.path)} |
| 219 | iconColor={iconColor} | |
| 220 | 216 | onPressDirectory={toggleDirectory} |
| 221 | 217 | onPreviewFile={onPreviewFile} |
| 222 | 218 | onPressFile={handleSelectFile} |
| 223 | 219 | /> |
| 224 | 220 | ), |
| 225 | [expandedPaths, handleSelectFile, iconColor, onPreviewFile, selectedPath, toggleDirectory], | |
| 221 | [expandedPaths, handleSelectFile, onPreviewFile, selectedPath, toggleDirectory], | |
| 226 | 222 | ); |
| 227 | 223 | |
| 228 | 224 | if (props.error && props.entries.length === 0) { |
apps/mobile/src/features/files/SourceFileSurface.tsx+4-2| ... | ... | @@ -17,6 +17,7 @@ import { cn } from "../../lib/cn"; |
| 17 | 17 | import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences"; |
| 18 | 18 | import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; |
| 19 | 19 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 20 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 20 | 21 | import { |
| 21 | 22 | buildNativeSourceTokens, |
| 22 | 23 | NATIVE_SOURCE_CONTENT_WIDTH, |
| ... | ... | @@ -153,6 +154,7 @@ function NativeSourceFileSurface( |
| 153 | 154 | const { NativeView, onRefresh } = props; |
| 154 | 155 | const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface(); |
| 155 | 156 | const { themeAppearance, themeId } = useAppearancePreferences(); |
| 157 | const appTheme = useUniwindTheme(); | |
| 156 | 158 | const { width: viewportWidth } = useWindowDimensions(); |
| 157 | 159 | const { rowsJson, status, targetIndex, tokens } = useSourceFileModel(props); |
| 158 | 160 | const [isPullRefreshing, setIsPullRefreshing] = useState(false); |
| ... | ... | @@ -173,8 +175,8 @@ function NativeSourceFileSurface( |
| 173 | 175 | [targetIndex], |
| 174 | 176 | ); |
| 175 | 177 | const themeJson = useMemo( |
| 176 | () => JSON.stringify(createNativeReviewDiffTheme(themeAppearance, themeId)), | |
| 177 | [themeAppearance, themeId], | |
| 178 | () => JSON.stringify(createNativeReviewDiffTheme(themeAppearance, themeId, appTheme)), | |
| 179 | [appTheme, themeAppearance, themeId], | |
| 178 | 180 | ); |
| 179 | 181 | const styleJson = useMemo(() => JSON.stringify(nativeSourceStyle), [nativeSourceStyle]); |
| 180 | 182 | const contentWidth = codeWordBreak |
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+14-9| ... | ... | @@ -19,7 +19,7 @@ import { LoadingScreen } from "../../components/LoadingScreen"; |
| 19 | 19 | import { resolveFileSelectionNavigationAction } from "../../lib/adaptive-navigation"; |
| 20 | 20 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; |
| 21 | 21 | import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; |
| 22 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 22 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 23 | 23 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 24 | 24 | import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree"; |
| 25 | 25 | import { useEnvironmentQuery } from "../../state/query"; |
| ... | ... | @@ -135,11 +135,11 @@ function FileContent(props: { |
| 135 | 135 | return ( |
| 136 | 136 | <View className="flex-1 bg-sheet"> |
| 137 | 137 | {props.truncated ? ( |
| 138 | <View className="border-b border-amber-200 bg-amber-50 px-4 py-2 dark:border-amber-900/60 dark:bg-amber-950/40"> | |
| 139 | <Text className="text-2xs font-t3-bold uppercase text-amber-700 dark:text-amber-300"> | |
| 138 | <View className="border-b border-adaptive-amber-200-900-a60 bg-adaptive-amber-50-950-a40 px-4 py-2"> | |
| 139 | <Text className="text-2xs font-t3-bold uppercase text-adaptive-amber-700-300"> | |
| 140 | 140 | Partial file |
| 141 | 141 | </Text> |
| 142 | <Text className="text-xs leading-snug text-amber-800 dark:text-amber-200"> | |
| 142 | <Text className="text-xs leading-snug text-adaptive-amber-800-200"> | |
| 143 | 143 | Preview limited to the first 1 MB of a truncated file. |
| 144 | 144 | </Text> |
| 145 | 145 | </View> |
| ... | ... | @@ -210,7 +210,7 @@ function FilesUnavailable() { |
| 210 | 210 | } |
| 211 | 211 | |
| 212 | 212 | function FilesToolbarBottomFade() { |
| 213 | const sheetColor = String(useThemeColor("--color-sheet")); | |
| 213 | const sheetColor = String(useUniwindTheme()["--color-sheet"]); | |
| 214 | 214 | |
| 215 | 215 | if (process.env.EXPO_OS !== "ios") { |
| 216 | 216 | return null; |
| ... | ... | @@ -245,8 +245,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { |
| 245 | 245 | const [searchQuery, setSearchQuery] = useState(""); |
| 246 | 246 | const isAndroid = Platform.OS === "android"; |
| 247 | 247 | const { themeAppearance: highlightTheme } = useAppearancePreferences(); |
| 248 | const iconColor = String(useThemeColor("--color-icon-muted")); | |
| 249 | const sheetSurfaceColor = String(useThemeColor("--color-sheet-solid")); | |
| 248 | const theme = useUniwindTheme(); | |
| 249 | const sheetSurfaceColor = theme["--color-sheet-solid"]; | |
| 250 | 250 | const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace( |
| 251 | 251 | props.route.params, |
| 252 | 252 | ); |
| ... | ... | @@ -413,7 +413,12 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { |
| 413 | 413 | ]} |
| 414 | 414 | /> |
| 415 | 415 | <View className="flex-row items-center gap-2 border-b border-border px-3 py-2"> |
| 416 | <SymbolView name="magnifyingglass" size={17} tintColor={iconColor} type="monochrome" /> | |
| 416 | <SymbolView | |
| 417 | name="magnifyingglass" | |
| 418 | size={17} | |
| 419 | tintColorClassName={"accent-icon-muted"} | |
| 420 | type="monochrome" | |
| 421 | /> | |
| 417 | 422 | <TextInput |
| 418 | 423 | accessibilityLabel="Search files" |
| 419 | 424 | autoCapitalize="none" |
| ... | ... | @@ -467,7 +472,7 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { |
| 467 | 472 | useAdaptiveWorkspacePaneRole("inspector"); |
| 468 | 473 | const navigation = useNavigation(); |
| 469 | 474 | const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); |
| 470 | const iconColor = useThemeColor("--color-icon"); | |
| 475 | const iconColor = useUniwindTheme()["--color-icon"]; | |
| 471 | 476 | const params = props.route.params; |
| 472 | 477 | const relativePath = normalizeRoutePath(params.path); |
| 473 | 478 | const targetLine = normalizeRouteLine(firstRouteParam(params.line)); |
apps/mobile/src/features/files/thread-file-navigator-pane.tsx+16-6| ... | ... | @@ -12,7 +12,7 @@ import { |
| 12 | 12 | |
| 13 | 13 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; |
| 14 | 14 | import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; |
| 15 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 15 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 16 | 16 | import { projectEnvironment } from "../../state/projects"; |
| 17 | 17 | import { useEnvironmentQuery } from "../../state/query"; |
| 18 | 18 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| ... | ... | @@ -29,9 +29,9 @@ export function ThreadFileNavigatorPane(props: { |
| 29 | 29 | }) { |
| 30 | 30 | const [searchQuery, setSearchQuery] = useState(""); |
| 31 | 31 | const { themeAppearance: highlightTheme } = useAppearancePreferences(); |
| 32 | const iconColor = String(useThemeColor("--color-icon-muted")); | |
| 33 | const foregroundColor = String(useThemeColor("--color-foreground")); | |
| 34 | const sheetColor = String(useThemeColor("--color-sheet")); | |
| 32 | const theme = useUniwindTheme(); | |
| 33 | const foregroundColor = theme["--color-foreground"]; | |
| 34 | const sheetColor = theme["--color-sheet"]; | |
| 35 | 35 | const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); |
| 36 | 36 | const entriesQuery = useEnvironmentQuery( |
| 37 | 37 | projectEnvironment.listEntries({ |
| ... | ... | @@ -152,11 +152,21 @@ export function ThreadFileNavigatorPane(props: { |
| 152 | 152 | className="h-8 w-8 items-center justify-center rounded-full active:bg-subtle" |
| 153 | 153 | onPress={entriesQuery.refresh} |
| 154 | 154 | > |
| 155 | <SymbolView name="arrow.clockwise" size={14} tintColor={iconColor} type="monochrome" /> | |
| 155 | <SymbolView | |
| 156 | name="arrow.clockwise" | |
| 157 | size={14} | |
| 158 | tintColorClassName={"accent-icon-muted"} | |
| 159 | type="monochrome" | |
| 160 | /> | |
| 156 | 161 | </Pressable> |
| 157 | 162 | </View> |
| 158 | 163 | <View className="flex-row items-center gap-2 border-t border-border px-3 py-2"> |
| 159 | <SymbolView name="magnifyingglass" size={15} tintColor={iconColor} type="monochrome" /> | |
| 164 | <SymbolView | |
| 165 | name="magnifyingglass" | |
| 166 | size={15} | |
| 167 | tintColorClassName={"accent-icon-muted"} | |
| 168 | type="monochrome" | |
| 169 | /> | |
| 160 | 170 | <TextInput |
| 161 | 171 | accessibilityLabel="Search files" |
| 162 | 172 | autoCapitalize="none" |
apps/mobile/src/features/home/AndroidHomeFab.tsx+1-4| ... | ... | @@ -3,7 +3,6 @@ import { Platform, Pressable, View } from "react-native"; |
| 3 | 3 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 4 | 4 | |
| 5 | 5 | import { SymbolView } from "../../components/AppSymbol"; |
| 6 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 7 | 6 | |
| 8 | 7 | /** |
| 9 | 8 | * Android-only wrapper that overlays a bottom-right new-task FAB on a thread |
| ... | ... | @@ -25,8 +24,6 @@ function AndroidHomeFab(props: { |
| 25 | 24 | readonly children: ReactNode; |
| 26 | 25 | }) { |
| 27 | 26 | const insets = useSafeAreaInsets(); |
| 28 | const primaryForegroundColor = useThemeColor("--color-primary-foreground"); | |
| 29 | ||
| 30 | 27 | return ( |
| 31 | 28 | <View className="flex-1"> |
| 32 | 29 | {props.children} |
| ... | ... | @@ -42,7 +39,7 @@ function AndroidHomeFab(props: { |
| 42 | 39 | <SymbolView |
| 43 | 40 | name="square.and.pencil" |
| 44 | 41 | size={22} |
| 45 | tintColor={primaryForegroundColor} | |
| 42 | tintColorClassName={"accent-primary-foreground"} | |
| 46 | 43 | type="monochrome" |
| 47 | 44 | /> |
| 48 | 45 | </Pressable> |
apps/mobile/src/features/home/HomeHeader.tsx+17-9| ... | ... | @@ -12,7 +12,7 @@ import { SymbolView } from "../../components/AppSymbol"; |
| 12 | 12 | import { T3Wordmark } from "../../components/T3Wordmark"; |
| 13 | 13 | import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics"; |
| 14 | 14 | import { resolveMobileStageLabel } from "../../lib/mobileBranding"; |
| 15 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 15 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 16 | 16 | import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; |
| 17 | 17 | import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; |
| 18 | 18 | import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; |
| ... | ... | @@ -67,8 +67,6 @@ function checkedMenuState(checked: boolean) { |
| 67 | 67 | |
| 68 | 68 | function AndroidHomeHeader(props: HomeHeaderProps) { |
| 69 | 69 | const insets = useSafeAreaInsets(); |
| 70 | const iconColor = useThemeColor("--color-icon"); | |
| 71 | const mutedColor = useThemeColor("--color-foreground-muted"); | |
| 72 | 70 | const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); |
| 73 | 71 | // Thread List v2 lays the list out in fixed creation order, so the |
| 74 | 72 | // sort/group filter controls would be silently ignored — hide them and |
| ... | ... | @@ -218,7 +216,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) { |
| 218 | 216 | brand={ |
| 219 | 217 | <View className="flex-row items-center gap-2"> |
| 220 | 218 | {/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */} |
| 221 | <T3Wordmark color={iconColor} height={15} /> | |
| 219 | <T3Wordmark colorClassName="accent-icon" height={15} /> | |
| 222 | 220 | <RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted"> |
| 223 | 221 | Code |
| 224 | 222 | </RNText> |
| ... | ... | @@ -248,7 +246,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) { |
| 248 | 246 | : "line.3.horizontal.decrease.circle" |
| 249 | 247 | } |
| 250 | 248 | size={16} |
| 251 | tintColor={iconColor} | |
| 249 | tintColorClassName={"accent-icon"} | |
| 252 | 250 | type="monochrome" |
| 253 | 251 | /> |
| 254 | 252 | </Pressable> |
| ... | ... | @@ -262,12 +260,22 @@ function AndroidHomeHeader(props: HomeHeaderProps) { |
| 262 | 260 | onPress={props.onOpenSettings} |
| 263 | 261 | className="size-11 items-center justify-center rounded-full bg-subtle" |
| 264 | 262 | > |
| 265 | <SymbolView name="gearshape" size={18} tintColor={iconColor} type="monochrome" /> | |
| 263 | <SymbolView | |
| 264 | name="gearshape" | |
| 265 | size={18} | |
| 266 | tintColorClassName={"accent-icon"} | |
| 267 | type="monochrome" | |
| 268 | /> | |
| 266 | 269 | </Pressable> |
| 267 | 270 | </View> |
| 268 | 271 | |
| 269 | 272 | <View className="min-h-12 flex-row items-center gap-2.5 rounded-2xl border border-input-border bg-input px-3.5"> |
| 270 | <SymbolView name="magnifyingglass" size={17} tintColor={mutedColor} type="monochrome" /> | |
| 273 | <SymbolView | |
| 274 | name="magnifyingglass" | |
| 275 | size={17} | |
| 276 | tintColorClassName={"accent-foreground-muted"} | |
| 277 | type="monochrome" | |
| 278 | /> | |
| 271 | 279 | <TextInput |
| 272 | 280 | accessibilityLabel="Search threads" |
| 273 | 281 | autoCapitalize="none" |
| ... | ... | @@ -286,7 +294,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) { |
| 286 | 294 | <SymbolView |
| 287 | 295 | name="xmark.circle.fill" |
| 288 | 296 | size={17} |
| 289 | tintColor={mutedColor} | |
| 297 | tintColorClassName={"accent-foreground-muted"} | |
| 290 | 298 | type="monochrome" |
| 291 | 299 | /> |
| 292 | 300 | </Pressable> |
| ... | ... | @@ -300,7 +308,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) { |
| 300 | 308 | |
| 301 | 309 | function IosHomeHeader(props: HomeHeaderProps) { |
| 302 | 310 | const searchBarRef = useRef<SearchBarCommands>(null); |
| 303 | const iconColor = useThemeColor("--color-icon"); | |
| 311 | const iconColor = useUniwindTheme()["--color-icon"]; | |
| 304 | 312 | // Thread List v2 lays the list out in fixed creation order, so the |
| 305 | 313 | // sort/group filter controls would be silently ignored — hide them and |
| 306 | 314 | // key the "customized" icon state off the environment filter alone. |
apps/mobile/src/features/home/HomeScreen.tsx+1-3| ... | ... | @@ -23,7 +23,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 23 | 23 | import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-native"; |
| 24 | 24 | import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; |
| 25 | 25 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 26 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 27 | 26 | |
| 28 | 27 | import { AppText as Text } from "../../components/AppText"; |
| 29 | 28 | import { EmptyState } from "../../components/EmptyState"; |
| ... | ... | @@ -216,7 +215,6 @@ export function HomeScreen(props: HomeScreenProps) { |
| 216 | 215 | const openSwipeableRef = useRef<SwipeableMethods | null>(null); |
| 217 | 216 | const listRef = useRef<LegendListRef | null>(null); |
| 218 | 217 | const insets = useSafeAreaInsets(); |
| 219 | const accentColor = useThemeColor("--color-icon-muted"); | |
| 220 | 218 | const iosBottomToolbarClearance = |
| 221 | 219 | Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED |
| 222 | 220 | ? PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT |
| ... | ... | @@ -1088,7 +1086,7 @@ export function HomeScreen(props: HomeScreenProps) { |
| 1088 | 1086 | /> |
| 1089 | 1087 | {emptyState.loading ? ( |
| 1090 | 1088 | <View className="mt-4 items-center"> |
| 1091 | <ActivityIndicator color={accentColor} /> | |
| 1089 | <ActivityIndicator colorClassName={"accent-icon-muted"} /> | |
| 1092 | 1090 | </View> |
| 1093 | 1091 | ) : null} |
| 1094 | 1092 | </View> |
apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx+2-4| ... | ... | @@ -8,7 +8,6 @@ import { ActivityIndicator, Animated, Platform, Pressable, View } from "react-na |
| 8 | 8 | import { SymbolView } from "../../components/AppSymbol"; |
| 9 | 9 | import { AppText as Text } from "../../components/AppText"; |
| 10 | 10 | import { brandTitleOffset, CompactBrandTitle } from "../../components/CompactBrandTitle"; |
| 11 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 12 | 11 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 13 | 12 | import { useWorkspaceState } from "../../state/workspace"; |
| 14 | 13 | import { |
| ... | ... | @@ -98,7 +97,6 @@ export function WorkspaceConnectionTitle(props: { |
| 98 | 97 | /** Horizontal correction so the status aligns with the brand in native title slots. */ |
| 99 | 98 | readonly statusOffset?: number; |
| 100 | 99 | }) { |
| 101 | const iconColor = String(useThemeColor("--color-icon-muted")); | |
| 102 | 100 | const status = useDelayedConnectionStatus(); |
| 103 | 101 | const size = props.size ?? "navbar"; |
| 104 | 102 | |
| ... | ... | @@ -125,12 +123,12 @@ export function WorkspaceConnectionTitle(props: { |
| 125 | 123 | style={{ marginLeft: props.statusOffset ?? 0 }} |
| 126 | 124 | > |
| 127 | 125 | {status.showsProgress ? ( |
| 128 | <ActivityIndicator color={iconColor} size="small" /> | |
| 126 | <ActivityIndicator colorClassName={"accent-icon-muted"} size="small" /> | |
| 129 | 127 | ) : ( |
| 130 | 128 | <SymbolView |
| 131 | 129 | name="wifi.slash" |
| 132 | 130 | size={size === "pageTitle" ? 17 : 15} |
| 133 | tintColor={iconColor} | |
| 131 | tintColorClassName={"accent-icon-muted"} | |
| 134 | 132 | type="monochrome" |
| 135 | 133 | /> |
| 136 | 134 | )} |
apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx+6-4| ... | ... | @@ -2,15 +2,17 @@ import { SymbolView } from "../../components/AppSymbol"; |
| 2 | 2 | import { Pressable, View } from "react-native"; |
| 3 | 3 | |
| 4 | 4 | import { AppText as Text } from "../../components/AppText"; |
| 5 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 6 | 5 | |
| 7 | 6 | export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) { |
| 8 | const iconColor = useThemeColor("--color-icon-subtle"); | |
| 9 | ||
| 10 | 7 | return ( |
| 11 | 8 | <View className="flex-1 items-center justify-center bg-screen px-10"> |
| 12 | 9 | <View className="max-w-[360px] items-center gap-3"> |
| 13 | <SymbolView name="sidebar.left" size={34} tintColor={iconColor} type="hierarchical" /> | |
| 10 | <SymbolView | |
| 11 | name="sidebar.left" | |
| 12 | size={34} | |
| 13 | tintColorClassName={"accent-icon-subtle"} | |
| 14 | type="hierarchical" | |
| 15 | /> | |
| 14 | 16 | <Text className="text-center text-xl font-t3-bold">Select a thread</Text> |
| 15 | 17 | <Text className="text-center text-base text-foreground-muted"> |
| 16 | 18 | Choose a thread from the sidebar or start a new task. |
apps/mobile/src/features/layout/workspace-pane-divider.tsx+6-10| ... | ... | @@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from "react"; |
| 2 | 2 | import { Pressable, StyleSheet, View, type AccessibilityActionEvent } from "react-native"; |
| 3 | 3 | import { Gesture, GestureDetector } from "react-native-gesture-handler"; |
| 4 | 4 | import { runOnJS } from "react-native-reanimated"; |
| 5 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 5 | import { cn } from "../../lib/cn"; | |
| 6 | 6 | |
| 7 | 7 | const ACCESSIBILITY_RESIZE_STEP = 24; |
| 8 | 8 | |
| ... | ... | @@ -22,8 +22,6 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { |
| 22 | 22 | latestProps.current = props; |
| 23 | 23 | const [hovered, setHovered] = useState(false); |
| 24 | 24 | const [dragging, setDragging] = useState(false); |
| 25 | const dividerColor = useThemeColor("--color-border"); | |
| 26 | const activeDividerColor = useThemeColor("--color-primary"); | |
| 27 | 25 | const handleResizeStart = useCallback(() => { |
| 28 | 26 | setDragging(true); |
| 29 | 27 | latestProps.current.onResizeStart?.(); |
| ... | ... | @@ -81,11 +79,11 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { |
| 81 | 79 | onHoverOut={() => setHovered(false)} |
| 82 | 80 | > |
| 83 | 81 | <View |
| 84 | style={[ | |
| 85 | styles.line, | |
| 86 | { backgroundColor: dividerColor }, | |
| 87 | (hovered || dragging) && [styles.activeLine, { backgroundColor: activeDividerColor }], | |
| 88 | ]} | |
| 82 | className={cn( | |
| 83 | "h-full self-center bg-border opacity-70", | |
| 84 | hovered || dragging ? "w-0.5 bg-primary opacity-100" : "w-px", | |
| 85 | )} | |
| 86 | style={[styles.line, (hovered || dragging) && styles.activeLine]} | |
| 89 | 87 | /> |
| 90 | 88 | </Pressable> |
| 91 | 89 | </GestureDetector> |
| ... | ... | @@ -96,11 +94,9 @@ const styles = StyleSheet.create({ |
| 96 | 94 | line: { |
| 97 | 95 | alignSelf: "center", |
| 98 | 96 | height: "100%", |
| 99 | opacity: 0.7, | |
| 100 | 97 | width: StyleSheet.hairlineWidth, |
| 101 | 98 | }, |
| 102 | 99 | activeLine: { |
| 103 | opacity: 1, | |
| 104 | 100 | width: 2, |
| 105 | 101 | }, |
| 106 | 102 | }); |
apps/mobile/src/features/projects/AddProjectScreen.tsx+25-20| ... | ... | @@ -51,7 +51,6 @@ import { sourceControlEnvironment } from "../../state/sourceControl"; |
| 51 | 51 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; |
| 52 | 52 | import { ErrorBanner } from "../../components/ErrorBanner"; |
| 53 | 53 | import { SourceControlIcon } from "../../components/SourceControlIcon"; |
| 54 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 55 | 54 | import { uuidv4 } from "../../lib/uuid"; |
| 56 | 55 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 57 | 56 | import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; |
| ... | ... | @@ -159,8 +158,6 @@ function ListRow(props: { |
| 159 | 158 | readonly right?: ReactNode; |
| 160 | 159 | readonly onPress?: () => void; |
| 161 | 160 | }) { |
| 162 | const chevronColor = useThemeColor("--color-chevron"); | |
| 163 | ||
| 164 | 161 | return ( |
| 165 | 162 | <Pressable |
| 166 | 163 | disabled={props.disabled} |
| ... | ... | @@ -192,7 +189,12 @@ function ListRow(props: { |
| 192 | 189 | {"right" in props ? ( |
| 193 | 190 | props.right |
| 194 | 191 | ) : !props.disabled ? ( |
| 195 | <SymbolView name="chevron.right" size={13} tintColor={chevronColor} type="monochrome" /> | |
| 192 | <SymbolView | |
| 193 | name="chevron.right" | |
| 194 | size={13} | |
| 195 | tintColorClassName={"accent-chevron"} | |
| 196 | type="monochrome" | |
| 197 | /> | |
| 196 | 198 | ) : null} |
| 197 | 199 | </View> |
| 198 | 200 | </Pressable> |
| ... | ... | @@ -205,8 +207,6 @@ function PrimaryActionButton(props: { |
| 205 | 207 | readonly loading?: boolean; |
| 206 | 208 | readonly onPress: () => void; |
| 207 | 209 | }) { |
| 208 | const primaryForeground = useThemeColor("--color-primary-foreground"); | |
| 209 | ||
| 210 | 210 | return ( |
| 211 | 211 | <Pressable |
| 212 | 212 | disabled={props.disabled} |
| ... | ... | @@ -214,7 +214,7 @@ function PrimaryActionButton(props: { |
| 214 | 214 | className="h-12 items-center justify-center rounded-full bg-primary active:opacity-70 disabled:opacity-45" |
| 215 | 215 | > |
| 216 | 216 | {props.loading ? ( |
| 217 | <ActivityIndicator color={String(primaryForeground)} /> | |
| 217 | <ActivityIndicator colorClassName={String("accent-primary-foreground")} /> | |
| 218 | 218 | ) : ( |
| 219 | 219 | <Text className="text-base font-t3-bold text-primary-foreground">{props.label}</Text> |
| 220 | 220 | )} |
| ... | ... | @@ -414,7 +414,6 @@ function SourceControlRow(props: { |
| 414 | 414 | readonly isFirst: boolean; |
| 415 | 415 | }) { |
| 416 | 416 | const navigation = useNavigation(); |
| 417 | const iconColor = useThemeColor("--color-icon"); | |
| 418 | 417 | const title = |
| 419 | 418 | props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`; |
| 420 | 419 | const subtitle = |
| ... | ... | @@ -423,9 +422,9 @@ function SourceControlRow(props: { |
| 423 | 422 | : `Clone ${addProjectRemoteSourceLabel(props.source)} ${props.hint}`; |
| 424 | 423 | const icon = |
| 425 | 424 | props.source === "url" ? ( |
| 426 | <SymbolView name="link" size={17} tintColor={iconColor} type="monochrome" /> | |
| 425 | <SymbolView name="link" size={17} tintColorClassName={"accent-icon"} type="monochrome" /> | |
| 427 | 426 | ) : ( |
| 428 | <SourceControlIcon kind={props.source} size={18} color={String(iconColor)} /> | |
| 427 | <SourceControlIcon kind={props.source} size={18} colorClassName="accent-icon" /> | |
| 429 | 428 | ); |
| 430 | 429 | |
| 431 | 430 | if (!props.ready) { |
| ... | ... | @@ -454,8 +453,6 @@ function SourceControlRow(props: { |
| 454 | 453 | |
| 455 | 454 | export function AddProjectSourceScreen() { |
| 456 | 455 | const navigation = useNavigation(); |
| 457 | const accentColor = useThemeColor("--color-icon-muted"); | |
| 458 | const iconColor = useThemeColor("--color-icon"); | |
| 459 | 456 | const { environmentOptions, selectedEnvironment, setSelectedEnvironmentId } = |
| 460 | 457 | useSelectedEnvironment(); |
| 461 | 458 | const discoveryState = useEnvironmentQuery( |
| ... | ... | @@ -496,7 +493,7 @@ export function AddProjectSourceScreen() { |
| 496 | 493 | <SymbolView |
| 497 | 494 | name="server.rack" |
| 498 | 495 | size={17} |
| 499 | tintColor={iconColor} | |
| 496 | tintColorClassName={"accent-icon"} | |
| 500 | 497 | type="monochrome" |
| 501 | 498 | /> |
| 502 | 499 | } |
| ... | ... | @@ -508,7 +505,7 @@ export function AddProjectSourceScreen() { |
| 508 | 505 | <SymbolView |
| 509 | 506 | name="checkmark" |
| 510 | 507 | size={14} |
| 511 | tintColor={iconColor} | |
| 508 | tintColorClassName={"accent-icon"} | |
| 512 | 509 | type="monochrome" |
| 513 | 510 | /> |
| 514 | 511 | ) : null |
| ... | ... | @@ -530,7 +527,7 @@ export function AddProjectSourceScreen() { |
| 530 | 527 | <SymbolView |
| 531 | 528 | name="folder.badge.plus" |
| 532 | 529 | size={17} |
| 533 | tintColor={iconColor} | |
| 530 | tintColorClassName={"accent-icon"} | |
| 534 | 531 | type="monochrome" |
| 535 | 532 | /> |
| 536 | 533 | } |
| ... | ... | @@ -560,7 +557,9 @@ export function AddProjectSourceScreen() { |
| 560 | 557 | ), |
| 561 | 558 | )} |
| 562 | 559 | </ListSection> |
| 563 | {discoveryState.isPending ? <ActivityIndicator color={accentColor} /> : null} | |
| 560 | {discoveryState.isPending ? ( | |
| 561 | <ActivityIndicator colorClassName={"accent-icon-muted"} /> | |
| 562 | ) : null} | |
| 564 | 563 | </> |
| 565 | 564 | ) : null} |
| 566 | 565 | </AddProjectShell> |
| ... | ... | @@ -745,7 +744,6 @@ function FolderBrowser(props: { |
| 745 | 744 | }) => Promise<boolean>; |
| 746 | 745 | readonly pinnedDirectoryName?: string; |
| 747 | 746 | }) { |
| 748 | const accentColor = useThemeColor("--color-icon-muted"); | |
| 749 | 747 | const browsePath = useMemo( |
| 750 | 748 | () => getFilesystemBrowsePath(props.pathInput, props.environment.platform), |
| 751 | 749 | [props.environment.platform, props.pathInput], |
| ... | ... | @@ -781,7 +779,7 @@ function FolderBrowser(props: { |
| 781 | 779 | <ListSection> |
| 782 | 780 | {browseState.isPending && browseState.data === null ? ( |
| 783 | 781 | <View className="items-center py-5"> |
| 784 | <ActivityIndicator color={accentColor} /> | |
| 782 | <ActivityIndicator colorClassName={"accent-icon-muted"} /> | |
| 785 | 783 | </View> |
| 786 | 784 | ) : null} |
| 787 | 785 | {browsePath.canBrowseUp ? ( |
| ... | ... | @@ -791,7 +789,7 @@ function FolderBrowser(props: { |
| 791 | 789 | <SymbolView |
| 792 | 790 | name="arrow.turn.left.up" |
| 793 | 791 | size={17} |
| 794 | tintColor={accentColor} | |
| 792 | tintColorClassName={"accent-icon-muted"} | |
| 795 | 793 | type="monochrome" |
| 796 | 794 | /> |
| 797 | 795 | } |
| ... | ... | @@ -810,7 +808,14 @@ function FolderBrowser(props: { |
| 810 | 808 | <ListRow |
| 811 | 809 | key={entry.fullPath} |
| 812 | 810 | title={entry.name} |
| 813 | icon={<SymbolView name="folder" size={17} tintColor={accentColor} type="monochrome" />} | |
| 811 | icon={ | |
| 812 | <SymbolView | |
| 813 | name="folder" | |
| 814 | size={17} | |
| 815 | tintColorClassName={"accent-icon-muted"} | |
| 816 | type="monochrome" | |
| 817 | /> | |
| 818 | } | |
| 814 | 819 | isFirst={index === 0 && !browsePath.canBrowseUp} |
| 815 | 820 | right={null} |
| 816 | 821 | onPress={() => { |
apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx+6-3| ... | ... | @@ -14,7 +14,6 @@ import { ControlPill } from "../../components/ControlPill"; |
| 14 | 14 | import { cn } from "../../lib/cn"; |
| 15 | 15 | import type { DraftComposerImageAttachment } from "../../lib/composerImages"; |
| 16 | 16 | import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages"; |
| 17 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 18 | 17 | import { useNativePaste } from "../../lib/useNativePaste"; |
| 19 | 18 | import { setPendingConnectionError } from "../../state/use-remote-environment-registry"; |
| 20 | 19 | import { appendReviewCommentToDraft } from "../../state/use-thread-composer-state"; |
| ... | ... | @@ -46,7 +45,6 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp |
| 46 | 45 | const insets = useSafeAreaInsets(); |
| 47 | 46 | const { width } = useWindowDimensions(); |
| 48 | 47 | const { themeAppearance: selectedTheme } = useAppearancePreferences(); |
| 49 | const iconTint = String(useThemeColor("--color-icon")); | |
| 50 | 48 | const target = useReviewCommentTarget(); |
| 51 | 49 | const { codeSurface } = useAppearanceCodeSurface(); |
| 52 | 50 | const { environmentId, threadId } = props.route.params; |
| ... | ... | @@ -168,7 +166,12 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp |
| 168 | 166 | className="bg-subtle h-12 w-12 items-center justify-center rounded-full" |
| 169 | 167 | onPress={dismissComposer} |
| 170 | 168 | > |
| 171 | <SymbolView name="xmark" size={18} tintColor={iconTint} type="monochrome" /> | |
| 169 | <SymbolView | |
| 170 | name="xmark" | |
| 171 | size={18} | |
| 172 | tintColorClassName={"accent-icon"} | |
| 173 | type="monochrome" | |
| 174 | /> | |
| 172 | 175 | </Pressable> |
| 173 | 176 | |
| 174 | 177 | <Text className="text-lg font-t3-bold text-foreground">Add Comment</Text> |
apps/mobile/src/features/review/ReviewSheet.tsx+15-12| ... | ... | @@ -37,7 +37,7 @@ import { ControlPillMenu } from "../../components/ControlPill"; |
| 37 | 37 | import { environmentCatalog } from "../../connection/catalog"; |
| 38 | 38 | import { useEnvironmentPresentation } from "../../state/presentation"; |
| 39 | 39 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 40 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 40 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 41 | 41 | import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics"; |
| 42 | 42 | import { useThreadDraftForThread } from "../../state/use-thread-composer-state"; |
| 43 | 43 | import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; |
| ... | ... | @@ -80,13 +80,11 @@ const SHOWCASE_ENABLED = process.env.EXPO_PUBLIC_SHOWCASE === "1"; |
| 80 | 80 | |
| 81 | 81 | const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) { |
| 82 | 82 | return ( |
| 83 | <View className="border-b border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-900/60 dark:bg-amber-950/40"> | |
| 84 | <Text className="text-xs font-t3-bold uppercase text-amber-700 dark:text-amber-300"> | |
| 83 | <View className="border-b border-adaptive-amber-200-900-a60 bg-adaptive-amber-50-950-a40 px-4 py-3"> | |
| 84 | <Text className="text-xs font-t3-bold uppercase text-adaptive-amber-700-300"> | |
| 85 | 85 | Partial diff |
| 86 | 86 | </Text> |
| 87 | <Text className="text-xs leading-normal text-amber-800 dark:text-amber-200"> | |
| 88 | {props.notice} | |
| 89 | </Text> | |
| 87 | <Text className="text-xs leading-normal text-adaptive-amber-800-200">{props.notice}</Text> | |
| 90 | 88 | </View> |
| 91 | 89 | ); |
| 92 | 90 | }); |
| ... | ... | @@ -97,7 +95,6 @@ function ReviewSelectionActionBar(props: { |
| 97 | 95 | readonly onOpenComment: (() => void) | null; |
| 98 | 96 | readonly onClear: () => void; |
| 99 | 97 | }) { |
| 100 | const foreground = useThemeColor("--color-primary-foreground"); | |
| 101 | 98 | if (!props.title) { |
| 102 | 99 | return null; |
| 103 | 100 | } |
| ... | ... | @@ -107,7 +104,7 @@ function ReviewSelectionActionBar(props: { |
| 107 | 104 | <SymbolView |
| 108 | 105 | name={props.onOpenComment ? "text.bubble" : "line.3.horizontal.decrease.circle"} |
| 109 | 106 | size={16} |
| 110 | tintColor={foreground} | |
| 107 | tintColorClassName={"accent-primary-foreground"} | |
| 111 | 108 | type="monochrome" |
| 112 | 109 | /> |
| 113 | 110 | <Text className="text-base font-t3-bold text-primary-foreground">{props.title}</Text> |
| ... | ... | @@ -144,7 +141,12 @@ function ReviewSelectionActionBar(props: { |
| 144 | 141 | className="h-12 w-12 items-center justify-center rounded-full bg-primary" |
| 145 | 142 | onPress={props.onClear} |
| 146 | 143 | > |
| 147 | <SymbolView name="xmark" size={16} tintColor={foreground} type="monochrome" /> | |
| 144 | <SymbolView | |
| 145 | name="xmark" | |
| 146 | size={16} | |
| 147 | tintColorClassName={"accent-primary-foreground"} | |
| 148 | type="monochrome" | |
| 149 | /> | |
| 148 | 150 | </Pressable> |
| 149 | 151 | </View> |
| 150 | 152 | ); |
| ... | ... | @@ -217,8 +219,9 @@ function ReviewFileNavigator({ |
| 217 | 219 | ref, |
| 218 | 220 | }: ReviewFileNavigatorProps) { |
| 219 | 221 | const insets = useSafeAreaInsets(); |
| 220 | const sheetColor = String(useThemeColor("--color-sheet")); | |
| 221 | const foregroundColor = String(useThemeColor("--color-foreground")); | |
| 222 | const theme = useUniwindTheme(); | |
| 223 | const sheetColor = theme["--color-sheet"]; | |
| 224 | const foregroundColor = theme["--color-foreground"]; | |
| 222 | 225 | const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); |
| 223 | 226 | const [fileSelection, setFileSelection] = useState<{ |
| 224 | 227 | readonly sectionId: string | null; |
| ... | ... | @@ -348,7 +351,7 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 348 | 351 | const navigation = useNavigation(); |
| 349 | 352 | const insets = useSafeAreaInsets(); |
| 350 | 353 | const { themeAppearance: selectedTheme } = useAppearancePreferences(); |
| 351 | const headerIcon = String(useThemeColor("--color-icon")); | |
| 354 | const headerIcon = String(useUniwindTheme()["--color-icon"]); | |
| 352 | 355 | const { environmentId, threadId } = props.route.params; |
| 353 | 356 | const environment = useEnvironmentPresentation(environmentId); |
| 354 | 357 | const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); |
apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts+21-4| ... | ... | @@ -1,5 +1,12 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { MOBILE_THEME_IDS } from "../../lib/mobileTheme"; | |
| 2 | import { | |
| 3 | DEFAULT_MOBILE_THEME_ID, | |
| 4 | getMobileThemeVariables, | |
| 5 | MOBILE_THEME_IDS, | |
| 6 | type MobileThemeAppearance, | |
| 7 | type MobileThemeId, | |
| 8 | } from "../../lib/mobileTheme"; | |
| 9 | import { readDefaultMobileThemeVariables } from "../../lib/mobileTheme.test-support"; | |
| 3 | 10 | |
| 4 | 11 | import { |
| 5 | 12 | createNativeReviewDiffTheme, |
| ... | ... | @@ -39,6 +46,12 @@ function buildInput(comments: BuildNativeReviewDiffDataInput["comments"]) { |
| 39 | 46 | return { parsedDiff, comments } satisfies BuildNativeReviewDiffDataInput; |
| 40 | 47 | } |
| 41 | 48 | |
| 49 | function appTheme(themeId: MobileThemeId, appearance: MobileThemeAppearance) { | |
| 50 | return themeId === DEFAULT_MOBILE_THEME_ID | |
| 51 | ? readDefaultMobileThemeVariables(appearance) | |
| 52 | : getMobileThemeVariables(themeId, appearance); | |
| 53 | } | |
| 54 | ||
| 42 | 55 | describe("getCachedNativeReviewDiffData", () => { |
| 43 | 56 | it("reuses the row model for equivalent empty comment arrays", () => { |
| 44 | 57 | const first = getCachedNativeReviewDiffData(buildInput([])); |
| ... | ... | @@ -61,7 +74,11 @@ describe("createNativeReviewDiffTheme", () => { |
| 61 | 74 | it("serializes every native color as cross-platform opaque hex", () => { |
| 62 | 75 | for (const themeId of MOBILE_THEME_IDS) { |
| 63 | 76 | for (const appearance of ["light", "dark"] as const) { |
| 64 | const theme = createNativeReviewDiffTheme(appearance, themeId); | |
| 77 | const theme = createNativeReviewDiffTheme( | |
| 78 | appearance, | |
| 79 | themeId, | |
| 80 | appTheme(themeId, appearance), | |
| 81 | ); | |
| 65 | 82 | for (const color of Object.values(theme)) { |
| 66 | 83 | expect(color, `${themeId}/${appearance}`).toMatch(/^#[\da-f]{6}$/i); |
| 67 | 84 | } |
| ... | ... | @@ -70,8 +87,8 @@ describe("createNativeReviewDiffTheme", () => { |
| 70 | 87 | }); |
| 71 | 88 | |
| 72 | 89 | it("uses the selected app palette for native code surfaces", () => { |
| 73 | const standard = createNativeReviewDiffTheme("dark", "t3-code"); | |
| 74 | const iris = createNativeReviewDiffTheme("dark", "iris"); | |
| 90 | const standard = createNativeReviewDiffTheme("dark", "t3-code", appTheme("t3-code", "dark")); | |
| 91 | const iris = createNativeReviewDiffTheme("dark", "iris", appTheme("iris", "dark")); | |
| 75 | 92 | |
| 76 | 93 | expect(iris.background).not.toBe(standard.background); |
| 77 | 94 | expect(iris.hunkText).not.toBe(standard.hunkText); |
apps/mobile/src/features/review/nativeReviewDiffAdapter.ts+3-7| ... | ... | @@ -8,11 +8,7 @@ import { pipe } from "effect/Function"; |
| 8 | 8 | import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences"; |
| 9 | 9 | import { resolveMobileCodeSurface } from "../../lib/appearancePreferences"; |
| 10 | 10 | import { MOBILE_CODE_SURFACE } from "../../lib/typography"; |
| 11 | import { | |
| 12 | DEFAULT_MOBILE_THEME_ID, | |
| 13 | getMobileThemeVariables, | |
| 14 | type MobileThemeId, | |
| 15 | } from "../../lib/mobileTheme"; | |
| 11 | import { type MobileThemeId, type MobileThemeVariables } from "../../lib/mobileTheme"; | |
| 16 | 12 | import { getMobileTerminalTheme, type TerminalAppearanceScheme } from "../terminal/terminalTheme"; |
| 17 | 13 | import { computeWordAltDiffRanges } from "./reviewWordDiffs"; |
| 18 | 14 | import { |
| ... | ... | @@ -137,10 +133,10 @@ function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment |
| 137 | 133 | |
| 138 | 134 | export function createNativeReviewDiffTheme( |
| 139 | 135 | scheme: TerminalAppearanceScheme, |
| 140 | themeId: MobileThemeId = DEFAULT_MOBILE_THEME_ID, | |
| 136 | themeId: MobileThemeId, | |
| 137 | appTheme: MobileThemeVariables, | |
| 141 | 138 | ): NativeReviewDiffTheme { |
| 142 | 139 | const terminalTheme = getMobileTerminalTheme(themeId, scheme); |
| 143 | const appTheme = getMobileThemeVariables(themeId, scheme); | |
| 144 | 140 | const [, terminalRed] = terminalTheme.palette; |
| 145 | 141 | // Swift expects #RRGGBB/#RRGGBBAA while Android expects #RRGGBB/#AARRGGBB. |
| 146 | 142 | // Flatten translucent app tokens onto the code surface so both native |
apps/mobile/src/features/review/useNativeReviewDiffBridge.ts+6-1| ... | ... | @@ -6,6 +6,7 @@ import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCo |
| 6 | 6 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 7 | 7 | import { useNativeReviewDiffHighlighting } from "./useNativeReviewDiffHighlighting"; |
| 8 | 8 | import { buildNativeReviewTokensResetKey } from "./reviewDiffBridgeKeys"; |
| 9 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 9 | 10 | |
| 10 | 11 | export { buildNativeReviewTokensResetKey, hashReviewDiffKey } from "./reviewDiffBridgeKeys"; |
| 11 | 12 | |
| ... | ... | @@ -31,11 +32,15 @@ export function useNativeReviewDiffBridge(input: { |
| 31 | 32 | } = input; |
| 32 | 33 | const { nativeReviewDiffStyle } = useAppearanceCodeSurface(); |
| 33 | 34 | const { themeAppearance: scheme, themeId } = useAppearancePreferences(); |
| 35 | const appTheme = useUniwindTheme(); | |
| 34 | 36 | const [collapsedCommentIds, setCollapsedCommentIds] = useState<ReadonlySet<string>>( |
| 35 | 37 | () => new Set(), |
| 36 | 38 | ); |
| 37 | 39 | |
| 38 | const theme = useMemo(() => createNativeReviewDiffTheme(scheme, themeId), [scheme, themeId]); | |
| 40 | const theme = useMemo( | |
| 41 | () => createNativeReviewDiffTheme(scheme, themeId, appTheme), | |
| 42 | [appTheme, scheme, themeId], | |
| 43 | ); | |
| 39 | 44 | const rowsJson = useMemo(() => JSON.stringify(data.rows), [data.rows]); |
| 40 | 45 | const collapsedFileIdsJson = useMemo(() => JSON.stringify(collapsedFileIds), [collapsedFileIds]); |
| 41 | 46 | const viewedFileIdsJson = useMemo(() => JSON.stringify(viewedFileIds), [viewedFileIds]); |
apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx+8-10| ... | ... | @@ -1,12 +1,11 @@ |
| 1 | 1 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 2 | 2 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 3 | import { SymbolView } from "expo-symbols"; | |
| 4 | 3 | import { useMemo } from "react"; |
| 5 | 4 | import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; |
| 6 | 5 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 7 | 6 | |
| 8 | 7 | import { AppText as Text } from "../../components/AppText"; |
| 9 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 8 | import { SymbolView } from "../../components/AppSymbol"; | |
| 10 | 9 | import { |
| 11 | 10 | clearClientCacheAtom, |
| 12 | 11 | clientCacheSummaryAtom, |
| ... | ... | @@ -17,8 +16,6 @@ import { SettingsSection } from "./components/SettingsSection"; |
| 17 | 16 | |
| 18 | 17 | export function SettingsClientStorageRouteScreen() { |
| 19 | 18 | const insets = useSafeAreaInsets(); |
| 20 | const iconColor = useThemeColor("--color-icon"); | |
| 21 | const dangerForegroundColor = useThemeColor("--color-danger-foreground"); | |
| 22 | 19 | const summaryResult = useAtomValue(clientCacheSummaryAtom); |
| 23 | 20 | const clearResult = useAtomValue(clearClientCacheAtom); |
| 24 | 21 | const clearCache = useAtomSet(clearClientCacheAtom); |
| ... | ... | @@ -84,7 +81,7 @@ export function SettingsClientStorageRouteScreen() { |
| 84 | 81 | <SymbolView |
| 85 | 82 | name="exclamationmark.triangle" |
| 86 | 83 | size={28} |
| 87 | tintColor={dangerForegroundColor} | |
| 84 | tintColorClassName={"accent-danger-foreground"} | |
| 88 | 85 | type="monochrome" |
| 89 | 86 | weight="regular" |
| 90 | 87 | /> |
| ... | ... | @@ -119,7 +116,7 @@ export function SettingsClientStorageRouteScreen() { |
| 119 | 116 | <SymbolView |
| 120 | 117 | name="checkmark.circle" |
| 121 | 118 | size={28} |
| 122 | tintColor={iconColor} | |
| 119 | tintColorClassName={"accent-icon"} | |
| 123 | 120 | type="monochrome" |
| 124 | 121 | weight="regular" |
| 125 | 122 | /> |
| ... | ... | @@ -142,14 +139,16 @@ export function SettingsClientStorageRouteScreen() { |
| 142 | 139 | <SymbolView |
| 143 | 140 | name="trash" |
| 144 | 141 | size={22} |
| 145 | tintColor={dangerForegroundColor} | |
| 142 | tintColorClassName={"accent-danger-foreground"} | |
| 146 | 143 | type="monochrome" |
| 147 | 144 | weight="regular" |
| 148 | 145 | /> |
| 149 | 146 | <Text className="flex-1 text-lg tabular-nums text-danger-foreground"> |
| 150 | 147 | {summary ? `Clear ${formatBytes(summary.payloadBytes)}` : "Clear caches"} |
| 151 | 148 | </Text> |
| 152 | {isClearing ? <ActivityIndicator color={dangerForegroundColor} /> : null} | |
| 149 | {isClearing ? ( | |
| 150 | <ActivityIndicator colorClassName={"accent-danger-foreground"} /> | |
| 151 | ) : null} | |
| 153 | 152 | </Pressable> |
| 154 | 153 | </SettingsSection> |
| 155 | 154 | <Text className="px-2 text-sm leading-normal text-foreground-muted"> |
| ... | ... | @@ -174,7 +173,6 @@ function CacheEnvironmentRow(props: { |
| 174 | 173 | readonly first: boolean; |
| 175 | 174 | readonly onClear: () => void; |
| 176 | 175 | }) { |
| 177 | const iconColor = useThemeColor("--color-icon"); | |
| 178 | 176 | return ( |
| 179 | 177 | <View |
| 180 | 178 | className={ |
| ... | ... | @@ -186,7 +184,7 @@ function CacheEnvironmentRow(props: { |
| 186 | 184 | <SymbolView |
| 187 | 185 | name="desktopcomputer" |
| 188 | 186 | size={22} |
| 189 | tintColor={iconColor} | |
| 187 | tintColorClassName={"accent-icon"} | |
| 190 | 188 | type="monochrome" |
| 191 | 189 | weight="regular" |
| 192 | 190 | /> |
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+3-4| ... | ... | @@ -12,7 +12,7 @@ import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows"; |
| 12 | 12 | import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow"; |
| 13 | 13 | import { splitEnvironmentSections } from "../connection/environmentSections"; |
| 14 | 14 | import { cn } from "../../lib/cn"; |
| 15 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 15 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 16 | 16 | import { useRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 17 | 17 | import { |
| 18 | 18 | applyShowcaseLocalEnvironmentDisplayUrls, |
| ... | ... | @@ -44,8 +44,7 @@ export function SettingsEnvironmentsRouteScreen() { |
| 44 | 44 | : environmentSections.connectedCloudEnvironments; |
| 45 | 45 | const hasLocalEnvironments = localEnvironments.length > 0; |
| 46 | 46 | const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null); |
| 47 | const accentColor = useThemeColor("--color-icon-muted"); | |
| 48 | const headerIconColor = useThemeColor("--color-icon"); | |
| 47 | const headerIconColor = useUniwindTheme()["--color-icon"]; | |
| 49 | 48 | |
| 50 | 49 | const handleToggle = useCallback((environmentId: EnvironmentId) => { |
| 51 | 50 | setExpandedId((prev) => (prev === environmentId ? null : environmentId)); |
| ... | ... | @@ -148,7 +147,7 @@ export function SettingsEnvironmentsRouteScreen() { |
| 148 | 147 | <SymbolView |
| 149 | 148 | name="point.3.connected.trianglepath.dotted" |
| 150 | 149 | size={20} |
| 151 | tintColor={accentColor} | |
| 150 | tintColorClassName={"accent-icon-muted"} | |
| 152 | 151 | type="monochrome" |
| 153 | 152 | /> |
| 154 | 153 | </View> |
apps/mobile/src/features/settings/SettingsProjectGroupingRouteScreen.tsx+1-3| ... | ... | @@ -8,7 +8,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 8 | 8 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 9 | 9 | import { AppText as Text } from "../../components/AppText"; |
| 10 | 10 | import { SymbolView } from "../../components/AppSymbol"; |
| 11 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 12 | 11 | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 13 | 12 | import { |
| 14 | 13 | mobileProjectGroupingModePatch, |
| ... | ... | @@ -42,7 +41,6 @@ const GROUPING_OPTIONS: ReadonlyArray<{ |
| 42 | 41 | export function SettingsProjectGroupingRouteScreen() { |
| 43 | 42 | const navigation = useNavigation(); |
| 44 | 43 | const insets = useSafeAreaInsets(); |
| 45 | const checkmarkColor = useThemeColor("--color-icon"); | |
| 46 | 44 | const preferencesResult = useAtomValue(mobilePreferencesAtom); |
| 47 | 45 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); |
| 48 | 46 | const preferencesReady = AsyncResult.isSuccess(preferencesResult) && !preferencesResult.waiting; |
| ... | ... | @@ -92,7 +90,7 @@ export function SettingsProjectGroupingRouteScreen() { |
| 92 | 90 | <SymbolView |
| 93 | 91 | name="checkmark" |
| 94 | 92 | size={18} |
| 95 | tintColor={checkmarkColor} | |
| 93 | tintColorClassName={"accent-icon"} | |
| 96 | 94 | type="monochrome" |
| 97 | 95 | weight="semibold" |
| 98 | 96 | /> |
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+1-3| ... | ... | @@ -33,7 +33,6 @@ import { hasCloudPublicConfig, resolveRelayClerkTokenOptions } from "../cloud/pu |
| 33 | 33 | import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; |
| 34 | 34 | import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; |
| 35 | 35 | import { runtime } from "../../lib/runtime"; |
| 36 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 37 | 36 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; |
| 38 | 37 | import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; |
| 39 | 38 | import { |
| ... | ... | @@ -585,7 +584,6 @@ function LegacySettingsSection() { |
| 585 | 584 | } |
| 586 | 585 | |
| 587 | 586 | function AppSettingsSection() { |
| 588 | const icon = useThemeColor("--color-icon"); | |
| 589 | 587 | const [updateState, setUpdateState] = useState<AppUpdateCheckState>("idle"); |
| 590 | 588 | const updateInFlight = useRef(false); |
| 591 | 589 | const hiddenUpdateTapCount = useRef(0); |
| ... | ... | @@ -655,7 +653,7 @@ function AppSettingsSection() { |
| 655 | 653 | <SymbolView |
| 656 | 654 | name="info.circle" |
| 657 | 655 | size={22} |
| 658 | tintColor={icon} | |
| 656 | tintColorClassName={"accent-icon"} | |
| 659 | 657 | type="monochrome" |
| 660 | 658 | weight="regular" |
| 661 | 659 | /> |
apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx+118-49| ... | ... | @@ -1,15 +1,23 @@ |
| 1 | import { createContext, use, useCallback, useLayoutEffect, useMemo, type ReactNode } from "react"; | |
| 2 | import { useColorScheme } from "react-native"; | |
| 1 | import { | |
| 2 | createContext, | |
| 3 | startTransition, | |
| 4 | use, | |
| 5 | useCallback, | |
| 6 | useLayoutEffect, | |
| 7 | useMemo, | |
| 8 | useRef, | |
| 9 | type ReactNode, | |
| 10 | } from "react"; | |
| 11 | import { Appearance, useColorScheme } from "react-native"; | |
| 3 | 12 | |
| 4 | 13 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 5 | 14 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 6 | 15 | |
| 7 | import { Uniwind } from "uniwind"; | |
| 16 | import { ScopedTheme, Uniwind } from "uniwind"; | |
| 8 | 17 | |
| 9 | 18 | import { |
| 10 | 19 | resolveAppearance, |
| 11 | 20 | resolveAppearancePreferences, |
| 12 | resolveTextScaleVariables, | |
| 13 | 21 | type ResolvedAppearance, |
| 14 | 22 | } from "../../../lib/appearancePreferences"; |
| 15 | 23 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../state/preferences"; |
| ... | ... | @@ -17,7 +25,6 @@ import type { Preferences } from "../../../persistence/mobile-preferences"; |
| 17 | 25 | import { |
| 18 | 26 | createMobileThemePairPatch, |
| 19 | 27 | createMobileThemeSelectionPatch, |
| 20 | getMobileThemeVariables, | |
| 21 | 28 | normalizeMobileThemeMode, |
| 22 | 29 | resolveMobileThemeIds, |
| 23 | 30 | type MobileThemeAppearance, |
| ... | ... | @@ -25,6 +32,11 @@ import { |
| 25 | 32 | type MobileThemeIds, |
| 26 | 33 | type MobileThemeMode, |
| 27 | 34 | } from "../../../lib/mobileTheme"; |
| 35 | import { | |
| 36 | createMobileThemeRuntimeOperations, | |
| 37 | getMobileUniwindThemeName, | |
| 38 | type MobileThemeRuntimeState, | |
| 39 | } from "../../../lib/mobileThemeRuntime"; | |
| 28 | 40 | import { cacheTerminalFontSize } from "../../terminal/terminalUiState"; |
| 29 | 41 | |
| 30 | 42 | interface AppearancePreferencesContextValue { |
| ... | ... | @@ -51,30 +63,6 @@ interface AppearancePreferencesContextValue { |
| 51 | 63 | |
| 52 | 64 | const AppearancePreferencesContext = createContext<AppearancePreferencesContextValue | null>(null); |
| 53 | 65 | |
| 54 | /** | |
| 55 | * Injects palette and text-scale variables into both adaptive stylesheets. | |
| 56 | * Updating the active sheet last lets the visible app settle in one pass. | |
| 57 | */ | |
| 58 | function applyAppearanceVariables(baseFontSize: number, themeIds: MobileThemeIds) { | |
| 59 | const textVariables = resolveTextScaleVariables(baseFontSize); | |
| 60 | const currentTheme = Uniwind.currentTheme; | |
| 61 | const activeAppearance = | |
| 62 | currentTheme === "light" || currentTheme === "dark" ? currentTheme : null; | |
| 63 | ||
| 64 | for (const theme of ["light", "dark"] as const) { | |
| 65 | const variables = { ...getMobileThemeVariables(themeIds[theme], theme), ...textVariables }; | |
| 66 | if (theme !== activeAppearance) { | |
| 67 | Uniwind.updateCSSVariables(theme, variables); | |
| 68 | } | |
| 69 | } | |
| 70 | if (activeAppearance !== null) { | |
| 71 | Uniwind.updateCSSVariables(activeAppearance, { | |
| 72 | ...getMobileThemeVariables(themeIds[activeAppearance], activeAppearance), | |
| 73 | ...textVariables, | |
| 74 | }); | |
| 75 | } | |
| 76 | } | |
| 77 | ||
| 78 | 66 | export function AppearancePreferencesProvider(props: { readonly children: ReactNode }) { |
| 79 | 67 | const preferencesResult = useAtomValue(mobilePreferencesAtom); |
| 80 | 68 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); |
| ... | ... | @@ -88,54 +76,135 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 88 | 76 | ); |
| 89 | 77 | const themeMode = normalizeMobileThemeMode(storedPreferences?.themeMode); |
| 90 | 78 | const themeAppearance = themeMode === "system" ? systemColorScheme : themeMode; |
| 91 | const themeIds = useMemo( | |
| 92 | () => resolveMobileThemeIds(storedPreferences ?? {}), | |
| 93 | [storedPreferences], | |
| 79 | const resolvedThemeIds = resolveMobileThemeIds(storedPreferences ?? {}); | |
| 80 | const themeIds = useMemo<MobileThemeIds>( | |
| 81 | () => ({ light: resolvedThemeIds.light, dark: resolvedThemeIds.dark }), | |
| 82 | [resolvedThemeIds.dark, resolvedThemeIds.light], | |
| 94 | 83 | ); |
| 95 | 84 | const themeId = themeIds[themeAppearance]; |
| 96 | const isReady = AsyncResult.isSuccess(preferencesResult) && !preferencesResult.waiting; | |
| 85 | const activeThemeName = getMobileUniwindThemeName(themeId, themeAppearance); | |
| 86 | const { baseFontSize, codeFontSize, codeWordBreak, terminalFontSize } = preferences; | |
| 87 | const appearance = useMemo( | |
| 88 | () => resolveAppearance({ baseFontSize, codeFontSize, codeWordBreak, terminalFontSize }), | |
| 89 | [baseFontSize, codeFontSize, codeWordBreak, terminalFontSize], | |
| 90 | ); | |
| 91 | // Preference patches are optimistic. Keep controls interactive while a save is | |
| 92 | // in flight so rapid theme choices can supersede one another immediately. | |
| 93 | const isReady = AsyncResult.isSuccess(preferencesResult); | |
| 94 | const runtimeState = useMemo<MobileThemeRuntimeState>( | |
| 95 | () => ({ | |
| 96 | baseFontSize, | |
| 97 | themeAppearance, | |
| 98 | themeMode, | |
| 99 | }), | |
| 100 | [baseFontSize, themeAppearance, themeMode], | |
| 101 | ); | |
| 102 | const appliedRuntimeStateRef = useRef<MobileThemeRuntimeState | null>(null); | |
| 103 | const selectedThemeIdsRef = useRef(themeIds); | |
| 97 | 104 | |
| 98 | useLayoutEffect(() => { | |
| 99 | applyAppearanceVariables(preferences.baseFontSize, themeIds); | |
| 100 | Uniwind.setTheme(themeMode); | |
| 101 | cacheTerminalFontSize(resolveAppearance(preferences).terminalFontSize); | |
| 102 | }, [preferences, themeIds, themeMode]); | |
| 105 | const applyThemeRuntime = useCallback((next: MobileThemeRuntimeState) => { | |
| 106 | const operations = createMobileThemeRuntimeOperations(appliedRuntimeStateRef.current, next); | |
| 107 | for (const operation of operations) { | |
| 108 | if (operation.kind === "update-text-variables") { | |
| 109 | Uniwind.updateCSSVariables(operation.themeName, operation.variables); | |
| 110 | continue; | |
| 111 | } | |
| 112 | if (operation.kind === "set-appearance-mode") { | |
| 113 | Appearance.setColorScheme( | |
| 114 | operation.themeMode === "system" ? "unspecified" : operation.appearance, | |
| 115 | ); | |
| 116 | } | |
| 117 | } | |
| 118 | appliedRuntimeStateRef.current = next; | |
| 119 | }, []); | |
| 120 | ||
| 121 | const syncThemeRuntime = useCallback( | |
| 122 | (next: MobileThemeRuntimeState) => applyThemeRuntime(next), | |
| 123 | [applyThemeRuntime], | |
| 124 | ); | |
| 103 | 125 | |
| 104 | 126 | const updatePreferences = useCallback( |
| 105 | 127 | (patch: Partial<Preferences>) => { |
| 128 | startTransition(() => savePreferences(patch)); | |
| 129 | }, | |
| 130 | [savePreferences], | |
| 131 | ); | |
| 132 | ||
| 133 | const updateThemePreferences = useCallback( | |
| 134 | (patch: Partial<Preferences>) => { | |
| 135 | // Theme selection owns the visible root ScopedTheme value. Keep its | |
| 136 | // optimistic atom update urgent so the first frame after a press is the | |
| 137 | // complete new palette rather than a deferred transition render. | |
| 106 | 138 | savePreferences(patch); |
| 107 | 139 | }, |
| 108 | 140 | [savePreferences], |
| 109 | 141 | ); |
| 110 | 142 | |
| 143 | useLayoutEffect(() => { | |
| 144 | selectedThemeIdsRef.current = themeIds; | |
| 145 | syncThemeRuntime(runtimeState); | |
| 146 | cacheTerminalFontSize(appearance.terminalFontSize); | |
| 147 | }, [appearance.terminalFontSize, runtimeState, syncThemeRuntime, themeIds]); | |
| 148 | ||
| 111 | 149 | const setThemeIdForAppearance = useCallback( |
| 112 | 150 | (appearance: MobileThemeAppearance, value: MobileThemeId) => { |
| 113 | updatePreferences( | |
| 114 | createMobileThemeSelectionPatch(themeIds, themeAppearance, appearance, value), | |
| 151 | const patch = createMobileThemeSelectionPatch( | |
| 152 | selectedThemeIdsRef.current, | |
| 153 | themeAppearance, | |
| 154 | appearance, | |
| 155 | value, | |
| 115 | 156 | ); |
| 157 | selectedThemeIdsRef.current = resolveMobileThemeIds(patch); | |
| 158 | updateThemePreferences(patch); | |
| 116 | 159 | }, |
| 117 | [themeAppearance, themeIds, updatePreferences], | |
| 160 | [themeAppearance, updateThemePreferences], | |
| 118 | 161 | ); |
| 119 | 162 | |
| 120 | 163 | const setThemeIdForBothAppearances = useCallback( |
| 121 | 164 | (value: MobileThemeId) => { |
| 122 | updatePreferences(createMobileThemePairPatch(value)); | |
| 165 | const patch = createMobileThemePairPatch(value); | |
| 166 | selectedThemeIdsRef.current = resolveMobileThemeIds(patch); | |
| 167 | updateThemePreferences(patch); | |
| 123 | 168 | }, |
| 124 | [updatePreferences], | |
| 169 | [updateThemePreferences], | |
| 125 | 170 | ); |
| 126 | 171 | |
| 127 | 172 | const setThemeMode = useCallback( |
| 128 | 173 | (value: MobileThemeMode) => { |
| 129 | updatePreferences({ themeMode: value }); | |
| 174 | const current = appliedRuntimeStateRef.current ?? runtimeState; | |
| 175 | ||
| 176 | // Clear a forced native appearance before publishing System. The | |
| 177 | // resulting useColorScheme notification still sees the previous forced | |
| 178 | // preference, so React batches the actual system palette into the one | |
| 179 | // urgent preference commit below. | |
| 180 | if (value === "system") { | |
| 181 | Appearance.setColorScheme("unspecified"); | |
| 182 | } | |
| 183 | const nextAppearance = | |
| 184 | value === "system" ? (Appearance.getColorScheme() === "dark" ? "dark" : "light") : value; | |
| 185 | const next = { | |
| 186 | ...current, | |
| 187 | themeAppearance: nextAppearance, | |
| 188 | themeMode: value, | |
| 189 | }; | |
| 190 | ||
| 191 | updateThemePreferences({ themeMode: value }); | |
| 192 | if (value === "system") { | |
| 193 | appliedRuntimeStateRef.current = next; | |
| 194 | } else { | |
| 195 | syncThemeRuntime(next); | |
| 196 | } | |
| 130 | 197 | }, |
| 131 | [updatePreferences], | |
| 198 | [runtimeState, syncThemeRuntime, updateThemePreferences], | |
| 132 | 199 | ); |
| 133 | 200 | |
| 134 | 201 | const setBaseFontSize = useCallback( |
| 135 | 202 | (value: number) => { |
| 203 | const current = appliedRuntimeStateRef.current ?? runtimeState; | |
| 204 | syncThemeRuntime({ ...current, baseFontSize: value }); | |
| 136 | 205 | updatePreferences({ baseFontSize: value }); |
| 137 | 206 | }, |
| 138 | [updatePreferences], | |
| 207 | [runtimeState, syncThemeRuntime, updatePreferences], | |
| 139 | 208 | ); |
| 140 | 209 | |
| 141 | 210 | const setTerminalFontSize = useCallback( |
| ... | ... | @@ -161,7 +230,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 161 | 230 | |
| 162 | 231 | const value = useMemo( |
| 163 | 232 | (): AppearancePreferencesContextValue => ({ |
| 164 | appearance: resolveAppearance(preferences), | |
| 233 | appearance, | |
| 165 | 234 | themeId, |
| 166 | 235 | themeIds, |
| 167 | 236 | themeMode, |
| ... | ... | @@ -176,7 +245,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 176 | 245 | setCodeWordBreak, |
| 177 | 246 | }), |
| 178 | 247 | [ |
| 179 | preferences, | |
| 248 | appearance, | |
| 180 | 249 | themeId, |
| 181 | 250 | themeIds, |
| 182 | 251 | themeMode, |
| ... | ... | @@ -194,7 +263,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN |
| 194 | 263 | |
| 195 | 264 | return ( |
| 196 | 265 | <AppearancePreferencesContext.Provider value={value}> |
| 197 | {props.children} | |
| 266 | <ScopedTheme theme={activeThemeName}>{props.children}</ScopedTheme> | |
| 198 | 267 | </AppearancePreferencesContext.Provider> |
| 199 | 268 | ); |
| 200 | 269 | } |
apps/mobile/src/features/settings/appearance/components/AppearancePreviews.tsx+4-3| ... | ... | @@ -5,7 +5,7 @@ import { |
| 5 | 5 | resolveMarkdownFontSizes, |
| 6 | 6 | resolveMobileCodeSurface, |
| 7 | 7 | } from "../../../../lib/appearancePreferences"; |
| 8 | import { useThemeColor } from "../../../../lib/useThemeColor"; | |
| 8 | import { useUniwindTheme } from "../../../../lib/useUniwindTheme"; | |
| 9 | 9 | import { getMobileTerminalTheme } from "../../../terminal/terminalTheme"; |
| 10 | 10 | import { useAppearancePreferences } from "../AppearancePreferencesProvider"; |
| 11 | 11 | |
| ... | ... | @@ -138,8 +138,9 @@ export function CodeAppearancePreview(props: { |
| 138 | 138 | readonly wordBreak: boolean; |
| 139 | 139 | }) { |
| 140 | 140 | const surface = resolveMobileCodeSurface(props.fontSize); |
| 141 | const lineNumberColor = useThemeColor("--color-icon-subtle"); | |
| 142 | const keywordColor = useThemeColor("--color-md-link"); | |
| 141 | const theme = useUniwindTheme(); | |
| 142 | const lineNumberColor = theme["--color-icon-subtle"]; | |
| 143 | const keywordColor = theme["--color-md-link"]; | |
| 143 | 144 | |
| 144 | 145 | const lineNumber = (line: CodePreviewLine, index: number) => ( |
| 145 | 146 | <Text |
apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx+7-8| ... | ... | @@ -12,7 +12,7 @@ import Animated, { |
| 12 | 12 | import type { ComponentProps } from "react"; |
| 13 | 13 | |
| 14 | 14 | import { AppText as Text } from "../../../../components/AppText"; |
| 15 | import { useThemeColor } from "../../../../lib/useThemeColor"; | |
| 15 | import { useUniwindTheme } from "../../../../lib/useUniwindTheme"; | |
| 16 | 16 | |
| 17 | 17 | type SymbolName = ComponentProps<typeof SymbolView>["name"]; |
| 18 | 18 | |
| ... | ... | @@ -36,10 +36,9 @@ export function FontSizeSliderRow(props: { |
| 36 | 36 | readonly value: number; |
| 37 | 37 | readonly onChange: (value: number) => void; |
| 38 | 38 | }) { |
| 39 | const icon = useThemeColor("--color-icon"); | |
| 40 | const iconMuted = String(useThemeColor("--color-icon-muted")); | |
| 41 | const trackColor = String(useThemeColor("--color-secondary-border")); | |
| 42 | const fillColor = String(useThemeColor("--color-primary")); | |
| 39 | const theme = useUniwindTheme(); | |
| 40 | const trackColor = theme["--color-secondary-border"]; | |
| 41 | const fillColor = theme["--color-primary"]; | |
| 43 | 42 | |
| 44 | 43 | const latest = useRef(props); |
| 45 | 44 | latest.current = props; |
| ... | ... | @@ -141,7 +140,7 @@ export function FontSizeSliderRow(props: { |
| 141 | 140 | <SymbolView |
| 142 | 141 | name={props.icon} |
| 143 | 142 | size={22} |
| 144 | tintColor={icon} | |
| 143 | tintColorClassName={"accent-icon"} | |
| 145 | 144 | type="monochrome" |
| 146 | 145 | weight="regular" |
| 147 | 146 | /> |
| ... | ... | @@ -152,7 +151,7 @@ export function FontSizeSliderRow(props: { |
| 152 | 151 | <SymbolView |
| 153 | 152 | name="textformat.size.smaller" |
| 154 | 153 | size={15} |
| 155 | tintColor={iconMuted} | |
| 154 | tintColorClassName={"accent-icon-muted"} | |
| 156 | 155 | type="monochrome" |
| 157 | 156 | weight="regular" |
| 158 | 157 | /> |
| ... | ... | @@ -204,7 +203,7 @@ export function FontSizeSliderRow(props: { |
| 204 | 203 | <SymbolView |
| 205 | 204 | name="textformat.size.larger" |
| 206 | 205 | size={22} |
| 207 | tintColor={iconMuted} | |
| 206 | tintColorClassName={"accent-icon-muted"} | |
| 208 | 207 | type="monochrome" |
| 209 | 208 | weight="regular" |
| 210 | 209 | /> |
apps/mobile/src/features/settings/appearance/sections/ThemeAppearanceSection.tsx+73-117| ... | ... | @@ -1,22 +1,22 @@ |
| 1 | 1 | import { memo, useId } from "react"; |
| 2 | 2 | import { Pressable, View } from "react-native"; |
| 3 | 3 | import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg"; |
| 4 | import { ScopedTheme } from "uniwind"; | |
| 4 | 5 | |
| 5 | 6 | import { mixThemePreviewBase, THEME_PREVIEW_RENDER_SPECS } from "@t3tools/shared/themePreview"; |
| 6 | 7 | |
| 7 | 8 | import { SymbolView } from "../../../../components/AppSymbol"; |
| 8 | 9 | import { AppText as Text } from "../../../../components/AppText"; |
| 9 | 10 | import { |
| 10 | getMobileThemeVariables, | |
| 11 | 11 | getMobileThemePreviewColors, |
| 12 | 12 | MOBILE_THEME_OPTIONS, |
| 13 | 13 | type MobileThemeAppearance, |
| 14 | 14 | type MobileThemeId, |
| 15 | 15 | type MobileThemeIds, |
| 16 | 16 | type MobileThemeMode, |
| 17 | type MobileThemeVariables, | |
| 18 | 17 | } from "../../../../lib/mobileTheme"; |
| 19 | import { useThemeColor } from "../../../../lib/useThemeColor"; | |
| 18 | import { getMobileUniwindThemeName } from "../../../../lib/mobileThemeRuntime"; | |
| 19 | import { cn } from "../../../../lib/cn"; | |
| 20 | 20 | import { useAppearancePreferences } from "../AppearancePreferencesProvider"; |
| 21 | 21 | |
| 22 | 22 | const APPEARANCE_MODES: ReadonlyArray<{ |
| ... | ... | @@ -28,6 +28,8 @@ const APPEARANCE_MODES: ReadonlyArray<{ |
| 28 | 28 | { id: "dark", label: "Dark" }, |
| 29 | 29 | ]; |
| 30 | 30 | |
| 31 | const previewPercentage = (value: number) => `${value * 100}%`; | |
| 32 | ||
| 31 | 33 | const PreviewOrb = memo(function PreviewOrb(props: { |
| 32 | 34 | readonly appearance: MobileThemeAppearance; |
| 33 | 35 | readonly compact?: boolean; |
| ... | ... | @@ -46,9 +48,6 @@ const PreviewOrb = memo(function PreviewOrb(props: { |
| 46 | 48 | Math.max(spec.action.center[0], 1 - spec.action.center[0]), |
| 47 | 49 | Math.max(spec.action.center[1], 1 - spec.action.center[1]), |
| 48 | 50 | ); |
| 49 | const position = (value: number) => `${value * 100}%`; | |
| 50 | const radius = (value: number) => `${value * 100}%`; | |
| 51 | ||
| 52 | 51 | return ( |
| 53 | 52 | <View |
| 54 | 53 | className={`${props.compact ? "size-14" : "size-16"} overflow-hidden rounded-full border`} |
| ... | ... | @@ -60,33 +59,33 @@ const PreviewOrb = memo(function PreviewOrb(props: { |
| 60 | 59 | <Svg accessibilityElementsHidden height="100%" viewBox="0 0 64 64" width="100%"> |
| 61 | 60 | <Defs> |
| 62 | 61 | <RadialGradient |
| 63 | cx={position(spec.accent.center[0])} | |
| 64 | cy={position(spec.accent.center[1])} | |
| 65 | fx={position(spec.accent.center[0])} | |
| 66 | fy={position(spec.accent.center[1])} | |
| 62 | cx={previewPercentage(spec.accent.center[0])} | |
| 63 | cy={previewPercentage(spec.accent.center[1])} | |
| 64 | fx={previewPercentage(spec.accent.center[0])} | |
| 65 | fy={previewPercentage(spec.accent.center[1])} | |
| 67 | 66 | id={accentGradientId} |
| 68 | r={radius(accentRadius)} | |
| 67 | r={previewPercentage(accentRadius)} | |
| 69 | 68 | > |
| 70 | 69 | <Stop offset="0%" stopColor={colors.accent} stopOpacity={1} /> |
| 71 | 70 | <Stop |
| 72 | offset={position(spec.accent.middleOffset)} | |
| 71 | offset={previewPercentage(spec.accent.middleOffset)} | |
| 73 | 72 | stopColor={colors.accent} |
| 74 | 73 | stopOpacity={spec.accent.middleOpacity} |
| 75 | 74 | /> |
| 76 | 75 | <Stop |
| 77 | offset={position(spec.accent.endOffset)} | |
| 76 | offset={previewPercentage(spec.accent.endOffset)} | |
| 78 | 77 | stopColor={colors.accent} |
| 79 | 78 | stopOpacity={0} |
| 80 | 79 | /> |
| 81 | 80 | <Stop offset="100%" stopColor={colors.accent} stopOpacity={0} /> |
| 82 | 81 | </RadialGradient> |
| 83 | 82 | <RadialGradient |
| 84 | cx={position(spec.action.center[0])} | |
| 85 | cy={position(spec.action.center[1])} | |
| 86 | fx={position(spec.action.center[0])} | |
| 87 | fy={position(spec.action.center[1])} | |
| 83 | cx={previewPercentage(spec.action.center[0])} | |
| 84 | cy={previewPercentage(spec.action.center[1])} | |
| 85 | fx={previewPercentage(spec.action.center[0])} | |
| 86 | fy={previewPercentage(spec.action.center[1])} | |
| 88 | 87 | id={actionGradientId} |
| 89 | r={radius(actionRadius)} | |
| 88 | r={previewPercentage(actionRadius)} | |
| 90 | 89 | > |
| 91 | 90 | <Stop |
| 92 | 91 | offset="0%" |
| ... | ... | @@ -94,7 +93,7 @@ const PreviewOrb = memo(function PreviewOrb(props: { |
| 94 | 93 | stopOpacity={spec.action.startOpacity} |
| 95 | 94 | /> |
| 96 | 95 | <Stop |
| 97 | offset={position(spec.action.endOffset)} | |
| 96 | offset={previewPercentage(spec.action.endOffset)} | |
| 98 | 97 | stopColor={colors.messageAction} |
| 99 | 98 | stopOpacity={0} |
| 100 | 99 | /> |
| ... | ... | @@ -118,32 +117,26 @@ function ThemeCard(props: { |
| 118 | 117 | readonly onSelect: (appearance: MobileThemeAppearance) => void; |
| 119 | 118 | readonly themeId: MobileThemeId; |
| 120 | 119 | }) { |
| 121 | const badgeBackground = useThemeColor("--color-card"); | |
| 122 | const badgeIcon = useThemeColor("--color-icon"); | |
| 123 | ||
| 124 | 120 | const choice = (appearance: MobileThemeAppearance, selected: boolean) => ( |
| 125 | 121 | <Pressable |
| 122 | accessibilityHint={`Sets the ${appearance} appearance only`} | |
| 126 | 123 | accessibilityLabel={`${props.label} ${appearance} theme`} |
| 127 | accessibilityRole="radio" | |
| 128 | accessibilityState={{ checked: selected, disabled: props.disabled }} | |
| 129 | className={ | |
| 130 | selected | |
| 131 | ? "size-[66px] items-center justify-center rounded-full border-[3px] border-primary" | |
| 132 | : "size-[66px] items-center justify-center rounded-full border-[3px] border-transparent" | |
| 133 | } | |
| 124 | accessibilityRole="button" | |
| 125 | accessibilityState={{ disabled: props.disabled, selected }} | |
| 126 | className={cn( | |
| 127 | "size-[66px] items-center justify-center rounded-full border-[3px] active:scale-[0.94]", | |
| 128 | selected ? "border-primary" : "border-transparent", | |
| 129 | )} | |
| 134 | 130 | disabled={props.disabled} |
| 135 | 131 | onPress={() => props.onSelect(appearance)} |
| 136 | 132 | > |
| 137 | 133 | <PreviewOrb appearance={appearance} compact themeId={props.themeId} /> |
| 138 | 134 | {selected ? ( |
| 139 | <View | |
| 140 | className="absolute -bottom-0.5 -right-0.5 size-5 items-center justify-center rounded-full border border-border" | |
| 141 | style={{ backgroundColor: badgeBackground }} | |
| 142 | > | |
| 135 | <View className="absolute -bottom-0.5 -right-0.5 size-5 items-center justify-center rounded-full border border-border bg-card"> | |
| 143 | 136 | <SymbolView |
| 144 | 137 | name={appearance === "light" ? "sun.max" : "moon"} |
| 145 | 138 | size={12} |
| 146 | tintColor={badgeIcon} | |
| 139 | tintColorClassName="accent-icon" | |
| 147 | 140 | type="monochrome" |
| 148 | 141 | weight="medium" |
| 149 | 142 | /> |
| ... | ... | @@ -158,7 +151,10 @@ function ThemeCard(props: { |
| 158 | 151 | accessibilityHint="Sets both light and dark appearances" |
| 159 | 152 | accessibilityLabel={`${props.label} theme`} |
| 160 | 153 | accessibilityRole="button" |
| 161 | accessibilityState={{ disabled: props.disabled }} | |
| 154 | accessibilityState={{ | |
| 155 | disabled: props.disabled, | |
| 156 | selected: props.lightSelected && props.darkSelected, | |
| 157 | }} | |
| 162 | 158 | className="absolute inset-0 rounded-[24px] active:bg-subtle" |
| 163 | 159 | disabled={props.disabled} |
| 164 | 160 | onPress={props.onSelectBoth} |
| ... | ... | @@ -167,34 +163,26 @@ function ThemeCard(props: { |
| 167 | 163 | {choice("light", props.lightSelected)} |
| 168 | 164 | {choice("dark", props.darkSelected)} |
| 169 | 165 | </View> |
| 170 | <View className="min-h-8 flex-row items-center" pointerEvents="none"> | |
| 171 | <Text className="min-w-0 flex-1 text-lg font-t3-medium" numberOfLines={1}> | |
| 172 | {props.label} | |
| 173 | </Text> | |
| 174 | </View> | |
| 166 | <Text | |
| 167 | className="min-w-0 flex-1 px-1 text-lg font-t3-medium" | |
| 168 | numberOfLines={1} | |
| 169 | pointerEvents="none" | |
| 170 | > | |
| 171 | {props.label} | |
| 172 | </Text> | |
| 175 | 173 | </View> |
| 176 | 174 | ); |
| 177 | 175 | } |
| 178 | 176 | |
| 179 | function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly compact?: boolean }) { | |
| 177 | function PreviewPane(props: { readonly compact?: boolean }) { | |
| 180 | 178 | return ( |
| 181 | <View | |
| 182 | className="flex-1 overflow-hidden" | |
| 183 | style={{ backgroundColor: props.colors["--color-screen"] }} | |
| 184 | > | |
| 179 | <View className="flex-1 overflow-hidden bg-screen"> | |
| 185 | 180 | <View |
| 186 | className={props.compact ? "h-[18px] gap-0.5 px-1" : "h-[18px] gap-1 px-1.5"} | |
| 187 | style={{ backgroundColor: props.colors["--color-card"] }} | |
| 181 | className={cn("bg-card", props.compact ? "h-[18px] gap-0.5 px-1" : "h-[18px] gap-1 px-1.5")} | |
| 188 | 182 | > |
| 189 | 183 | <View className="mt-2 flex-row items-center gap-1"> |
| 190 | <View | |
| 191 | className="size-1.5 rounded-full" | |
| 192 | style={{ backgroundColor: props.colors["--color-primary"] }} | |
| 193 | /> | |
| 194 | <View | |
| 195 | className="h-1 flex-1 rounded-full" | |
| 196 | style={{ backgroundColor: props.colors["--color-foreground-muted"] }} | |
| 197 | /> | |
| 184 | <View className="size-1.5 rounded-full bg-primary" /> | |
| 185 | <View className="h-1 flex-1 rounded-full bg-foreground-muted" /> | |
| 198 | 186 | </View> |
| 199 | 187 | </View> |
| 200 | 188 | <View |
| ... | ... | @@ -203,24 +191,12 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co |
| 203 | 191 | } |
| 204 | 192 | > |
| 205 | 193 | <View className="gap-1"> |
| 206 | <View | |
| 207 | className="h-1.5 w-[72%] rounded-full" | |
| 208 | style={{ backgroundColor: props.colors["--color-subtle-strong"] }} | |
| 209 | /> | |
| 210 | <View | |
| 211 | className="h-1.5 w-[46%] rounded-full" | |
| 212 | style={{ backgroundColor: props.colors["--color-subtle-strong"] }} | |
| 213 | /> | |
| 194 | <View className="h-1.5 w-[72%] rounded-full bg-subtle-strong" /> | |
| 195 | <View className="h-1.5 w-[46%] rounded-full bg-subtle-strong" /> | |
| 214 | 196 | </View> |
| 215 | 197 | <View className="items-end gap-1 pb-2"> |
| 216 | <View | |
| 217 | className="h-3 w-[78%] rounded-full" | |
| 218 | style={{ backgroundColor: props.colors["--color-user-bubble"] }} | |
| 219 | /> | |
| 220 | <View | |
| 221 | className="h-1 w-[38%] rounded-full" | |
| 222 | style={{ backgroundColor: props.colors["--color-foreground-muted"] }} | |
| 223 | /> | |
| 198 | <View className="h-3 w-[78%] rounded-full bg-user-bubble" /> | |
| 199 | <View className="h-1 w-[38%] rounded-full bg-foreground-muted" /> | |
| 224 | 200 | </View> |
| 225 | 201 | </View> |
| 226 | 202 | </View> |
| ... | ... | @@ -228,50 +204,31 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co |
| 228 | 204 | } |
| 229 | 205 | |
| 230 | 206 | function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: MobileThemeIds }) { |
| 231 | const light = getMobileThemeVariables(props.themeIds.light, "light"); | |
| 232 | const dark = getMobileThemeVariables(props.themeIds.dark, "dark"); | |
| 233 | const currentBorder = useThemeColor("--color-border"); | |
| 234 | const currentFrame = useThemeColor("--color-drawer"); | |
| 235 | const currentIndicator = useThemeColor("--color-foreground-muted"); | |
| 236 | const frameColor = | |
| 237 | props.mode === "light" | |
| 238 | ? light["--color-border"] | |
| 239 | : props.mode === "dark" | |
| 240 | ? dark["--color-border"] | |
| 241 | : currentBorder; | |
| 242 | const frameBackground = | |
| 243 | props.mode === "light" | |
| 244 | ? light["--color-drawer"] | |
| 245 | : props.mode === "dark" | |
| 246 | ? dark["--color-drawer"] | |
| 247 | : currentFrame; | |
| 248 | const indicatorColor = | |
| 249 | props.mode === "light" | |
| 250 | ? light["--color-foreground-muted"] | |
| 251 | : props.mode === "dark" | |
| 252 | ? dark["--color-foreground-muted"] | |
| 253 | : currentIndicator; | |
| 207 | if (props.mode === "system") { | |
| 208 | return ( | |
| 209 | <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]"> | |
| 210 | <View className="flex-1 flex-row overflow-hidden rounded-[11px]"> | |
| 211 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.light, "light")}> | |
| 212 | <PreviewPane compact /> | |
| 213 | </ScopedTheme> | |
| 214 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.dark, "dark")}> | |
| 215 | <PreviewPane compact /> | |
| 216 | </ScopedTheme> | |
| 217 | </View> | |
| 218 | <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" /> | |
| 219 | </View> | |
| 220 | ); | |
| 221 | } | |
| 254 | 222 | |
| 255 | 223 | return ( |
| 256 | <View | |
| 257 | className="h-24 w-14 self-center rounded-[16px] p-[3px]" | |
| 258 | style={{ backgroundColor: frameBackground, borderColor: frameColor, borderWidth: 1.5 }} | |
| 259 | > | |
| 260 | <View className="flex-1 flex-row overflow-hidden rounded-[11px]"> | |
| 261 | {props.mode === "system" ? ( | |
| 262 | <> | |
| 263 | <PreviewPane colors={light} compact /> | |
| 264 | <PreviewPane colors={dark} compact /> | |
| 265 | </> | |
| 266 | ) : ( | |
| 267 | <PreviewPane colors={props.mode === "light" ? light : dark} /> | |
| 268 | )} | |
| 224 | <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds[props.mode], props.mode)}> | |
| 225 | <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]"> | |
| 226 | <View className="flex-1 flex-row overflow-hidden rounded-[11px]"> | |
| 227 | <PreviewPane /> | |
| 228 | </View> | |
| 229 | <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" /> | |
| 269 | 230 | </View> |
| 270 | <View | |
| 271 | className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full" | |
| 272 | style={{ backgroundColor: indicatorColor }} | |
| 273 | /> | |
| 274 | </View> | |
| 231 | </ScopedTheme> | |
| 275 | 232 | ); |
| 276 | 233 | } |
| 277 | 234 | |
| ... | ... | @@ -288,11 +245,10 @@ function ModeCard(props: { |
| 288 | 245 | accessibilityLabel={`${props.label} appearance`} |
| 289 | 246 | accessibilityRole="radio" |
| 290 | 247 | accessibilityState={{ checked: props.selected, disabled: props.disabled }} |
| 291 | className={ | |
| 292 | props.selected | |
| 293 | ? "min-w-0 flex-1 gap-2 rounded-[24px] border-2 border-primary bg-subtle p-2" | |
| 294 | : "min-w-0 flex-1 gap-2 rounded-[24px] border border-border bg-card p-2" | |
| 295 | } | |
| 248 | className={cn( | |
| 249 | "min-w-0 flex-1 gap-2 rounded-[24px] p-2 active:scale-[0.97]", | |
| 250 | props.selected ? "border-2 border-primary bg-subtle" : "border border-border bg-card", | |
| 251 | )} | |
| 296 | 252 | disabled={props.disabled} |
| 297 | 253 | onPress={props.onPress} |
| 298 | 254 | > |
apps/mobile/src/features/settings/appearance/useScaledTextRole.ts+22-24| ... | ... | @@ -1,18 +1,12 @@ |
| 1 | import { useCSSVariable } from "uniwind"; | |
| 1 | import { useMemo } from "react"; | |
| 2 | 2 | |
| 3 | import { | |
| 4 | DEFAULT_BASE_FONT_SIZE, | |
| 5 | normalizeBaseFontSize, | |
| 6 | scaledTypographyLineHeight, | |
| 7 | } from "../../../lib/appearancePreferences"; | |
| 3 | 8 | import { MOBILE_TYPOGRAPHY } from "../../../lib/typography"; |
| 4 | ||
| 5 | const TEXT_ROLE_VARIABLES = { | |
| 6 | micro: "--text-3xs", | |
| 7 | caption: "--text-2xs", | |
| 8 | label: "--text-xs", | |
| 9 | footnote: "--text-sm", | |
| 10 | body: "--text-base", | |
| 11 | headline: "--text-lg", | |
| 12 | title: "--text-xl", | |
| 13 | largeTitle: "--text-2xl", | |
| 14 | display: "--text-3xl", | |
| 15 | } as const satisfies Record<keyof typeof MOBILE_TYPOGRAPHY, string>; | |
| 9 | import { useAppearancePreferences } from "./AppearancePreferencesProvider"; | |
| 16 | 10 | |
| 17 | 11 | export interface ScaledTextRole { |
| 18 | 12 | readonly fontSize: number; |
| ... | ... | @@ -20,17 +14,21 @@ export interface ScaledTextRole { |
| 20 | 14 | } |
| 21 | 15 | |
| 22 | 16 | /** |
| 23 | * Reads a typography role's current size from the Uniwind `--text-*` CSS | |
| 24 | * variables (scaled at runtime with the base font size). Use for style-prop | |
| 25 | * consumers that can't express their size as a `text-*` className. Reactive: | |
| 26 | * re-renders when the appearance provider re-injects the variables. | |
| 17 | * Mirrors the values injected into Uniwind for style-prop consumers that | |
| 18 | * cannot use a `text-*` class. This deliberately does not subscribe to CSS | |
| 19 | * variables, so palette-only setTheme calls remain native-only. | |
| 27 | 20 | */ |
| 28 | 21 | export function useScaledTextRole(role: keyof typeof MOBILE_TYPOGRAPHY): ScaledTextRole { |
| 29 | const variable = TEXT_ROLE_VARIABLES[role]; | |
| 30 | const [fontSize, lineHeight] = useCSSVariable([variable, `${variable}--line-height`]); | |
| 31 | ||
| 32 | return { | |
| 33 | fontSize: typeof fontSize === "number" ? fontSize : MOBILE_TYPOGRAPHY[role].fontSize, | |
| 34 | lineHeight: typeof lineHeight === "number" ? lineHeight : MOBILE_TYPOGRAPHY[role].lineHeight, | |
| 35 | }; | |
| 22 | const { appearance } = useAppearancePreferences(); | |
| 23 | return useMemo(() => { | |
| 24 | const baseFontSize = normalizeBaseFontSize(appearance.baseFontSize); | |
| 25 | const typography = MOBILE_TYPOGRAPHY[role]; | |
| 26 | return { | |
| 27 | fontSize: Math.max( | |
| 28 | 8, | |
| 29 | Math.round(typography.fontSize * (baseFontSize / DEFAULT_BASE_FONT_SIZE)), | |
| 30 | ), | |
| 31 | lineHeight: scaledTypographyLineHeight(typography, baseFontSize), | |
| 32 | }; | |
| 33 | }, [appearance.baseFontSize, role]); | |
| 36 | 34 | } |
apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx+3-7| ... | ... | @@ -6,12 +6,10 @@ import { WebView } from "react-native-webview"; |
| 6 | 6 | import { AppText as Text } from "../../../components/AppText"; |
| 7 | 7 | import { LoadingStrip } from "../../../components/LoadingStrip"; |
| 8 | 8 | import { SymbolView } from "../../../components/AppSymbol"; |
| 9 | import { useThemeColor } from "../../../lib/useThemeColor"; | |
| 10 | 9 | import { isLegalDocumentUrl, LEGAL_URL } from "../lib/legal-document-url"; |
| 11 | 10 | |
| 12 | 11 | export function SettingsLegalDocumentCloseHeaderButton() { |
| 13 | 12 | const navigation = useNavigation(); |
| 14 | const iconColor = useThemeColor("--color-icon"); | |
| 15 | 13 | |
| 16 | 14 | return ( |
| 17 | 15 | <Pressable |
| ... | ... | @@ -24,7 +22,7 @@ export function SettingsLegalDocumentCloseHeaderButton() { |
| 24 | 22 | <SymbolView |
| 25 | 23 | name="xmark" |
| 26 | 24 | size={18} |
| 27 | tintColor={iconColor} | |
| 25 | tintColorClassName={"accent-icon"} | |
| 28 | 26 | type="monochrome" |
| 29 | 27 | weight="semibold" |
| 30 | 28 | /> |
| ... | ... | @@ -37,7 +35,6 @@ export function SettingsLegalDocumentExternalHeaderButton({ |
| 37 | 35 | }: { |
| 38 | 36 | readonly externalUrl?: string; |
| 39 | 37 | }) { |
| 40 | const iconColor = useThemeColor("--color-icon"); | |
| 41 | 38 | const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : LEGAL_URL; |
| 42 | 39 | |
| 43 | 40 | return ( |
| ... | ... | @@ -51,7 +48,7 @@ export function SettingsLegalDocumentExternalHeaderButton({ |
| 51 | 48 | <SymbolView |
| 52 | 49 | name="safari" |
| 53 | 50 | size={19} |
| 54 | tintColor={iconColor} | |
| 51 | tintColorClassName={"accent-icon"} | |
| 55 | 52 | type="monochrome" |
| 56 | 53 | weight="regular" |
| 57 | 54 | /> |
| ... | ... | @@ -69,7 +66,6 @@ export function SettingsLegalDocumentRouteScreen({ |
| 69 | 66 | documentUrl, |
| 70 | 67 | }: SettingsLegalDocumentRouteScreenProps) { |
| 71 | 68 | const navigation = useNavigation<NavigationProp<ParamListBase>>(); |
| 72 | const iconColor = useThemeColor("--color-icon"); | |
| 73 | 69 | const [reloadKey, setReloadKey] = useState(0); |
| 74 | 70 | const [loadProgress, setLoadProgress] = useState(0); |
| 75 | 71 | const [loadError, setLoadError] = useState<string | null>(null); |
| ... | ... | @@ -94,7 +90,7 @@ export function SettingsLegalDocumentRouteScreen({ |
| 94 | 90 | <SymbolView |
| 95 | 91 | name="exclamationmark.triangle" |
| 96 | 92 | size={32} |
| 97 | tintColor={iconColor} | |
| 93 | tintColorClassName={"accent-icon"} | |
| 98 | 94 | type="monochrome" |
| 99 | 95 | weight="regular" |
| 100 | 96 | /> |
apps/mobile/src/features/settings/components/SettingsRow.tsx+8-5| ... | ... | @@ -5,7 +5,6 @@ import { Pressable, View } from "react-native"; |
| 5 | 5 | import { SymbolView } from "../../../components/AppSymbol"; |
| 6 | 6 | |
| 7 | 7 | import { AppText as Text } from "../../../components/AppText"; |
| 8 | import { useThemeColor } from "../../../lib/useThemeColor"; | |
| 9 | 8 | import type { SettingsLegalDocumentTarget, SettingsSheetTarget } from "./settings-sheet-targets"; |
| 10 | 9 | |
| 11 | 10 | type SymbolName = ComponentProps<typeof SymbolView>["name"]; |
| ... | ... | @@ -20,8 +19,6 @@ export function SettingsRow(props: { |
| 20 | 19 | readonly onPress?: () => void; |
| 21 | 20 | }) { |
| 22 | 21 | const navigation = useNavigation(); |
| 23 | const icon = useThemeColor("--color-icon"); | |
| 24 | const chevron = useThemeColor("--color-chevron"); | |
| 25 | 22 | const content = ( |
| 26 | 23 | <View |
| 27 | 24 | className={ |
| ... | ... | @@ -30,7 +27,13 @@ export function SettingsRow(props: { |
| 30 | 27 | : "flex-row items-center gap-4 p-4" |
| 31 | 28 | } |
| 32 | 29 | > |
| 33 | <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" /> | |
| 30 | <SymbolView | |
| 31 | name={props.icon} | |
| 32 | size={22} | |
| 33 | tintColorClassName={"accent-icon"} | |
| 34 | type="monochrome" | |
| 35 | weight="regular" | |
| 36 | /> | |
| 34 | 37 | <Text className="shrink-0 text-lg text-foreground" numberOfLines={1}> |
| 35 | 38 | {props.label} |
| 36 | 39 | </Text> |
| ... | ... | @@ -48,7 +51,7 @@ export function SettingsRow(props: { |
| 48 | 51 | <SymbolView |
| 49 | 52 | name="chevron.right" |
| 50 | 53 | size={16} |
| 51 | tintColor={chevron} | |
| 54 | tintColorClassName={"accent-chevron"} | |
| 52 | 55 | type="monochrome" |
| 53 | 56 | weight="semibold" |
| 54 | 57 | /> |
apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx+7-4| ... | ... | @@ -4,7 +4,6 @@ import { View } from "react-native"; |
| 4 | 4 | import { SymbolView } from "../../../components/AppSymbol"; |
| 5 | 5 | import { AppText as Text } from "../../../components/AppText"; |
| 6 | 6 | import { ThemedSwitch } from "../../../components/ThemedSwitch"; |
| 7 | import { useThemeColor } from "../../../lib/useThemeColor"; | |
| 8 | 7 | |
| 9 | 8 | type SymbolName = ComponentProps<typeof SymbolView>["name"]; |
| 10 | 9 | |
| ... | ... | @@ -16,8 +15,6 @@ export function SettingsSwitchRow(props: { |
| 16 | 15 | readonly value: boolean; |
| 17 | 16 | readonly onValueChange: (value: boolean) => void; |
| 18 | 17 | }) { |
| 19 | const icon = useThemeColor("--color-icon"); | |
| 20 | ||
| 21 | 18 | return ( |
| 22 | 19 | <View |
| 23 | 20 | className={ |
| ... | ... | @@ -26,7 +23,13 @@ export function SettingsSwitchRow(props: { |
| 26 | 23 | : "flex-row items-center gap-4 p-4" |
| 27 | 24 | } |
| 28 | 25 | > |
| 29 | <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" /> | |
| 26 | <SymbolView | |
| 27 | name={props.icon} | |
| 28 | size={22} | |
| 29 | tintColorClassName={"accent-icon"} | |
| 30 | type="monochrome" | |
| 31 | weight="regular" | |
| 32 | /> | |
| 30 | 33 | <View className="min-w-0 flex-1"> |
| 31 | 34 | <Text className="text-lg text-foreground">{props.label}</Text> |
| 32 | 35 | {props.subtitle ? ( |
apps/mobile/src/features/terminal/ThreadTerminalPanel.tsx+6-3| ... | ... | @@ -4,7 +4,6 @@ import { memo, useCallback, useEffect, useMemo, useRef } from "react"; |
| 4 | 4 | import { Pressable, View } from "react-native"; |
| 5 | 5 | |
| 6 | 6 | import { AppText as Text } from "../../components/AppText"; |
| 7 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 8 | 7 | import { terminalEnvironment } from "../../state/terminal"; |
| 9 | 8 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 10 | 9 | import { useAttachedTerminalSession } from "../../state/use-terminal-session"; |
| ... | ... | @@ -36,7 +35,6 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel( |
| 36 | 35 | const closeTerminal = useAtomCommand(terminalEnvironment.close, "terminal close"); |
| 37 | 36 | const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open"); |
| 38 | 37 | const nativeTerminalAvailable = hasNativeTerminalSurface(); |
| 39 | const iconColor = useThemeColor("--color-icon"); | |
| 40 | 38 | const terminalId = DEFAULT_TERMINAL_ID; |
| 41 | 39 | const lastGridSizeRef = useRef<TerminalGridSize>({ |
| 42 | 40 | cols: DEFAULT_TERMINAL_COLS, |
| ... | ... | @@ -236,7 +234,12 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel( |
| 236 | 234 | className="h-8 w-8 items-center justify-center rounded-[8px] bg-subtle" |
| 237 | 235 | onPress={props.onClose} |
| 238 | 236 | > |
| 239 | <SymbolView name="xmark" size={13} tintColor={iconColor} type="monochrome" /> | |
| 237 | <SymbolView | |
| 238 | name="xmark" | |
| 239 | size={13} | |
| 240 | tintColorClassName={"accent-icon"} | |
| 241 | type="monochrome" | |
| 242 | /> | |
| 240 | 243 | </Pressable> |
| 241 | 244 | </View> |
| 242 | 245 | </View> |
apps/mobile/src/features/threads/ComposerCommandPopover.tsx+14-17| ... | ... | @@ -5,13 +5,12 @@ import { |
| 5 | 5 | import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts"; |
| 6 | 6 | import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger"; |
| 7 | 7 | import { memo } from "react"; |
| 8 | import { Pressable, ScrollView, View, type ViewStyle } from "react-native"; | |
| 8 | import { Pressable, ScrollView, StyleSheet, View, type ViewStyle } from "react-native"; | |
| 9 | 9 | |
| 10 | 10 | import { SymbolView, type AppSymbolName } from "../../components/AppSymbol"; |
| 11 | 11 | import { AppText as Text } from "../../components/AppText"; |
| 12 | 12 | import { GlassSurface } from "../../components/GlassSurface"; |
| 13 | 13 | import { PierreEntryIcon } from "../../components/PierreEntryIcon"; |
| 14 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 15 | 14 | export type ComposerCommandItem = |
| 16 | 15 | | { |
| 17 | 16 | readonly id: string; |
| ... | ... | @@ -51,7 +50,6 @@ interface ComposerCommandPopoverProps { |
| 51 | 50 | } |
| 52 | 51 | |
| 53 | 52 | function PopoverSurface(props: { readonly children: React.ReactNode; readonly style?: ViewStyle }) { |
| 54 | const tintColor = useThemeColor("--color-glass-surface"); | |
| 55 | 53 | const baseStyle: ViewStyle = { |
| 56 | 54 | borderRadius: 16, |
| 57 | 55 | overflow: "hidden", |
| ... | ... | @@ -59,7 +57,11 @@ function PopoverSurface(props: { readonly children: React.ReactNode; readonly st |
| 59 | 57 | }; |
| 60 | 58 | |
| 61 | 59 | return ( |
| 62 | <GlassSurface glassEffectStyle="clear" tintColor={tintColor} style={baseStyle}> | |
| 60 | <GlassSurface | |
| 61 | glassEffectStyle="clear" | |
| 62 | tintColorClassName="accent-glass-surface" | |
| 63 | style={baseStyle} | |
| 64 | > | |
| 63 | 65 | {props.children} |
| 64 | 66 | </GlassSurface> |
| 65 | 67 | ); |
| ... | ... | @@ -122,27 +124,22 @@ const CommandRow = memo(function CommandRow(props: { |
| 122 | 124 | readonly isSlashSkill: boolean; |
| 123 | 125 | }) { |
| 124 | 126 | const iconName = itemIcon(props.item); |
| 125 | const iconColor = useThemeColor("--color-icon-subtle"); | |
| 126 | const borderColor = useThemeColor("--color-border"); | |
| 127 | 127 | |
| 128 | 128 | return ( |
| 129 | 129 | <Pressable |
| 130 | 130 | onPress={props.onPress} |
| 131 | style={({ pressed }) => ({ | |
| 132 | flexDirection: "row", | |
| 133 | alignItems: "center", | |
| 134 | paddingHorizontal: 14, | |
| 135 | paddingVertical: 10, | |
| 136 | gap: 10, | |
| 137 | opacity: pressed ? 0.6 : 1, | |
| 138 | borderBottomWidth: props.isLast ? 0 : 0.5, | |
| 139 | borderBottomColor: borderColor, | |
| 140 | })} | |
| 131 | className="flex-row items-center gap-2.5 border-border px-3.5 py-2.5 active:opacity-60" | |
| 132 | style={{ borderBottomWidth: props.isLast ? 0 : StyleSheet.hairlineWidth }} | |
| 141 | 133 | > |
| 142 | 134 | {props.item.type === "path" ? ( |
| 143 | 135 | <PierreEntryIcon path={props.item.path} kind={props.item.kind} size={16} /> |
| 144 | 136 | ) : iconName ? ( |
| 145 | <SymbolView name={iconName} size={14} tintColor={iconColor} type="monochrome" /> | |
| 137 | <SymbolView | |
| 138 | name={iconName} | |
| 139 | size={14} | |
| 140 | tintColorClassName={"accent-icon-subtle"} | |
| 141 | type="monochrome" | |
| 142 | /> | |
| 146 | 143 | ) : null} |
| 147 | 144 | <Text className="shrink-0 text-base font-t3-medium text-foreground" numberOfLines={1}> |
| 148 | 145 | {props.isSlashSkill && props.item.type === "skill" ? ( |
apps/mobile/src/features/threads/GitActionProgressOverlay.tsx+17-17| ... | ... | @@ -2,7 +2,7 @@ import * as Haptics from "expo-haptics"; |
| 2 | 2 | import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass"; |
| 3 | 3 | import { SymbolView } from "../../components/AppSymbol"; |
| 4 | 4 | import { useCallback, useEffect, useRef } from "react"; |
| 5 | import { ActivityIndicator, Pressable, StyleSheet, View } from "react-native"; | |
| 5 | import { ActivityIndicator, Pressable, StyleSheet, useColorScheme, View } from "react-native"; | |
| 6 | 6 | import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated"; |
| 7 | 7 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 8 | 8 | |
| ... | ... | @@ -10,9 +10,8 @@ import { AppText as Text } from "../../components/AppText"; |
| 10 | 10 | import { APP_BAR_HEIGHT } from "../../lib/layoutMetrics"; |
| 11 | 11 | import { themeColorWithAlpha } from "../../lib/mobileTheme"; |
| 12 | 12 | import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; |
| 13 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 13 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 14 | 14 | import type { GitActionProgress } from "../../state/use-vcs-action-state"; |
| 15 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | |
| 16 | 15 | |
| 17 | 16 | const OVERLAY_LAYOUT_TRANSITION = LinearTransition.duration(220); |
| 18 | 17 | const OVERLAY_TOP_GAP = 8; |
| ... | ... | @@ -68,17 +67,16 @@ export function GitActionProgressOverlay(props: { |
| 68 | 67 | |
| 69 | 68 | function OverlayContent(props: { readonly progress: GitActionProgress }) { |
| 70 | 69 | const { progress } = props; |
| 71 | const iconColor = useThemeColor("--color-icon"); | |
| 72 | const glassSurface = useThemeColor("--color-glass-surface"); | |
| 73 | const foreground = useThemeColor("--color-foreground"); | |
| 74 | const shadowColor = useThemeColor("--color-primary-shadow"); | |
| 75 | const { themeAppearance } = useAppearancePreferences(); | |
| 76 | const isDarkMode = themeAppearance === "dark"; | |
| 70 | const colors = useUniwindTheme(); | |
| 71 | const glassSurface = colors["--color-glass-surface"]; | |
| 72 | const foreground = colors["--color-foreground"]; | |
| 73 | const shadowColor = colors["--color-primary-shadow"]; | |
| 74 | const isDarkMode = useColorScheme() === "dark"; | |
| 77 | 75 | const glassTint = themeColorWithAlpha(String(glassSurface), isDarkMode ? 0.48 : 0.38); |
| 78 | 76 | const glassBorder = themeColorWithAlpha(String(foreground), isDarkMode ? 0.18 : 0.12); |
| 79 | 77 | const content = ( |
| 80 | 78 | <> |
| 81 | <OverlayIcon phase={progress.phase} iconColor={iconColor} /> | |
| 79 | <OverlayIcon phase={progress.phase} /> | |
| 82 | 80 | |
| 83 | 81 | <View className="flex-1 gap-0.5"> |
| 84 | 82 | {progress.label ? ( |
| ... | ... | @@ -94,7 +92,12 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) { |
| 94 | 92 | </View> |
| 95 | 93 | |
| 96 | 94 | {progress.prUrl ? ( |
| 97 | <SymbolView name="arrow.up.right" size={13} tintColor={iconColor} type="monochrome" /> | |
| 95 | <SymbolView | |
| 96 | name="arrow.up.right" | |
| 97 | size={13} | |
| 98 | tintColorClassName={"accent-icon"} | |
| 99 | type="monochrome" | |
| 100 | /> | |
| 98 | 101 | ) : null} |
| 99 | 102 | </> |
| 100 | 103 | ); |
| ... | ... | @@ -140,7 +143,7 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) { |
| 140 | 143 | |
| 141 | 144 | const bgClass = |
| 142 | 145 | progress.phase === "error" |
| 143 | ? "bg-red-50 dark:bg-red-950/80 border-red-200 dark:border-red-800" | |
| 146 | ? "border-adaptive-red-200-800 bg-adaptive-red-50-950-a80" | |
| 144 | 147 | : "bg-card border-border"; |
| 145 | 148 | |
| 146 | 149 | return ( |
| ... | ... | @@ -153,13 +156,10 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) { |
| 153 | 156 | ); |
| 154 | 157 | } |
| 155 | 158 | |
| 156 | function OverlayIcon(props: { | |
| 157 | readonly phase: GitActionProgress["phase"]; | |
| 158 | readonly iconColor: ReturnType<typeof useThemeColor>; | |
| 159 | }) { | |
| 159 | function OverlayIcon(props: { readonly phase: GitActionProgress["phase"] }) { | |
| 160 | 160 | switch (props.phase) { |
| 161 | 161 | case "running": |
| 162 | return <ActivityIndicator size="small" />; | |
| 162 | return <ActivityIndicator size="small" colorClassName={"accent-icon"} />; | |
| 163 | 163 | case "success": |
| 164 | 164 | return ( |
| 165 | 165 | <View className="h-6 w-6 items-center justify-center rounded-full bg-green-500"> |
apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx+10-9| ... | ... | @@ -24,7 +24,7 @@ import { AppText as Text } from "../../components/AppText"; |
| 24 | 24 | import { ThemedSwitch } from "../../components/ThemedSwitch"; |
| 25 | 25 | import { cn } from "../../lib/cn"; |
| 26 | 26 | import { useFontFamily } from "../../lib/useFontFamily"; |
| 27 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 27 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 28 | 28 | import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; |
| 29 | 29 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 30 | 30 | import { vcsEnvironment } from "../../state/vcs"; |
| ... | ... | @@ -45,9 +45,6 @@ function SelectionRow(props: { |
| 45 | 45 | readonly subtitle?: string; |
| 46 | 46 | readonly title: string; |
| 47 | 47 | }) { |
| 48 | const iconColor = useThemeColor("--color-icon-muted"); | |
| 49 | const checkmarkColor = useThemeColor("--color-icon"); | |
| 50 | ||
| 51 | 48 | return ( |
| 52 | 49 | <Pressable |
| 53 | 50 | accessibilityLabel={[props.title, props.subtitle].filter(Boolean).join(", ")} |
| ... | ... | @@ -62,7 +59,12 @@ function SelectionRow(props: { |
| 62 | 59 | style={{ opacity: props.disabled ? 0.45 : 1 }} |
| 63 | 60 | > |
| 64 | 61 | {props.icon ? ( |
| 65 | <SymbolView name={props.icon} size={17} tintColor={iconColor} type="monochrome" /> | |
| 62 | <SymbolView | |
| 63 | name={props.icon} | |
| 64 | size={17} | |
| 65 | tintColorClassName={"accent-icon-muted"} | |
| 66 | type="monochrome" | |
| 67 | /> | |
| 66 | 68 | ) : null} |
| 67 | 69 | <View className="min-w-0 flex-1 gap-0.5"> |
| 68 | 70 | <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}> |
| ... | ... | @@ -78,7 +80,7 @@ function SelectionRow(props: { |
| 78 | 80 | <SymbolView |
| 79 | 81 | name="checkmark" |
| 80 | 82 | size={16} |
| 81 | tintColor={checkmarkColor} | |
| 83 | tintColorClassName={"accent-icon"} | |
| 82 | 84 | type="monochrome" |
| 83 | 85 | weight="semibold" |
| 84 | 86 | /> |
| ... | ... | @@ -191,8 +193,7 @@ export function NewTaskBranchPickerRouteScreen() { |
| 191 | 193 | const flow = useNewTaskFlow(); |
| 192 | 194 | const navigation = useNavigation(); |
| 193 | 195 | const insets = useSafeAreaInsets(); |
| 194 | const placeholderColor = useThemeColor("--color-placeholder"); | |
| 195 | const foregroundColor = useThemeColor("--color-foreground"); | |
| 196 | const foregroundColor = useUniwindTheme()["--color-foreground"]; | |
| 196 | 197 | const fontFamily = useFontFamily("regular"); |
| 197 | 198 | const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false }); |
| 198 | 199 | const [switchingBranchName, setSwitchingBranchName] = useState<string | null>(null); |
| ... | ... | @@ -419,7 +420,7 @@ export function NewTaskBranchPickerRouteScreen() { |
| 419 | 420 | className="h-11 rounded-xl bg-card px-4 text-base text-foreground" |
| 420 | 421 | onChangeText={flow.setBranchQuery} |
| 421 | 422 | placeholder="Find a branch" |
| 422 | placeholderTextColor={placeholderColor} | |
| 423 | placeholderTextColorClassName={"accent-placeholder"} | |
| 423 | 424 | style={{ color: foregroundColor, fontFamily }} |
| 424 | 425 | value={flow.branchQuery} |
| 425 | 426 | /> |
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+24-26| ... | ... | @@ -13,8 +13,7 @@ import { |
| 13 | 13 | useKeyboardState, |
| 14 | 14 | } from "react-native-keyboard-controller"; |
| 15 | 15 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 16 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 17 | import { themeColorWithAlpha } from "../../lib/mobileTheme"; | |
| 16 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 18 | 17 | import { useFontFamily } from "../../lib/useFontFamily"; |
| 19 | 18 | |
| 20 | 19 | import { |
| ... | ... | @@ -43,7 +42,6 @@ import { |
| 43 | 42 | import { makeTurnCommandMetadata } from "../../lib/commandMetadata"; |
| 44 | 43 | import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages"; |
| 45 | 44 | import { useScaledTextRole } from "../settings/appearance/useScaledTextRole"; |
| 46 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | |
| 47 | 45 | import { |
| 48 | 46 | clearComposerDraftContent, |
| 49 | 47 | getComposerDraftSnapshot, |
| ... | ... | @@ -71,17 +69,32 @@ function NewTaskWorkspaceIcon(props: { |
| 71 | 69 | readonly workspaceMode: "local" | "worktree"; |
| 72 | 70 | readonly worktreePath: string | null; |
| 73 | 71 | }) { |
| 74 | const iconColor = useThemeColor("--color-icon-muted"); | |
| 75 | ||
| 76 | 72 | if (props.workspaceMode === "local" && props.worktreePath === null) { |
| 77 | return <SymbolView name="folder" size={16} tintColor={iconColor} type="monochrome" />; | |
| 73 | return ( | |
| 74 | <SymbolView | |
| 75 | name="folder" | |
| 76 | size={16} | |
| 77 | tintColorClassName={"accent-icon-muted"} | |
| 78 | type="monochrome" | |
| 79 | /> | |
| 80 | ); | |
| 78 | 81 | } |
| 79 | 82 | |
| 80 | 83 | return ( |
| 81 | 84 | <View className="size-4"> |
| 82 | <SymbolView name="folder" size={16} tintColor={iconColor} type="monochrome" /> | |
| 85 | <SymbolView | |
| 86 | name="folder" | |
| 87 | size={16} | |
| 88 | tintColorClassName={"accent-icon-muted"} | |
| 89 | type="monochrome" | |
| 90 | /> | |
| 83 | 91 | <View className="absolute -right-1 -bottom-1"> |
| 84 | <SymbolView name="arrow.triangle.branch" size={9} tintColor={iconColor} type="monochrome" /> | |
| 92 | <SymbolView | |
| 93 | name="arrow.triangle.branch" | |
| 94 | size={9} | |
| 95 | tintColorClassName={"accent-icon-muted"} | |
| 96 | type="monochrome" | |
| 97 | /> | |
| 85 | 98 | </View> |
| 86 | 99 | </View> |
| 87 | 100 | ); |
| ... | ... | @@ -109,7 +122,6 @@ export function NewTaskDraftScreen(props: { |
| 109 | 122 | reserveShare, |
| 110 | 123 | } = useIncomingShare(); |
| 111 | 124 | const insets = useSafeAreaInsets(); |
| 112 | const { themeAppearance: colorScheme } = useAppearancePreferences(); | |
| 113 | 125 | const isKeyboardVisible = useKeyboardState((state) => state.isVisible); |
| 114 | 126 | const controlsBottomPadding = Math.max(insets.bottom, 10); |
| 115 | 127 | const keyboardOpenedOffset = Math.max(0, controlsBottomPadding - 8); |
| ... | ... | @@ -291,13 +303,9 @@ export function NewTaskDraftScreen(props: { |
| 291 | 303 | }; |
| 292 | 304 | }, [props.pendingTaskId, cancelEditingPendingTask]); |
| 293 | 305 | |
| 294 | const foregroundColor = useThemeColor("--color-foreground"); | |
| 295 | const sheetColor = String(useThemeColor("--color-sheet")); | |
| 296 | const projectUnderlineColor = useThemeColor("--color-foreground-muted"); | |
| 306 | const foregroundColor = useUniwindTheme()["--color-foreground"]; | |
| 297 | 307 | const regularFontFamily = useFontFamily("regular"); |
| 298 | 308 | const bodyText = useScaledTextRole("body"); |
| 299 | const sheetFadeOpaque = sheetColor; | |
| 300 | const sheetFadeTransparent = themeColorWithAlpha(sheetColor, 0); | |
| 301 | 309 | |
| 302 | 310 | // A new navigation to this mounted screen delivers a fresh initialProjectRef |
| 303 | 311 | // reference — treat it as a new request and let it apply again. |
| ... | ... | @@ -797,7 +805,6 @@ export function NewTaskDraftScreen(props: { |
| 797 | 805 | } |
| 798 | 806 | |
| 799 | 807 | const isAndroid = Platform.OS === "android"; |
| 800 | const isDarkMode = colorScheme === "dark"; | |
| 801 | 808 | const canStart = |
| 802 | 809 | Boolean(flow.selectedProject) && |
| 803 | 810 | Boolean(flow.selectedModel) && |
| ... | ... | @@ -874,11 +881,7 @@ export function NewTaskDraftScreen(props: { |
| 874 | 881 | accessibilityRole="button" |
| 875 | 882 | disabled={isIncomingShareTransferPending} |
| 876 | 883 | onPress={chooseProject} |
| 877 | className="min-w-0 max-w-[250px] active:opacity-65" | |
| 878 | style={{ | |
| 879 | borderBottomColor: projectUnderlineColor, | |
| 880 | borderBottomWidth: 1, | |
| 881 | }} | |
| 884 | className="min-w-0 max-w-[250px] border-b border-foreground-muted active:opacity-65" | |
| 882 | 885 | > |
| 883 | 886 | <Text |
| 884 | 887 | className="text-2xl font-t3-medium tracking-tight text-foreground" |
| ... | ... | @@ -960,7 +963,6 @@ export function NewTaskDraftScreen(props: { |
| 960 | 963 | |
| 961 | 964 | <ComposerSurface |
| 962 | 965 | animateLayout={false} |
| 963 | isDarkMode={isDarkMode} | |
| 964 | 966 | style={{ |
| 965 | 967 | borderRadius: 26, |
| 966 | 968 | minHeight: 140, |
| ... | ... | @@ -984,11 +986,7 @@ export function NewTaskDraftScreen(props: { |
| 984 | 986 | {promptEditor} |
| 985 | 987 | |
| 986 | 988 | <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}> |
| 987 | <ComposerToolbarScroller | |
| 988 | fadeOpaque={sheetFadeOpaque} | |
| 989 | fadeTransparent={sheetFadeTransparent} | |
| 990 | contentPaddingRight={8} | |
| 991 | > | |
| 989 | <ComposerToolbarScroller contentPaddingRight={8} fadeSurface="sheet"> | |
| 992 | 990 | <ComposerToolbarButton |
| 993 | 991 | accessibilityLabel="Add attachment" |
| 994 | 992 | disabled={isIncomingShareTransferPending} |
apps/mobile/src/features/threads/NewTaskRouteScreen.tsx+4-5| ... | ... | @@ -10,7 +10,6 @@ import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; |
| 10 | 10 | import { useEffect, useRef } from "react"; |
| 11 | 11 | import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, View } from "react-native"; |
| 12 | 12 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 13 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 14 | 13 | import { cn } from "../../lib/cn"; |
| 15 | 14 | |
| 16 | 15 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| ... | ... | @@ -91,8 +90,6 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 91 | 90 | const isFocused = useIsFocused(); |
| 92 | 91 | const { layout } = useAdaptiveWorkspaceLayout(); |
| 93 | 92 | const insets = useSafeAreaInsets(); |
| 94 | const chevronColor = useThemeColor("--color-chevron"); | |
| 95 | const accentColor = useThemeColor("--color-icon-muted"); | |
| 96 | 93 | const { getShare, releaseShareReservation } = useIncomingShare(); |
| 97 | 94 | const routeShareId = Array.isArray(route.params?.incomingShareId) |
| 98 | 95 | ? route.params.incomingShareId[0] |
| ... | ... | @@ -242,7 +239,9 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 242 | 239 | > |
| 243 | 240 | {projectScopes.length === 0 ? ( |
| 244 | 241 | <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8"> |
| 245 | {projectEmptyState.loading ? <ActivityIndicator color={accentColor} /> : null} | |
| 242 | {projectEmptyState.loading ? ( | |
| 243 | <ActivityIndicator colorClassName={"accent-icon-muted"} /> | |
| 244 | ) : null} | |
| 246 | 245 | <Text className="text-center text-lg font-t3-bold text-foreground"> |
| 247 | 246 | {projectEmptyState.title} |
| 248 | 247 | </Text> |
| ... | ... | @@ -308,7 +307,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara |
| 308 | 307 | <SymbolView |
| 309 | 308 | name="chevron.right" |
| 310 | 309 | size={14} |
| 311 | tintColor={chevronColor} | |
| 310 | tintColorClassName={"accent-chevron"} | |
| 312 | 311 | type="monochrome" |
| 313 | 312 | /> |
| 314 | 313 | </Pressable> |
apps/mobile/src/features/threads/PendingApprovalCard.tsx+8-8| ... | ... | @@ -28,15 +28,15 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) { |
| 28 | 28 | // Opaque for the same reason as PendingUserInputCard: nothing blurs the feed |
| 29 | 29 | // behind this card, so a translucent surface bleeds messages through it. |
| 30 | 30 | return ( |
| 31 | <View className="gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100 p-4 dark:border-white/6 dark:bg-neutral-900"> | |
| 32 | <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300"> | |
| 31 | <View className="gap-2.5 rounded-[20px] border border-adaptive-neutral-200-white-a6 bg-adaptive-neutral-100-900 p-4"> | |
| 32 | <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-adaptive-sky-700-300"> | |
| 33 | 33 | Approval needed |
| 34 | 34 | </Text> |
| 35 | <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50"> | |
| 35 | <Text className="font-t3-bold text-lg text-adaptive-neutral-950-50"> | |
| 36 | 36 | {props.approval.appName ?? props.approval.requestKind} |
| 37 | 37 | </Text> |
| 38 | 38 | {props.approval.detail ? ( |
| 39 | <Text className="font-sans text-sm leading-normal text-neutral-600 dark:text-neutral-400"> | |
| 39 | <Text className="font-sans text-sm leading-normal text-adaptive-neutral-600-400"> | |
| 40 | 40 | {props.approval.detail} |
| 41 | 41 | </Text> |
| 42 | 42 | ) : null} |
| ... | ... | @@ -48,8 +48,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) { |
| 48 | 48 | option.decision === "accept" |
| 49 | 49 | ? "bg-blue-500" |
| 50 | 50 | : option.decision === "decline" |
| 51 | ? "bg-rose-100 dark:bg-rose-500/18" | |
| 52 | : "bg-neutral-200 dark:bg-neutral-800" | |
| 51 | ? "bg-adaptive-rose-100-500-a18" | |
| 52 | : "bg-adaptive-neutral-200-800" | |
| 53 | 53 | }`} |
| 54 | 54 | disabled={props.respondingApprovalId === props.approval.requestId} |
| 55 | 55 | onPress={() => void props.onRespond(props.approval.requestId, option.decision)} |
| ... | ... | @@ -59,8 +59,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) { |
| 59 | 59 | option.decision === "accept" |
| 60 | 60 | ? "font-t3-extrabold text-white" |
| 61 | 61 | : option.decision === "decline" |
| 62 | ? "font-t3-bold text-rose-700 dark:text-rose-300" | |
| 63 | : "font-t3-bold text-neutral-950 dark:text-neutral-50" | |
| 62 | ? "font-t3-bold text-adaptive-rose-700-300" | |
| 63 | : "font-t3-bold text-adaptive-neutral-950-50" | |
| 64 | 64 | }`} |
| 65 | 65 | > |
| 66 | 66 | {option.label} |
apps/mobile/src/features/threads/PendingUserInputCard.tsx+28-20| ... | ... | @@ -18,7 +18,6 @@ import { SymbolView } from "../../components/AppSymbol"; |
| 18 | 18 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; |
| 19 | 19 | import { ControlPill } from "../../components/ControlPill"; |
| 20 | 20 | import { cn } from "../../lib/cn"; |
| 21 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 22 | 21 | import { |
| 23 | 22 | isPendingUserInputOptionSelected, |
| 24 | 23 | type PendingUserInput, |
| ... | ... | @@ -87,7 +86,6 @@ const EXPANDED_CARD_IS_OVERLAY = Platform.OS === "ios"; |
| 87 | 86 | const CARD_LAYOUT_TRANSITION = LinearTransition.duration(200); |
| 88 | 87 | |
| 89 | 88 | export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 90 | const iconSubtle = useThemeColor("--color-icon-subtle"); | |
| 91 | 89 | const questionCount = props.pendingUserInput.questions.length; |
| 92 | 90 | |
| 93 | 91 | const cardCoverage = props.cardCoverage; |
| ... | ... | @@ -163,7 +161,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 163 | 161 | pointerEvents={props.collapsed ? "auto" : "none"} |
| 164 | 162 | accessibilityElementsHidden={!props.collapsed} |
| 165 | 163 | importantForAccessibility={props.collapsed ? "auto" : "no-hide-descendants"} |
| 166 | className="flex-row items-center gap-2 rounded-full border border-neutral-200 bg-neutral-100 py-1.5 pl-4 pr-1.5 dark:border-white/6 dark:bg-neutral-900" | |
| 164 | className="flex-row items-center gap-2 rounded-full border border-adaptive-neutral-200-white-a6 bg-adaptive-neutral-100-900 py-1.5 pl-4 pr-1.5" | |
| 167 | 165 | > |
| 168 | 166 | <Pressable |
| 169 | 167 | accessibilityRole="button" |
| ... | ... | @@ -173,14 +171,19 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 173 | 171 | onPress={props.onToggleCollapsed} |
| 174 | 172 | className="min-h-10 flex-1 flex-row items-center gap-2 active:opacity-70" |
| 175 | 173 | > |
| 176 | <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300"> | |
| 174 | <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-adaptive-sky-700-300"> | |
| 177 | 175 | User input needed |
| 178 | 176 | </Text> |
| 179 | <Text className="font-sans text-xs text-neutral-500 dark:text-neutral-400"> | |
| 177 | <Text className="font-sans text-xs text-adaptive-neutral-500-400"> | |
| 180 | 178 | {questionCount} question{questionCount === 1 ? "" : "s"} |
| 181 | 179 | </Text> |
| 182 | 180 | <View className="flex-1" /> |
| 183 | <SymbolView name="chevron.up" size={12} tintColor={iconSubtle} type="monochrome" /> | |
| 181 | <SymbolView | |
| 182 | name="chevron.up" | |
| 183 | size={12} | |
| 184 | tintColorClassName={"accent-icon-subtle"} | |
| 185 | type="monochrome" | |
| 186 | /> | |
| 184 | 187 | </Pressable> |
| 185 | 188 | {props.onStopThread ? ( |
| 186 | 189 | <ControlPill |
| ... | ... | @@ -213,7 +216,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 213 | 216 | : FadeOutDown.duration(USER_INPUT_TOGGLE_DURATION_MS).easing(Easing.out(Easing.cubic)) |
| 214 | 217 | } |
| 215 | 218 | layout={CARD_LAYOUT_TRANSITION} |
| 216 | className="overflow-hidden gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100 p-4 dark:border-white/6 dark:bg-neutral-900" | |
| 219 | className="overflow-hidden gap-2.5 rounded-[20px] border border-adaptive-neutral-200-white-a6 bg-adaptive-neutral-100-900 p-4" | |
| 217 | 220 | style={ |
| 218 | 221 | EXPANDED_CARD_IS_OVERLAY |
| 219 | 222 | ? [{ maxHeight: props.maxHeight }, cardAnimatedStyle] |
| ... | ... | @@ -227,15 +230,20 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 227 | 230 | className="flex-row items-start gap-2" |
| 228 | 231 | > |
| 229 | 232 | <View className="flex-1 gap-2.5"> |
| 230 | <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300"> | |
| 233 | <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-adaptive-sky-700-300"> | |
| 231 | 234 | User input needed |
| 232 | 235 | </Text> |
| 233 | <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50"> | |
| 236 | <Text className="font-t3-bold text-lg text-adaptive-neutral-950-50"> | |
| 234 | 237 | Fill in the pending answers |
| 235 | 238 | </Text> |
| 236 | 239 | </View> |
| 237 | <View className="h-8 w-8 items-center justify-center rounded-full bg-neutral-200/70 dark:bg-white/8"> | |
| 238 | <SymbolView name="chevron.down" size={13} tintColor={iconSubtle} type="monochrome" /> | |
| 240 | <View className="h-8 w-8 items-center justify-center rounded-full bg-adaptive-neutral-200-a70-white-a8"> | |
| 241 | <SymbolView | |
| 242 | name="chevron.down" | |
| 243 | size={13} | |
| 244 | tintColorClassName={"accent-icon-subtle"} | |
| 245 | type="monochrome" | |
| 246 | /> | |
| 239 | 247 | </View> |
| 240 | 248 | </Pressable> |
| 241 | 249 | <ScrollView |
| ... | ... | @@ -251,10 +259,10 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 251 | 259 | const draft = props.drafts[question.id]; |
| 252 | 260 | return ( |
| 253 | 261 | <View key={question.id} className="gap-2 pt-1"> |
| 254 | <Text className="font-t3-bold text-xs uppercase tracking-[1px] text-neutral-500 dark:text-neutral-500"> | |
| 262 | <Text className="font-t3-bold text-xs uppercase tracking-[1px] text-neutral-500"> | |
| 255 | 263 | {question.header} |
| 256 | 264 | </Text> |
| 257 | <Text className="font-sans text-base leading-snug text-neutral-950 dark:text-neutral-50"> | |
| 265 | <Text className="font-sans text-base leading-snug text-adaptive-neutral-950-50"> | |
| 258 | 266 | {question.question} |
| 259 | 267 | </Text> |
| 260 | 268 | <View className="gap-2"> |
| ... | ... | @@ -268,8 +276,8 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 268 | 276 | className={cn( |
| 269 | 277 | "min-h-12 w-full rounded-2xl border px-3.5 py-3", |
| 270 | 278 | selected |
| 271 | ? "border-blue-300/50 bg-blue-50 dark:border-blue-400/28 dark:bg-blue-400/14" | |
| 272 | : "border-neutral-200 bg-white dark:border-white/6 dark:bg-neutral-950/70", | |
| 279 | ? "border-adaptive-blue-300-a50-blue-400-a28 bg-adaptive-blue-50-blue-400-a14" | |
| 280 | : "border-adaptive-neutral-200-white-a6 bg-adaptive-white-neutral-950-a70", | |
| 273 | 281 | )} |
| 274 | 282 | onPress={() => |
| 275 | 283 | props.onSelectOption( |
| ... | ... | @@ -284,14 +292,14 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 284 | 292 | className={cn( |
| 285 | 293 | "font-t3-bold text-sm", |
| 286 | 294 | selected |
| 287 | ? "text-sky-700 dark:text-sky-300" | |
| 288 | : "text-neutral-700 dark:text-neutral-200", | |
| 295 | ? "text-adaptive-sky-700-300" | |
| 296 | : "text-adaptive-neutral-600-300", | |
| 289 | 297 | )} |
| 290 | 298 | > |
| 291 | 299 | {option.label} |
| 292 | 300 | </Text> |
| 293 | 301 | {description ? ( |
| 294 | <Text className="font-sans text-sm leading-5 text-neutral-500 dark:text-neutral-400"> | |
| 302 | <Text className="font-sans text-sm leading-5 text-adaptive-neutral-500-400"> | |
| 295 | 303 | {description} |
| 296 | 304 | </Text> |
| 297 | 305 | ) : null} |
| ... | ... | @@ -308,7 +316,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 308 | 316 | onFocus={() => props.onInputFocusChange?.(true)} |
| 309 | 317 | onBlur={() => props.onInputFocusChange?.(false)} |
| 310 | 318 | placeholder="Or type a custom answer" |
| 311 | className="min-h-[54px] rounded-2xl border border-neutral-200 bg-white px-3.5 py-3 font-sans text-base text-neutral-950 dark:border-white/8 dark:bg-neutral-950/70 dark:text-neutral-50" | |
| 319 | className="min-h-[54px] rounded-2xl border border-adaptive-neutral-200-white-a8 bg-adaptive-white-neutral-950-a70 px-3.5 py-3 font-sans text-base text-adaptive-neutral-950-50" | |
| 312 | 320 | /> |
| 313 | 321 | </View> |
| 314 | 322 | ); |
| ... | ... | @@ -317,7 +325,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { |
| 317 | 325 | <Pressable |
| 318 | 326 | className={cn( |
| 319 | 327 | "items-center justify-center rounded-2xl px-4 py-3.5", |
| 320 | props.answers ? "bg-blue-500" : "bg-neutral-200 dark:bg-neutral-700/60", | |
| 328 | props.answers ? "bg-blue-500" : "bg-adaptive-neutral-200-700-a60", | |
| 321 | 329 | )} |
| 322 | 330 | disabled={ |
| 323 | 331 | props.answers === null || props.respondingUserInputId === props.pendingUserInput.requestId |
apps/mobile/src/features/threads/ThreadComposer.tsx+9-46| ... | ... | @@ -16,15 +16,7 @@ import { |
| 16 | 16 | import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; |
| 17 | 17 | import type { ReactNode } from "react"; |
| 18 | 18 | import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"; |
| 19 | import { | |
| 20 | ActivityIndicator, | |
| 21 | Image, | |
| 22 | Platform, | |
| 23 | Pressable, | |
| 24 | StyleSheet, | |
| 25 | View, | |
| 26 | type ViewStyle, | |
| 27 | } from "react-native"; | |
| 19 | import { ActivityIndicator, Image, Platform, Pressable, View, type ViewStyle } from "react-native"; | |
| 28 | 20 | import ImageViewing from "react-native-image-viewing"; |
| 29 | 21 | import Animated, { |
| 30 | 22 | FadeIn, |
| ... | ... | @@ -33,8 +25,7 @@ import Animated, { |
| 33 | 25 | FadeOutDown, |
| 34 | 26 | LinearTransition, |
| 35 | 27 | } from "react-native-reanimated"; |
| 36 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 37 | import { themeColorWithAlpha } from "../../lib/mobileTheme"; | |
| 28 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 38 | 29 | import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration"; |
| 39 | 30 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| 40 | 31 | |
| ... | ... | @@ -57,7 +48,6 @@ import { ProviderIcon } from "../../components/ProviderIcon"; |
| 57 | 48 | import type { DraftComposerImageAttachment } from "../../lib/composerImages"; |
| 58 | 49 | import { buildModelOptions, groupByProvider } from "../../lib/modelOptions"; |
| 59 | 50 | import { useScaledTextRole } from "../settings/appearance/useScaledTextRole"; |
| 60 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | |
| 61 | 51 | import type { RemoteClientConnectionState } from "../../lib/connection"; |
| 62 | 52 | import { |
| 63 | 53 | insertRankedSearchResult, |
| ... | ... | @@ -142,19 +132,14 @@ const COMPOSER_LAYOUT_TRANSITION = |
| 142 | 132 | export function ComposerSurface(props: { |
| 143 | 133 | readonly children: ReactNode; |
| 144 | 134 | readonly style: ViewStyle; |
| 145 | readonly isDarkMode: boolean; | |
| 146 | 135 | /** Existing thread composers morph between pill and card layouts. */ |
| 147 | 136 | readonly animateLayout?: boolean; |
| 148 | 137 | }) { |
| 149 | const cardColor = useThemeColor("--color-card-translucent"); | |
| 150 | const borderColor = useThemeColor("--color-border"); | |
| 151 | const shadowColor = useThemeColor("--color-primary-shadow"); | |
| 152 | 138 | // Drop shadow lives on a wrapper: `overflow: "hidden"` on the surface itself |
| 153 | 139 | // (needed to clip content to the pill shape) would clip the shadow on iOS. |
| 154 | 140 | const shadowStyle: ViewStyle = { |
| 155 | 141 | borderRadius: props.style.borderRadius, |
| 156 | shadowColor, | |
| 157 | shadowOpacity: props.isDarkMode ? 0.35 : 0.12, | |
| 142 | shadowOpacity: 1, | |
| 158 | 143 | shadowRadius: 14, |
| 159 | 144 | shadowOffset: { width: 0, height: 6 }, |
| 160 | 145 | elevation: 10, |
| ... | ... | @@ -162,16 +147,13 @@ export function ComposerSurface(props: { |
| 162 | 147 | |
| 163 | 148 | return ( |
| 164 | 149 | <Animated.View |
| 150 | className="shadow-adaptive-black-a15-a35" | |
| 165 | 151 | layout={props.animateLayout === false ? undefined : COMPOSER_LAYOUT_TRANSITION} |
| 166 | 152 | style={shadowStyle} |
| 167 | 153 | > |
| 168 | 154 | <GlassSurface |
| 169 | 155 | chrome="none" |
| 170 | fallbackStyle={{ | |
| 171 | backgroundColor: cardColor, | |
| 172 | borderWidth: 1, | |
| 173 | borderColor, | |
| 174 | }} | |
| 156 | fallbackClassName="border border-border bg-card-translucent" | |
| 175 | 157 | glassEffectStyle="regular" |
| 176 | 158 | // The composer is a passive material containing interactive controls. |
| 177 | 159 | // Expo GlassView defaults to non-interactive and both layouts share it. |
| ... | ... | @@ -240,8 +222,6 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill( |
| 240 | 222 | readonly status: ComposerStatusPillState; |
| 241 | 223 | }) { |
| 242 | 224 | const isReconnecting = props.status.kind !== "unavailable"; |
| 243 | const indicatorColor = useThemeColor("--color-icon-muted"); | |
| 244 | ||
| 245 | 225 | return ( |
| 246 | 226 | <Animated.View |
| 247 | 227 | className="absolute inset-x-0 bottom-full items-center pb-2" |
| ... | ... | @@ -255,7 +235,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill( |
| 255 | 235 | className="max-w-full flex-row items-center gap-2 rounded-full bg-card px-3 py-2 shadow-sm active:opacity-70" |
| 256 | 236 | > |
| 257 | 237 | {isReconnecting ? ( |
| 258 | <ActivityIndicator size="small" color={indicatorColor} /> | |
| 238 | <ActivityIndicator size="small" colorClassName={"accent-icon-muted"} /> | |
| 259 | 239 | ) : ( |
| 260 | 240 | <View className="h-2 w-2 rounded-full bg-red-500" /> |
| 261 | 241 | )} |
| ... | ... | @@ -272,9 +252,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill( |
| 272 | 252 | |
| 273 | 253 | export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) { |
| 274 | 254 | const navigation = useNavigation(); |
| 275 | const { themeAppearance } = useAppearancePreferences(); | |
| 276 | const isDarkMode = themeAppearance === "dark"; | |
| 277 | const foregroundColor = useThemeColor("--color-foreground"); | |
| 255 | const foregroundColor = useUniwindTheme()["--color-foreground"]; | |
| 278 | 256 | const bodyText = useScaledTextRole("body"); |
| 279 | 257 | const fallbackInputRef = useRef<ComposerEditorHandle>(null); |
| 280 | 258 | const inputRef = props.editorRef ?? fallbackInputRef; |
| ... | ... | @@ -342,11 +320,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 342 | 320 | environmentLabel: props.environmentLabel, |
| 343 | 321 | threadSyncPhase: props.threadSyncPhase, |
| 344 | 322 | }); |
| 345 | const toolbarSurface = String(useThemeColor("--color-card")); | |
| 346 | const backdropSurface = String(useThemeColor("--color-screen")); | |
| 347 | const toolbarFadeOpaque = themeColorWithAlpha(toolbarSurface, 0.95); | |
| 348 | const toolbarFadeTransparent = themeColorWithAlpha(toolbarSurface, 0); | |
| 349 | const backdropGradient = `linear-gradient(to bottom, ${themeColorWithAlpha(backdropSurface, 0)} 0%, ${themeColorWithAlpha(backdropSurface, 0.6)} 55%, ${themeColorWithAlpha(backdropSurface, 0.9)} 100%)`; | |
| 350 | 323 | const selectedProviderStatus = useMemo(() => { |
| 351 | 324 | if (!props.serverConfig) return null; |
| 352 | 325 | return ( |
| ... | ... | @@ -723,13 +696,8 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 723 | 696 | silently drops experimental_backgroundImage on Android, which left this |
| 724 | 697 | strip fully transparent and the feed text legible through the composer. */} |
| 725 | 698 | <View |
| 699 | className="absolute inset-0 bg-linear-to-b from-screen/0 via-screen/60 to-screen/90" | |
| 726 | 700 | pointerEvents="none" |
| 727 | style={[ | |
| 728 | StyleSheet.absoluteFill, | |
| 729 | { | |
| 730 | experimental_backgroundImage: backdropGradient, | |
| 731 | }, | |
| 732 | ]} | |
| 733 | 701 | /> |
| 734 | 702 | <Animated.View |
| 735 | 703 | className="relative w-full self-center" |
| ... | ... | @@ -755,7 +723,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 755 | 723 | ) : null} |
| 756 | 724 | |
| 757 | 725 | <ComposerSurface |
| 758 | isDarkMode={isDarkMode} | |
| 759 | 726 | style={ |
| 760 | 727 | isExpanded |
| 761 | 728 | ? { |
| ... | ... | @@ -865,11 +832,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 865 | 832 | ) : null} |
| 866 | 833 | {isExpanded ? ( |
| 867 | 834 | <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}> |
| 868 | <ComposerToolbarScroller | |
| 869 | fadeOpaque={toolbarFadeOpaque} | |
| 870 | fadeTransparent={toolbarFadeTransparent} | |
| 871 | contentPaddingRight={8} | |
| 872 | > | |
| 835 | <ComposerToolbarScroller contentPaddingRight={8}> | |
| 873 | 836 | <ComposerToolbarButton |
| 874 | 837 | accessibilityLabel="Add attachment" |
| 875 | 838 | icon="plus" |
apps/mobile/src/features/threads/ThreadFeed.tsx+46-56| ... | ... | @@ -46,7 +46,7 @@ import { TouchableOpacity } from "react-native-gesture-handler"; |
| 46 | 46 | import ImageViewing from "react-native-image-viewing"; |
| 47 | 47 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 48 | 48 | import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated"; |
| 49 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 49 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 50 | 50 | import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics"; |
| 51 | 51 | import { useFontFamily } from "../../lib/useFontFamily"; |
| 52 | 52 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| ... | ... | @@ -209,7 +209,6 @@ function ThreadMarkdownImageView(props: { |
| 209 | 209 | readonly alt: string | null; |
| 210 | 210 | readonly onPressImage: (uri: string) => void; |
| 211 | 211 | }) { |
| 212 | const codeBackground = useThemeColor("--color-md-code-bg"); | |
| 213 | 212 | const [availableWidth, setAvailableWidth] = useState(0); |
| 214 | 213 | const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); |
| 215 | 214 | const [failedUri, setFailedUri] = useState<string | null>(null); |
| ... | ... | @@ -242,12 +241,9 @@ function ThreadMarkdownImageView(props: { |
| 242 | 241 | > |
| 243 | 242 | {props.uri === null || failed ? ( |
| 244 | 243 | <View |
| 244 | className="items-center justify-center rounded-[10px] bg-md-code-bg" | |
| 245 | 245 | style={{ |
| 246 | 246 | ...frameStyle, |
| 247 | borderRadius: 10, | |
| 248 | backgroundColor: codeBackground, | |
| 249 | alignItems: "center", | |
| 250 | justifyContent: "center", | |
| 251 | 247 | }} |
| 252 | 248 | > |
| 253 | 249 | {failed ? ( |
| ... | ... | @@ -265,13 +261,9 @@ function ThreadMarkdownImageView(props: { |
| 265 | 261 | style={{ alignSelf: "flex-start" }} |
| 266 | 262 | > |
| 267 | 263 | <View |
| 264 | className="items-center justify-center overflow-hidden rounded-[10px] bg-md-code-bg" | |
| 268 | 265 | style={{ |
| 269 | 266 | ...frameStyle, |
| 270 | borderRadius: 10, | |
| 271 | backgroundColor: codeBackground, | |
| 272 | alignItems: "center", | |
| 273 | justifyContent: "center", | |
| 274 | overflow: "hidden", | |
| 275 | 267 | }} |
| 276 | 268 | > |
| 277 | 269 | <ThreadMarkdownImageRequest |
| ... | ... | @@ -556,23 +548,18 @@ function MarkdownCodeBlock(props: { |
| 556 | 548 | } |
| 557 | 549 | |
| 558 | 550 | function useReviewCommentColors(): ReviewCommentColors { |
| 559 | const background = useThemeColor("--color-card"); | |
| 560 | const border = useThemeColor("--color-border"); | |
| 561 | const mutedBackground = useThemeColor("--color-subtle"); | |
| 562 | const text = useThemeColor("--color-foreground"); | |
| 563 | const mutedText = useThemeColor("--color-foreground-muted"); | |
| 564 | const codeBackground = useThemeColor("--color-md-code-bg"); | |
| 551 | const theme = useUniwindTheme(); | |
| 565 | 552 | |
| 566 | 553 | return useMemo( |
| 567 | 554 | () => ({ |
| 568 | background, | |
| 569 | border, | |
| 570 | mutedBackground, | |
| 571 | text, | |
| 572 | mutedText, | |
| 573 | codeBackground, | |
| 555 | background: theme["--color-card"], | |
| 556 | border: theme["--color-border"], | |
| 557 | mutedBackground: theme["--color-subtle"], | |
| 558 | text: theme["--color-foreground"], | |
| 559 | mutedText: theme["--color-foreground-muted"], | |
| 560 | codeBackground: theme["--color-md-code-bg"], | |
| 574 | 561 | }), |
| 575 | [background, border, codeBackground, mutedBackground, mutedText, text], | |
| 562 | [theme], | |
| 576 | 563 | ); |
| 577 | 564 | } |
| 578 | 565 | |
| ... | ... | @@ -590,25 +577,26 @@ function useMarkdownStyles( |
| 590 | 577 | [appearance.baseFontSize], |
| 591 | 578 | ); |
| 592 | 579 | const themeMode = themeAppearance; |
| 593 | const markdownBodyColor = String(useThemeColor("--color-md-body")); | |
| 594 | const markdownStrongColor = String(useThemeColor("--color-md-strong")); | |
| 595 | const markdownLinkColor = String(useThemeColor("--color-md-link")); | |
| 596 | const markdownBlockquoteBg = String(useThemeColor("--color-md-blockquote-bg")); | |
| 597 | const markdownBlockquoteBorder = String(useThemeColor("--color-md-blockquote-border")); | |
| 598 | const markdownCodeBg = String(useThemeColor("--color-md-code-bg")); | |
| 599 | const markdownCodeText = String(useThemeColor("--color-md-code-text")); | |
| 600 | const markdownInlineCodeText = String(useThemeColor("--color-foreground-secondary")); | |
| 601 | const markdownHrColor = String(useThemeColor("--color-md-hr")); | |
| 602 | const markdownUserBodyColor = String(useThemeColor("--color-user-bubble-foreground")); | |
| 603 | const markdownUserCodeBg = String(useThemeColor("--color-md-user-code-bg")); | |
| 604 | const markdownUserCodeText = String(useThemeColor("--color-md-user-code-text")); | |
| 605 | const markdownUserInlineCodeText = String(useThemeColor("--color-user-bubble-foreground-muted")); | |
| 606 | const markdownUserFenceBg = String(useThemeColor("--color-md-user-fence-bg")); | |
| 607 | const markdownUserFenceText = String(useThemeColor("--color-md-user-fence-text")); | |
| 608 | const iconSubtleColor = String(useThemeColor("--color-icon-subtle")); | |
| 609 | const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); | |
| 610 | const userBubbleSkillForeground = String(useThemeColor("--color-user-bubble-skill-foreground")); | |
| 611 | const userBubbleForegroundMuted = String(useThemeColor("--color-user-bubble-foreground-muted")); | |
| 580 | const theme = useUniwindTheme(); | |
| 581 | const markdownBodyColor = theme["--color-md-body"]; | |
| 582 | const markdownStrongColor = theme["--color-md-strong"]; | |
| 583 | const markdownLinkColor = theme["--color-md-link"]; | |
| 584 | const markdownBlockquoteBg = theme["--color-md-blockquote-bg"]; | |
| 585 | const markdownBlockquoteBorder = theme["--color-md-blockquote-border"]; | |
| 586 | const markdownCodeBg = theme["--color-md-code-bg"]; | |
| 587 | const markdownCodeText = theme["--color-md-code-text"]; | |
| 588 | const markdownInlineCodeText = theme["--color-foreground-secondary"]; | |
| 589 | const markdownHrColor = theme["--color-md-hr"]; | |
| 590 | const markdownUserBodyColor = theme["--color-user-bubble-foreground"]; | |
| 591 | const markdownUserCodeBg = theme["--color-md-user-code-bg"]; | |
| 592 | const markdownUserCodeText = theme["--color-md-user-code-text"]; | |
| 593 | const markdownUserInlineCodeText = theme["--color-user-bubble-foreground-muted"]; | |
| 594 | const markdownUserFenceBg = theme["--color-md-user-fence-bg"]; | |
| 595 | const markdownUserFenceText = theme["--color-md-user-fence-text"]; | |
| 596 | const iconSubtleColor = theme["--color-icon-subtle"]; | |
| 597 | const inlineSkillForeground = theme["--color-inline-skill-foreground"]; | |
| 598 | const userBubbleSkillForeground = theme["--color-user-bubble-skill-foreground"]; | |
| 599 | const userBubbleForegroundMuted = theme["--color-user-bubble-foreground-muted"]; | |
| 612 | 600 | const regularFontFamily = useFontFamily("regular"); |
| 613 | 601 | const boldFontFamily = useFontFamily("bold"); |
| 614 | 602 | |
| ... | ... | @@ -1008,7 +996,7 @@ function renderFeedEntry( |
| 1008 | 996 | accessibilityState={{ expanded: entry.expanded }} |
| 1009 | 997 | onPress={() => props.onToggleTurnFold(entry.turnId)} |
| 1010 | 998 | hitSlop={4} |
| 1011 | className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-neutral-200/80 px-2 dark:border-white/[0.08]" | |
| 999 | className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2" | |
| 1012 | 1000 | > |
| 1013 | 1001 | <Text className="font-t3-medium text-sm tabular-nums text-foreground-muted"> |
| 1014 | 1002 | {entry.label} |
| ... | ... | @@ -1016,7 +1004,7 @@ function renderFeedEntry( |
| 1016 | 1004 | <SymbolView |
| 1017 | 1005 | name={entry.expanded ? "chevron.down" : "chevron.right"} |
| 1018 | 1006 | size={15} |
| 1019 | tintColor={iconSubtleColor} | |
| 1007 | tintColorClassName={"accent-icon-subtle"} | |
| 1020 | 1008 | type="monochrome" |
| 1021 | 1009 | /> |
| 1022 | 1010 | </Pressable> |
| ... | ... | @@ -1102,7 +1090,7 @@ function renderFeedEntry( |
| 1102 | 1090 | })} |
| 1103 | 1091 | </View> |
| 1104 | 1092 | <View className="mt-1 flex-row items-center justify-end gap-1 pr-0.5"> |
| 1105 | <Text className="font-t3-medium text-xs tabular-nums text-neutral-600 dark:text-neutral-400"> | |
| 1093 | <Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400"> | |
| 1106 | 1094 | {timestampLabel} |
| 1107 | 1095 | </Text> |
| 1108 | 1096 | {message.text.trim().length > 0 ? ( |
| ... | ... | @@ -1157,7 +1145,7 @@ function renderFeedEntry( |
| 1157 | 1145 | key={attachment.id} |
| 1158 | 1146 | environmentId={props.environmentId} |
| 1159 | 1147 | attachmentId={attachment.id} |
| 1160 | className="mt-1.5 aspect-[1.3] w-full rounded-[18px] bg-neutral-200 dark:bg-neutral-800" | |
| 1148 | className="mt-1.5 aspect-[1.3] w-full rounded-[18px] bg-adaptive-neutral-200-800" | |
| 1161 | 1149 | onPressImage={props.onPressImage} |
| 1162 | 1150 | /> |
| 1163 | 1151 | ); |
| ... | ... | @@ -1171,7 +1159,7 @@ function renderFeedEntry( |
| 1171 | 1159 | buttonSize={28} |
| 1172 | 1160 | iconSize={13} |
| 1173 | 1161 | /> |
| 1174 | <Text className="font-t3-medium text-xs tabular-nums text-neutral-600 dark:text-neutral-400"> | |
| 1162 | <Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400"> | |
| 1175 | 1163 | {timestampLabel} |
| 1176 | 1164 | </Text> |
| 1177 | 1165 | </View> |
| ... | ... | @@ -1207,11 +1195,11 @@ const WorkingTimelineRow = memo(function WorkingTimelineRow(props: { readonly st |
| 1207 | 1195 | return ( |
| 1208 | 1196 | <View className="mb-4 flex-row items-center gap-2 px-1.5 py-1"> |
| 1209 | 1197 | <View className="flex-row items-center gap-1"> |
| 1210 | <View className="h-1 w-1 rounded-full bg-neutral-400 dark:bg-neutral-500" /> | |
| 1211 | <View className="h-1 w-1 rounded-full bg-neutral-400/80 dark:bg-neutral-500/80" /> | |
| 1212 | <View className="h-1 w-1 rounded-full bg-neutral-400/60 dark:bg-neutral-500/60" /> | |
| 1198 | <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-500" /> | |
| 1199 | <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a80-500-a80" /> | |
| 1200 | <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a60-500-a60" /> | |
| 1213 | 1201 | </View> |
| 1214 | <Text className="font-t3-medium text-xs tabular-nums text-neutral-600 dark:text-neutral-400"> | |
| 1202 | <Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400"> | |
| 1215 | 1203 | Working for {durationLabel} |
| 1216 | 1204 | </Text> |
| 1217 | 1205 | </View> |
| ... | ... | @@ -1303,6 +1291,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { |
| 1303 | 1291 | }) { |
| 1304 | 1292 | const { codeSurface, nativeReviewDiffStyle } = useAppearanceCodeSurface(); |
| 1305 | 1293 | const { themeAppearance: appearanceScheme, themeId } = useAppearancePreferences(); |
| 1294 | const appTheme = useUniwindTheme(); | |
| 1306 | 1295 | const NativeReviewDiffView = resolveNativeReviewDiffView(); |
| 1307 | 1296 | const patch = useMemo(() => buildReviewCommentPatch(props.comment), [props.comment]); |
| 1308 | 1297 | const parsedDiff = useMemo( |
| ... | ... | @@ -1315,8 +1304,8 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { |
| 1315 | 1304 | [nativeReviewDiffData.rows], |
| 1316 | 1305 | ); |
| 1317 | 1306 | const nativeReviewDiffTheme = useMemo( |
| 1318 | () => createNativeReviewDiffTheme(appearanceScheme, themeId), | |
| 1319 | [appearanceScheme, themeId], | |
| 1307 | () => createNativeReviewDiffTheme(appearanceScheme, themeId, appTheme), | |
| 1308 | [appearanceScheme, appTheme, themeId], | |
| 1320 | 1309 | ); |
| 1321 | 1310 | const nativeRowsJson = useMemo(() => JSON.stringify(compactNativeRows), [compactNativeRows]); |
| 1322 | 1311 | const nativeThemeJson = useMemo( |
| ... | ... | @@ -1578,8 +1567,9 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1578 | 1567 | ? navigationHeaderHeight || insets.top + IOS_NAV_BAR_HEIGHT |
| 1579 | 1568 | : topContentInset; |
| 1580 | 1569 | |
| 1581 | const iconSubtleColor = useThemeColor("--color-icon-subtle"); | |
| 1582 | const userBubbleColor = useThemeColor("--color-user-bubble"); | |
| 1570 | const theme = useUniwindTheme(); | |
| 1571 | const iconSubtleColor = theme["--color-icon-subtle"]; | |
| 1572 | const userBubbleColor = theme["--color-user-bubble"]; | |
| 1583 | 1573 | const onMarkdownLinkPress = useCallback( |
| 1584 | 1574 | (href: string) => { |
| 1585 | 1575 | const presentation = resolveMarkdownLinkPresentation(href); |
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+13-29| ... | ... | @@ -27,7 +27,6 @@ import { SymbolView } from "../../components/AppSymbol"; |
| 27 | 27 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 28 | 28 | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 29 | 29 | import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities"; |
| 30 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 31 | 30 | import { useProjects, useThreadShells } from "../../state/entities"; |
| 32 | 31 | import { mobilePreferencesAtom } from "../../state/preferences"; |
| 33 | 32 | import { useThreadSearch } from "../../state/queries"; |
| ... | ... | @@ -127,19 +126,11 @@ export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) { |
| 127 | 126 | } |
| 128 | 127 | |
| 129 | 128 | function NativeSidebarContainer(props: ThreadNavigationSidebarProps) { |
| 130 | const backgroundColor = useThemeColor("--color-drawer"); | |
| 131 | const borderColor = useThemeColor("--color-border"); | |
| 132 | ||
| 133 | 129 | return ( |
| 134 | 130 | <View |
| 135 | 131 | testID="thread-navigation-sidebar" |
| 136 | className="flex-1" | |
| 137 | style={{ | |
| 138 | width: props.width, | |
| 139 | backgroundColor, | |
| 140 | borderRightColor: borderColor, | |
| 141 | borderRightWidth: StyleSheet.hairlineWidth, | |
| 142 | }} | |
| 132 | className="flex-1 border-border bg-drawer" | |
| 133 | style={{ borderRightWidth: StyleSheet.hairlineWidth, width: props.width }} | |
| 143 | 134 | > |
| 144 | 135 | <SidebarNavigationShell> |
| 145 | 136 | <ThreadNavigationSidebarPane {...props} nativeChrome /> |
| ... | ... | @@ -731,10 +722,6 @@ function ThreadNavigationSidebarPane( |
| 731 | 722 | ], |
| 732 | 723 | ); |
| 733 | 724 | |
| 734 | const backgroundColor = useThemeColor("--color-drawer"); | |
| 735 | const borderColor = useThemeColor("--color-border"); | |
| 736 | const mutedColor = useThemeColor("--color-foreground-muted"); | |
| 737 | const placeholderColor = useThemeColor("--color-placeholder"); | |
| 738 | 725 | const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null); |
| 739 | 726 | // The sticky header (title row, search field, optional connection status) |
| 740 | 727 | // is measured so the list inset always matches its real height — no |
| ... | ... | @@ -1242,13 +1229,8 @@ function ThreadNavigationSidebarPane( |
| 1242 | 1229 | return ( |
| 1243 | 1230 | <View |
| 1244 | 1231 | testID="thread-navigation-sidebar" |
| 1245 | className="flex-1" | |
| 1246 | style={{ | |
| 1247 | width: props.width, | |
| 1248 | backgroundColor, | |
| 1249 | borderRightColor: borderColor, | |
| 1250 | borderRightWidth: StyleSheet.hairlineWidth, | |
| 1251 | }} | |
| 1232 | className="flex-1 border-r border-border bg-drawer" | |
| 1233 | style={{ width: props.width }} | |
| 1252 | 1234 | > |
| 1253 | 1235 | <View className="flex-1" style={{ paddingBottom: insets.bottom }}> |
| 1254 | 1236 | <SwipeableScrollGateProvider enabled={swipeEnabled}> |
| ... | ... | @@ -1286,14 +1268,11 @@ function ThreadNavigationSidebarPane( |
| 1286 | 1268 | </View> |
| 1287 | 1269 | |
| 1288 | 1270 | <View |
| 1289 | className="absolute inset-x-0 top-0 z-[4]" | |
| 1271 | className="absolute inset-x-0 top-0 z-[4] bg-drawer" | |
| 1290 | 1272 | collapsable={false} |
| 1291 | 1273 | onLayout={handleStickyHeaderLayout} |
| 1292 | 1274 | pointerEvents="auto" |
| 1293 | style={{ | |
| 1294 | paddingTop: insets.top, | |
| 1295 | backgroundColor, | |
| 1296 | }} | |
| 1275 | style={{ paddingTop: insets.top }} | |
| 1297 | 1276 | > |
| 1298 | 1277 | <View className="h-[50px] flex-row items-end gap-0.5 pr-2 pl-5"> |
| 1299 | 1278 | {/* Title slot doubles as the connection status surface: while an |
| ... | ... | @@ -1318,7 +1297,12 @@ function ThreadNavigationSidebarPane( |
| 1318 | 1297 | </View> |
| 1319 | 1298 | |
| 1320 | 1299 | <View className="mx-4 mt-[9px] h-[38px] flex-row items-center gap-1.5 rounded-xl bg-sidebar-search pr-2.5 pl-[11px]"> |
| 1321 | <SymbolView name="magnifyingglass" size={15} tintColor={mutedColor} type="monochrome" /> | |
| 1300 | <SymbolView | |
| 1301 | name="magnifyingglass" | |
| 1302 | size={15} | |
| 1303 | tintColorClassName={"accent-foreground-muted"} | |
| 1304 | type="monochrome" | |
| 1305 | /> | |
| 1322 | 1306 | <TextInput |
| 1323 | 1307 | ref={searchInputRef} |
| 1324 | 1308 | accessibilityLabel="Search threads" |
| ... | ... | @@ -1327,7 +1311,7 @@ function ThreadNavigationSidebarPane( |
| 1327 | 1311 | clearButtonMode="while-editing" |
| 1328 | 1312 | onChangeText={props.onSearchQueryChange} |
| 1329 | 1313 | placeholder="Search" |
| 1330 | placeholderTextColor={placeholderColor} | |
| 1314 | placeholderTextColorClassName={"accent-placeholder"} | |
| 1331 | 1315 | returnKeyType="search" |
| 1332 | 1316 | className="h-[34px] flex-1 px-0 py-0 font-sans text-base text-foreground" |
| 1333 | 1317 | value={props.searchQuery} |
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+13-11| ... | ... | @@ -40,7 +40,7 @@ import { cn } from "../../lib/cn"; |
| 40 | 40 | import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; |
| 41 | 41 | import { applyProviderOptionSelection } from "../../lib/providerOptions"; |
| 42 | 42 | import { resolveProviderOptionDescriptors } from "../../lib/providerOptions"; |
| 43 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 43 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 44 | 44 | import { |
| 45 | 45 | NativeHeaderToolbar, |
| 46 | 46 | NativeStackScreenOptions, |
| ... | ... | @@ -91,7 +91,6 @@ function ModelRow(props: { |
| 91 | 91 | readonly isFirst: boolean; |
| 92 | 92 | readonly isLast: boolean; |
| 93 | 93 | }) { |
| 94 | const checkmarkColor = useThemeColor("--color-icon"); | |
| 95 | 94 | return ( |
| 96 | 95 | <Pressable |
| 97 | 96 | accessibilityLabel={props.option.label} |
| ... | ... | @@ -122,7 +121,7 @@ function ModelRow(props: { |
| 122 | 121 | <SymbolView |
| 123 | 122 | name="checkmark" |
| 124 | 123 | size={16} |
| 125 | tintColor={checkmarkColor} | |
| 124 | tintColorClassName={"accent-icon"} | |
| 126 | 125 | type="monochrome" |
| 127 | 126 | weight="semibold" |
| 128 | 127 | /> |
| ... | ... | @@ -140,7 +139,6 @@ function ProviderHeader(props: { |
| 140 | 139 | readonly modelCount: number; |
| 141 | 140 | readonly onToggle: () => void; |
| 142 | 141 | }) { |
| 143 | const iconSubtle = useThemeColor("--color-icon-subtle"); | |
| 144 | 142 | const content = ( |
| 145 | 143 | <> |
| 146 | 144 | <ProviderIcon provider={props.driver} size={15} /> |
| ... | ... | @@ -156,7 +154,7 @@ function ProviderHeader(props: { |
| 156 | 154 | <SymbolView |
| 157 | 155 | name={props.collapsed ? "chevron.down" : "chevron.up"} |
| 158 | 156 | size={12} |
| 159 | tintColor={iconSubtle} | |
| 157 | tintColorClassName={"accent-icon-subtle"} | |
| 160 | 158 | type="monochrome" |
| 161 | 159 | /> |
| 162 | 160 | </> |
| ... | ... | @@ -192,7 +190,6 @@ function DisclosureRow(props: { |
| 192 | 190 | readonly onPress: () => void; |
| 193 | 191 | readonly isLast?: boolean; |
| 194 | 192 | }) { |
| 195 | const iconSubtle = useThemeColor("--color-icon-subtle"); | |
| 196 | 193 | return ( |
| 197 | 194 | <Pressable |
| 198 | 195 | accessibilityRole="button" |
| ... | ... | @@ -209,7 +206,12 @@ function DisclosureRow(props: { |
| 209 | 206 | {props.value} |
| 210 | 207 | </Text> |
| 211 | 208 | ) : null} |
| 212 | <SymbolView name="chevron.right" size={12} tintColor={iconSubtle} type="monochrome" /> | |
| 209 | <SymbolView | |
| 210 | name="chevron.right" | |
| 211 | size={12} | |
| 212 | tintColorClassName={"accent-icon-subtle"} | |
| 213 | type="monochrome" | |
| 214 | /> | |
| 213 | 215 | </Pressable> |
| 214 | 216 | ); |
| 215 | 217 | } |
| ... | ... | @@ -222,7 +224,6 @@ function ChoiceRow(props: { |
| 222 | 224 | readonly onPress: () => void; |
| 223 | 225 | readonly isLast: boolean; |
| 224 | 226 | }) { |
| 225 | const checkmarkColor = useThemeColor("--color-icon"); | |
| 226 | 227 | return ( |
| 227 | 228 | <Pressable |
| 228 | 229 | accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label} |
| ... | ... | @@ -244,7 +245,7 @@ function ChoiceRow(props: { |
| 244 | 245 | <SymbolView |
| 245 | 246 | name="checkmark" |
| 246 | 247 | size={16} |
| 247 | tintColor={checkmarkColor} | |
| 248 | tintColorClassName={"accent-icon"} | |
| 248 | 249 | type="monochrome" |
| 249 | 250 | weight="semibold" |
| 250 | 251 | /> |
| ... | ... | @@ -1125,8 +1126,9 @@ function ThreadSettingsChoiceScreen() { |
| 1125 | 1126 | } |
| 1126 | 1127 | |
| 1127 | 1128 | function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) { |
| 1128 | const solidSheetBackground = String(useThemeColor("--color-sheet-solid")); | |
| 1129 | const foreground = String(useThemeColor("--color-foreground")); | |
| 1129 | const theme = useUniwindTheme(); | |
| 1130 | const solidSheetBackground = theme["--color-sheet-solid"]; | |
| 1131 | const foreground = theme["--color-foreground"]; | |
| 1130 | 1132 | const presentation = useMemo( |
| 1131 | 1133 | () => ({ |
| 1132 | 1134 | onClose: props.onClose, |
apps/mobile/src/features/threads/git/GitCommitSheet.tsx+1-1| ... | ... | @@ -85,7 +85,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { |
| 85 | 85 | </Text> |
| 86 | 86 | </View> |
| 87 | 87 | {isDefaultRef ? ( |
| 88 | <Text className="text-xs leading-normal text-amber-700 dark:text-amber-400"> | |
| 88 | <Text className="text-xs leading-normal text-adaptive-amber-700-400"> | |
| 89 | 89 | Warning: this is the default branch. |
| 90 | 90 | </Text> |
| 91 | 91 | ) : null} |
apps/mobile/src/features/threads/git/GitOverviewSheet.tsx+5-6| ... | ... | @@ -17,7 +17,7 @@ import { Alert, Platform, Pressable, RefreshControl, ScrollView, View } from "re |
| 17 | 17 | |
| 18 | 18 | import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens"; |
| 19 | 19 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 20 | import { useThemeColor } from "../../../lib/useThemeColor"; | |
| 20 | import { useUniwindTheme } from "../../../lib/useUniwindTheme"; | |
| 21 | 21 | |
| 22 | 22 | import { AndroidSheetHeader } from "../../../components/AndroidScreenHeader"; |
| 23 | 23 | import { AppText as Text } from "../../../components/AppText"; |
| ... | ... | @@ -53,10 +53,9 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 53 | 53 | const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); |
| 54 | 54 | const gitState = useSelectedThreadGitState(); |
| 55 | 55 | const gitActions = useSelectedThreadGitActions(); |
| 56 | ||
| 57 | const iconColor = useThemeColor("--color-icon"); | |
| 58 | const foregroundColor = String(useThemeColor("--color-foreground")); | |
| 59 | const sheetColor = String(useThemeColor("--color-sheet")); | |
| 56 | const theme = useUniwindTheme(); | |
| 57 | const foregroundColor = theme["--color-foreground"]; | |
| 58 | const sheetColor = theme["--color-sheet"]; | |
| 60 | 59 | |
| 61 | 60 | const gitStatus = useEnvironmentQuery( |
| 62 | 61 | selectedThread !== null && selectedThreadCwd !== null |
| ... | ... | @@ -385,7 +384,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { |
| 385 | 384 | <SymbolView |
| 386 | 385 | name="arrow.clockwise" |
| 387 | 386 | size={16} |
| 388 | tintColor={iconColor} | |
| 387 | tintColorClassName={"accent-icon"} | |
| 389 | 388 | type="monochrome" |
| 390 | 389 | weight="medium" |
| 391 | 390 | /> |
apps/mobile/src/features/threads/git/gitSheetComponents.tsx+24-12| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import { SymbolView } from "../../../components/AppSymbol"; |
| 2 | 2 | import type { ComponentProps } from "react"; |
| 3 | 3 | import { Pressable, View } from "react-native"; |
| 4 | import { useThemeColor } from "../../../lib/useThemeColor"; | |
| 5 | 4 | import { AppText as Text } from "../../../components/AppText"; |
| 6 | 5 | import { cn } from "../../../lib/cn"; |
| 7 | 6 | |
| ... | ... | @@ -14,12 +13,13 @@ export function SheetActionButton(props: { |
| 14 | 13 | readonly tone?: "primary" | "secondary" | "danger"; |
| 15 | 14 | readonly onPress: () => void; |
| 16 | 15 | }) { |
| 17 | const primaryFg = useThemeColor("--color-primary-foreground"); | |
| 18 | const dangerFg = useThemeColor("--color-danger-foreground"); | |
| 19 | const secondaryFg = useThemeColor("--color-secondary-foreground"); | |
| 20 | ||
| 21 | 16 | const tone = props.tone ?? "secondary"; |
| 22 | const textColor = tone === "primary" ? primaryFg : tone === "danger" ? dangerFg : secondaryFg; | |
| 17 | const textColorClassName = | |
| 18 | tone === "primary" | |
| 19 | ? "accent-primary-foreground" | |
| 20 | : tone === "danger" | |
| 21 | ? "accent-danger-foreground" | |
| 22 | : "accent-secondary-foreground"; | |
| 23 | 23 | |
| 24 | 24 | return ( |
| 25 | 25 | <Pressable |
| ... | ... | @@ -34,7 +34,12 @@ export function SheetActionButton(props: { |
| 34 | 34 | disabled={props.disabled} |
| 35 | 35 | onPress={props.onPress} |
| 36 | 36 | > |
| 37 | <SymbolView name={props.icon} size={16} tintColor={textColor} type="monochrome" /> | |
| 37 | <SymbolView | |
| 38 | name={props.icon} | |
| 39 | size={16} | |
| 40 | tintColorClassName={textColorClassName} | |
| 41 | type="monochrome" | |
| 42 | /> | |
| 38 | 43 | <Text |
| 39 | 44 | className={cn( |
| 40 | 45 | "text-xs font-t3-bold tracking-[0.9px] uppercase", |
| ... | ... | @@ -71,9 +76,6 @@ export function SheetListRow(props: { |
| 71 | 76 | readonly disabled?: boolean; |
| 72 | 77 | readonly onPress: () => void; |
| 73 | 78 | }) { |
| 74 | const iconColor = useThemeColor("--color-icon"); | |
| 75 | const iconSubtleColor = useThemeColor("--color-icon-subtle"); | |
| 76 | ||
| 77 | 79 | return ( |
| 78 | 80 | <Pressable |
| 79 | 81 | className="flex-row items-center gap-3 px-1 py-3 disabled:opacity-[0.45]" |
| ... | ... | @@ -81,7 +83,12 @@ export function SheetListRow(props: { |
| 81 | 83 | onPress={props.onPress} |
| 82 | 84 | > |
| 83 | 85 | <View className="bg-subtle h-9 w-9 items-center justify-center rounded-full"> |
| 84 | <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" /> | |
| 86 | <SymbolView | |
| 87 | name={props.icon} | |
| 88 | size={16} | |
| 89 | tintColorClassName={"accent-icon"} | |
| 90 | type="monochrome" | |
| 91 | /> | |
| 85 | 92 | </View> |
| 86 | 93 | <View className="flex-1 gap-0.5"> |
| 87 | 94 | <Text className="text-foreground text-base font-t3-bold">{props.title}</Text> |
| ... | ... | @@ -89,7 +96,12 @@ export function SheetListRow(props: { |
| 89 | 96 | <Text className="text-foreground-muted text-xs leading-snug">{props.subtitle}</Text> |
| 90 | 97 | ) : null} |
| 91 | 98 | </View> |
| 92 | <SymbolView name="chevron.right" size={13} tintColor={iconSubtleColor} type="monochrome" /> | |
| 99 | <SymbolView | |
| 100 | name="chevron.right" | |
| 101 | size={13} | |
| 102 | tintColorClassName={"accent-icon-subtle"} | |
| 103 | type="monochrome" | |
| 104 | /> | |
| 93 | 105 | </Pressable> |
| 94 | 106 | ); |
| 95 | 107 | } |
apps/mobile/src/features/threads/sidebar-filter-button.tsx+6-5| ... | ... | @@ -1,8 +1,6 @@ |
| 1 | 1 | import { SymbolView } from "../../components/AppSymbol"; |
| 2 | 2 | import { Pressable } from "react-native"; |
| 3 | 3 | |
| 4 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 5 | ||
| 6 | 4 | export type SidebarFilterButtonIcon = |
| 7 | 5 | | "line.3.horizontal.decrease.circle" |
| 8 | 6 | | "line.3.horizontal.decrease.circle.fill"; |
| ... | ... | @@ -11,8 +9,6 @@ export function SidebarFilterButton(props: { |
| 11 | 9 | readonly accessibilityLabel: string; |
| 12 | 10 | readonly icon: SidebarFilterButtonIcon; |
| 13 | 11 | }) { |
| 14 | const iconColor = useThemeColor("--color-foreground"); | |
| 15 | ||
| 16 | 12 | return ( |
| 17 | 13 | <Pressable |
| 18 | 14 | className="size-11 cursor-pointer items-center justify-center rounded-full bg-subtle active:opacity-70" |
| ... | ... | @@ -20,7 +16,12 @@ export function SidebarFilterButton(props: { |
| 20 | 16 | accessibilityRole="button" |
| 21 | 17 | hitSlop={4} |
| 22 | 18 | > |
| 23 | <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" /> | |
| 19 | <SymbolView | |
| 20 | name={props.icon} | |
| 21 | size={16} | |
| 22 | tintColorClassName="accent-foreground" | |
| 23 | type="monochrome" | |
| 24 | /> | |
| 24 | 25 | </Pressable> |
| 25 | 26 | ); |
| 26 | 27 | } |
apps/mobile/src/features/threads/sidebar-header-actions.tsx+6-5| ... | ... | @@ -1,8 +1,6 @@ |
| 1 | 1 | import { SymbolView } from "../../components/AppSymbol"; |
| 2 | 2 | import { Pressable, View } from "react-native"; |
| 3 | 3 | |
| 4 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 5 | ||
| 6 | 4 | export interface SidebarHeaderActionsProps { |
| 7 | 5 | readonly onOpenSettings: () => void; |
| 8 | 6 | } |
| ... | ... | @@ -12,8 +10,6 @@ function FallbackHeaderButton(props: { |
| 12 | 10 | readonly icon: "gearshape" | "square.and.pencil"; |
| 13 | 11 | readonly onPress: () => void; |
| 14 | 12 | }) { |
| 15 | const iconColor = useThemeColor("--color-foreground"); | |
| 16 | ||
| 17 | 13 | return ( |
| 18 | 14 | <Pressable |
| 19 | 15 | className="size-11 items-center justify-center rounded-full bg-subtle active:opacity-70" |
| ... | ... | @@ -22,7 +18,12 @@ function FallbackHeaderButton(props: { |
| 22 | 18 | hitSlop={4} |
| 23 | 19 | onPress={props.onPress} |
| 24 | 20 | > |
| 25 | <SymbolView name={props.icon} size={18} tintColor={iconColor} type="monochrome" /> | |
| 21 | <SymbolView | |
| 22 | name={props.icon} | |
| 23 | size={18} | |
| 24 | tintColorClassName="accent-foreground" | |
| 25 | type="monochrome" | |
| 26 | /> | |
| 26 | 27 | </Pressable> |
| 27 | 28 | ); |
| 28 | 29 | } |
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+1-3| ... | ... | @@ -10,7 +10,6 @@ import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle |
| 10 | 10 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 11 | 11 | import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; |
| 12 | 12 | import { useMobileNavigationTheme } from "../../lib/useMobileNavigationTheme"; |
| 13 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | |
| 14 | 13 | |
| 15 | 14 | const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); |
| 16 | 15 | |
| ... | ... | @@ -53,8 +52,7 @@ const SidebarStack = createNativeStackNavigator(); |
| 53 | 52 | * navigation hooks used for header configuration inside the pane. |
| 54 | 53 | */ |
| 55 | 54 | export function SidebarNavigationShell(props: { readonly children: ReactNode }) { |
| 56 | const { themeAppearance } = useAppearancePreferences(); | |
| 57 | const navigationTheme = useMobileNavigationTheme(themeAppearance); | |
| 55 | const navigationTheme = useMobileNavigationTheme(); | |
| 58 | 56 | |
| 59 | 57 | return ( |
| 60 | 58 | <NavigationIndependentTree> |
apps/mobile/src/features/threads/thread-list-items.tsx+18-21| ... | ... | @@ -19,7 +19,7 @@ import { cn } from "../../lib/cn"; |
| 19 | 19 | import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics"; |
| 20 | 20 | import { relativeTime } from "../../lib/time"; |
| 21 | 21 | import { themeColorWithAlpha } from "../../lib/mobileTheme"; |
| 22 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 22 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 23 | 23 | import type { PendingNewTask } from "../../state/use-pending-new-tasks"; |
| 24 | 24 | import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr"; |
| 25 | 25 | import type { HomeGroupDisplayAction } from "../home/homeListItems"; |
| ... | ... | @@ -87,7 +87,6 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props: |
| 87 | 87 | readonly newThreadTarget?: EnvironmentProject | null; |
| 88 | 88 | readonly onNewThread?: (project: EnvironmentProject) => void; |
| 89 | 89 | }) { |
| 90 | const iconMutedColor = useThemeColor("--color-icon-muted"); | |
| 91 | 90 | const { groupKey, onGroupAction, onNewThread } = props; |
| 92 | 91 | const newThreadTarget = props.newThreadTarget ?? null; |
| 93 | 92 | const compact = props.variant === "compact"; |
| ... | ... | @@ -171,7 +170,7 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props: |
| 171 | 170 | <SymbolView |
| 172 | 171 | name="plus" |
| 173 | 172 | size={compact ? 20 : 16} |
| 174 | tintColor={iconMutedColor} | |
| 173 | tintColorClassName={"accent-icon-muted"} | |
| 175 | 174 | type="monochrome" |
| 176 | 175 | weight="medium" |
| 177 | 176 | /> |
| ... | ... | @@ -190,7 +189,6 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props: |
| 190 | 189 | readonly groupKey: string; |
| 191 | 190 | readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void; |
| 192 | 191 | }) { |
| 193 | const iconSubtleColor = useThemeColor("--color-icon-subtle"); | |
| 194 | 192 | const showsMore = props.hiddenCount > 0; |
| 195 | 193 | const compact = props.variant === "compact"; |
| 196 | 194 | const { groupKey, onGroupAction } = props; |
| ... | ... | @@ -221,7 +219,7 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props: |
| 221 | 219 | <SymbolView |
| 222 | 220 | name={icon} |
| 223 | 221 | size={10} |
| 224 | tintColor={iconSubtleColor} | |
| 222 | tintColorClassName={"accent-icon-subtle"} | |
| 225 | 223 | type="monochrome" |
| 226 | 224 | weight="semibold" |
| 227 | 225 | /> |
| ... | ... | @@ -275,10 +273,9 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: { |
| 275 | 273 | readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void; |
| 276 | 274 | }) { |
| 277 | 275 | const compact = props.variant === "compact"; |
| 278 | const separatorColor = useThemeColor("--color-separator"); | |
| 279 | const iconSubtleColor = useThemeColor("--color-icon-subtle"); | |
| 280 | const mutedColor = useThemeColor("--color-foreground-muted"); | |
| 281 | const pressedBackgroundColor = useThemeColor("--color-subtle"); | |
| 276 | const theme = useUniwindTheme(); | |
| 277 | const separatorColor = theme["--color-separator"]; | |
| 278 | const pressedBackgroundColor = theme["--color-subtle"]; | |
| 282 | 279 | |
| 283 | 280 | const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props; |
| 284 | 281 | const timestamp = relativeTime(pendingTask.message.createdAt); |
| ... | ... | @@ -294,8 +291,8 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: { |
| 294 | 291 | ); |
| 295 | 292 | |
| 296 | 293 | const statusPill = ( |
| 297 | <View className="rounded-full bg-zinc-500/12 px-1.5 py-0.5 dark:bg-zinc-500/16"> | |
| 298 | <Text className="text-3xs font-t3-bold text-zinc-600 dark:text-zinc-300">Pending</Text> | |
| 294 | <View className="rounded-full bg-adaptive-zinc-500-a12-a16 px-1.5 py-0.5"> | |
| 295 | <Text className="text-3xs font-t3-bold text-adaptive-zinc-600-300">Pending</Text> | |
| 299 | 296 | </View> |
| 300 | 297 | ); |
| 301 | 298 | |
| ... | ... | @@ -305,7 +302,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: { |
| 305 | 302 | <SymbolView |
| 306 | 303 | name="tray.and.arrow.up" |
| 307 | 304 | size={10} |
| 308 | tintColor={compact ? iconSubtleColor : mutedColor} | |
| 305 | tintColorClassName={compact ? "accent-icon-subtle" : "accent-foreground-muted"} | |
| 309 | 306 | type="monochrome" |
| 310 | 307 | /> |
| 311 | 308 | <Text |
| ... | ... | @@ -355,7 +352,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: { |
| 355 | 352 | <SymbolView |
| 356 | 353 | name="chevron.right" |
| 357 | 354 | size={13} |
| 358 | tintColor={iconSubtleColor} | |
| 355 | tintColorClassName={"accent-icon-subtle"} | |
| 359 | 356 | type="monochrome" |
| 360 | 357 | /> |
| 361 | 358 | </View> |
| ... | ... | @@ -446,13 +443,13 @@ export const ThreadListRow = memo(function ThreadListRow(props: { |
| 446 | 443 | // thread, so a hover highlight can't leak across rows. |
| 447 | 444 | const [hovered, setHovered] = useRecyclingState(false); |
| 448 | 445 | |
| 449 | const separatorColor = useThemeColor("--color-separator"); | |
| 450 | const iconSubtleColor = useThemeColor("--color-icon-subtle"); | |
| 451 | const screenColor = useThemeColor("--color-screen"); | |
| 452 | const drawerColor = useThemeColor("--color-drawer"); | |
| 453 | const pressedBackgroundColor = useThemeColor("--color-subtle"); | |
| 454 | const selectedBackgroundColor = useThemeColor("--color-user-bubble"); | |
| 455 | const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground"); | |
| 446 | const theme = useUniwindTheme(); | |
| 447 | const separatorColor = theme["--color-separator"]; | |
| 448 | const screenColor = theme["--color-screen"]; | |
| 449 | const drawerColor = theme["--color-drawer"]; | |
| 450 | const pressedBackgroundColor = theme["--color-subtle"]; | |
| 451 | const selectedBackgroundColor = theme["--color-user-bubble"]; | |
| 452 | const selectedForegroundColor = theme["--color-user-bubble-foreground"]; | |
| 456 | 453 | |
| 457 | 454 | const { thread, onSelectThread, onArchiveThread, onDeleteThread, onRegenerateThreadTitle } = |
| 458 | 455 | props; |
| ... | ... | @@ -600,7 +597,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { |
| 600 | 597 | <SymbolView |
| 601 | 598 | name="chevron.right" |
| 602 | 599 | size={13} |
| 603 | tintColor={iconSubtleColor} | |
| 600 | tintColorClassName={"accent-icon-subtle"} | |
| 604 | 601 | type="monochrome" |
| 605 | 602 | /> |
| 606 | 603 | </View> |
apps/mobile/src/features/threads/thread-list-v2-items.tsx+25-23| ... | ... | @@ -16,7 +16,7 @@ import { ProjectFavicon } from "../../components/ProjectFavicon"; |
| 16 | 16 | import { ProviderIcon } from "../../components/ProviderIcon"; |
| 17 | 17 | import { cn } from "../../lib/cn"; |
| 18 | 18 | import { relativeTime } from "../../lib/time"; |
| 19 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 19 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 20 | 20 | import type { PendingNewTask } from "../../state/use-pending-new-tasks"; |
| 21 | 21 | import { useThreadPr } from "../../state/use-thread-pr"; |
| 22 | 22 | import { ThreadSwipeable } from "../home/thread-swipe-actions"; |
| ... | ... | @@ -52,10 +52,10 @@ const MONO_FONT = Platform.select({ |
| 52 | 52 | const STATUS_LABEL_BY_STATUS: Partial< |
| 53 | 53 | Record<ThreadListV2Status, { label: string; className: string }> |
| 54 | 54 | > = { |
| 55 | approval: { label: "Approval", className: "text-amber-700 dark:text-amber-300" }, | |
| 56 | input: { label: "Input", className: "text-indigo-600 dark:text-indigo-300" }, | |
| 57 | working: { label: "Working", className: "text-sky-600 dark:text-sky-400" }, | |
| 58 | failed: { label: "Failed", className: "text-red-700 dark:text-red-300" }, | |
| 55 | approval: { label: "Approval", className: "text-adaptive-amber-700-300" }, | |
| 56 | input: { label: "Input", className: "text-adaptive-indigo-600-300" }, | |
| 57 | working: { label: "Working", className: "text-adaptive-sky-600-400" }, | |
| 58 | failed: { label: "Failed", className: "text-adaptive-red-700-300" }, | |
| 59 | 59 | }; |
| 60 | 60 | |
| 61 | 61 | function threadTimeLabel(thread: EnvironmentThreadShell): string { |
| ... | ... | @@ -93,7 +93,6 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid |
| 93 | 93 | readonly label: string; |
| 94 | 94 | readonly pane?: "screen" | "sidebar"; |
| 95 | 95 | }) { |
| 96 | const borderColor = useThemeColor("--color-border"); | |
| 97 | 96 | return ( |
| 98 | 97 | <View |
| 99 | 98 | className={cn( |
| ... | ... | @@ -102,7 +101,7 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid |
| 102 | 101 | )} |
| 103 | 102 | > |
| 104 | 103 | <Text className="text-xs font-t3-medium text-foreground-tertiary">{props.label}</Text> |
| 105 | <View className="h-px flex-1" style={{ backgroundColor: borderColor }} /> | |
| 104 | <View className="h-px flex-1 bg-border" /> | |
| 106 | 105 | </View> |
| 107 | 106 | ); |
| 108 | 107 | }); |
| ... | ... | @@ -134,10 +133,10 @@ export const ThreadListV2SnoozedShelfHeader = memo(function ThreadListV2SnoozedS |
| 134 | 133 | onPress={props.onToggle} |
| 135 | 134 | style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} |
| 136 | 135 | > |
| 137 | <Text className="text-xs font-t3-medium text-blue-600 dark:text-blue-400"> | |
| 136 | <Text className="text-xs font-t3-medium text-adaptive-blue-600-400"> | |
| 138 | 137 | {props.expanded ? "Snoozed" : `Snoozed (${props.count})`} |
| 139 | 138 | </Text> |
| 140 | <View className="h-px flex-1 bg-blue-500/20 dark:bg-blue-400/15" /> | |
| 139 | <View className="h-px flex-1 bg-adaptive-blue-500-a20-blue-400-a15" /> | |
| 141 | 140 | <SymbolView |
| 142 | 141 | name="chevron.down" |
| 143 | 142 | size={10} |
| ... | ... | @@ -156,7 +155,6 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS |
| 156 | 155 | readonly onToggle: () => void; |
| 157 | 156 | readonly pane?: "screen" | "sidebar"; |
| 158 | 157 | }) { |
| 159 | const mutedColor = useThemeColor("--color-foreground-muted"); | |
| 160 | 158 | return ( |
| 161 | 159 | <Pressable |
| 162 | 160 | accessibilityHint={ |
| ... | ... | @@ -180,7 +178,7 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS |
| 180 | 178 | <SymbolView |
| 181 | 179 | name="chevron.down" |
| 182 | 180 | size={10} |
| 183 | tintColor={mutedColor} | |
| 181 | tintColorClassName={"accent-foreground-muted"} | |
| 184 | 182 | type="monochrome" |
| 185 | 183 | style={{ transform: [{ rotate: props.expanded ? "180deg" : "0deg" }] }} |
| 186 | 184 | /> |
| ... | ... | @@ -213,8 +211,9 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props |
| 213 | 211 | readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void; |
| 214 | 212 | }) { |
| 215 | 213 | const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props; |
| 216 | const drawerColor = useThemeColor("--color-drawer"); | |
| 217 | const pressedBackgroundColor = useThemeColor("--color-subtle"); | |
| 214 | const theme = useUniwindTheme(); | |
| 215 | const drawerColor = theme["--color-drawer"]; | |
| 216 | const pressedBackgroundColor = theme["--color-subtle"]; | |
| 218 | 217 | const sidebarPane = props.pane === "sidebar"; |
| 219 | 218 | const projectTitle = |
| 220 | 219 | props.projectTitle ?? props.project?.title ?? pendingTask.creation.projectTitle ?? ""; |
| ... | ... | @@ -418,11 +417,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 418 | 417 | onChangeRequestState?.(threadKey, changeRequest); |
| 419 | 418 | }, [onChangeRequestState, prState, prUpdatedAt, thread.linkedPullRequest, threadKey]); |
| 420 | 419 | |
| 421 | const screenColor = useThemeColor("--color-screen"); | |
| 422 | const drawerColor = useThemeColor("--color-drawer"); | |
| 423 | const pressedBackgroundColor = useThemeColor("--color-subtle"); | |
| 424 | const selectedBackgroundColor = useThemeColor("--color-user-bubble"); | |
| 425 | const pinTintColor = useThemeColor("--color-foreground-muted"); | |
| 420 | const theme = useUniwindTheme(); | |
| 421 | const screenColor = theme["--color-screen"]; | |
| 422 | const drawerColor = theme["--color-drawer"]; | |
| 423 | const pressedBackgroundColor = theme["--color-subtle"]; | |
| 424 | const selectedBackgroundColor = theme["--color-user-bubble"]; | |
| 426 | 425 | const sidebarPane = props.pane === "sidebar"; |
| 427 | 426 | const selected = props.selected === true; |
| 428 | 427 | |
| ... | ... | @@ -699,7 +698,12 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 699 | 698 | {props.projectTitle ?? props.project?.title ?? ""} |
| 700 | 699 | </Text> |
| 701 | 700 | {pinnedRow ? ( |
| 702 | <SymbolView name="pin" size={11} tintColor={pinTintColor} type="monochrome" /> | |
| 701 | <SymbolView | |
| 702 | name="pin" | |
| 703 | size={11} | |
| 704 | tintColorClassName={"accent-foreground-muted"} | |
| 705 | type="monochrome" | |
| 706 | /> | |
| 703 | 707 | ) : null} |
| 704 | 708 | <Text |
| 705 | 709 | className={cn( |
| ... | ... | @@ -735,9 +739,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 735 | 739 | <Text |
| 736 | 740 | className={cn( |
| 737 | 741 | "flex-1 text-xs", |
| 738 | selected | |
| 739 | ? "text-user-bubble-foreground-muted" | |
| 740 | : "text-red-600/80 dark:text-red-400/80", | |
| 742 | selected ? "text-user-bubble-foreground-muted" : "text-adaptive-red-600-a80-400-a80", | |
| 741 | 743 | )} |
| 742 | 744 | numberOfLines={1} |
| 743 | 745 | > |
| ... | ... | @@ -905,7 +907,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { |
| 905 | 907 | selected |
| 906 | 908 | ? "text-user-bubble-foreground-muted" |
| 907 | 909 | : snoozedRow |
| 908 | ? "text-blue-600 dark:text-blue-400" | |
| 910 | ? "text-adaptive-blue-600-400" | |
| 909 | 911 | : "text-foreground-tertiary", |
| 910 | 912 | )} |
| 911 | 913 | style={{ fontFamily: MONO_FONT }} |
apps/mobile/src/features/threads/thread-search-match.tsx+2-2| ... | ... | @@ -65,8 +65,8 @@ export function ThreadSearchMatchExcerpt(props: { |
| 65 | 65 | props.selected |
| 66 | 66 | ? "text-user-bubble-foreground" |
| 67 | 67 | : isUser |
| 68 | ? "text-blue-500 dark:text-blue-400" | |
| 69 | : "text-emerald-600 dark:text-emerald-400", | |
| 68 | ? "text-adaptive-blue-500-400" | |
| 69 | : "text-adaptive-emerald-600-400", | |
| 70 | 70 | )} |
| 71 | 71 | > |
| 72 | 72 | {isUser ? "You:" : "Agent:"}{" "} |
apps/mobile/src/features/threads/thread-work-log.tsx+5-14| ... | ... | @@ -7,7 +7,6 @@ import { scaledTypographyLineHeight } from "../../lib/appearancePreferences"; |
| 7 | 7 | import { cn } from "../../lib/cn"; |
| 8 | 8 | import type { ThreadFeedActivity } from "../../lib/threadActivity"; |
| 9 | 9 | import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; |
| 10 | import { useThemeColor } from "../../lib/useThemeColor"; | |
| 11 | 10 | import Animated, { FadeIn } from "react-native-reanimated"; |
| 12 | 11 | |
| 13 | 12 | const WORK_LOG_LAYOUT_ANIMATION = { |
| ... | ... | @@ -128,7 +127,6 @@ export function ThreadWorkLog(props: { |
| 128 | 127 | readonly onCopyRow: (rowId: string, value: string) => void; |
| 129 | 128 | readonly onToggleRow: (rowId: string) => void; |
| 130 | 129 | }) { |
| 131 | const pressedBackground = useThemeColor("--color-subtle"); | |
| 132 | 130 | const rows = visibleWorkLogActivities(props.activities).map((activity) => ({ |
| 133 | 131 | ...activity, |
| 134 | 132 | detail: compactActivityDetail(activity.detail), |
| ... | ... | @@ -178,10 +176,7 @@ export function ThreadWorkLog(props: { |
| 178 | 176 | } |
| 179 | 177 | }} |
| 180 | 178 | onLongPress={() => props.onCopyRow(row.id, row.getCopyText())} |
| 181 | style={({ pressed }) => ({ | |
| 182 | backgroundColor: pressed ? pressedBackground : "transparent", | |
| 183 | })} | |
| 184 | className="rounded-md px-0.5 py-0" | |
| 179 | className="rounded-md px-0.5 py-0 active:bg-subtle" | |
| 185 | 180 | > |
| 186 | 181 | <View className="min-h-8 flex-row items-center gap-1.5"> |
| 187 | 182 | <View className="h-[18px] w-5 shrink-0 items-center justify-center"> |
| ... | ... | @@ -198,7 +193,7 @@ export function ThreadWorkLog(props: { |
| 198 | 193 | <Text |
| 199 | 194 | className={cn( |
| 200 | 195 | "font-t3-medium text-foreground", |
| 201 | iconIsDestructive && "text-rose-600 dark:text-rose-400", | |
| 196 | iconIsDestructive && "text-adaptive-rose-600-400", | |
| 202 | 197 | )} |
| 203 | 198 | > |
| 204 | 199 | {row.summary} |
| ... | ... | @@ -210,7 +205,7 @@ export function ThreadWorkLog(props: { |
| 210 | 205 | |
| 211 | 206 | <View className="shrink-0 flex-row items-center gap-px"> |
| 212 | 207 | {props.copiedRowId === row.id ? ( |
| 213 | <Text className="pr-1 font-t3-medium text-3xs text-emerald-600 dark:text-emerald-400"> | |
| 208 | <Text className="pr-1 font-t3-medium text-3xs text-adaptive-emerald-600-400"> | |
| 214 | 209 | Copied |
| 215 | 210 | </Text> |
| 216 | 211 | ) : null} |
| ... | ... | @@ -249,7 +244,7 @@ export function ThreadWorkLog(props: { |
| 249 | 244 | </Pressable> |
| 250 | 245 | |
| 251 | 246 | {fullDetail ? ( |
| 252 | <View className="ml-7 border-l border-neutral-300/60 pb-1 pl-3 pt-0.5 dark:border-white/[0.12]"> | |
| 247 | <View className="ml-7 border-l border-adaptive-neutral-300-a60-white-a12 pb-1 pl-3 pt-0.5"> | |
| 253 | 248 | <ScrollView |
| 254 | 249 | nestedScrollEnabled |
| 255 | 250 | directionalLockEnabled |
| ... | ... | @@ -281,7 +276,6 @@ export function ThreadWorkGroupToggle(props: { |
| 281 | 276 | readonly onlyToolActivities: boolean; |
| 282 | 277 | readonly onToggle: () => void; |
| 283 | 278 | }) { |
| 284 | const pressedBackground = useThemeColor("--color-subtle"); | |
| 285 | 279 | const noun = props.onlyToolActivities |
| 286 | 280 | ? props.hiddenCount === 1 |
| 287 | 281 | ? "tool call" |
| ... | ... | @@ -305,10 +299,7 @@ export function ThreadWorkGroupToggle(props: { |
| 305 | 299 | void Haptics.selectionAsync(); |
| 306 | 300 | props.onToggle(); |
| 307 | 301 | }} |
| 308 | style={({ pressed }) => ({ | |
| 309 | backgroundColor: pressed ? pressedBackground : "transparent", | |
| 310 | })} | |
| 311 | className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0" | |
| 302 | className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0 active:bg-subtle" | |
| 312 | 303 | > |
| 313 | 304 | <View className="h-[18px] w-5 items-center justify-center"> |
| 314 | 305 | <SymbolView |
apps/mobile/src/features/threads/threadPresentation.ts+12-12| ... | ... | @@ -53,8 +53,8 @@ export function resolveThreadStatus( |
| 53 | 53 | return { |
| 54 | 54 | kind: "pending-approval", |
| 55 | 55 | label: "Needs Approval", |
| 56 | pillClassName: "bg-amber-500/12 dark:bg-amber-500/16", | |
| 57 | textClassName: "text-amber-700 dark:text-amber-300", | |
| 56 | pillClassName: "bg-adaptive-amber-500-a12-a16", | |
| 57 | textClassName: "text-adaptive-amber-700-300", | |
| 58 | 58 | iconColor: "#ff9f0a", |
| 59 | 59 | iconBackground: "rgba(255,159,10,0.22)", |
| 60 | 60 | pulse: false, |
| ... | ... | @@ -65,8 +65,8 @@ export function resolveThreadStatus( |
| 65 | 65 | return { |
| 66 | 66 | kind: "awaiting-input", |
| 67 | 67 | label: "Awaiting Input", |
| 68 | pillClassName: "bg-indigo-500/12 dark:bg-indigo-500/16", | |
| 69 | textClassName: "text-indigo-700 dark:text-indigo-300", | |
| 68 | pillClassName: "bg-adaptive-indigo-500-a12-a16", | |
| 69 | textClassName: "text-adaptive-indigo-700-300", | |
| 70 | 70 | iconColor: "#5e5ce6", |
| 71 | 71 | iconBackground: "rgba(94,92,230,0.22)", |
| 72 | 72 | pulse: false, |
| ... | ... | @@ -77,8 +77,8 @@ export function resolveThreadStatus( |
| 77 | 77 | return { |
| 78 | 78 | kind: "working", |
| 79 | 79 | label: "Working", |
| 80 | pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", | |
| 81 | textClassName: "text-sky-700 dark:text-sky-300", | |
| 80 | pillClassName: "bg-adaptive-sky-500-a12-a16", | |
| 81 | textClassName: "text-adaptive-sky-700-300", | |
| 82 | 82 | iconColor: "#0a84ff", |
| 83 | 83 | iconBackground: "rgba(10,132,255,0.22)", |
| 84 | 84 | pulse: true, |
| ... | ... | @@ -89,8 +89,8 @@ export function resolveThreadStatus( |
| 89 | 89 | return { |
| 90 | 90 | kind: "connecting", |
| 91 | 91 | label: "Connecting", |
| 92 | pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", | |
| 93 | textClassName: "text-sky-700 dark:text-sky-300", | |
| 92 | pillClassName: "bg-adaptive-sky-500-a12-a16", | |
| 93 | textClassName: "text-adaptive-sky-700-300", | |
| 94 | 94 | iconColor: "#0a84ff", |
| 95 | 95 | iconBackground: "rgba(10,132,255,0.22)", |
| 96 | 96 | pulse: true, |
| ... | ... | @@ -101,8 +101,8 @@ export function resolveThreadStatus( |
| 101 | 101 | return { |
| 102 | 102 | kind: "error", |
| 103 | 103 | label: "Error", |
| 104 | pillClassName: "bg-rose-500/12 dark:bg-rose-500/16", | |
| 105 | textClassName: "text-rose-700 dark:text-rose-300", | |
| 104 | pillClassName: "bg-adaptive-rose-500-a12-a16", | |
| 105 | textClassName: "text-adaptive-rose-700-300", | |
| 106 | 106 | iconColor: "#ff453a", |
| 107 | 107 | iconBackground: "rgba(255,69,58,0.22)", |
| 108 | 108 | pulse: false, |
| ... | ... | @@ -117,8 +117,8 @@ export function resolveThreadStatus( |
| 117 | 117 | return { |
| 118 | 118 | kind: "plan-ready", |
| 119 | 119 | label: "Plan Ready", |
| 120 | pillClassName: "bg-violet-500/12 dark:bg-violet-500/16", | |
| 121 | textClassName: "text-violet-700 dark:text-violet-300", | |
| 120 | pillClassName: "bg-adaptive-violet-500-a12-a16", | |
| 121 | textClassName: "text-adaptive-violet-700-300", | |
| 122 | 122 | iconColor: "#bf5af2", |
| 123 | 123 | iconBackground: "rgba(191,90,242,0.22)", |
| 124 | 124 | pulse: false, |
apps/mobile/src/lib/mobileDefaultTheme.ts deleted-139| ... | ... | @@ -1,139 +0,0 @@ |
| 1 | import type { MobileThemeVariables } from "./mobileTheme"; | |
| 2 | ||
| 3 | /** The existing T3 Code mobile palette, retained as the upgrade-safe default. */ | |
| 4 | export const DEFAULT_MOBILE_THEME_VARIABLES = { | |
| 5 | light: { | |
| 6 | "--color-screen": "#f2f2f7", | |
| 7 | "--color-sheet": "rgba(242, 242, 247, 0.98)", | |
| 8 | "--color-sheet-solid": "#f2f2f7", | |
| 9 | "--color-card": "#ffffff", | |
| 10 | "--color-card-alt": "#f5f5f5", | |
| 11 | "--color-card-translucent": "rgba(255, 255, 255, 0.8)", | |
| 12 | "--color-foreground": "#262626", | |
| 13 | "--color-foreground-secondary": "#525252", | |
| 14 | "--color-foreground-muted": "#737373", | |
| 15 | "--color-foreground-tertiary": "#8e8e93", | |
| 16 | "--color-border": "rgba(0, 0, 0, 0.08)", | |
| 17 | "--color-border-subtle": "rgba(0, 0, 0, 0.06)", | |
| 18 | "--color-separator": "rgba(0, 0, 0, 0.04)", | |
| 19 | "--color-subtle": "rgba(0, 0, 0, 0.04)", | |
| 20 | "--color-subtle-strong": "rgba(0, 0, 0, 0.08)", | |
| 21 | "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)", | |
| 22 | "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)", | |
| 23 | "--color-inline-skill-foreground": "#a21caf", | |
| 24 | "--color-primary": "#262626", | |
| 25 | "--color-primary-foreground": "#ffffff", | |
| 26 | "--color-primary-shadow": "#000000", | |
| 27 | "--color-secondary": "#ffffff", | |
| 28 | "--color-secondary-foreground": "#262626", | |
| 29 | "--color-secondary-border": "rgba(0, 0, 0, 0.08)", | |
| 30 | "--color-switch-active-track": "#34c759", | |
| 31 | "--color-switch-active-thumb": "#ffffff", | |
| 32 | "--color-switch-inactive-track": "rgba(0, 0, 0, 0.08)", | |
| 33 | "--color-switch-inactive-thumb": "#8e8e93", | |
| 34 | "--color-danger": "#fef2f2", | |
| 35 | "--color-danger-border": "rgba(239, 68, 68, 0.12)", | |
| 36 | "--color-danger-foreground": "#dc2626", | |
| 37 | "--color-input": "#ffffff", | |
| 38 | "--color-input-border": "rgba(0, 0, 0, 0.1)", | |
| 39 | "--color-sidebar-search": "rgba(118, 118, 128, 0.12)", | |
| 40 | "--color-placeholder": "#737373", | |
| 41 | "--color-icon": "#262626", | |
| 42 | "--color-icon-muted": "#525252", | |
| 43 | "--color-icon-subtle": "#a3a3a3", | |
| 44 | "--color-header": "rgba(255, 255, 255, 0.97)", | |
| 45 | "--color-header-border": "rgba(0, 0, 0, 0.06)", | |
| 46 | "--color-glass-surface": "rgba(255, 255, 255, 0.72)", | |
| 47 | "--color-glass-tint": "rgba(255, 255, 255, 0.18)", | |
| 48 | "--color-status-bar": "#f2f2f7", | |
| 49 | "--color-md-body": "#111111", | |
| 50 | "--color-md-strong": "#000000", | |
| 51 | "--color-md-link": "#2563eb", | |
| 52 | "--color-md-blockquote-border": "rgba(0, 0, 0, 0.08)", | |
| 53 | "--color-md-blockquote-bg": "rgba(0, 0, 0, 0.02)", | |
| 54 | "--color-md-code-bg": "rgba(0, 0, 0, 0.04)", | |
| 55 | "--color-md-code-text": "#262626", | |
| 56 | "--color-md-user-code-bg": "rgba(255, 255, 255, 0.22)", | |
| 57 | "--color-md-user-code-text": "#ffffff", | |
| 58 | "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.16)", | |
| 59 | "--color-md-user-fence-text": "#ffffff", | |
| 60 | "--color-md-hr": "rgba(0, 0, 0, 0.08)", | |
| 61 | "--color-user-bubble": "#007aff", | |
| 62 | "--color-user-bubble-foreground": "#ffffff", | |
| 63 | "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)", | |
| 64 | "--color-user-bubble-skill-foreground": "#f0abfc", | |
| 65 | "--color-backdrop": "rgba(0, 0, 0, 0.22)", | |
| 66 | "--color-drawer": "rgba(255, 255, 255, 0.99)", | |
| 67 | "--color-drawer-shadow": "rgba(0, 0, 0, 0.12)", | |
| 68 | "--color-dot-separator": "rgba(0, 0, 0, 0.2)", | |
| 69 | "--color-wordmark": "#262626", | |
| 70 | "--color-chevron": "rgba(0, 0, 0, 0.2)", | |
| 71 | }, | |
| 72 | dark: { | |
| 73 | "--color-screen": "#0a0a0a", | |
| 74 | "--color-sheet": "rgba(14, 14, 14, 0.98)", | |
| 75 | "--color-sheet-solid": "#0e0e0e", | |
| 76 | "--color-card": "#171717", | |
| 77 | "--color-card-alt": "#1c1c1c", | |
| 78 | "--color-card-translucent": "rgba(17, 17, 17, 0.8)", | |
| 79 | "--color-foreground": "#f5f5f5", | |
| 80 | "--color-foreground-secondary": "#a3a3a3", | |
| 81 | "--color-foreground-muted": "#8e8e93", | |
| 82 | "--color-foreground-tertiary": "#636366", | |
| 83 | "--color-border": "rgba(255, 255, 255, 0.06)", | |
| 84 | "--color-border-subtle": "rgba(255, 255, 255, 0.04)", | |
| 85 | "--color-separator": "rgba(255, 255, 255, 0.03)", | |
| 86 | "--color-subtle": "rgba(255, 255, 255, 0.04)", | |
| 87 | "--color-subtle-strong": "rgba(255, 255, 255, 0.08)", | |
| 88 | "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)", | |
| 89 | "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)", | |
| 90 | "--color-inline-skill-foreground": "#f0abfc", | |
| 91 | "--color-primary": "#f5f5f5", | |
| 92 | "--color-primary-foreground": "#0a0a0a", | |
| 93 | "--color-primary-shadow": "#000000", | |
| 94 | "--color-secondary": "rgba(255, 255, 255, 0.04)", | |
| 95 | "--color-secondary-foreground": "#f5f5f5", | |
| 96 | "--color-secondary-border": "rgba(255, 255, 255, 0.06)", | |
| 97 | "--color-switch-active-track": "#30d158", | |
| 98 | "--color-switch-active-thumb": "#ffffff", | |
| 99 | "--color-switch-inactive-track": "rgba(255, 255, 255, 0.06)", | |
| 100 | "--color-switch-inactive-thumb": "#8e8e93", | |
| 101 | "--color-danger": "rgba(239, 68, 68, 0.14)", | |
| 102 | "--color-danger-border": "rgba(248, 113, 113, 0.18)", | |
| 103 | "--color-danger-foreground": "#fca5a5", | |
| 104 | "--color-input": "#141414", | |
| 105 | "--color-input-border": "rgba(255, 255, 255, 0.08)", | |
| 106 | "--color-sidebar-search": "rgba(118, 118, 128, 0.24)", | |
| 107 | "--color-placeholder": "#8e8e93", | |
| 108 | "--color-icon": "#f5f5f5", | |
| 109 | "--color-icon-muted": "#a3a3a3", | |
| 110 | "--color-icon-subtle": "#8e8e93", | |
| 111 | "--color-header": "rgba(10, 10, 10, 0.97)", | |
| 112 | "--color-header-border": "rgba(255, 255, 255, 0.06)", | |
| 113 | "--color-glass-surface": "rgba(23, 23, 23, 0.78)", | |
| 114 | "--color-glass-tint": "rgba(23, 23, 23, 0.24)", | |
| 115 | "--color-status-bar": "#0a0a0a", | |
| 116 | "--color-md-body": "#e5e5e5", | |
| 117 | "--color-md-strong": "#f5f5f5", | |
| 118 | "--color-md-link": "#60a5fa", | |
| 119 | "--color-md-blockquote-border": "rgba(255, 255, 255, 0.1)", | |
| 120 | "--color-md-blockquote-bg": "rgba(255, 255, 255, 0.03)", | |
| 121 | "--color-md-code-bg": "rgba(255, 255, 255, 0.06)", | |
| 122 | "--color-md-code-text": "#e5e5e5", | |
| 123 | "--color-md-user-code-bg": "rgba(255, 255, 255, 0.18)", | |
| 124 | "--color-md-user-code-text": "#ffffff", | |
| 125 | "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.28)", | |
| 126 | "--color-md-user-fence-text": "#ffffff", | |
| 127 | "--color-md-hr": "rgba(255, 255, 255, 0.08)", | |
| 128 | "--color-user-bubble": "#0a84ff", | |
| 129 | "--color-user-bubble-foreground": "#ffffff", | |
| 130 | "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)", | |
| 131 | "--color-user-bubble-skill-foreground": "#f0abfc", | |
| 132 | "--color-backdrop": "rgba(0, 0, 0, 0.48)", | |
| 133 | "--color-drawer": "rgba(14, 14, 14, 0.99)", | |
| 134 | "--color-drawer-shadow": "rgba(0, 0, 0, 0.32)", | |
| 135 | "--color-dot-separator": "rgba(255, 255, 255, 0.2)", | |
| 136 | "--color-wordmark": "#f5f5f5", | |
| 137 | "--color-chevron": "rgba(255, 255, 255, 0.2)", | |
| 138 | }, | |
| 139 | } as const satisfies Readonly<Record<"light" | "dark", MobileThemeVariables>>; |
apps/mobile/src/lib/mobileTheme.test-support.ts created+20| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | import * as NodeFS from "node:fs"; | |
| 2 | ||
| 3 | import type { MobileThemeAppearance, MobileThemeVariables } from "./mobileTheme"; | |
| 4 | ||
| 5 | export function readDefaultMobileThemeVariables( | |
| 6 | appearance: MobileThemeAppearance, | |
| 7 | ): MobileThemeVariables { | |
| 8 | const stylesheet = NodeFS.readFileSync(new URL("../../global.css", import.meta.url), "utf8"); | |
| 9 | const variant = new RegExp(`@variant ${appearance} \\{([\\s\\S]*?)\\n \\}`, "u").exec( | |
| 10 | stylesheet, | |
| 11 | )?.[1]; | |
| 12 | if (variant === undefined) throw new Error(`Missing default ${appearance} theme in global.css.`); | |
| 13 | ||
| 14 | return Object.fromEntries( | |
| 15 | Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [ | |
| 16 | name, | |
| 17 | value.trim(), | |
| 18 | ]), | |
| 19 | ) as MobileThemeVariables; | |
| 20 | } |
apps/mobile/src/lib/mobileTheme.test.ts+13-29| ... | ... | @@ -1,8 +1,6 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import * as NodeFS from "node:fs"; | |
| 3 | ||
| 4 | 2 | import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES } from "@t3tools/shared/themePalettes"; |
| 5 | import { DEFAULT_MOBILE_THEME_VARIABLES } from "./mobileDefaultTheme"; | |
| 3 | import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support"; | |
| 6 | 4 | |
| 7 | 5 | import { |
| 8 | 6 | createMobileThemePairPatch, |
| ... | ... | @@ -11,7 +9,6 @@ import { |
| 11 | 9 | DEFAULT_MOBILE_THEME_ID, |
| 12 | 10 | getMobileThemePreviewColors, |
| 13 | 11 | getMobileThemeVariables, |
| 14 | MOBILE_THEME_IDS, | |
| 15 | 12 | normalizeMobileThemeId, |
| 16 | 13 | normalizeMobileThemeMode, |
| 17 | 14 | resolveMobileThemeIds, |
| ... | ... | @@ -52,13 +49,12 @@ function compositeOver(overlay: string, background: string): string { |
| 52 | 49 | |
| 53 | 50 | describe("mobile themes", () => { |
| 54 | 51 | it("declares every runtime theme variable in the static stylesheet", () => { |
| 55 | const stylesheet = NodeFS.readFileSync(new URL("../../global.css", import.meta.url), "utf8"); | |
| 56 | const stylesheetVariables = new Set( | |
| 57 | Array.from(stylesheet.matchAll(/--color-[a-z0-9-]+/g), ([variable]) => variable), | |
| 52 | const generatedVariables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light"); | |
| 53 | expect(Object.keys(readDefaultMobileThemeVariables("light")).sort()).toEqual( | |
| 54 | Object.keys(generatedVariables).sort(), | |
| 58 | 55 | ); |
| 59 | ||
| 60 | expect(Array.from(stylesheetVariables).sort()).toEqual( | |
| 61 | Object.keys(DEFAULT_MOBILE_THEME_VARIABLES.light).sort(), | |
| 56 | expect(Object.keys(readDefaultMobileThemeVariables("dark")).sort()).toEqual( | |
| 57 | Object.keys(generatedVariables).sort(), | |
| 62 | 58 | ); |
| 63 | 59 | }); |
| 64 | 60 | |
| ... | ... | @@ -71,17 +67,11 @@ describe("mobile themes", () => { |
| 71 | 67 | }); |
| 72 | 68 | |
| 73 | 69 | it("preserves the existing mobile palette as the default", () => { |
| 74 | expect(getMobileThemeVariables(DEFAULT_MOBILE_THEME_ID, "light")["--color-screen"]).toBe( | |
| 75 | "#f2f2f7", | |
| 70 | expect(readDefaultMobileThemeVariables("light")["--color-screen"]).toBe("#f2f2f7"); | |
| 71 | expect(readDefaultMobileThemeVariables("dark")["--color-screen"]).toBe("#0a0a0a"); | |
| 72 | expect(readDefaultMobileThemeVariables("light")["--color-user-bubble-skill-foreground"]).toBe( | |
| 73 | "#f0abfc", | |
| 76 | 74 | ); |
| 77 | expect(getMobileThemeVariables(DEFAULT_MOBILE_THEME_ID, "dark")["--color-screen"]).toBe( | |
| 78 | "#0a0a0a", | |
| 79 | ); | |
| 80 | expect( | |
| 81 | getMobileThemeVariables(DEFAULT_MOBILE_THEME_ID, "light")[ | |
| 82 | "--color-user-bubble-skill-foreground" | |
| 83 | ], | |
| 84 | ).toBe("#f0abfc"); | |
| 85 | 75 | }); |
| 86 | 76 | |
| 87 | 77 | it("applies palette overrides on top of the selected built-in theme", () => { |
| ... | ... | @@ -172,17 +162,11 @@ describe("mobile themes", () => { |
| 172 | 162 | expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.22)"); |
| 173 | 163 | expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.12)"); |
| 174 | 164 | expect(variables["--color-user-bubble-foreground"]).toMatch(/^#/); |
| 175 | expect(Object.keys(DEFAULT_MOBILE_THEME_VARIABLES.light).sort()).toEqual( | |
| 176 | Object.keys(variables).sort(), | |
| 177 | ); | |
| 178 | expect(Object.keys(DEFAULT_MOBILE_THEME_VARIABLES.dark).sort()).toEqual( | |
| 179 | Object.keys(variables).sort(), | |
| 180 | ); | |
| 181 | 165 | }); |
| 182 | 166 | |
| 183 | 167 | it("keeps every built-in shadow and backdrop black-based in dark mode", () => { |
| 184 | for (const theme of BUILT_IN_THEMES) { | |
| 185 | const variables = getMobileThemeVariables(normalizeMobileThemeId(theme.id), "dark"); | |
| 168 | for (const themeId of BUILT_IN_THEME_IDS) { | |
| 169 | const variables = getMobileThemeVariables(themeId, "dark"); | |
| 186 | 170 | expect(variables["--color-primary-shadow"]).toBe("#000000"); |
| 187 | 171 | expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.48)"); |
| 188 | 172 | expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.32)"); |
| ... | ... | @@ -190,7 +174,7 @@ describe("mobile themes", () => { |
| 190 | 174 | }); |
| 191 | 175 | |
| 192 | 176 | it("keeps placeholders and selected-row labels readable on their mobile surfaces", () => { |
| 193 | for (const themeId of MOBILE_THEME_IDS) { | |
| 177 | for (const themeId of BUILT_IN_THEME_IDS) { | |
| 194 | 178 | for (const appearance of ["light", "dark"] as const) { |
| 195 | 179 | const variables = getMobileThemeVariables(themeId, appearance); |
| 196 | 180 | expect( |
apps/mobile/src/lib/mobileTheme.ts+10-10| ... | ... | @@ -3,6 +3,7 @@ import { |
| 3 | 3 | getThemeColorsForAppearance, |
| 4 | 4 | MOBILE_DEFAULT_THEME_ID, |
| 5 | 5 | MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS, |
| 6 | type BuiltInThemeId, | |
| 6 | 7 | type MobileThemeId as SharedMobileThemeId, |
| 7 | 8 | type ThemeAppearance, |
| 8 | 9 | type ThemeColors, |
| ... | ... | @@ -11,7 +12,6 @@ import { |
| 11 | 12 | STANDARD_THEME_PREVIEW_COLORS, |
| 12 | 13 | type ThemePreviewColors, |
| 13 | 14 | } from "@t3tools/shared/themePreview"; |
| 14 | import { DEFAULT_MOBILE_THEME_VARIABLES } from "./mobileDefaultTheme"; | |
| 15 | 15 | |
| 16 | 16 | export const DEFAULT_MOBILE_THEME_ID = MOBILE_DEFAULT_THEME_ID; |
| 17 | 17 | export const MOBILE_THEME_IDS = SHARED_MOBILE_THEME_IDS; |
| ... | ... | @@ -28,7 +28,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{ |
| 28 | 28 | ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })), |
| 29 | 29 | ]; |
| 30 | 30 | |
| 31 | type MobileThemeVariable = `--color-${string}`; | |
| 31 | export type MobileThemeVariable = `--color-${string}`; | |
| 32 | 32 | export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>; |
| 33 | 33 | |
| 34 | 34 | export function normalizeMobileThemeId(value: unknown): MobileThemeId { |
| ... | ... | @@ -282,18 +282,18 @@ export function createMobileThemeVariables( |
| 282 | 282 | }; |
| 283 | 283 | } |
| 284 | 284 | |
| 285 | export const MOBILE_THEME_VARIABLE_NAMES = Object.keys( | |
| 286 | createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light"), | |
| 287 | ) as ReadonlyArray<MobileThemeVariable>; | |
| 288 | ||
| 285 | 289 | export function getMobileThemeVariables( |
| 286 | themeId: MobileThemeId, | |
| 290 | themeId: BuiltInThemeId, | |
| 287 | 291 | appearance: MobileThemeAppearance, |
| 288 | 292 | overrides: Partial<MobileThemeVariables> | null = null, |
| 289 | 293 | ): MobileThemeVariables { |
| 290 | const baseVariables = (() => { | |
| 291 | if (themeId === DEFAULT_MOBILE_THEME_ID) return DEFAULT_MOBILE_THEME_VARIABLES[appearance]; | |
| 292 | const theme = | |
| 293 | BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0]; | |
| 294 | const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors; | |
| 295 | return createMobileThemeVariables(colors, appearance); | |
| 296 | })(); | |
| 294 | const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0]; | |
| 295 | const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors; | |
| 296 | const baseVariables = createMobileThemeVariables(colors, appearance); | |
| 297 | 297 | |
| 298 | 298 | // The complete base record guarantees that optional overrides cannot leave a token undefined. |
| 299 | 299 | return overrides ? ({ ...baseVariables, ...overrides } as MobileThemeVariables) : baseVariables; |
apps/mobile/src/lib/mobileThemeRuntime.test.ts created+82| ... | ... | @@ -0,0 +1,82 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { | |
| 4 | createMobileThemeRuntimeOperations, | |
| 5 | getMobileUniwindThemeName, | |
| 6 | type MobileThemeRuntimeState, | |
| 7 | } from "./mobileThemeRuntime"; | |
| 8 | ||
| 9 | const initialState: MobileThemeRuntimeState = { | |
| 10 | baseFontSize: 16, | |
| 11 | themeAppearance: "light", | |
| 12 | themeMode: "system", | |
| 13 | }; | |
| 14 | ||
| 15 | describe("mobileThemeRuntime", () => { | |
| 16 | it("keeps the default palette on Uniwind's built-in appearance themes", () => { | |
| 17 | expect(getMobileUniwindThemeName("t3-code", "light")).toBe("light"); | |
| 18 | expect(getMobileUniwindThemeName("t3-code", "dark")).toBe("dark"); | |
| 19 | }); | |
| 20 | ||
| 21 | it("maps custom palettes and appearances to registered themes", () => { | |
| 22 | expect(getMobileUniwindThemeName("t3-chat", "dark")).toBe("t3-chat-dark"); | |
| 23 | }); | |
| 24 | ||
| 25 | it("hydrates text variables and clears the native appearance override", () => { | |
| 26 | const operations = createMobileThemeRuntimeOperations(null, initialState); | |
| 27 | const variableOperations = operations.filter( | |
| 28 | (operation) => operation.kind === "update-text-variables", | |
| 29 | ); | |
| 30 | ||
| 31 | expect(variableOperations).toHaveLength(12); | |
| 32 | expect(variableOperations.at(-1)?.themeName).toBe("iris-dark"); | |
| 33 | expect(operations.at(-1)).toEqual({ | |
| 34 | kind: "set-appearance-mode", | |
| 35 | appearance: "light", | |
| 36 | themeMode: "system", | |
| 37 | }); | |
| 38 | }); | |
| 39 | ||
| 40 | it("lets system appearance changes flow through the root ScopedTheme only", () => { | |
| 41 | const operations = createMobileThemeRuntimeOperations(initialState, { | |
| 42 | ...initialState, | |
| 43 | themeAppearance: "dark", | |
| 44 | }); | |
| 45 | ||
| 46 | expect(operations).toEqual([]); | |
| 47 | }); | |
| 48 | ||
| 49 | it("updates native appearance once when the selected mode changes", () => { | |
| 50 | const operations = createMobileThemeRuntimeOperations(initialState, { | |
| 51 | ...initialState, | |
| 52 | themeAppearance: "dark", | |
| 53 | themeMode: "dark", | |
| 54 | }); | |
| 55 | ||
| 56 | expect(operations).toEqual([ | |
| 57 | { | |
| 58 | kind: "set-appearance-mode", | |
| 59 | appearance: "dark", | |
| 60 | themeMode: "dark", | |
| 61 | }, | |
| 62 | ]); | |
| 63 | }); | |
| 64 | ||
| 65 | it("updates text variables for every theme without switching palettes", () => { | |
| 66 | const operations = createMobileThemeRuntimeOperations(initialState, { | |
| 67 | ...initialState, | |
| 68 | baseFontSize: 18, | |
| 69 | }); | |
| 70 | ||
| 71 | expect(operations).toHaveLength(12); | |
| 72 | expect(operations.every((operation) => operation.kind === "update-text-variables")).toBe(true); | |
| 73 | expect(operations.at(-1)).toMatchObject({ | |
| 74 | kind: "update-text-variables", | |
| 75 | themeName: "iris-dark", | |
| 76 | }); | |
| 77 | }); | |
| 78 | ||
| 79 | it("does no native work when persistence echoes an already-applied state", () => { | |
| 80 | expect(createMobileThemeRuntimeOperations(initialState, initialState)).toEqual([]); | |
| 81 | }); | |
| 82 | }); |
apps/mobile/src/lib/mobileThemeRuntime.ts created+75| ... | ... | @@ -0,0 +1,75 @@ |
| 1 | import { resolveTextScaleVariables } from "./appearancePreferences"; | |
| 2 | import { BUILT_IN_THEME_IDS, type BuiltInThemeId } from "@t3tools/shared/themePalettes"; | |
| 3 | import { | |
| 4 | DEFAULT_MOBILE_THEME_ID, | |
| 5 | type MobileThemeAppearance, | |
| 6 | type MobileThemeId, | |
| 7 | type MobileThemeMode, | |
| 8 | } from "./mobileTheme"; | |
| 9 | ||
| 10 | export type MobileUniwindThemeName = | |
| 11 | | MobileThemeAppearance | |
| 12 | | `${BuiltInThemeId}-${MobileThemeAppearance}`; | |
| 13 | ||
| 14 | export interface MobileThemeRuntimeState { | |
| 15 | readonly baseFontSize: number; | |
| 16 | readonly themeAppearance: MobileThemeAppearance; | |
| 17 | readonly themeMode: MobileThemeMode; | |
| 18 | } | |
| 19 | ||
| 20 | export type MobileThemeRuntimeOperation = | |
| 21 | | { | |
| 22 | readonly kind: "update-text-variables"; | |
| 23 | readonly themeName: "light" | "dark" | MobileUniwindThemeName; | |
| 24 | readonly variables: Readonly<Record<string, number>>; | |
| 25 | } | |
| 26 | | { | |
| 27 | readonly kind: "set-appearance-mode"; | |
| 28 | readonly appearance: MobileThemeAppearance; | |
| 29 | readonly themeMode: MobileThemeMode; | |
| 30 | }; | |
| 31 | ||
| 32 | const UNIWIND_THEME_NAMES: ReadonlyArray<"light" | "dark" | MobileUniwindThemeName> = [ | |
| 33 | "light", | |
| 34 | "dark", | |
| 35 | ...BUILT_IN_THEME_IDS.flatMap((themeId) => [ | |
| 36 | `${themeId}-light` as const, | |
| 37 | `${themeId}-dark` as const, | |
| 38 | ]), | |
| 39 | ]; | |
| 40 | ||
| 41 | export function getMobileUniwindThemeName( | |
| 42 | themeId: MobileThemeId, | |
| 43 | appearance: MobileThemeAppearance, | |
| 44 | ): MobileUniwindThemeName { | |
| 45 | return themeId === DEFAULT_MOBILE_THEME_ID ? appearance : `${themeId}-${appearance}`; | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Plans imperative runtime work separately from theme selection. Palette | |
| 50 | * changes are handled by one root ScopedTheme render; only typography and the | |
| 51 | * native appearance override need imperative Uniwind/React Native updates. | |
| 52 | */ | |
| 53 | export function createMobileThemeRuntimeOperations( | |
| 54 | previous: MobileThemeRuntimeState | null, | |
| 55 | next: MobileThemeRuntimeState, | |
| 56 | ): ReadonlyArray<MobileThemeRuntimeOperation> { | |
| 57 | const operations: MobileThemeRuntimeOperation[] = []; | |
| 58 | ||
| 59 | if (previous === null || previous.baseFontSize !== next.baseFontSize) { | |
| 60 | const variables = resolveTextScaleVariables(next.baseFontSize); | |
| 61 | for (const themeName of UNIWIND_THEME_NAMES) { | |
| 62 | operations.push({ kind: "update-text-variables", themeName, variables }); | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | if (previous === null || previous.themeMode !== next.themeMode) { | |
| 67 | operations.push({ | |
| 68 | kind: "set-appearance-mode", | |
| 69 | appearance: next.themeAppearance, | |
| 70 | themeMode: next.themeMode, | |
| 71 | }); | |
| 72 | } | |
| 73 | ||
| 74 | return operations; | |
| 75 | } |
apps/mobile/src/lib/mobileThemeVariables.test.ts created+25| ... | ... | @@ -0,0 +1,25 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | |
| 2 | ||
| 3 | import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support"; | |
| 4 | import { getMobileThemeVariables } from "./mobileTheme"; | |
| 5 | import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables"; | |
| 6 | ||
| 7 | describe("mobile theme runtime variables", () => { | |
| 8 | it("derives the standard runtime palette from global.css", () => { | |
| 9 | expect(getMobileThemeRuntimeVariables("t3-code", "light")).toEqual( | |
| 10 | readDefaultMobileThemeVariables("light"), | |
| 11 | ); | |
| 12 | expect(getMobileThemeRuntimeVariables("t3-code", "dark")).toEqual( | |
| 13 | readDefaultMobileThemeVariables("dark"), | |
| 14 | ); | |
| 15 | }); | |
| 16 | ||
| 17 | it("uses the same shared palette source as generated custom themes", () => { | |
| 18 | expect(getMobileThemeRuntimeVariables("ocean", "light")).toEqual( | |
| 19 | getMobileThemeVariables("ocean", "light"), | |
| 20 | ); | |
| 21 | expect(getMobileThemeRuntimeVariables("iris", "dark")).toEqual( | |
| 22 | getMobileThemeVariables("iris", "dark"), | |
| 23 | ); | |
| 24 | }); | |
| 25 | }); |
apps/mobile/src/lib/mobileThemeVariables.ts created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | import defaultThemeVariables from "../../generated-uniwind-default-theme-variables.json"; | |
| 2 | ||
| 3 | import { | |
| 4 | DEFAULT_MOBILE_THEME_ID, | |
| 5 | getMobileThemeVariables, | |
| 6 | type MobileThemeAppearance, | |
| 7 | type MobileThemeId, | |
| 8 | type MobileThemeVariables, | |
| 9 | } from "./mobileTheme"; | |
| 10 | ||
| 11 | const defaults = defaultThemeVariables as Readonly< | |
| 12 | Record<MobileThemeAppearance, MobileThemeVariables> | |
| 13 | >; | |
| 14 | ||
| 15 | /** | |
| 16 | * Complete palette for native and third-party APIs that cannot consume a | |
| 17 | * Uniwind className. The standard palette is generated from global.css; custom | |
| 18 | * palettes share the same source that generates their registered CSS themes. | |
| 19 | */ | |
| 20 | export function getMobileThemeRuntimeVariables( | |
| 21 | themeId: MobileThemeId, | |
| 22 | appearance: MobileThemeAppearance, | |
| 23 | ): MobileThemeVariables { | |
| 24 | return themeId === DEFAULT_MOBILE_THEME_ID | |
| 25 | ? defaults[appearance] | |
| 26 | : getMobileThemeVariables(themeId, appearance); | |
| 27 | } |
apps/mobile/src/lib/storage.test.ts+6| ... | ... | @@ -196,6 +196,12 @@ describe("mobile connection storage", () => { |
| 196 | 196 | }); |
| 197 | 197 | }); |
| 198 | 198 | |
| 199 | it("drops the removed theme transition preference", async () => { | |
| 200 | mocks.setPreferencesJson(JSON.stringify({ themeTransition: "circle-bottom-left" }), 10); | |
| 201 | ||
| 202 | await expect(loadPreferences()).resolves.toEqual({}); | |
| 203 | }); | |
| 204 | ||
| 199 | 205 | it("falls back to secure storage when SQLite cannot save preferences", async () => { |
| 200 | 206 | mocks.setDatabaseFailures(true, true); |
| 201 | 207 | await expect(savePreferencesPatch({ baseFontSize: 19 })).resolves.toEqual({ baseFontSize: 19 }); |
apps/mobile/src/lib/useFontFamily.ts+6-8| ... | ... | @@ -1,15 +1,13 @@ |
| 1 | import { useCSSVariable } from "uniwind"; | |
| 2 | ||
| 3 | const FONT_FAMILY_VARIABLES = { | |
| 4 | regular: "--font-sans", | |
| 5 | medium: "--font-medium", | |
| 6 | bold: "--font-bold", | |
| 1 | const FONT_FAMILIES = { | |
| 2 | regular: "DMSans-Regular", | |
| 3 | medium: "DMSans-Medium", | |
| 4 | bold: "DMSans-Bold", | |
| 7 | 5 | } as const; |
| 8 | 6 | |
| 9 | 7 | /** |
| 10 | 8 | * Resolves a font family for APIs that require a style object or native prop. |
| 11 | 9 | * Prefer Uniwind font classes when the target component accepts `className`. |
| 12 | 10 | */ |
| 13 | export function useFontFamily(weight: keyof typeof FONT_FAMILY_VARIABLES): string { | |
| 14 | return useCSSVariable(FONT_FAMILY_VARIABLES[weight]) as string; | |
| 11 | export function useFontFamily(weight: keyof typeof FONT_FAMILIES): string { | |
| 12 | return FONT_FAMILIES[weight]; | |
| 15 | 13 | } |
apps/mobile/src/lib/useMobileNavigationTheme.ts+21-12| ... | ... | @@ -1,22 +1,31 @@ |
| 1 | 1 | import { DarkTheme, DefaultTheme, type Theme } from "@react-navigation/native"; |
| 2 | 2 | import { useMemo } from "react"; |
| 3 | 3 | |
| 4 | import type { MobileThemeAppearance } from "./mobileTheme"; | |
| 5 | import { useThemeColor } from "./useThemeColor"; | |
| 6 | ||
| 7 | export function useMobileNavigationTheme(appearance: MobileThemeAppearance): Theme { | |
| 8 | const primary = String(useThemeColor("--color-primary")); | |
| 9 | const background = String(useThemeColor("--color-screen")); | |
| 10 | const card = String(useThemeColor("--color-sheet-solid")); | |
| 11 | const text = String(useThemeColor("--color-foreground")); | |
| 12 | const border = String(useThemeColor("--color-header-border")); | |
| 13 | const notification = String(useThemeColor("--color-danger-foreground")); | |
| 4 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | |
| 5 | import { useUniwindTheme } from "./useUniwindTheme"; | |
| 14 | 6 | |
| 7 | /** | |
| 8 | * React Navigation requires a JS theme object. Derive it from the same palette | |
| 9 | * source as Uniwind instead of subscribing the app root to CSS variables. The | |
| 10 | * preferences provider applies the registered Uniwind theme first, then | |
| 11 | * publishes this matching navigation palette through React. | |
| 12 | */ | |
| 13 | export function useMobileNavigationTheme(): Theme { | |
| 14 | const { themeAppearance: appearance } = useAppearancePreferences(); | |
| 15 | const variables = useUniwindTheme(); | |
| 15 | 16 | return useMemo(() => { |
| 16 | 17 | const base = appearance === "dark" ? DarkTheme : DefaultTheme; |
| 17 | 18 | return { |
| 18 | 19 | ...base, |
| 19 | colors: { ...base.colors, primary, background, card, text, border, notification }, | |
| 20 | colors: { | |
| 21 | ...base.colors, | |
| 22 | primary: variables["--color-primary"], | |
| 23 | background: variables["--color-screen"], | |
| 24 | card: variables["--color-sheet-solid"], | |
| 25 | text: variables["--color-foreground"], | |
| 26 | border: variables["--color-header-border"], | |
| 27 | notification: variables["--color-danger-foreground"], | |
| 28 | }, | |
| 20 | 29 | }; |
| 21 | }, [appearance, background, border, card, notification, primary, text]); | |
| 30 | }, [appearance, variables]); | |
| 22 | 31 | } |
apps/mobile/src/lib/useThemeColor.ts deleted-12| ... | ... | @@ -1,12 +0,0 @@ |
| 1 | import type { ColorValue } from "react-native"; | |
| 2 | import { useCSSVariable } from "uniwind"; | |
| 3 | ||
| 4 | /** | |
| 5 | * Typed wrapper around `useCSSVariable` that returns a `ColorValue` for use | |
| 6 | * in React Native style props (backgroundColor, tintColor, etc.). | |
| 7 | * | |
| 8 | * Usage: `const color = useThemeColor("--color-icon");` | |
| 9 | */ | |
| 10 | export function useThemeColor(variable: `--color-${string}`): ColorValue { | |
| 11 | return useCSSVariable(variable) as string as ColorValue; | |
| 12 | } |
apps/mobile/src/lib/useUniwindTheme.ts created+21| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | import { useMemo } from "react"; | |
| 2 | ||
| 3 | import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; | |
| 4 | import type { MobileThemeVariables } from "./mobileTheme"; | |
| 5 | import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables"; | |
| 6 | ||
| 7 | /** | |
| 8 | * Complete JS palette for native and third-party APIs that cannot consume a | |
| 9 | * Uniwind className (React Navigation, native editors, Markdown, SVG gradients, | |
| 10 | * Reanimated worklets). Ordinary React Native rendering must use className. | |
| 11 | * | |
| 12 | * This bridge follows the same single React theme commit as the root | |
| 13 | * ScopedTheme instead of subscribing every consumer to CSS-variable updates. | |
| 14 | */ | |
| 15 | export function useUniwindTheme(): MobileThemeVariables { | |
| 16 | const { themeAppearance, themeId } = useAppearancePreferences(); | |
| 17 | return useMemo( | |
| 18 | () => getMobileThemeRuntimeVariables(themeId, themeAppearance), | |
| 19 | [themeAppearance, themeId], | |
| 20 | ); | |
| 21 | } |
apps/mobile/src/native/T3ComposerEditor.ios.tsx+11-19| ... | ... | @@ -14,7 +14,7 @@ import { Image, StyleSheet } from "react-native"; |
| 14 | 14 | |
| 15 | 15 | import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; |
| 16 | 16 | import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; |
| 17 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 17 | import { useUniwindTheme } from "../lib/useUniwindTheme"; | |
| 18 | 18 | import { useFontFamily } from "../lib/useFontFamily"; |
| 19 | 19 | import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole"; |
| 20 | 20 | import { |
| ... | ... | @@ -110,15 +110,7 @@ export function ComposerEditor({ |
| 110 | 110 | const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]); |
| 111 | 111 | const confirmedTokensRef = useRef(collectComposerInlineTokens(props.value)); |
| 112 | 112 | const bodyText = useScaledTextRole("body"); |
| 113 | const textColor = useThemeColor("--color-foreground"); | |
| 114 | const placeholderColor = useThemeColor("--color-placeholder"); | |
| 115 | const chipBackground = useThemeColor("--color-subtle"); | |
| 116 | const chipBorder = useThemeColor("--color-border"); | |
| 117 | const chipText = useThemeColor("--color-foreground"); | |
| 118 | const skillBackground = useThemeColor("--color-inline-skill-background"); | |
| 119 | const skillBorder = useThemeColor("--color-inline-skill-border"); | |
| 120 | const skillText = useThemeColor("--color-inline-skill-foreground"); | |
| 121 | const fileTint = useThemeColor("--color-icon-muted"); | |
| 113 | const theme = useUniwindTheme(); | |
| 122 | 114 | const fontFamily = useFontFamily("regular"); |
| 123 | 115 | |
| 124 | 116 | useImperativeHandle( |
| ... | ... | @@ -219,15 +211,15 @@ export function ComposerEditor({ |
| 219 | 211 | [], |
| 220 | 212 | ); |
| 221 | 213 | const themeJson = JSON.stringify({ |
| 222 | text: String(textColor), | |
| 223 | placeholder: String(placeholderColor), | |
| 224 | chipBackground: String(chipBackground), | |
| 225 | chipBorder: String(chipBorder), | |
| 226 | chipText: String(chipText), | |
| 227 | skillBackground: String(skillBackground), | |
| 228 | skillBorder: String(skillBorder), | |
| 229 | skillText: String(skillText), | |
| 230 | fileTint: String(fileTint), | |
| 214 | text: theme["--color-foreground"], | |
| 215 | placeholder: theme["--color-placeholder"], | |
| 216 | chipBackground: theme["--color-subtle"], | |
| 217 | chipBorder: theme["--color-border"], | |
| 218 | chipText: theme["--color-foreground"], | |
| 219 | skillBackground: theme["--color-inline-skill-background"], | |
| 220 | skillBorder: theme["--color-inline-skill-border"], | |
| 221 | skillText: theme["--color-inline-skill-foreground"], | |
| 222 | fileTint: theme["--color-icon-muted"], | |
| 231 | 223 | }); |
| 232 | 224 | const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {}; |
| 233 | 225 | return ( |
apps/mobile/src/native/T3ComposerEditor.native.tsx+11-19| ... | ... | @@ -18,7 +18,7 @@ import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; |
| 18 | 18 | import { MOBILE_TYPOGRAPHY } from "../lib/typography"; |
| 19 | 19 | import { useNativePaste } from "../lib/useNativePaste"; |
| 20 | 20 | import { useFontFamily } from "../lib/useFontFamily"; |
| 21 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 21 | import { useUniwindTheme } from "../lib/useUniwindTheme"; | |
| 22 | 22 | import { |
| 23 | 23 | acknowledgeComposerNativeEvent, |
| 24 | 24 | assumeComposerControlledState, |
| ... | ... | @@ -111,15 +111,7 @@ export function ComposerEditor({ |
| 111 | 111 | const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]); |
| 112 | 112 | const [initialConfirmedTokens] = useState(() => collectComposerInlineTokens(props.value)); |
| 113 | 113 | const confirmedTokensRef = useRef(initialConfirmedTokens); |
| 114 | const textColor = useThemeColor("--color-foreground"); | |
| 115 | const placeholderColor = useThemeColor("--color-placeholder"); | |
| 116 | const chipBackground = useThemeColor("--color-subtle"); | |
| 117 | const chipBorder = useThemeColor("--color-border"); | |
| 118 | const chipText = useThemeColor("--color-foreground"); | |
| 119 | const skillBackground = useThemeColor("--color-inline-skill-background"); | |
| 120 | const skillBorder = useThemeColor("--color-inline-skill-border"); | |
| 121 | const skillText = useThemeColor("--color-inline-skill-foreground"); | |
| 122 | const fileTint = useThemeColor("--color-icon-muted"); | |
| 114 | const theme = useUniwindTheme(); | |
| 123 | 115 | const handlePaste = useNativePaste((uris) => onPasteImages?.(uris)); |
| 124 | 116 | |
| 125 | 117 | useImperativeHandle( |
| ... | ... | @@ -220,15 +212,15 @@ export function ComposerEditor({ |
| 220 | 212 | [], |
| 221 | 213 | ); |
| 222 | 214 | const themeJson = JSON.stringify({ |
| 223 | text: String(textColor), | |
| 224 | placeholder: String(placeholderColor), | |
| 225 | chipBackground: String(chipBackground), | |
| 226 | chipBorder: String(chipBorder), | |
| 227 | chipText: String(chipText), | |
| 228 | skillBackground: String(skillBackground), | |
| 229 | skillBorder: String(skillBorder), | |
| 230 | skillText: String(skillText), | |
| 231 | fileTint: String(fileTint), | |
| 215 | text: theme["--color-foreground"], | |
| 216 | placeholder: theme["--color-placeholder"], | |
| 217 | chipBackground: theme["--color-subtle"], | |
| 218 | chipBorder: theme["--color-border"], | |
| 219 | chipText: theme["--color-foreground"], | |
| 220 | skillBackground: theme["--color-inline-skill-background"], | |
| 221 | skillBorder: theme["--color-inline-skill-border"], | |
| 222 | skillText: theme["--color-inline-skill-foreground"], | |
| 223 | fileTint: theme["--color-icon-muted"], | |
| 232 | 224 | }); |
| 233 | 225 | const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {}; |
| 234 | 226 | const regularFontFamily = useFontFamily("regular"); |
apps/mobile/src/native/T3ComposerEditor.tsx+2-5| ... | ... | @@ -2,7 +2,6 @@ import { TextInputWrapper } from "expo-paste-input"; |
| 2 | 2 | import { useImperativeHandle, useRef } from "react"; |
| 3 | 3 | import { TextInput, type TextInput as RNTextInput } from "react-native"; |
| 4 | 4 | |
| 5 | import { useThemeColor } from "../lib/useThemeColor"; | |
| 6 | 5 | import { useFontFamily } from "../lib/useFontFamily"; |
| 7 | 6 | import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole"; |
| 8 | 7 | import { useNativePaste } from "../lib/useNativePaste"; |
| ... | ... | @@ -21,8 +20,6 @@ export function ComposerEditor({ |
| 21 | 20 | }: ComposerEditorProps) { |
| 22 | 21 | const inputRef = useRef<RNTextInput>(null); |
| 23 | 22 | const bodyText = useScaledTextRole("body"); |
| 24 | const foregroundColor = useThemeColor("--color-foreground"); | |
| 25 | const placeholderColor = useThemeColor("--color-placeholder"); | |
| 26 | 23 | const fontFamily = useFontFamily("regular"); |
| 27 | 24 | const handlePaste = useNativePaste((uris) => onPasteImages?.(uris)); |
| 28 | 25 | |
| ... | ... | @@ -45,12 +42,12 @@ export function ComposerEditor({ |
| 45 | 42 | selection={selection} |
| 46 | 43 | onSelectionChange={(event) => props.onSelectionChange?.(event.nativeEvent.selection)} |
| 47 | 44 | multiline={props.multiline ?? true} |
| 48 | placeholderTextColor={placeholderColor} | |
| 45 | placeholderTextColorClassName={"accent-placeholder"} | |
| 46 | className="text-foreground" | |
| 49 | 47 | style={[ |
| 50 | 48 | { |
| 51 | 49 | flex: 1, |
| 52 | 50 | minHeight: 0, |
| 53 | color: foregroundColor, | |
| 54 | 51 | fontFamily, |
| 55 | 52 | ...bodyText, |
| 56 | 53 | paddingVertical: contentInsetVertical, |
apps/mobile/src/state/thread-pr-presentation.ts+3-3| ... | ... | @@ -17,9 +17,9 @@ export interface ThreadPrPresentation { |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | 19 | const PR_STATE_TEXT_CLASS: Record<ThreadPr["state"], string> = { |
| 20 | open: "text-emerald-600 dark:text-emerald-400", | |
| 21 | merged: "text-violet-600 dark:text-violet-400", | |
| 22 | closed: "text-zinc-500 dark:text-zinc-400", | |
| 20 | open: "text-adaptive-emerald-600-400", | |
| 21 | merged: "text-adaptive-violet-600-400", | |
| 22 | closed: "text-adaptive-zinc-500-400", | |
| 23 | 23 | }; |
| 24 | 24 | |
| 25 | 25 | export function presentThreadPr( |
apps/mobile/src/state/use-thread-pr.test.ts+1-1| ... | ... | @@ -17,7 +17,7 @@ describe("presentThreadPr", () => { |
| 17 | 17 | expect(presentThreadPr(pullRequest, undefined)).toMatchObject({ |
| 18 | 18 | label: "3774", |
| 19 | 19 | accessibilityLabel: "#3774 pull request merged", |
| 20 | textClassName: "text-violet-600 dark:text-violet-400", | |
| 20 | textClassName: "text-adaptive-violet-600-400", | |
| 21 | 21 | }); |
| 22 | 22 | }); |
| 23 | 23 |
apps/mobile/uniwind-types.d.ts+1-1| ... | ... | @@ -3,7 +3,7 @@ |
| 3 | 3 | |
| 4 | 4 | declare module 'uniwind' { |
| 5 | 5 | export interface UniwindConfig { |
| 6 | themes: readonly ['light', 'dark'] | |
| 6 | themes: readonly ['light', 'dark', 't3-chat-light', 't3-chat-dark', 'grove-light', 'grove-dark', 'ocean-light', 'ocean-dark', 'ember-light', 'ember-dark', 'iris-light', 'iris-dark'] | |
| 7 | 7 | } |
| 8 | 8 | } |
| 9 | 9 |
docs/user/mobile-appearance.md+3| ... | ... | @@ -11,5 +11,8 @@ To change themes: |
| 11 | 11 | 3. Choose a theme. |
| 12 | 12 | 4. Select **System**, **Light**, or **Dark**. |
| 13 | 13 | |
| 14 | Tap a theme card to use it for both light and dark appearance. To mix themes, tap the light or dark | |
| 15 | preview circle inside a card to change only that appearance. | |
| 16 | ||
| 14 | 17 | **System** follows the device appearance automatically. Theme, text, code, and terminal appearance |
| 15 | 18 | preferences are stored on the device. |
oxlint-plugin-t3code/index.ts+2| ... | ... | @@ -4,6 +4,7 @@ import namespaceNodeImports from "./rules/namespace-node-imports.ts"; |
| 4 | 4 | import noGlobalProcessRuntime from "./rules/no-global-process-runtime.ts"; |
| 5 | 5 | import noInlineSchemaCompile from "./rules/no-inline-schema-compile.ts"; |
| 6 | 6 | import noManualEffectRuntimeInTests from "./rules/no-manual-effect-runtime-in-tests.ts"; |
| 7 | import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-escape-hatches.ts"; | |
| 7 | 8 | import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts"; |
| 8 | 9 | |
| 9 | 10 | export default definePlugin({ |
| ... | ... | @@ -15,6 +16,7 @@ export default definePlugin({ |
| 15 | 16 | "no-global-process-runtime": noGlobalProcessRuntime, |
| 16 | 17 | "no-inline-schema-compile": noInlineSchemaCompile, |
| 17 | 18 | "no-manual-effect-runtime-in-tests": noManualEffectRuntimeInTests, |
| 19 | "no-mobile-uniwind-theme-escape-hatches": noMobileUniwindThemeEscapeHatches, | |
| 18 | 20 | "no-native-title-tooltip": noNativeTitleTooltip, |
| 19 | 21 | }, |
| 20 | 22 | }); |
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.test.ts created+264| ... | ... | @@ -0,0 +1,264 @@ |
| 1 | import { assert, describe, expect } from "@effect/vitest"; | |
| 2 | ||
| 3 | import { createOxlintRuleHarness } from "../test/utils.ts"; | |
| 4 | ||
| 5 | const guardedMobileFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-theme-escape-hatches", { | |
| 6 | filename: "apps/mobile/src/features/settings/NewThemeSurface.tsx", | |
| 7 | }); | |
| 8 | const reviewedInteropFile = createOxlintRuleHarness( | |
| 9 | "t3code/no-mobile-uniwind-theme-escape-hatches", | |
| 10 | { filename: "apps/mobile/src/features/home/HomeHeader.tsx" }, | |
| 11 | ); | |
| 12 | const gitOverlayInteropFile = createOxlintRuleHarness( | |
| 13 | "t3code/no-mobile-uniwind-theme-escape-hatches", | |
| 14 | { filename: "apps/mobile/src/features/threads/GitActionProgressOverlay.tsx" }, | |
| 15 | ); | |
| 16 | const webFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-theme-escape-hatches", { | |
| 17 | filename: "apps/web/src/ThemeSurface.tsx", | |
| 18 | }); | |
| 19 | ||
| 20 | describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => { | |
| 21 | guardedMobileFile.valid( | |
| 22 | "allows semantic mobile theme classes", | |
| 23 | `const surface = <View className="bg-surface text-foreground" />;`, | |
| 24 | ); | |
| 25 | ||
| 26 | guardedMobileFile.valid( | |
| 27 | "allows unrelated functions with legacy hook names", | |
| 28 | ` | |
| 29 | const useCSSVariable = () => "local value"; | |
| 30 | const useThemeColor = () => "local color"; | |
| 31 | ||
| 32 | export const values = [useCSSVariable(), useThemeColor()]; | |
| 33 | `, | |
| 34 | ); | |
| 35 | ||
| 36 | guardedMobileFile.valid( | |
| 37 | "allows unrelated imports with legacy hook names", | |
| 38 | ` | |
| 39 | import { useThemeColor } from "./unrelated-library"; | |
| 40 | ||
| 41 | export const foreground = useThemeColor(); | |
| 42 | `, | |
| 43 | ); | |
| 44 | ||
| 45 | guardedMobileFile.valid( | |
| 46 | "allows erased type-only theme imports", | |
| 47 | ` | |
| 48 | import type { useCSSVariable } from "uniwind"; | |
| 49 | import { type useCSSVariable as CSSVariableHook } from "uniwind"; | |
| 50 | import type { useThemeColor } from "../../../hooks/useThemeColor"; | |
| 51 | import type { useUniwindTheme } from "../../../lib/useUniwindTheme"; | |
| 52 | ||
| 53 | export type ThemeHooks = [ | |
| 54 | typeof useCSSVariable, | |
| 55 | typeof CSSVariableHook, | |
| 56 | typeof useThemeColor, | |
| 57 | typeof useUniwindTheme, | |
| 58 | ]; | |
| 59 | `, | |
| 60 | ); | |
| 61 | ||
| 62 | guardedMobileFile.valid( | |
| 63 | "allows appearance words in ordinary copy", | |
| 64 | ` | |
| 65 | const message = "Set dark mode manually"; | |
| 66 | const detail = "For dark: high contrast mode is available"; | |
| 67 | const label = <Text accessibilityLabel="Light theme">Theme</Text>; | |
| 68 | `, | |
| 69 | ); | |
| 70 | ||
| 71 | guardedMobileFile.valid( | |
| 72 | "does not conflate shadowed Uniwind namespaces", | |
| 73 | ` | |
| 74 | import * as Uniwind from "uniwind"; | |
| 75 | ||
| 76 | export function readDirect(Uniwind: { useCSSVariable: () => string }) { | |
| 77 | return Uniwind.useCSSVariable(); | |
| 78 | } | |
| 79 | ||
| 80 | export function readDestructured(Uniwind: { useCSSVariable: () => string }) { | |
| 81 | const { useCSSVariable } = Uniwind; | |
| 82 | return useCSSVariable(); | |
| 83 | } | |
| 84 | `, | |
| 85 | ); | |
| 86 | ||
| 87 | reviewedInteropFile.valid( | |
| 88 | "allows reviewed native interop boundaries", | |
| 89 | ` | |
| 90 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 91 | ||
| 92 | export const foreground = useUniwindTheme().colors.foreground; | |
| 93 | `, | |
| 94 | ); | |
| 95 | ||
| 96 | gitOverlayInteropFile.valid( | |
| 97 | "allows the native liquid-glass theme boundary", | |
| 98 | ` | |
| 99 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | |
| 100 | ||
| 101 | export const tint = useUniwindTheme()["--color-glass-surface"]; | |
| 102 | `, | |
| 103 | ); | |
| 104 | ||
| 105 | webFile.valid( | |
| 106 | "does not impose the mobile custom-theme policy on web code", | |
| 107 | `const surface = <div className="bg-white dark:bg-black" />;`, | |
| 108 | ); | |
| 109 | ||
| 110 | guardedMobileFile.invalid( | |
| 111 | "reports new React theme subscriptions", | |
| 112 | ` | |
| 113 | import { useCSSVariable } from "uniwind"; | |
| 114 | ||
| 115 | export const foreground = useCSSVariable("--color-foreground"); | |
| 116 | `, | |
| 117 | (output) => { | |
| 118 | assert.match(output, /semantic className/); | |
| 119 | }, | |
| 120 | ); | |
| 121 | ||
| 122 | guardedMobileFile.invalid( | |
| 123 | "reports the retired theme color hook", | |
| 124 | ` | |
| 125 | import { useThemeColor } from "../../../hooks/useThemeColor"; | |
| 126 | ||
| 127 | export const foreground = useThemeColor({}, "foreground"); | |
| 128 | `, | |
| 129 | (output) => { | |
| 130 | assert.match(output, /replaced by semantic Uniwind classes/); | |
| 131 | }, | |
| 132 | ); | |
| 133 | ||
| 134 | guardedMobileFile.invalid( | |
| 135 | "reports guarded hooks imported with TypeScript extensions", | |
| 136 | ` | |
| 137 | import { useThemeColor } from "../../../hooks/useThemeColor.ts"; | |
| 138 | import { useUniwindTheme } from "../../../lib/useUniwindTheme.ts"; | |
| 139 | ||
| 140 | export const foreground = [ | |
| 141 | useThemeColor({}, "foreground"), | |
| 142 | useUniwindTheme().colors.foreground, | |
| 143 | ]; | |
| 144 | `, | |
| 145 | (output) => { | |
| 146 | assert.match(output, /replaced by semantic Uniwind classes/); | |
| 147 | assert.match(output, /native\/third-party interop boundary/); | |
| 148 | }, | |
| 149 | ); | |
| 150 | ||
| 151 | guardedMobileFile.invalid( | |
| 152 | "reports unreviewed native interop subscriptions", | |
| 153 | ` | |
| 154 | import { useUniwindTheme } from "../../../lib/useUniwindTheme"; | |
| 155 | ||
| 156 | export const foreground = useUniwindTheme().colors.foreground; | |
| 157 | `, | |
| 158 | (output) => { | |
| 159 | assert.match(output, /native\/third-party interop boundary/); | |
| 160 | }, | |
| 161 | ); | |
| 162 | ||
| 163 | guardedMobileFile.invalid( | |
| 164 | "reports appearance variants in string literals", | |
| 165 | `const surface = <View className="bg-white dark:bg-black" />;`, | |
| 166 | (output) => { | |
| 167 | assert.match(output, /registered custom themes/); | |
| 168 | }, | |
| 169 | ); | |
| 170 | ||
| 171 | guardedMobileFile.invalid( | |
| 172 | "reports appearance variants in template literals", | |
| 173 | "const className = `bg-black light:bg-white`;", | |
| 174 | ); | |
| 175 | ||
| 176 | guardedMobileFile.invalid( | |
| 177 | "reports escaped appearance variants in template literals", | |
| 178 | "const className = `dark\\u003abg-black`;", | |
| 179 | ); | |
| 180 | ||
| 181 | guardedMobileFile.invalid( | |
| 182 | "reports appearance variants through nested class-map indirection", | |
| 183 | ` | |
| 184 | const styles = { | |
| 185 | variants: { root: "bg-white dark:bg-black" }, | |
| 186 | }; | |
| 187 | ||
| 188 | const root = styles.variants.root; | |
| 189 | export const surface = <View className={root} />; | |
| 190 | `, | |
| 191 | (output) => { | |
| 192 | expect(output.match(/registered custom themes/g)).toHaveLength(1); | |
| 193 | }, | |
| 194 | ); | |
| 195 | ||
| 196 | guardedMobileFile.invalid( | |
| 197 | "reports appearance variants passed to class builders", | |
| 198 | `const surface = cn("bg-white", enabled && "dark:bg-black");`, | |
| 199 | ); | |
| 200 | ||
| 201 | guardedMobileFile.invalid( | |
| 202 | "reports negative and important appearance variants", | |
| 203 | `const className = "dark:-mt-2 light:!bg-white";`, | |
| 204 | ); | |
| 205 | ||
| 206 | guardedMobileFile.invalid( | |
| 207 | "reports namespace CSS variable subscriptions", | |
| 208 | ` | |
| 209 | import * as Uniwind from "uniwind"; | |
| 210 | ||
| 211 | export const foreground = Uniwind.useCSSVariable("--color-foreground"); | |
| 212 | `, | |
| 213 | ); | |
| 214 | ||
| 215 | guardedMobileFile.invalid( | |
| 216 | "reports destructured namespace CSS variable subscriptions", | |
| 217 | ` | |
| 218 | import * as Uniwind from "uniwind"; | |
| 219 | ||
| 220 | const { useCSSVariable: resolveVariable } = Uniwind; | |
| 221 | export const foreground = resolveVariable("--color-foreground"); | |
| 222 | `, | |
| 223 | ); | |
| 224 | ||
| 225 | guardedMobileFile.invalid( | |
| 226 | "reports aliased namespace CSS variable subscriptions", | |
| 227 | ` | |
| 228 | import * as Uniwind from "uniwind"; | |
| 229 | ||
| 230 | const Theme = Uniwind; | |
| 231 | const NestedTheme = Theme; | |
| 232 | export const foreground = NestedTheme.useCSSVariable("--color-foreground"); | |
| 233 | `, | |
| 234 | ); | |
| 235 | ||
| 236 | guardedMobileFile.invalid( | |
| 237 | "reports object-rest namespace CSS variable subscriptions", | |
| 238 | ` | |
| 239 | import * as Uniwind from "uniwind"; | |
| 240 | ||
| 241 | const { ...Theme } = Uniwind; | |
| 242 | export const foreground = Theme.useCSSVariable("--color-foreground"); | |
| 243 | `, | |
| 244 | ); | |
| 245 | ||
| 246 | guardedMobileFile.invalid( | |
| 247 | "reports namespace access to the retired theme hook", | |
| 248 | ` | |
| 249 | import * as ThemeColor from "../../../hooks/useThemeColor"; | |
| 250 | ||
| 251 | export const foreground = ThemeColor.useThemeColor({}, "foreground"); | |
| 252 | `, | |
| 253 | ); | |
| 254 | ||
| 255 | guardedMobileFile.invalid( | |
| 256 | "reports destructured namespace access to the retired theme hook", | |
| 257 | ` | |
| 258 | import * as ThemeColor from "../../../hooks/useThemeColor.ts"; | |
| 259 | ||
| 260 | const { useThemeColor: resolveThemeColor } = ThemeColor; | |
| 261 | export const foreground = resolveThemeColor({}, "foreground"); | |
| 262 | `, | |
| 263 | ); | |
| 264 | }); |
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.ts created+225| ... | ... | @@ -0,0 +1,225 @@ |
| 1 | import { defineRule, type Variable } from "@oxlint/plugins"; | |
| 2 | import * as Option from "effect/Option"; | |
| 3 | ||
| 4 | import { getPropertyName, unwrapExpression } from "../utils.ts"; | |
| 5 | ||
| 6 | const MOBILE_SOURCE_MARKER = "/apps/mobile/src/"; | |
| 7 | const APPEARANCE_VARIANT_PATTERN = /\b(?:dark|light):(?=\S)/u; | |
| 8 | const APPEARANCE_VARIANT_MESSAGE = | |
| 9 | "dark:/light: utilities do not follow registered custom themes; use an adaptive semantic token."; | |
| 10 | const THEME_INTEROP_ALLOWLIST = new Set([ | |
| 11 | "features/archive/ArchivedThreadsScreen.tsx", | |
| 12 | "features/connection/ConnectionsNewRouteScreen.tsx", | |
| 13 | "features/files/FileMarkdownPreview.tsx", | |
| 14 | "features/files/SourceFileSurface.tsx", | |
| 15 | "features/files/ThreadFilesRouteScreen.tsx", | |
| 16 | "features/files/thread-file-navigator-pane.tsx", | |
| 17 | "features/home/HomeHeader.tsx", | |
| 18 | "features/review/ReviewSheet.tsx", | |
| 19 | "features/review/useNativeReviewDiffBridge.ts", | |
| 20 | "features/settings/SettingsEnvironmentsRouteScreen.tsx", | |
| 21 | "features/settings/appearance/components/AppearancePreviews.tsx", | |
| 22 | "features/settings/appearance/components/FontSizeSliderRow.tsx", | |
| 23 | "features/threads/GitActionProgressOverlay.tsx", | |
| 24 | "features/threads/NewTaskContextPickerScreens.tsx", | |
| 25 | "features/threads/NewTaskDraftScreen.tsx", | |
| 26 | "features/threads/ThreadComposer.tsx", | |
| 27 | "features/threads/ThreadFeed.tsx", | |
| 28 | "features/threads/ThreadSettingsSheet.tsx", | |
| 29 | "features/threads/git/GitOverviewSheet.tsx", | |
| 30 | "features/threads/thread-list-items.tsx", | |
| 31 | "features/threads/thread-list-v2-items.tsx", | |
| 32 | "lib/useMobileNavigationTheme.ts", | |
| 33 | "native/T3ComposerEditor.ios.tsx", | |
| 34 | "native/T3ComposerEditor.native.tsx", | |
| 35 | ]); | |
| 36 | ||
| 37 | const mobileSourcePath = (filename: string): string | undefined => { | |
| 38 | const normalized = `/${filename.replaceAll("\\", "/")}`; | |
| 39 | const markerIndex = normalized.lastIndexOf(MOBILE_SOURCE_MARKER); | |
| 40 | return markerIndex === -1 | |
| 41 | ? undefined | |
| 42 | : normalized.slice(markerIndex + MOBILE_SOURCE_MARKER.length); | |
| 43 | }; | |
| 44 | ||
| 45 | const literalStringValue = (node: unknown): Option.Option<string> => { | |
| 46 | if (typeof node !== "object" || node === null) return Option.none(); | |
| 47 | if (!("type" in node) || node.type !== "Literal") return Option.none(); | |
| 48 | if (!("value" in node) || typeof node.value !== "string") return Option.none(); | |
| 49 | return Option.some(node.value); | |
| 50 | }; | |
| 51 | ||
| 52 | const reportsAppearanceVariant = (value: string) => APPEARANCE_VARIANT_PATTERN.test(value); | |
| 53 | ||
| 54 | const importsModule = (source: string, modulePath: string): boolean => | |
| 55 | source.replace(/\.[cm]?[jt]sx?$/u, "").endsWith(modulePath); | |
| 56 | ||
| 57 | export default defineRule({ | |
| 58 | meta: { | |
| 59 | type: "problem", | |
| 60 | docs: { | |
| 61 | description: | |
| 62 | "Keep mobile theme styling on semantic Uniwind classes and reviewed native interop boundaries.", | |
| 63 | }, | |
| 64 | }, | |
| 65 | create(context) { | |
| 66 | const sourcePath = mobileSourcePath(context.filename); | |
| 67 | if (sourcePath === undefined) return {}; | |
| 68 | ||
| 69 | const uniwindNamespaces = new Set<Variable>(); | |
| 70 | ||
| 71 | const resolveVariable = (node: unknown): Variable | undefined => { | |
| 72 | const identifier = unwrapExpression(node); | |
| 73 | if (Option.isNone(identifier) || identifier.value.type !== "Identifier") return undefined; | |
| 74 | ||
| 75 | let scope = context.sourceCode.getScope(identifier.value); | |
| 76 | while (true) { | |
| 77 | const variable = scope.set.get(identifier.value.name); | |
| 78 | if (variable !== undefined || scope.upper === null) return variable; | |
| 79 | scope = scope.upper; | |
| 80 | } | |
| 81 | }; | |
| 82 | ||
| 83 | return { | |
| 84 | ImportDeclaration(node) { | |
| 85 | const source = literalStringValue(node.source); | |
| 86 | if (Option.isNone(source)) return; | |
| 87 | const declaredVariables = context.sourceCode.getDeclaredVariables(node); | |
| 88 | ||
| 89 | for (const specifier of node.specifiers) { | |
| 90 | const local = unwrapExpression(specifier.local); | |
| 91 | const importedName = | |
| 92 | specifier.type === "ImportSpecifier" | |
| 93 | ? getPropertyName(specifier.imported) | |
| 94 | : Option.none(); | |
| 95 | const isTypeOnly = | |
| 96 | node.importKind === "type" || | |
| 97 | (specifier.type === "ImportSpecifier" && specifier.importKind === "type"); | |
| 98 | ||
| 99 | if ( | |
| 100 | !isTypeOnly && | |
| 101 | specifier.type === "ImportNamespaceSpecifier" && | |
| 102 | Option.isSome(local) && | |
| 103 | local.value.type === "Identifier" | |
| 104 | ) { | |
| 105 | const localName = local.value.name; | |
| 106 | const variable = declaredVariables.find((candidate) => candidate.name === localName); | |
| 107 | if (source.value === "uniwind" && variable !== undefined) { | |
| 108 | uniwindNamespaces.add(variable); | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | if ( | |
| 113 | !isTypeOnly && | |
| 114 | source.value === "uniwind" && | |
| 115 | Option.isSome(importedName) && | |
| 116 | importedName.value === "useCSSVariable" | |
| 117 | ) { | |
| 118 | context.report({ | |
| 119 | node: specifier, | |
| 120 | message: | |
| 121 | "Use a semantic className instead of useCSSVariable; it adds a React theme subscription.", | |
| 122 | }); | |
| 123 | } | |
| 124 | ||
| 125 | if (!isTypeOnly && importsModule(source.value, "/useThemeColor")) { | |
| 126 | context.report({ | |
| 127 | node: specifier, | |
| 128 | message: "useThemeColor was replaced by semantic Uniwind classes.", | |
| 129 | }); | |
| 130 | } | |
| 131 | ||
| 132 | if ( | |
| 133 | !isTypeOnly && | |
| 134 | importsModule(source.value, "/useUniwindTheme") && | |
| 135 | !THEME_INTEROP_ALLOWLIST.has(sourcePath) | |
| 136 | ) { | |
| 137 | context.report({ | |
| 138 | node: specifier, | |
| 139 | message: | |
| 140 | "Use className for theme styling, or review and add this native/third-party interop boundary to the lint allowlist.", | |
| 141 | }); | |
| 142 | } | |
| 143 | } | |
| 144 | }, | |
| 145 | MemberExpression(node) { | |
| 146 | const object = unwrapExpression(node.object); | |
| 147 | if (Option.isNone(object) || object.value.type !== "Identifier") return; | |
| 148 | ||
| 149 | const property = getPropertyName(node.property); | |
| 150 | if (Option.isNone(property)) return; | |
| 151 | ||
| 152 | const namespace = resolveVariable(object.value); | |
| 153 | if ( | |
| 154 | namespace !== undefined && | |
| 155 | uniwindNamespaces.has(namespace) && | |
| 156 | property.value === "useCSSVariable" | |
| 157 | ) { | |
| 158 | context.report({ | |
| 159 | node, | |
| 160 | message: | |
| 161 | "Use a semantic className instead of useCSSVariable; it adds a React theme subscription.", | |
| 162 | }); | |
| 163 | } | |
| 164 | }, | |
| 165 | VariableDeclarator(node) { | |
| 166 | const initializer = unwrapExpression(node.init); | |
| 167 | const binding = unwrapExpression(node.id); | |
| 168 | if ( | |
| 169 | Option.isNone(initializer) || | |
| 170 | initializer.value.type !== "Identifier" || | |
| 171 | Option.isNone(binding) | |
| 172 | ) { | |
| 173 | return; | |
| 174 | } | |
| 175 | ||
| 176 | const namespace = resolveVariable(initializer.value); | |
| 177 | if (namespace === undefined || !uniwindNamespaces.has(namespace)) return; | |
| 178 | ||
| 179 | if (binding.value.type === "Identifier") { | |
| 180 | const bindingName = binding.value.name; | |
| 181 | const variable = context.sourceCode | |
| 182 | .getDeclaredVariables(node) | |
| 183 | .find((candidate) => candidate.name === bindingName); | |
| 184 | if (variable !== undefined) uniwindNamespaces.add(variable); | |
| 185 | return; | |
| 186 | } | |
| 187 | ||
| 188 | if (binding.value.type !== "ObjectPattern") return; | |
| 189 | ||
| 190 | const declaredVariables = context.sourceCode.getDeclaredVariables(node); | |
| 191 | for (const propertyNode of binding.value.properties) { | |
| 192 | if (propertyNode.type === "RestElement") { | |
| 193 | const restBinding = unwrapExpression(propertyNode.argument); | |
| 194 | if (Option.isNone(restBinding) || restBinding.value.type !== "Identifier") continue; | |
| 195 | ||
| 196 | const restName = restBinding.value.name; | |
| 197 | const variable = declaredVariables.find((candidate) => candidate.name === restName); | |
| 198 | if (variable !== undefined) uniwindNamespaces.add(variable); | |
| 199 | continue; | |
| 200 | } | |
| 201 | ||
| 202 | if (propertyNode.type !== "Property") continue; | |
| 203 | const property = getPropertyName(propertyNode.key); | |
| 204 | if (Option.isNone(property)) continue; | |
| 205 | ||
| 206 | if (property.value === "useCSSVariable") { | |
| 207 | context.report({ | |
| 208 | node: propertyNode, | |
| 209 | message: | |
| 210 | "Use a semantic className instead of useCSSVariable; it adds a React theme subscription.", | |
| 211 | }); | |
| 212 | } | |
| 213 | } | |
| 214 | }, | |
| 215 | Literal(node) { | |
| 216 | if (typeof node.value !== "string" || !reportsAppearanceVariant(node.value)) return; | |
| 217 | context.report({ node, message: APPEARANCE_VARIANT_MESSAGE }); | |
| 218 | }, | |
| 219 | TemplateElement(node) { | |
| 220 | if (!reportsAppearanceVariant(node.value.cooked ?? "")) return; | |
| 221 | context.report({ node, message: APPEARANCE_VARIANT_MESSAGE }); | |
| 222 | }, | |
| 223 | }; | |
| 224 | }, | |
| 225 | }); |
oxlint-plugin-t3code/test/utils.ts+1| ... | ... | @@ -114,6 +114,7 @@ export const createOxlintRuleHarness = ( |
| 114 | 114 | rules: { [ruleName]: "error" }, |
| 115 | 115 | }), |
| 116 | 116 | ); |
| 117 | yield* fs.makeDirectory(path.dirname(sourcePath), { recursive: true }); | |
| 117 | 118 | yield* fs.writeFileString(sourcePath, source); |
| 118 | 119 | |
| 119 | 120 | // Run through the current Node binary: oxlint's bin is an extensionless |
vite.config.ts+1| ... | ... | @@ -120,6 +120,7 @@ export default defineConfig({ |
| 120 | 120 | "t3code/no-global-process-runtime": "error", |
| 121 | 121 | "t3code/no-inline-schema-compile": "warn", |
| 122 | 122 | "t3code/no-manual-effect-runtime-in-tests": "error", |
| 123 | "t3code/no-mobile-uniwind-theme-escape-hatches": "error", | |
| 123 | 124 | "t3code/no-native-title-tooltip": "error", |
| 124 | 125 | "t3code/namespace-node-imports": "error", |
| 125 | 126 | }, |