Redesign study navigation and light theme

This commit is contained in:
hz
2026-07-16 02:37:37 -05:00
parent 0eca0c4cbf
commit 6d1787b3eb
57 changed files with 2629 additions and 301 deletions
+43
View File
@@ -0,0 +1,43 @@
ISC License
Copyright (c) 2026 Lucide Icons and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
The following Lucide icons are derived from the Feather project:
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
The MIT License (MIT) (for the icons listed above)
Copyright (c) 2013-present Cole Bemis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M17 3a2 2 0 0 1 2 2v15a1 1 0 0 1-1.496.868l-4.512-2.578a2 2 0 0 0-1.984 0l-4.512 2.578A1 1 0 0 1 5 20V5a2 2 0 0 1 2-2z" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://jb8wvibepxbq"
path="res://.godot/imported/bookmark.svg-23c935a718ba8d7df3467ff01da01a70.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/bookmark.svg"
dest_files=["res://.godot/imported/bookmark.svg-23c935a718ba8d7df3467ff01da01a70.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+22
View File
@@ -0,0 +1,22 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 2v4" />
<path d="M16 2v4" />
<rect width="18" height="18" x="3" y="4" rx="2" />
<path d="M3 10h18" />
<path d="M8 14h.01" />
<path d="M12 14h.01" />
<path d="M16 14h.01" />
<path d="M8 18h.01" />
<path d="M12 18h.01" />
<path d="M16 18h.01" />
</svg>

After

Width:  |  Height:  |  Size: 479 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnkicokle4vvk"
path="res://.godot/imported/calendar-days.svg-fc1ccf5488eab33e4f820fc86635e906.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/calendar-days.svg"
dest_files=["res://.godot/imported/calendar-days.svg-fc1ccf5488eab33e4f820fc86635e906.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 21v-6" />
<path d="M12 21V9" />
<path d="M19 21V3" />
</svg>

After

Width:  |  Height:  |  Size: 275 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bs2lhser6788y"
path="res://.godot/imported/chart-no-axes-column-increasing.svg-c1dd27f4f00d377ce4f24887f65196ca.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/chart-no-axes-column-increasing.svg"
dest_files=["res://.godot/imported/chart-no-axes-column-increasing.svg-c1dd27f4f00d377ce4f24887f65196ca.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>

After

Width:  |  Height:  |  Size: 231 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bn80rqmembejx"
path="res://.godot/imported/chevron-down.svg-3472a8e55f50eae61ed3358b7822d8c3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/chevron-down.svg"
dest_files=["res://.godot/imported/chevron-down.svg-3472a8e55f50eae61ed3358b7822d8c3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m9 18 6-6-6-6" />
</svg>

After

Width:  |  Height:  |  Size: 232 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://chjh1wlw18igo"
path="res://.godot/imported/chevron-right.svg-121fa436a57e1bf3c9c04fb85209215b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/chevron-right.svg"
dest_files=["res://.godot/imported/chevron-right.svg-121fa436a57e1bf3c9c04fb85209215b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>

After

Width:  |  Height:  |  Size: 268 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cvsmkxhkcsrmg"
path="res://.godot/imported/circle-check.svg-55ba8c706e311cdaced45d8f0283ead8.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/circle-check.svg"
dest_files=["res://.godot/imported/circle-check.svg-55ba8c706e311cdaced45d8f0283ead8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+18
View File
@@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
<path d="M12 11h4" />
<path d="M12 16h4" />
<path d="M8 11h.01" />
<path d="M8 16h.01" />
</svg>

After

Width:  |  Height:  |  Size: 447 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c41sfij6qiosw"
path="res://.godot/imported/clipboard-list.svg-780e1d149463f9f1c164149bb46db603.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/clipboard-list.svg"
dest_files=["res://.godot/imported/clipboard-list.svg-780e1d149463f9f1c164149bb46db603.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
<path d="M9 14h6" />
<path d="M12 17v-6" />
</svg>

After

Width:  |  Height:  |  Size: 397 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://1vba78p3xy6i"
path="res://.godot/imported/clipboard-plus.svg-22177b9c8e20cca24c06a707c0a8d0ed.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/clipboard-plus.svg"
dest_files=["res://.godot/imported/clipboard-plus.svg-22177b9c8e20cca24c06a707c0a8d0ed.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 6v6h4" />
</svg>

After

Width:  |  Height:  |  Size: 264 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://debx4tb54iml7"
path="res://.godot/imported/clock-3.svg-419740b82791b92e94698aa6c6f5f206.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/clock-3.svg"
dest_files=["res://.godot/imported/clock-3.svg-419740b82791b92e94698aa6c6f5f206.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>

After

Width:  |  Height:  |  Size: 334 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ewbjovlxdk6p"
path="res://.godot/imported/copy.svg-705ce6a71ec742cb89963d05d17e314f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/copy.svg"
dest_files=["res://.godot/imported/copy.svg-705ce6a71ec742cb89963d05d17e314f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" />
<path d="M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>

After

Width:  |  Height:  |  Size: 378 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://crvanqj3vxyyh"
path="res://.godot/imported/house.svg-c7cc8e5ad8c838ca4f80ba2c6538e12c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/house.svg"
dest_files=["res://.godot/imported/house.svg-c7cc8e5ad8c838ca4f80ba2c6538e12c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+15
View File
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16 17 5-5-5-5" />
<path d="M21 12H9" />
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
</svg>

After

Width:  |  Height:  |  Size: 312 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dtx66v1mlcq72"
path="res://.godot/imported/log-out.svg-5456af329981366d758277cac5c6a6cf.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/log-out.svg"
dest_files=["res://.godot/imported/log-out.svg-5456af329981366d758277cac5c6a6cf.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+15
View File
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 5h16" />
<path d="M4 12h16" />
<path d="M4 19h16" />
</svg>

After

Width:  |  Height:  |  Size: 274 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://crlwiheaylkro"
path="res://.godot/imported/menu.svg-2d407463e922c766d1d23e550a48e7c9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/menu.svg"
dest_files=["res://.godot/imported/menu.svg-2d407463e922c766d1d23e550a48e7c9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
</svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://boelht3cjm4ct"
path="res://.godot/imported/message-square.svg-08be599df049bfc2ca25401b0ad231d9.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/message-square.svg"
dest_files=["res://.godot/imported/message-square.svg-08be599df049bfc2ca25401b0ad231d9.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+18
View File
@@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 18h8" />
<path d="M3 22h18" />
<path d="M14 22a7 7 0 1 0 0-14h-1" />
<path d="M9 14h2" />
<path d="M9 12a2 2 0 0 1-2-2V6h6v4a2 2 0 0 1-2 2Z" />
<path d="M12 6V3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3" />
</svg>

After

Width:  |  Height:  |  Size: 424 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c751inmuklo36"
path="res://.godot/imported/microscope.svg-07c50ddd3dfc15c0756f38d6846be599.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/microscope.svg"
dest_files=["res://.godot/imported/microscope.svg-07c50ddd3dfc15c0756f38d6846be599.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+18
View File
@@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4" />
<path d="M2 6h4" />
<path d="M2 10h4" />
<path d="M2 14h4" />
<path d="M2 18h4" />
<path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z" />
</svg>

After

Width:  |  Height:  |  Size: 514 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cg7ss4onl0psc"
path="res://.godot/imported/notebook-pen.svg-467c393728a74a4885f273f024d05131.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/notebook-pen.svg"
dest_files=["res://.godot/imported/notebook-pen.svg-467c393728a74a4885f273f024d05131.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
<path d="m15 5 4 4" />
</svg>

After

Width:  |  Height:  |  Size: 372 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b0x02i8r1tku4"
path="res://.godot/imported/pencil.svg-0be14ee799da796affa95f389ae5d3fa.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/pencil.svg"
dest_files=["res://.godot/imported/pencil.svg-0be14ee799da796affa95f389ae5d3fa.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
<circle cx="12" cy="12" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 581 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cotsliwyawd13"
path="res://.godot/imported/settings.svg-7b1171c3f95598408e2808479e323e40.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/settings.svg"
dest_files=["res://.godot/imported/settings.svg-7b1171c3f95598408e2808479e323e40.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+17
View File
@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 2v2" />
<path d="M5 2v2" />
<path d="M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1" />
<path d="M8 15a6 6 0 0 0 12 0v-3" />
<circle cx="20" cy="10" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dq020t6nh8ral"
path="res://.godot/imported/stethoscope.svg-578447d9fde1b7a6f0e65dba234334d6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/stethoscope.svg"
dest_files=["res://.godot/imported/stethoscope.svg-578447d9fde1b7a6f0e65dba234334d6.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="8" r="5" />
<path d="M20 21a8 8 0 0 0-16 0" />
</svg>

After

Width:  |  Height:  |  Size: 274 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dgll3wwewgnog"
path="res://.godot/imported/user-round.svg-e0cc9caba4fa37423521ea54b88e11f2.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/user-round.svg"
dest_files=["res://.godot/imported/user-round.svg-e0cc9caba4fa37423521ea54b88e11f2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>

After

Width:  |  Height:  |  Size: 255 B

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ciu3na0cyx5dj"
path="res://.godot/imported/x.svg-45a5daf8cc6179ab48cc314195731a4f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/icons/lucide/x.svg"
dest_files=["res://.godot/imported/x.svg-45a5daf8cc6179ab48cc314195731a4f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+118 -44
View File
@@ -8,6 +8,7 @@ var main: Control
var auth_base_url := AUTH_BASE
var background: ColorRect
var left_brand: VBoxContainer
var brand_tagline: RichTextLabel
var auth_panel: PanelContainer
var panel_content: VBoxContainer
var heading: Label
@@ -63,8 +64,9 @@ func _ready() -> void:
poll_timer.timeout.connect(_poll_steam)
add_child(poll_timer)
get_viewport().size_changed.connect(_update_responsive_layout)
resized.connect(_update_responsive_layout)
UIThemeService.theme_changed.connect(_on_theme_changed)
_update_responsive_layout()
_update_responsive_layout.call_deferred()
_set_mode("login")
_apply_theme()
@@ -87,6 +89,7 @@ func _build_ui() -> void:
add_child(background)
_add_dot_grid(Vector2(22, 24))
_add_dot_grid(Vector2(22, 840))
theme_button = Button.new()
theme_button.tooltip_text = "Switch color theme"
@@ -98,14 +101,14 @@ func _build_ui() -> void:
var page_margin := MarginContainer.new()
page_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
page_margin.add_theme_constant_override("margin_left", 42)
page_margin.add_theme_constant_override("margin_right", 42)
page_margin.add_theme_constant_override("margin_top", 38)
page_margin.add_theme_constant_override("margin_bottom", 38)
page_margin.add_theme_constant_override("margin_left", 52)
page_margin.add_theme_constant_override("margin_right", 52)
page_margin.add_theme_constant_override("margin_top", 56)
page_margin.add_theme_constant_override("margin_bottom", 56)
add_child(page_margin)
var columns := HBoxContainer.new()
columns.add_theme_constant_override("separation", 54)
columns.add_theme_constant_override("separation", 72)
page_margin.add_child(columns)
left_brand = VBoxContainer.new()
@@ -119,35 +122,50 @@ func _build_ui() -> void:
left_brand.add_child(brand_row)
var large_icon := TextureRect.new()
large_icon.texture = preload("res://assets/branding/medmind-icon.png")
large_icon.custom_minimum_size = Vector2(172, 172)
large_icon.custom_minimum_size = Vector2(190, 190)
large_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
large_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
large_icon.material = Brand.logo_cutout_material()
brand_row.add_child(large_icon)
var wordmark := Label.new()
wordmark.text = "MedMind"
wordmark.add_theme_font_size_override("font_size", 76)
wordmark.add_theme_font_override("font", Brand.font)
var wordmark := HBoxContainer.new()
wordmark.add_theme_constant_override("separation", 0)
brand_row.add_child(wordmark)
var tagline := Label.new()
tagline.text = "Think like a physician."
tagline.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
tagline.add_theme_font_size_override("font_size", 22)
tagline.add_theme_font_override("font", Brand.font)
left_brand.add_child(tagline)
var med_wordmark := Label.new()
med_wordmark.text = "Med"
med_wordmark.add_theme_font_size_override("font_size", 76)
med_wordmark.add_theme_font_override("font", Brand.font)
wordmark.add_child(med_wordmark)
var mind_wordmark := Label.new()
mind_wordmark.text = "Mind"
mind_wordmark.add_theme_font_size_override("font_size", 76)
mind_wordmark.add_theme_font_override("font", Brand.font)
mind_wordmark.add_theme_color_override("font_color", Brand.PURPLE)
mind_wordmark.set_meta("brand_accent", true)
wordmark.add_child(mind_wordmark)
brand_tagline = RichTextLabel.new()
brand_tagline.bbcode_enabled = true
brand_tagline.fit_content = true
brand_tagline.scroll_active = false
brand_tagline.text = "Focused practice. Better decisions.\n[color=#2563eb]Step 3[/color] success."
brand_tagline.add_theme_font_override("normal_font", Brand.font)
brand_tagline.add_theme_font_size_override("normal_font_size", 20)
brand_tagline.custom_minimum_size = Vector2(520, 64)
brand_tagline.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
left_brand.add_child(brand_tagline)
var panel_center := CenterContainer.new()
panel_center.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel_center.size_flags_vertical = Control.SIZE_EXPAND_FILL
columns.add_child(panel_center)
auth_panel = PanelContainer.new()
auth_panel.custom_minimum_size = Vector2(610, 760)
auth_panel.custom_minimum_size = Vector2(710, 820)
panel_center.add_child(auth_panel)
var scroll := ScrollContainer.new()
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
auth_panel.add_child(scroll)
panel_content = VBoxContainer.new()
panel_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel_content.add_theme_constant_override("separation", 16)
panel_content.add_theme_constant_override("separation", 24)
scroll.add_child(panel_content)
var panel_header := HBoxContainer.new()
@@ -155,19 +173,20 @@ func _build_ui() -> void:
panel_content.add_child(panel_header)
var small_icon := TextureRect.new()
small_icon.texture = preload("res://assets/branding/medmind-icon.png")
small_icon.custom_minimum_size = Vector2(82, 82)
small_icon.custom_minimum_size = Vector2(72, 72)
small_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
small_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
small_icon.material = Brand.logo_cutout_material()
panel_header.add_child(small_icon)
var titles := VBoxContainer.new()
titles.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel_header.add_child(titles)
heading = Label.new()
heading.add_theme_font_size_override("font_size", 34)
heading.add_theme_font_size_override("font_size", 32)
heading.add_theme_font_override("font", Brand.font)
titles.add_child(heading)
subheading = Label.new()
subheading.add_theme_font_size_override("font_size", 18)
subheading.add_theme_font_size_override("font_size", 20)
subheading.add_theme_font_override("font", Brand.font)
titles.add_child(subheading)
@@ -190,7 +209,7 @@ func _build_login_view() -> VBoxContainer:
var view := _new_form()
login_input = _add_field(view, "Email or Username", "Enter your email or username")
password_input = _add_field(view, "Password", "Enter your password", true)
_add_show_password(view, [password_input])
_add_password_reveal(password_input)
var forgot := Button.new()
forgot.text = "Forgot password?"
forgot.flat = true
@@ -199,13 +218,10 @@ func _build_login_view() -> VBoxContainer:
view.add_child(forgot)
var sign_in := _add_primary_button(view, "Sign In", _on_login_pressed)
password_input.text_submitted.connect(func(_text: String): _on_login_pressed())
var divider := Label.new()
divider.text = "or"
divider.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
view.add_child(divider)
view.add_child(_build_divider())
var steam := Button.new()
steam.text = "Sign in with Steam"
steam.custom_minimum_size = Vector2(0, 58)
steam.custom_minimum_size = Vector2(0, 68)
steam.pressed.connect(_start_steam)
view.add_child(steam)
submit_buttons.append(steam)
@@ -270,10 +286,26 @@ func _build_steam_view() -> VBoxContainer:
func _new_form() -> VBoxContainer:
var form := VBoxContainer.new()
form.add_theme_constant_override("separation", 12)
form.add_theme_constant_override("separation", 16)
return form
func _build_divider() -> HBoxContainer:
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 22)
for index in range(3):
if index == 1:
var label := Label.new()
label.text = "or"
label.add_theme_font_override("font", Brand.font)
row.add_child(label)
else:
var line := HSeparator.new()
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(line)
return row
func _add_field(parent: VBoxContainer, label_text: String, placeholder: String, secret := false) -> LineEdit:
var label := Label.new()
label.text = label_text
@@ -283,8 +315,8 @@ func _add_field(parent: VBoxContainer, label_text: String, placeholder: String,
var input := LineEdit.new()
input.placeholder_text = placeholder
input.secret = secret
input.custom_minimum_size = Vector2(0, 54)
input.add_theme_font_size_override("font_size", 16)
input.custom_minimum_size = Vector2(0, 68)
input.add_theme_font_size_override("font_size", 17)
input.add_theme_font_override("font", Brand.font)
parent.add_child(input)
fields.append(input)
@@ -294,17 +326,33 @@ func _add_field(parent: VBoxContainer, label_text: String, placeholder: String,
func _add_show_password(parent: VBoxContainer, targets: Array) -> void:
var toggle := CheckButton.new()
toggle.text = "Show password"
toggle.toggled.connect(func(show: bool):
toggle.toggled.connect(func(should_reveal: bool):
for target in targets:
target.secret = not show
target.secret = not should_reveal
)
parent.add_child(toggle)
func _add_password_reveal(input: LineEdit) -> void:
var reveal := Button.new()
reveal.text = "\u25c9"
reveal.tooltip_text = "Show password"
reveal.flat = true
reveal.focus_mode = Control.FOCUS_NONE
reveal.custom_minimum_size = Vector2(52, 52)
reveal.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
reveal.position = Vector2(-60, -26)
reveal.pressed.connect(func():
input.secret = not input.secret
reveal.tooltip_text = "Show password" if input.secret else "Hide password"
)
input.add_child(reveal)
func _add_primary_button(parent: VBoxContainer, text_value: String, callback: Callable) -> Button:
var button := Button.new()
button.text = text_value
button.custom_minimum_size = Vector2(0, 58)
button.custom_minimum_size = Vector2(0, 68)
button.pressed.connect(callback)
parent.add_child(button)
submit_buttons.append(button)
@@ -349,17 +397,28 @@ func _set_mode(next_mode: String) -> void:
func _update_responsive_layout() -> void:
if left_brand == null or layout_updating: return
layout_updating = true
var narrow := size.x < 1120.0
var layout_width := size.x
if layout_width <= 0.0:
layout_width = get_viewport_rect().size.x
var compact_desktop := layout_width < 900.0
var mobile_web := false
if OS.has_feature("web"):
var browser_width: Variant = JavaScriptBridge.eval("window.innerWidth", true)
narrow = float(browser_width) < 760.0
left_brand.visible = not narrow
compact_layout = narrow
theme_button.custom_minimum_size = Vector2(160, 160) if narrow else Vector2(44, 44)
theme_button.position = Vector2(-190, 20) if narrow else Vector2(-58, 14)
theme_button.add_theme_font_size_override("font_size", 64 if narrow else 20)
auth_panel.custom_minimum_size = Vector2(1700, 2300) if narrow else Vector2(610, 780)
_apply_compact_scale(panel_content, 3.8 if narrow else 1.0)
mobile_web = float(browser_width) < 760.0
compact_layout = compact_desktop or mobile_web
left_brand.visible = not compact_layout
if mobile_web:
theme_button.custom_minimum_size = Vector2(160, 160)
theme_button.position = Vector2(-190, 20)
theme_button.add_theme_font_size_override("font_size", 64)
auth_panel.custom_minimum_size = Vector2(1700, 2300)
_apply_compact_scale(panel_content, 3.8)
else:
theme_button.custom_minimum_size = Vector2(44, 44)
theme_button.position = Vector2(-58, 14)
theme_button.add_theme_font_size_override("font_size", 20)
auth_panel.custom_minimum_size = Vector2(640, 790) if compact_desktop else Vector2(710, 820)
_apply_compact_scale(panel_content, 1.0)
_apply_theme()
layout_updating = false
@@ -381,12 +440,17 @@ func _apply_compact_scale(control: Control, factor: float) -> void:
func _apply_theme() -> void:
var dark := UIThemeService.is_dark_mode()
background.color = Brand.background_color(dark)
background.material = Brand.background_material(dark)
auth_panel.add_theme_stylebox_override("panel", Brand.panel_box(dark))
theme_button.text = "☀" if dark else "☾"
theme_button.tooltip_text = "Use light theme" if dark else "Use dark theme"
theme_button.add_theme_font_override("font", Brand.font)
for node in find_children("*", "Label", true, false):
(node as Label).add_theme_color_override("font_color", Brand.text_color(dark))
var label := node as Label
if label.has_meta("brand_accent"):
label.add_theme_color_override("font_color", Brand.PURPLE if dark else Brand.ROYAL_BLUE)
else:
label.add_theme_color_override("font_color", Brand.text_color(dark))
for input in fields:
input.add_theme_color_override("font_color", Brand.text_color(dark))
input.add_theme_color_override("font_placeholder_color", Brand.muted_color(dark))
@@ -403,8 +467,18 @@ func _apply_theme() -> void:
if button.text not in ["Sign in with Steam"]:
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
button.add_theme_color_override("font_color", Color.WHITE)
var theme_normal := StyleBoxFlat.new()
theme_normal.bg_color = Color("111b30") if dark else Brand.LIGHT_SURFACE
theme_normal.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER
theme_normal.set_border_width_all(1)
theme_normal.set_corner_radius_all(22)
theme_button.add_theme_stylebox_override("normal", theme_normal)
theme_button.add_theme_stylebox_override("hover", theme_normal)
theme_button.add_theme_color_override("font_color", Brand.AMBER)
subheading.add_theme_color_override("font_color", Brand.muted_color(dark))
message_label.add_theme_color_override("font_color", Brand.muted_color(dark))
if brand_tagline != null:
brand_tagline.add_theme_color_override("default_color", Brand.muted_color(dark))
func _on_theme_changed(_is_dark: bool) -> void:
+468 -46
View File
@@ -1,6 +1,8 @@
extends Control
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
const NavigationRoutes := preload("res://scripts/ui/navigation_routes.gd")
var screens := {}
var header_stats: Label
@@ -15,6 +17,20 @@ var feedback_dialog: Control
var theme_service
var current_screen_name := ""
var screens_margin: MarginContainer
var screens_host: MarginContainer
var shell_root: HBoxContainer
var sidebar: PanelContainer
var sidebar_content: VBoxContainer
var drawer_backdrop: ColorRect
var drawer_open := false
var menu_button: Button
var account_button: Button
var account_chevron_button: Button
var account_menu: PanelContainer
var nav_buttons: Dictionary = {}
var current_nav_key := "home"
var compact_shell := false
var app_background: ColorRect
func _ready() -> void:
theme_service = get_node("/root/UIThemeService")
@@ -27,8 +43,16 @@ func _ready() -> void:
func _update_responsive_layout() -> void:
compact_shell = size.x < 900.0
if sidebar != null:
sidebar.visible = AuthState.is_logged_in and not compact_shell and _screen_uses_shell(current_screen_name)
if menu_button != null:
menu_button.visible = AuthState.is_logged_in and compact_shell and _screen_uses_shell(current_screen_name)
_apply_screen_margins(current_screen_name)
if header_stats != null:
header_stats.visible = size.x >= 900.0
header_stats.visible = size.x >= 720.0
if drawer_open and not compact_shell:
_close_drawer()
if app_menu != null:
app_menu.custom_minimum_size.x = minf(340.0, maxf(280.0, size.x - 32.0))
if app_menu.visible:
@@ -38,7 +62,9 @@ func _update_responsive_layout() -> void:
func show_screen(screen_name: String, payload: Variant = null) -> void:
_close_app_menu()
for child in $Root/Screens.get_children():
_close_account_menu()
_close_drawer()
for child in screens_host.get_children():
if child.visible and child.has_method("on_hide"):
child.on_hide()
child.visible = false
@@ -48,88 +74,133 @@ func show_screen(screen_name: String, payload: Variant = null) -> void:
return
current_screen_name = screen_name
if screens_margin != null:
var margin := 0 if screen_name == "login" else 24
screens_margin.add_theme_constant_override("margin_left", margin)
screens_margin.add_theme_constant_override("margin_right", margin)
screens_margin.add_theme_constant_override("margin_top", 0 if screen_name == "login" else 18)
screens_margin.add_theme_constant_override("margin_bottom", 0 if screen_name == "login" else 22)
_apply_screen_margins(screen_name)
screen.visible = true
if header_panel != null:
header_panel.visible = screen_name not in ["login", "player", "ccs_player"]
header_panel.visible = _screen_uses_shell(screen_name)
_update_shell_visibility(screen_name)
_update_active_navigation(screen_name, payload)
if screen.has_method("on_show"):
screen.on_show(payload)
func _build_ui() -> void:
var background := ColorRect.new()
background.color = Style.BACKGROUND
background.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(background)
func _apply_screen_margins(screen_name: String) -> void:
if screens_margin == null or screen_name.is_empty():
return
var margin := 0 if screen_name == "login" else (20 if compact_shell else 44)
screens_margin.add_theme_constant_override("margin_left", margin)
screens_margin.add_theme_constant_override("margin_right", margin)
screens_margin.add_theme_constant_override("margin_top", 0 if screen_name == "login" else (20 if compact_shell else 34))
screens_margin.add_theme_constant_override("margin_bottom", 0 if screen_name == "login" else 28)
var root := VBoxContainer.new()
root.name = "Root"
root.set_anchors_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 0)
add_child(root)
func _build_ui() -> void:
app_background = ColorRect.new()
app_background.color = Brand.background_color(UIThemeService.is_dark_mode())
app_background.material = Brand.background_material(UIThemeService.is_dark_mode())
app_background.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(app_background)
shell_root = HBoxContainer.new()
shell_root.name = "Root"
shell_root.set_anchors_preset(Control.PRESET_FULL_RECT)
shell_root.add_theme_constant_override("separation", 0)
add_child(shell_root)
sidebar = _build_sidebar()
shell_root.add_child(sidebar)
var main_column := VBoxContainer.new()
main_column.name = "MainColumn"
main_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
main_column.add_theme_constant_override("separation", 0)
shell_root.add_child(main_column)
header_panel = PanelContainer.new()
header_panel.name = "Header"
header_panel.add_theme_stylebox_override("panel", Style.panel_box(Style.SURFACE_DARK, Color("263752")))
root.add_child(header_panel)
header_panel.add_theme_stylebox_override("panel", _top_bar_box())
main_column.add_child(header_panel)
var header := HBoxContainer.new()
header.custom_minimum_size = Vector2(0, 78)
header.custom_minimum_size = Vector2(0, 80)
header.add_theme_constant_override("separation", 14)
header.alignment = BoxContainer.ALIGNMENT_CENTER
header_panel.add_child(header)
var title := Label.new()
title.text = "MedMind"
Style.set_label(title, 31, Style.TEXT)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
menu_button = Button.new()
menu_button.icon = preload("res://assets/icons/lucide/menu.svg")
menu_button.add_theme_constant_override("icon_max_width", 24)
menu_button.tooltip_text = "Open navigation"
menu_button.custom_minimum_size = Vector2(48, 48)
menu_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
menu_button.visible = false
menu_button.pressed.connect(_open_drawer)
header.add_child(menu_button)
var header_spacer := Control.new()
header_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(header_spacer)
header_stats = Label.new()
header_stats.name = "HeaderStats"
Style.set_label(header_stats, 15, Style.MUTED)
header_stats.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header_stats.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
header_stats.size_flags_vertical = Control.SIZE_SHRINK_CENTER
header.add_child(header_stats)
user_label = Label.new()
Style.set_label(user_label, 15, Style.CYAN)
user_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
user_label.mouse_filter = Control.MOUSE_FILTER_STOP
user_label.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
user_label.tooltip_text = "Open profile"
user_label.gui_input.connect(_on_user_label_gui_input)
user_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
header.add_child(user_label)
feedback_button = Button.new()
feedback_button.text = "Feedback"
feedback_button.tooltip_text = "Send general feedback or report a bug"
feedback_button.custom_minimum_size = Vector2(100, 44)
Style.style_chip(feedback_button, Style.CYAN)
feedback_button.custom_minimum_size = Vector2(124, 42)
feedback_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
_style_header_feedback_button(feedback_button)
feedback_button.pressed.connect(open_feedback_dialog)
header.add_child(feedback_button)
account_button = Button.new()
account_button.tooltip_text = "Open account menu"
account_button.custom_minimum_size = Vector2(44, 44)
account_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
account_button.pressed.connect(_toggle_account_menu)
_style_account_avatar(account_button)
header.add_child(account_button)
account_chevron_button = Button.new()
account_chevron_button.icon = preload("res://assets/icons/lucide/chevron-down.svg")
account_chevron_button.tooltip_text = "Open account menu"
account_chevron_button.custom_minimum_size = Vector2(28, 44)
account_chevron_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
account_chevron_button.add_theme_constant_override("icon_max_width", 17)
account_chevron_button.pressed.connect(_toggle_account_menu)
_style_account_chevron(account_chevron_button)
header.add_child(account_chevron_button)
theme_button = Button.new()
theme_button.tooltip_text = "Switch color theme"
theme_button.custom_minimum_size = Vector2(44, 44)
theme_button.add_theme_font_size_override("font_size", 22)
UISoundService.set_button_sound(theme_button, "toggle")
theme_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
theme_button.pressed.connect(theme_service.toggle_theme)
_style_shell_theme_button(theme_button)
header.add_child(theme_button)
_refresh_theme_button()
var screens_container := MarginContainer.new()
screens_margin = screens_container
screens_host = screens_container
screens_container.name = "Screens"
screens_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
screens_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
screens_container.add_theme_constant_override("margin_left", 24)
screens_container.add_theme_constant_override("margin_right", 24)
screens_container.add_theme_constant_override("margin_top", 18)
screens_container.add_theme_constant_override("margin_bottom", 22)
root.add_child(screens_container)
main_column.add_child(screens_container)
var menu := preload("res://scripts/ui/main_menu.gd").new()
menu.name = "MainMenu"
@@ -146,6 +217,7 @@ func _build_ui() -> void:
var selector := preload("res://scripts/ui/study_selector.gd").new()
selector.name = "StudySelector"
selector.main = self
selector.stage_changed.connect(_on_selector_stage_changed)
screens_container.add_child(selector)
screens["selection"] = selector
@@ -212,9 +284,17 @@ func _build_ui() -> void:
screens_container.add_child(profile)
screens["profile"] = profile
var settings := preload("res://scripts/ui/settings_screen.gd").new()
settings.name = "SettingsScreen"
settings.main = self
screens_container.add_child(settings)
screens["settings"] = settings
feedback_dialog = preload("res://scripts/ui/app_feedback_dialog.gd").new()
feedback_dialog.name = "AppFeedbackDialog"
add_child(feedback_dialog)
_build_account_menu()
_build_drawer()
ProfileService.profile_changed.connect(_refresh_header_stats)
QuestionBankService.ratings_changed.connect(_refresh_header_stats)
@@ -223,26 +303,345 @@ func _build_ui() -> void:
_build_app_menu()
func _build_sidebar() -> PanelContainer:
var panel := PanelContainer.new()
panel.name = "Sidebar"
panel.custom_minimum_size = Vector2(304, 0)
panel.add_theme_stylebox_override("panel", _sidebar_box())
sidebar_content = VBoxContainer.new()
sidebar_content.add_theme_constant_override("separation", 8)
panel.add_child(sidebar_content)
_populate_navigation(sidebar_content, true)
return panel
func _populate_navigation(container: VBoxContainer, include_brand: bool) -> void:
if include_brand:
var brand_row := HBoxContainer.new()
brand_row.custom_minimum_size = Vector2(0, 56)
brand_row.add_theme_constant_override("separation", 10)
container.add_child(brand_row)
var mark := TextureRect.new()
mark.texture = preload("res://assets/branding/medmind-icon.png")
mark.material = Brand.logo_cutout_material()
mark.custom_minimum_size = Vector2(46, 46)
mark.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
mark.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
brand_row.add_child(mark)
var wordmark := HBoxContainer.new()
wordmark.add_theme_constant_override("separation", 0)
brand_row.add_child(wordmark)
var med := Label.new()
med.text = "Med"
med.add_theme_font_override("font", Brand.font)
med.add_theme_font_size_override("font_size", 27)
wordmark.add_child(med)
var mind := Label.new()
mind.text = "Mind"
mind.add_theme_font_override("font", Brand.font)
mind.add_theme_font_size_override("font_size", 27)
mind.add_theme_color_override("font_color", Brand.PURPLE)
wordmark.add_child(mind)
container.add_child(HSeparator.new())
_add_nav_button(container, "home", "Home", "house", true)
_add_nav_button(container, "practice", "Practice", "pencil", true)
_add_nav_button(container, "exams", "Exams", "clipboard-list", true)
_add_nav_button(container, "ccs", "CCS Cases", "stethoscope", true)
_add_nav_button(container, "performance", "Performance", "chart-no-axes-column-increasing", true)
_add_nav_button(container, "flashcards", "Flashcards", "copy", false)
_add_nav_button(container, "study_plan", "Study Plan", "calendar-days", true)
_add_nav_button(container, "bookmarks", "Bookmarks", "bookmark", false)
_add_nav_button(container, "notes", "Notes", "notebook-pen", false)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
container.add_child(spacer)
container.add_child(HSeparator.new())
_add_nav_button(container, "settings", "Settings", "settings", true)
_add_nav_button(container, "logout", "Log out", "log-out", true)
func _add_nav_button(container: VBoxContainer, key: String, label_text: String, icon_name: String, enabled: bool) -> void:
var button := Button.new()
button.text = label_text
button.name = "Nav%s" % key.to_pascal_case()
button.icon = load("res://assets/icons/lucide/%s.svg" % icon_name)
button.expand_icon = true
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.custom_minimum_size = Vector2(0, 58)
button.add_theme_constant_override("icon_max_width", 25)
button.add_theme_constant_override("h_separation", 18)
button.add_theme_font_override("font", Brand.font)
button.add_theme_font_size_override("font_size", 18)
button.disabled = not enabled
button.tooltip_text = "Coming Soon" if not enabled else label_text
button.pressed.connect(_on_navigation_pressed.bind(key))
container.add_child(button)
if not nav_buttons.has(key):
nav_buttons[key] = []
var instances: Array = nav_buttons[key]
instances.append(button)
nav_buttons[key] = instances
_style_nav_button(button, key == current_nav_key)
func _on_navigation_pressed(key: String) -> void:
match key:
"home": show_screen("selection", "exam")
"practice": show_screen("selection", "step3")
"exams": show_screen("simulation_history")
"ccs": show_screen("ccs_hub")
"performance": show_screen("profile")
"study_plan": show_screen("map")
"settings": show_screen("settings")
"logout": _logout()
func _build_account_menu() -> void:
account_menu = PanelContainer.new()
account_menu.visible = false
account_menu.top_level = true
account_menu.custom_minimum_size = Vector2(230, 230)
account_menu.add_theme_stylebox_override("panel", Brand.panel_box(UIThemeService.is_dark_mode()))
add_child(account_menu)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 8)
account_menu.add_child(content)
for spec in [["Profile", "user-round", "performance"], ["Settings", "settings", "settings"], ["Log out", "log-out", "logout"]]:
var button := Button.new()
button.text = spec[0]
button.icon = load("res://assets/icons/lucide/%s.svg" % spec[1])
button.add_theme_constant_override("icon_max_width", 22)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.custom_minimum_size = Vector2(0, 46)
button.pressed.connect(_on_navigation_pressed.bind(spec[2]))
content.add_child(button)
func _build_drawer() -> void:
drawer_backdrop = ColorRect.new()
drawer_backdrop.visible = false
drawer_backdrop.top_level = true
drawer_backdrop.color = Color(0, 0, 0, 0.62)
drawer_backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
drawer_backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
drawer_backdrop.gui_input.connect(_on_drawer_backdrop_input)
add_child(drawer_backdrop)
var drawer := PanelContainer.new()
drawer.custom_minimum_size = Vector2(304, 0)
drawer.set_anchors_preset(Control.PRESET_LEFT_WIDE)
drawer.offset_right = 304
drawer.add_theme_stylebox_override("panel", _sidebar_box())
drawer.mouse_filter = Control.MOUSE_FILTER_STOP
drawer_backdrop.add_child(drawer)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 8)
drawer.add_child(content)
_populate_navigation(content, true)
func _open_drawer() -> void:
if drawer_backdrop == null:
return
drawer_open = true
drawer_backdrop.visible = true
func _close_drawer() -> void:
drawer_open = false
if drawer_backdrop != null:
drawer_backdrop.visible = false
func _on_drawer_backdrop_input(event: InputEvent) -> void:
var click := event as InputEventMouseButton
if click != null and click.pressed:
_close_drawer()
func _toggle_account_menu() -> void:
if account_menu == null:
return
if account_menu.visible:
_close_account_menu()
return
account_menu.visible = true
account_menu.size = account_menu.custom_minimum_size
account_menu.position = Vector2(
get_viewport_rect().size.x - account_menu.size.x - 24,
82
)
func _close_account_menu() -> void:
if account_menu != null:
account_menu.visible = false
func _screen_uses_shell(screen_name: String) -> bool:
return AuthState.is_logged_in and screen_name not in ["login", "player", "ccs_player"]
func _update_shell_visibility(screen_name: String) -> void:
var show_shell := _screen_uses_shell(screen_name)
if sidebar != null:
sidebar.visible = show_shell and not compact_shell
if header_panel != null:
header_panel.visible = show_shell
if menu_button != null:
menu_button.visible = show_shell and compact_shell
func _update_active_navigation(screen_name: String, payload: Variant = null) -> void:
var key := NavigationRoutes.key_for_screen(screen_name, payload)
current_nav_key = key
for nav_key in nav_buttons:
for button in nav_buttons[nav_key]:
_style_nav_button(button, nav_key == current_nav_key)
func _on_selector_stage_changed(stage_name: String) -> void:
if current_screen_name == "selection":
_update_active_navigation("selection", stage_name)
func _style_nav_button(button: Button, active: bool) -> void:
var dark := UIThemeService.is_dark_mode()
var normal := StyleBoxFlat.new()
normal.bg_color = (Color("14255b") if dark else Color("e5edff")) if active else Color(0, 0, 0, 0)
normal.set_corner_radius_all(10)
normal.content_margin_left = 18
normal.content_margin_right = 18
button.add_theme_stylebox_override("normal", normal)
var hover := normal.duplicate() as StyleBoxFlat
hover.bg_color = Color("172d68") if dark else Color("d8e5ff")
button.add_theme_stylebox_override("hover", hover)
var disabled := normal.duplicate() as StyleBoxFlat
disabled.bg_color = Color(0, 0, 0, 0)
button.add_theme_stylebox_override("disabled", disabled)
button.add_theme_color_override("font_color", (Color("f8fafc") if dark else Color("0f172a")) if active else (Color("b8c2d6") if dark else Color("53627a")))
button.add_theme_color_override("icon_normal_color", Brand.ROYAL_BLUE if active else (Color("b8c2d6") if dark else Color("53627a")))
button.add_theme_color_override("icon_hover_color", Brand.SKY_BLUE)
button.add_theme_color_override("font_disabled_color", Color("596982") if dark else Color("94a3b8"))
button.add_theme_color_override("icon_disabled_color", Color("596982") if dark else Color("94a3b8"))
func _sidebar_box() -> StyleBoxFlat:
var dark := UIThemeService.is_dark_mode()
var box := Brand.shell_box(dark, "right")
box.content_margin_left = 28
box.content_margin_right = 28
box.content_margin_top = 12
box.content_margin_bottom = 26
return box
func _top_bar_box() -> StyleBoxFlat:
var dark := UIThemeService.is_dark_mode()
var box := Brand.shell_box(dark, "bottom")
box.content_margin_left = 28
box.content_margin_right = 28
return box
func _style_header_feedback_button(button: Button) -> void:
var dark := UIThemeService.is_dark_mode()
var normal := Style.button_box(Color(0, 0, 0, 0), Brand.ROYAL_BLUE)
var hover := Style.button_box(Color(Brand.ROYAL_BLUE, 0.16), Brand.SKY_BLUE)
var pressed := Style.button_box(Color(Brand.ROYAL_BLUE, 0.24), Brand.SKY_BLUE)
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", pressed)
button.add_theme_color_override("font_color", Brand.ROYAL_BLUE if not dark else Brand.SKY_BLUE)
button.add_theme_color_override("font_hover_color", Brand.SKY_BLUE)
button.add_theme_color_override("icon_normal_color", Brand.ROYAL_BLUE if not dark else Brand.SKY_BLUE)
button.add_theme_color_override("icon_hover_color", Brand.SKY_BLUE)
func _style_account_avatar(button: Button) -> void:
var dark := UIThemeService.is_dark_mode()
var normal := StyleBoxFlat.new()
normal.bg_color = Color("26324f") if dark else Color("dce5f5")
normal.corner_radius_top_left = 22
normal.corner_radius_top_right = 22
normal.corner_radius_bottom_left = 22
normal.corner_radius_bottom_right = 22
var hover := normal.duplicate() as StyleBoxFlat
hover.bg_color = Color("344465") if dark else Color("cad7ed")
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", hover)
button.add_theme_color_override("font_color", Color("f8fafc") if dark else Color("172033"))
button.add_theme_color_override("font_hover_color", Color("ffffff") if dark else Color("0f172a"))
func _style_account_chevron(button: Button) -> void:
var empty := StyleBoxEmpty.new()
button.add_theme_stylebox_override("normal", empty)
button.add_theme_stylebox_override("hover", empty)
button.add_theme_stylebox_override("pressed", empty)
button.add_theme_color_override("icon_normal_color", Style.MUTED)
button.add_theme_color_override("icon_hover_color", Style.TEXT)
func _style_shell_theme_button(button: Button) -> void:
var dark := UIThemeService.is_dark_mode()
var normal := StyleBoxFlat.new()
normal.bg_color = Color("111b30") if dark else Brand.LIGHT_SURFACE
normal.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER
normal.set_border_width_all(1)
normal.set_corner_radius_all(22)
var hover := normal.duplicate() as StyleBoxFlat
hover.border_color = Brand.AMBER
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", hover)
button.add_theme_color_override("font_color", Brand.AMBER)
button.add_theme_color_override("font_hover_color", Brand.AMBER)
func _input(event: InputEvent) -> void:
if not AuthState.is_logged_in or current_screen_name == "login":
return
if event.is_action_pressed("ui_cancel"):
_toggle_app_menu()
get_viewport().set_input_as_handled()
if drawer_open:
_close_drawer()
get_viewport().set_input_as_handled()
return
if account_menu != null and account_menu.visible:
_close_account_menu()
get_viewport().set_input_as_handled()
return
if current_screen_name in ["player", "ccs_player"]:
_toggle_app_menu()
get_viewport().set_input_as_handled()
func _refresh_header_stats() -> void:
if header_stats == null:
return
var profile := ProfileService.get_profile()
header_stats.text = "Rating: %d | Streak %d" % [
QuestionBankService.overall_elo,
profile.get("current_streak", 0)
]
header_stats.text = "Rating: %d" % QuestionBankService.overall_elo
if user_label != null:
user_label.text = AuthState.username if AuthState.is_logged_in else ""
user_label.text = AuthState.role.to_upper() if AuthState.is_logged_in and not AuthState.role.is_empty() else ""
if account_button != null:
account_button.text = _account_initials(AuthState.username)
if feedback_button != null:
feedback_button.visible = AuthState.is_logged_in
feedback_button.icon = preload("res://assets/icons/lucide/message-square.svg")
feedback_button.add_theme_constant_override("icon_max_width", 21)
func _account_initials(username: String) -> String:
var cleaned := ""
for character in username:
if character.to_lower() != character.to_upper():
cleaned += character
elif character.is_valid_int():
cleaned += character
if cleaned.length() >= 2:
break
return cleaned.to_upper() if not cleaned.is_empty() else "ME"
func open_feedback_dialog() -> void:
@@ -254,6 +653,29 @@ func open_feedback_dialog() -> void:
func _on_theme_changed(is_dark_mode: bool) -> void:
Style.refresh_tree(self, not is_dark_mode)
_refresh_theme_button()
if app_background != null:
app_background.color = Brand.background_color(is_dark_mode)
app_background.material = Brand.background_material(is_dark_mode)
if account_menu != null:
account_menu.add_theme_stylebox_override("panel", Brand.panel_box(is_dark_mode))
if feedback_button != null:
_style_header_feedback_button(feedback_button)
if account_button != null:
_style_account_avatar(account_button)
if account_chevron_button != null:
_style_account_chevron(account_chevron_button)
if sidebar != null:
sidebar.add_theme_stylebox_override("panel", _sidebar_box())
if header_panel != null:
header_panel.add_theme_stylebox_override("panel", _top_bar_box())
if theme_button != null:
_style_shell_theme_button(theme_button)
for nav_key in nav_buttons:
for button in nav_buttons[nav_key]:
_style_nav_button(button, nav_key == current_nav_key)
var selector = screens.get("selection")
if selector != null and selector.has_method("refresh_theme_controls"):
selector.refresh_theme_controls()
var player = screens.get("player")
if player != null and player.has_method("refresh_theme_controls"):
player.refresh_theme_controls()
@@ -264,7 +686,7 @@ func _refresh_theme_button() -> void:
return
theme_button.text = "☀" if theme_service.is_dark_mode() else "☾"
theme_button.tooltip_text = "Switch to light mode" if theme_service.is_dark_mode() else "Switch to dark mode"
Style.style_chip(theme_button, Style.CYAN)
_style_shell_theme_button(theme_button)
func _logout() -> void:
+110 -18
View File
@@ -11,6 +11,11 @@ const AMBER := Color("f59e0b")
const SLATE := Color("94a3b8")
const DARK_TEXT := Color("f8fafc")
const LIGHT_TEXT := Color("0f172a")
const LIGHT_BACKGROUND := Color("f1f5f9")
const LIGHT_SURFACE := Color("ffffff")
const LIGHT_SURFACE_SUBTLE := Color("f8fafc")
const LIGHT_BORDER := Color("dbe3ef")
const LIGHT_MUTED := Color("64748b")
static var font: Font = preload("res://assets/fonts/Inter-Variable.ttf")
@@ -20,36 +25,80 @@ static func text_color(dark: bool) -> Color:
static func muted_color(dark: bool) -> Color:
return Color("a9b5ca") if dark else Color("53627a")
return Color("a9b5ca") if dark else LIGHT_MUTED
static func background_color(dark: bool) -> Color:
return DEEP_NAVY if dark else Color("f5f8ff")
return DEEP_NAVY if dark else LIGHT_BACKGROUND
static func background_material(dark: bool) -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type canvas_item;
uniform bool dark_mode = true;
void fragment() {
vec2 uv = UV;
vec3 base = dark_mode ? vec3(0.018, 0.043, 0.090) : vec3(0.965, 0.975, 0.990);
float left_glow = 1.0 - smoothstep(0.0, 0.58, distance(uv, vec2(-0.02, 0.12)));
float right_glow = 1.0 - smoothstep(0.0, 0.58, distance(uv, vec2(1.02, 0.92)));
vec3 blue = dark_mode ? vec3(0.075, 0.055, 0.35) : vec3(0.70, 0.80, 1.0);
vec3 purple = dark_mode ? vec3(0.18, 0.035, 0.52) : vec3(0.86, 0.78, 1.0);
vec3 color = base + blue * left_glow * (dark_mode ? 0.55 : 0.28);
color += purple * right_glow * (dark_mode ? 0.62 : 0.12);
COLOR = vec4(color, 1.0);
}
"""
var material := ShaderMaterial.new()
material.shader = shader
material.set_shader_parameter("dark_mode", dark)
return material
static func logo_cutout_material() -> ShaderMaterial:
var shader := Shader.new()
shader.code = """
shader_type canvas_item;
void fragment() {
vec4 source = texture(TEXTURE, UV);
float brightness = max(source.r, max(source.g, source.b));
float alpha = smoothstep(0.10, 0.24, brightness);
COLOR = vec4(source.rgb, source.a * alpha);
}
"""
var material := ShaderMaterial.new()
material.shader = shader
return material
static func panel_box(dark: bool) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("0b1427cc") if dark else Color("ffffffee")
box.border_color = Color("33415d") if dark else Color("d4def0")
box.bg_color = Color("0b1427cc") if dark else LIGHT_SURFACE
box.border_color = Color("33415d") if dark else LIGHT_BORDER
box.set_border_width_all(1)
box.set_corner_radius_all(8)
box.content_margin_left = 54
box.content_margin_right = 54
box.content_margin_top = 48
box.content_margin_bottom = 42
box.set_corner_radius_all(26)
if not dark:
box.shadow_color = Color(0.15, 0.23, 0.38, 0.12)
box.shadow_size = 12
box.content_margin_left = 64
box.content_margin_right = 64
box.content_margin_top = 62
box.content_margin_bottom = 48
return box
static func field_box(dark: bool, focused: bool = false) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("0d172a") if dark else Color("f8faff")
box.border_color = ROYAL_BLUE if focused else (Color("33415d") if dark else Color("cbd6e8"))
box.bg_color = Color("0d172a") if dark else LIGHT_SURFACE
box.border_color = ROYAL_BLUE if focused else (Color("33415d") if dark else LIGHT_BORDER)
box.set_border_width_all(1)
box.set_corner_radius_all(8)
box.content_margin_left = 18
box.content_margin_right = 18
box.content_margin_top = 15
box.content_margin_bottom = 15
box.set_corner_radius_all(10)
box.content_margin_left = 22
box.content_margin_right = 22
box.content_margin_top = 18
box.content_margin_bottom = 18
return box
@@ -59,6 +108,49 @@ static func secondary_button_box(dark: bool, hover: bool = false) -> StyleBoxFla
return box
static func shell_box(dark: bool, edge: String) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("071224") if dark else LIGHT_SURFACE
box.border_color = Color("1c2b49") if dark else LIGHT_BORDER
if edge == "right":
box.border_width_right = 1
elif edge == "bottom":
box.border_width_bottom = 1
return box
static func content_card_box(dark: bool, active: bool, accent: Color = ROYAL_BLUE) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = (Color("07192c") if active else Color("11182e")) if dark else LIGHT_SURFACE
box.border_color = accent if active else (Color("2a365a") if dark else LIGHT_BORDER)
box.set_border_width_all(1)
box.set_corner_radius_all(12)
if not dark:
box.shadow_color = Color(0.15, 0.23, 0.38, 0.06)
box.shadow_size = 5
return box
static func icon_circle_box(dark: bool, accent: Color, radius: int, filled: bool = false) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color(accent, 0.62 if filled else (0.06 if dark else 0.045))
box.border_color = Color(accent, 0.48 if dark else 0.18)
box.set_border_width_all(1)
box.set_corner_radius_all(radius)
return box
static func badge_box(dark: bool, accent: Color) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color(accent, 0.12 if dark else 0.09)
box.set_corner_radius_all(7)
box.content_margin_left = 10
box.content_margin_right = 10
box.content_margin_top = 5
box.content_margin_bottom = 5
return box
static func gradient_button_box() -> StyleBoxTexture:
var gradient := Gradient.new()
gradient.colors = PackedColorArray([Color("1677ff"), Color("8f3ff4")])
@@ -70,8 +162,8 @@ static func gradient_button_box() -> StyleBoxTexture:
texture.fill_to = Vector2(1, 0.5)
var box := StyleBoxTexture.new()
box.texture = texture
box.content_margin_top = 14
box.content_margin_bottom = 14
box.content_margin_top = 18
box.content_margin_bottom = 18
return box
+18
View File
@@ -0,0 +1,18 @@
extends RefCounted
static func key_for_screen(screen_name: String, payload: Variant = null) -> String:
var key := "home"
if screen_name == "selection":
key = "practice" if str(payload) == "step3" else "home"
elif screen_name in ["map", "setup", "review"]:
key = "study_plan" if screen_name == "map" else "practice"
elif screen_name.begins_with("simulation_"):
key = "exams"
elif screen_name.begins_with("ccs_"):
key = "ccs"
elif screen_name == "profile":
key = "performance"
elif screen_name == "settings":
key = "settings"
return key
+1
View File
@@ -0,0 +1 @@
uid://d1navigationroutes
+71
View File
@@ -0,0 +1,71 @@
extends VBoxContainer
const Brand := preload("res://scripts/ui/medmind_style.gd")
var main: Control
var theme_toggle: CheckButton
var sound_toggle: CheckButton
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_theme_constant_override("separation", 18)
var eyebrow := Label.new()
eyebrow.text = "PREFERENCES"
eyebrow.add_theme_font_override("font", Brand.font)
eyebrow.add_theme_font_size_override("font_size", 14)
eyebrow.add_theme_color_override("font_color", Brand.SKY_BLUE)
add_child(eyebrow)
var title := Label.new()
title.text = "Settings"
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 34)
add_child(title)
var subtitle := Label.new()
subtitle.text = "Adjust MedMind's appearance and interface sounds."
subtitle.add_theme_font_override("font", Brand.font)
subtitle.add_theme_font_size_override("font_size", 17)
subtitle.add_theme_color_override("font_color", Brand.SLATE)
add_child(subtitle)
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(520, 0)
panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
panel.add_theme_stylebox_override("panel", Brand.panel_box(UIThemeService.is_dark_mode()))
add_child(panel)
var content := VBoxContainer.new()
content.add_theme_constant_override("separation", 18)
panel.add_child(content)
theme_toggle = CheckButton.new()
theme_toggle.text = "Dark mode"
theme_toggle.custom_minimum_size = Vector2(0, 52)
theme_toggle.toggled.connect(_on_theme_toggled)
content.add_child(theme_toggle)
sound_toggle = CheckButton.new()
sound_toggle.text = "Interface sounds"
sound_toggle.custom_minimum_size = Vector2(0, 52)
sound_toggle.toggled.connect(UISoundService.set_sounds_enabled)
content.add_child(sound_toggle)
UIThemeService.theme_changed.connect(_on_theme_changed)
func on_show(_payload: Variant = null) -> void:
theme_toggle.set_pressed_no_signal(UIThemeService.is_dark_mode())
sound_toggle.set_pressed_no_signal(UISoundService.are_sounds_enabled())
func _on_theme_toggled(enabled: bool) -> void:
UIThemeService.set_dark_mode(enabled)
func _on_theme_changed(is_dark: bool) -> void:
theme_toggle.set_pressed_no_signal(is_dark)
var panel := theme_toggle.get_parent().get_parent() as PanelContainer
if panel != null:
panel.add_theme_stylebox_override("panel", Brand.panel_box(is_dark))
+1
View File
@@ -0,0 +1 @@
uid://d1medmindsettings
+92 -1
View File
@@ -6,7 +6,10 @@ var main: Control
var list: VBoxContainer
var status_label: Label
var load_more_button: Button
var launch_button: Button
var readiness_label: Label
var pending_simulation_id := ""
var pending_launch := false
func _ready() -> void:
@@ -14,6 +17,8 @@ func _ready() -> void:
add_theme_constant_override("separation", 12)
if not ExamSimulationService.history_changed.is_connected(_render_history):
ExamSimulationService.history_changed.connect(_render_history)
if not ExamSimulationService.readiness_changed.is_connected(_refresh_readiness):
ExamSimulationService.readiness_changed.connect(_refresh_readiness)
if not ExamSimulationService.state_changed.is_connected(_on_simulation_loaded):
ExamSimulationService.state_changed.connect(_on_simulation_loaded)
if not ExamSimulationService.request_failed.is_connected(_on_request_failed):
@@ -34,6 +39,32 @@ func _ready() -> void:
var balance := Control.new()
balance.custom_minimum_size = Vector2(100, 0)
top.add_child(balance)
var launch_panel := PanelContainer.new()
launch_panel.add_theme_stylebox_override("panel", Style.panel_box(Color("07192c"), Style.CYAN))
add_child(launch_panel)
var launch_content := HBoxContainer.new()
launch_content.add_theme_constant_override("separation", 18)
launch_panel.add_child(launch_content)
var launch_copy := VBoxContainer.new()
launch_copy.size_flags_horizontal = Control.SIZE_EXPAND_FILL
launch_copy.add_theme_constant_override("separation", 5)
launch_content.add_child(launch_copy)
var launch_title := Label.new()
launch_title.text = "Step 3 Day 1 Simulation"
Style.set_label(launch_title, 22, Style.TEXT)
launch_copy.add_child(launch_title)
readiness_label = Label.new()
readiness_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(readiness_label, 15, Style.MUTED)
launch_copy.add_child(readiness_label)
launch_button = Button.new()
launch_button.text = "Checking readiness..."
launch_button.custom_minimum_size = Vector2(210, 48)
launch_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER
launch_button.disabled = true
Style.style_primary_button(launch_button)
launch_button.pressed.connect(_launch_or_resume)
launch_content.add_child(launch_button)
status_label = Label.new()
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(status_label, 16, Style.MUTED)
@@ -57,6 +88,9 @@ func _ready() -> void:
func on_show(payload: Variant = null) -> void:
pending_simulation_id = ""
pending_launch = false
_refresh_readiness()
ExamSimulationService.load_readiness()
if str(payload) == "preserve" and not ExamSimulationService.history.is_empty():
return
status_label.text = "Loading simulation history..."
@@ -104,15 +138,26 @@ func _summary_text(summary: Dictionary) -> String:
func _open_simulation(simulation_id: String) -> void:
pending_launch = false
pending_simulation_id = simulation_id
status_label.text = "Opening simulation..."
ExamSimulationService.load_simulation(simulation_id)
func _on_simulation_loaded() -> void:
if not visible or pending_simulation_id.is_empty() or ExamSimulationService.simulation.is_empty():
if not visible or ExamSimulationService.simulation.is_empty():
return
if pending_launch:
pending_launch = false
_route_loaded_simulation()
return
if pending_simulation_id.is_empty():
return
pending_simulation_id = ""
_route_loaded_simulation()
func _route_loaded_simulation() -> void:
var state := str(ExamSimulationService.simulation.get("state", ""))
if state == "completed":
main.show_screen("simulation_results", {"simulation": ExamSimulationService.simulation, "return_screen": "simulation_history"})
@@ -128,6 +173,42 @@ func _on_simulation_loaded() -> void:
main.show_screen("simulation_break")
else:
status_label.text = "This simulation can no longer be resumed."
_refresh_readiness()
func _launch_or_resume() -> void:
pending_launch = true
pending_simulation_id = ""
launch_button.disabled = true
readiness_label.text = "Opening your simulation..."
if _has_active_simulation():
ExamSimulationService.load_active()
else:
ExamSimulationService.create_simulation()
func _refresh_readiness() -> void:
if launch_button == null:
return
var simulation_ready := bool(ExamSimulationService.readiness.get("ready", false))
var active := _has_active_simulation()
var available := int(ExamSimulationService.readiness.get("availableQuestions", 0))
launch_button.text = "Resume Simulation" if active else "Start Simulation"
launch_button.disabled = pending_launch or (not simulation_ready and not active)
if pending_launch:
readiness_label.text = "Opening your simulation..."
elif active:
readiness_label.text = "Your progress is saved between blocks. Resume where you left off."
elif simulation_ready:
readiness_label.text = "Full-length, 12-block simulation with a provisional predicted score."
else:
readiness_label.text = "Unlocks when 232 published questions are available. Currently %d/232." % available
func _has_active_simulation() -> bool:
return has_active_simulation(
ExamSimulationService.readiness,
ExamSimulationService.simulation)
func _load_more() -> void:
@@ -138,9 +219,11 @@ func _load_more() -> void:
func _on_request_failed(message: String) -> void:
if not visible:
return
pending_launch = false
pending_simulation_id = ""
status_label.text = message
load_more_button.disabled = false
_refresh_readiness()
static func _action_for_state(state: String) -> String:
@@ -153,3 +236,11 @@ static func _state_color(state: String) -> Color:
if state == "completed": return Style.LIME
if state == "block_active" or state == "between_blocks": return Style.CYAN
return Style.MUTED
static func has_active_simulation(readiness: Dictionary, simulation: Dictionary) -> bool:
if bool(readiness.get("hasActiveSimulation", false)):
return true
if simulation.is_empty():
return false
return str(simulation.get("state", "")) in ["between_blocks", "block_active"]
+440 -187
View File
@@ -1,25 +1,26 @@
extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
signal stage_changed(stage_name: String)
var main: Control
var exam_view: VBoxContainer
var mode_view: VBoxContainer
var exam_grid: GridContainer
var exam_grid: VBoxContainer
var exam_top_row: BoxContainer
var mode_grid: GridContainer
var question_bank_button: Button
var bank_status_label: Label
var bank_error_label: Label
var retry_button: Button
var simulation_button: Button
var simulation_status_label: Label
var simulation_preview_link: LinkButton
var ccs_button: Button
var ccs_status_label: Label
var pending_simulation_launch := false
var pending_ccs_launch := false
var current_stage := "exam"
var pending_question_bank_launch := false
var themed_controls: Array[Control] = []
func _ready() -> void:
@@ -30,14 +31,6 @@ func _ready() -> void:
QuestionBankService.questions_changed.connect(_on_questions_changed)
if not QuestionBankService.questions_load_failed.is_connected(_on_questions_load_failed):
QuestionBankService.questions_load_failed.connect(_on_questions_load_failed)
if not ExamSimulationService.readiness_changed.is_connected(_refresh_simulation_state):
ExamSimulationService.readiness_changed.connect(_refresh_simulation_state)
if not ExamSimulationService.state_changed.is_connected(_on_simulation_state_changed):
ExamSimulationService.state_changed.connect(_on_simulation_state_changed)
if not ExamSimulationService.request_failed.is_connected(_on_simulation_failed):
ExamSimulationService.request_failed.connect(_on_simulation_failed)
if not AuthState.auth_changed.is_connected(_refresh_simulation_preview_visibility):
AuthState.auth_changed.connect(_refresh_simulation_preview_visibility)
if not CCSService.attempt_changed.is_connected(_on_ccs_attempt_changed):
CCSService.attempt_changed.connect(_on_ccs_attempt_changed)
if not CCSService.request_failed.is_connected(_on_ccs_failed):
@@ -54,8 +47,6 @@ func on_show(payload: Variant = null) -> void:
elif str(payload) == "exam":
_show_stage("exam")
_refresh_bank_state()
_refresh_simulation_preview_visibility()
ExamSimulationService.load_readiness()
func on_hide() -> void:
@@ -70,63 +61,82 @@ func _build_exam_view() -> void:
add_child(exam_view)
var eyebrow := Label.new()
eyebrow.text = "CHOOSE YOUR EXAM"
Style.set_label(eyebrow, 14, Style.CYAN)
eyebrow.text = "GET STARTED"
eyebrow.add_theme_font_override("font", Brand.font)
eyebrow.add_theme_font_size_override("font_size", 14)
eyebrow.add_theme_color_override("font_color", Brand.SKY_BLUE)
exam_view.add_child(eyebrow)
var title := Label.new()
title.text = "What are you preparing for?"
Style.set_label(title, 34, Style.TEXT)
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 34)
exam_view.add_child(title)
var subtitle := Label.new()
subtitle.text = "Select a path to enter its study modes. More exams will be added as they are ready."
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(subtitle, 17, Style.MUTED)
subtitle.add_theme_font_override("font", Brand.font)
subtitle.add_theme_font_size_override("font_size", 17)
subtitle.add_theme_color_override("font_color", Brand.SLATE)
subtitle.set_meta("medmind_role", "muted")
themed_controls.append(subtitle)
exam_view.add_child(subtitle)
exam_grid = GridContainer.new()
exam_grid.columns = 2
exam_grid.add_theme_constant_override("h_separation", 16)
exam_grid.add_theme_constant_override("v_separation", 16)
exam_grid = VBoxContainer.new()
exam_grid.add_theme_constant_override("separation", 16)
exam_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
exam_grid.size_flags_vertical = Control.SIZE_EXPAND_FILL
exam_view.add_child(exam_grid)
exam_top_row = BoxContainer.new()
exam_top_row.add_theme_constant_override("separation", 16)
exam_top_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
exam_grid.add_child(exam_top_row)
_add_exam_option("STEP 1", "Foundational sciences", false)
_add_exam_option("STEP 2", "Clinical knowledge", false)
_add_exam_option("STEP 3", "Advanced clinical management", true)
_add_exam_option("STEP 1", "Foundational Sciences", "Build a strong understanding of the basic science concepts tested on Step 1.", "microscope", false)
_add_exam_option("STEP 2", "Clinical Knowledge", "Master clinical concepts and apply knowledge to patient care scenarios.", "stethoscope", false)
_add_exam_option("STEP 3", "Advanced Clinical Management", "Focus on advanced clinical management, CCS cases, and real-world decision making.", "clipboard-plus", true)
func _build_mode_view() -> void:
mode_view = VBoxContainer.new()
mode_view.add_theme_constant_override("separation", 18)
mode_view.add_theme_constant_override("separation", 16)
mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
add_child(mode_view)
var back_button := Button.new()
back_button.text = "< All exams"
back_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
back_button.custom_minimum_size = Vector2(150, 42)
Style.style_chip(back_button, Style.CYAN)
back_button.custom_minimum_size = Vector2(0, 48)
back_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_style_back_bar(back_button)
back_button.set_meta("medmind_role", "back_bar")
themed_controls.append(back_button)
UISoundService.set_button_sound(back_button, "back")
back_button.pressed.connect(func(): _show_stage("exam"))
mode_view.add_child(back_button)
var eyebrow := Label.new()
eyebrow.text = "STEP 3"
Style.set_label(eyebrow, 14, Style.LIME)
eyebrow.add_theme_font_override("font", Brand.font)
eyebrow.add_theme_font_size_override("font_size", 14)
eyebrow.add_theme_color_override("font_color", Brand.TEAL)
mode_view.add_child(eyebrow)
var title := Label.new()
title.text = "Choose a study mode"
Style.set_label(title, 34, Style.TEXT)
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 34)
mode_view.add_child(title)
var subtitle := Label.new()
subtitle.text = "Build clinical judgment through focused questions. Additional modes are in development."
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(subtitle, 17, Style.MUTED)
subtitle.add_theme_font_override("font", Brand.font)
subtitle.add_theme_font_size_override("font_size", 17)
subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
subtitle.set_meta("medmind_role", "muted")
themed_controls.append(subtitle)
mode_view.add_child(subtitle)
mode_grid = GridContainer.new()
@@ -136,106 +146,410 @@ func _build_mode_view() -> void:
mode_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
mode_view.add_child(mode_grid)
question_bank_button = Button.new()
question_bank_button.text = "Question Bank\nAdaptive core run | Available"
question_bank_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
question_bank_button.custom_minimum_size = Vector2(360, 150)
question_bank_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
question_bank_button.add_theme_font_size_override("font_size", 20)
Style.style_primary_button(question_bank_button)
var question_card := _add_mode_card(
"Question Bank",
"Adaptive core run",
"Available",
"Recommended",
"Targeted practice with adaptive learning\nto strengthen your weaknesses.",
"clipboard-list",
true,
Brand.TEAL)
question_bank_button = question_card.action
question_bank_button.pressed.connect(_select_question_bank)
mode_grid.add_child(question_bank_button)
var dynamic_button := Button.new()
dynamic_button.text = "Dynamic Questions\nAI-generated practice | Coming Soon"
dynamic_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
dynamic_button.custom_minimum_size = Vector2(360, 150)
dynamic_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
dynamic_button.add_theme_font_size_override("font_size", 20)
dynamic_button.disabled = true
mode_grid.add_child(dynamic_button)
simulation_button = Button.new()
simulation_button.text = "Step 3 Day 1 Simulation\n232 questions | Checking readiness"
simulation_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
simulation_button.custom_minimum_size = Vector2(360, 150)
simulation_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
simulation_button.add_theme_font_size_override("font_size", 20)
Style.style_primary_button(simulation_button)
simulation_button.disabled = true
simulation_button.pressed.connect(_select_simulation)
mode_grid.add_child(simulation_button)
ccs_button = Button.new()
ccs_button.text = "CCS Cases\nInteractive case simulations | Available"
ccs_button.alignment = HORIZONTAL_ALIGNMENT_LEFT
ccs_button.custom_minimum_size = Vector2(360, 150)
ccs_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
ccs_button.add_theme_font_size_override("font_size", 20)
Style.style_primary_button(ccs_button)
ccs_button.pressed.connect(_select_ccs)
mode_grid.add_child(ccs_button)
ccs_status_label = Label.new()
ccs_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(ccs_status_label, 15, Style.MUTED)
mode_view.add_child(ccs_status_label)
simulation_status_label = Label.new()
simulation_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(simulation_status_label, 15, Style.MUTED)
mode_view.add_child(simulation_status_label)
simulation_preview_link = LinkButton.new()
simulation_preview_link.text = "Temporary: preview completed exam results"
simulation_preview_link.tooltip_text = "SUPER only; generates a nonpersistent simulated result"
simulation_preview_link.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
simulation_preview_link.add_theme_color_override("font_color", Style.CYAN)
simulation_preview_link.add_theme_color_override("font_hover_color", Style.MAGENTA)
simulation_preview_link.pressed.connect(_preview_simulation_results)
mode_view.add_child(simulation_preview_link)
_refresh_simulation_preview_visibility()
bank_status_label = Label.new()
bank_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(bank_status_label, 16, Style.CYAN)
mode_view.add_child(bank_status_label)
bank_error_label = Label.new()
bank_error_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Style.set_label(bank_error_label, 15, Style.RED)
mode_view.add_child(bank_error_label)
retry_button = Button.new()
retry_button.text = "Retry Question Bank"
retry_button.custom_minimum_size = Vector2(220, 46)
retry_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_chip(retry_button, Style.AMBER)
bank_status_label = question_card.status
bank_error_label = question_card.error
retry_button = question_card.retry
retry_button.pressed.connect(_retry_question_bank)
mode_view.add_child(retry_button)
_add_mode_card(
"Dynamic Questions",
"AI-generated practice",
"Coming Soon",
"Coming Soon",
"Endless practice with AI-generated questions\ntailored to your performance.",
"medmind",
false,
Brand.PURPLE)
var ccs_card := _add_mode_card(
"CCS Cases",
"Interactive case simulations",
"Available",
"Available",
"Practice real-world clinical scenarios and\nimprove your management skills.",
"stethoscope",
true,
Brand.TEAL)
ccs_button = ccs_card.action
ccs_status_label = ccs_card.status
ccs_button.pressed.connect(_select_ccs)
var spacer := Control.new()
spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
mode_view.add_child(spacer)
var simulation_note := HBoxContainer.new()
simulation_note.add_theme_constant_override("separation", 8)
mode_view.add_child(simulation_note)
var info := Label.new()
info.text = "i"
info.add_theme_font_size_override("font_size", 17)
info.add_theme_color_override("font_color", Brand.SLATE)
simulation_note.add_child(info)
var note := RichTextLabel.new()
note.bbcode_enabled = true
note.fit_content = true
note.scroll_active = false
note.text = "Full-length, 12-block simulation is available in the [url=exams][color=#7c3aed]Exams[/color][/url] section."
note.add_theme_font_override("normal_font", Brand.font)
note.add_theme_font_size_override("normal_font_size", 15)
note.add_theme_color_override("default_color", Brand.SLATE)
note.set_meta("medmind_role", "muted_rich")
themed_controls.append(note)
note.meta_clicked.connect(func(_meta: Variant): main.show_screen("simulation_history"))
simulation_note.add_child(note)
func _add_exam_option(option_name: String, description: String, available: bool) -> void:
var button := Button.new()
button.text = "%s\n%s | %s" % [option_name, description, "Available" if available else "Coming Soon"]
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.custom_minimum_size = Vector2(360, 150)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.size_flags_vertical = Control.SIZE_EXPAND_FILL
button.add_theme_font_size_override("font_size", 20)
button.disabled = not available
func _add_mode_card(
title_text: String,
subtitle_text: String,
availability_text: String,
badge_text: String,
description: String,
icon_name: String,
enabled: bool,
accent: Color
) -> Dictionary:
var dark := UIThemeService.is_dark_mode()
var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE
var badge_accent := Brand.SUCCESS if enabled else accent
var card := PanelContainer.new()
card.custom_minimum_size = Vector2(430, 210)
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent))
card.set_meta("medmind_role", "mode_card")
card.set_meta("medmind_enabled", enabled)
card.set_meta("medmind_accent", accent)
themed_controls.append(card)
mode_grid.add_child(card)
var content := VBoxContainer.new()
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_theme_constant_override("separation", 14)
card.add_child(content)
var header := HBoxContainer.new()
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
header.add_theme_constant_override("separation", 20)
content.add_child(header)
var icon_panel := PanelContainer.new()
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon_panel.custom_minimum_size = Vector2(72, 72)
icon_panel.add_theme_stylebox_override("panel", _circle_box(visual_accent, 44))
icon_panel.set_meta("medmind_role", "icon_circle")
icon_panel.set_meta("medmind_accent", accent)
icon_panel.set_meta("medmind_enabled", enabled)
themed_controls.append(icon_panel)
header.add_child(icon_panel)
var icon := TextureRect.new()
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.texture = preload("res://assets/branding/medmind-icon.png") if icon_name == "medmind" else load("res://assets/icons/lucide/%s.svg" % icon_name)
icon.material = Brand.logo_cutout_material() if icon_name == "medmind" else null
icon.modulate = visual_accent
icon.set_meta("medmind_role", "accent_icon")
icon.set_meta("medmind_accent", accent)
icon.set_meta("medmind_enabled", enabled)
themed_controls.append(icon)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_panel.add_child(icon)
var details := VBoxContainer.new()
details.mouse_filter = Control.MOUSE_FILTER_IGNORE
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.alignment = BoxContainer.ALIGNMENT_CENTER
details.add_theme_constant_override("separation", 5)
header.add_child(details)
var title := Label.new()
title.text = title_text
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 22)
details.add_child(title)
var subtitle := Label.new()
subtitle.text = "%s | %s" % [subtitle_text, availability_text]
subtitle.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
subtitle.add_theme_font_override("font", Brand.font)
subtitle.add_theme_font_size_override("font_size", 16)
subtitle.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
subtitle.set_meta("medmind_role", "muted")
themed_controls.append(subtitle)
details.add_child(subtitle)
var badge := Label.new()
badge.text = "● %s" % badge_text
badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
badge.add_theme_font_override("font", Brand.font)
badge.add_theme_font_size_override("font_size", 13)
badge.add_theme_color_override("font_color", badge_accent)
badge.add_theme_stylebox_override("normal", _badge_box(badge_accent))
badge.set_meta("medmind_role", "badge")
badge.set_meta("medmind_accent", accent)
badge.set_meta("medmind_enabled", enabled)
themed_controls.append(badge)
details.add_child(badge)
content.add_child(HSeparator.new())
var bottom := HBoxContainer.new()
bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE
bottom.add_theme_constant_override("separation", 16)
content.add_child(bottom)
var description_label := Label.new()
description_label.text = description
description_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
description_label.add_theme_font_override("font", Brand.font)
description_label.add_theme_font_size_override("font_size", 16)
description_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
description_label.set_meta("medmind_role", "muted")
themed_controls.append(description_label)
bottom.add_child(description_label)
var arrow_panel := PanelContainer.new()
arrow_panel.custom_minimum_size = Vector2(48, 48)
arrow_panel.add_theme_stylebox_override("panel", _circle_box(visual_accent if enabled else Brand.SLATE, 24, enabled))
arrow_panel.set_meta("medmind_role", "arrow_circle")
arrow_panel.set_meta("medmind_accent", accent if enabled else Brand.SLATE)
arrow_panel.set_meta("medmind_enabled", enabled)
themed_controls.append(arrow_panel)
bottom.add_child(arrow_panel)
var arrow := TextureRect.new()
arrow.texture = preload("res://assets/icons/lucide/chevron-right.svg")
arrow.modulate = Color.WHITE if UIThemeService.is_dark_mode() else Brand.ROYAL_BLUE
arrow.set_meta("medmind_role", "arrow_icon")
themed_controls.append(arrow)
arrow.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
arrow.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
arrow_panel.add_child(arrow)
var status := Label.new()
status.visible = false
status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
status.add_theme_font_override("font", Brand.font)
status.add_theme_font_size_override("font_size", 14)
status.add_theme_color_override("font_color", accent)
content.add_child(status)
var error := Label.new()
error.visible = false
error.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
error.add_theme_font_override("font", Brand.font)
error.add_theme_font_size_override("font_size", 14)
error.add_theme_color_override("font_color", Style.RED)
content.add_child(error)
var retry := Button.new()
retry.text = "Retry"
retry.visible = false
retry.custom_minimum_size = Vector2(110, 38)
retry.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
Style.style_chip(retry, Style.AMBER)
content.add_child(retry)
var action := Button.new()
action.text = ""
action.flat = true
action.disabled = not enabled
action.tooltip_text = title_text if enabled else "Coming Soon"
action.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
card.add_child(action)
card.move_child(action, 0)
return {"action": action, "status": status, "error": error, "retry": retry}
func _mode_card_box(enabled: bool, accent: Color) -> StyleBoxFlat:
var dark := UIThemeService.is_dark_mode()
var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE
var box := Brand.content_card_box(dark, enabled, visual_accent)
box.content_margin_left = 24
box.content_margin_right = 24
box.content_margin_top = 20
box.content_margin_bottom = 18
return box
func _circle_box(accent: Color, radius: int, filled: bool = false) -> StyleBoxFlat:
var box := Brand.icon_circle_box(UIThemeService.is_dark_mode(), accent, radius, filled)
box.content_margin_left = 22 if radius > 30 else 12
box.content_margin_right = box.content_margin_left
box.content_margin_top = box.content_margin_left
box.content_margin_bottom = box.content_margin_left
return box
func _badge_box(accent: Color) -> StyleBoxFlat:
return Brand.badge_box(UIThemeService.is_dark_mode(), accent)
func _style_back_bar(button: Button) -> void:
var dark := UIThemeService.is_dark_mode()
var normal := StyleBoxFlat.new()
normal.bg_color = Color("0b1427aa") if dark else Color("ffffffdd")
normal.border_color = Color("33415d") if dark else Color("d4def0")
normal.set_border_width_all(1)
normal.set_corner_radius_all(8)
normal.content_margin_left = 16
var hover := normal.duplicate() as StyleBoxFlat
hover.border_color = Brand.SKY_BLUE
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
func _add_exam_option(option_name: String, title_text: String, description: String, icon_name: String, available: bool) -> void:
var card := PanelContainer.new()
card.custom_minimum_size = Vector2(470, 178)
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
card.add_theme_stylebox_override("panel", _exam_card_box(available))
card.set_meta("medmind_role", "exam_card")
card.set_meta("medmind_enabled", available)
themed_controls.append(card)
if available:
Style.style_primary_button(button)
button.pressed.connect(func(): _show_stage("step3"))
exam_grid.add_child(button)
exam_grid.add_child(card)
else:
exam_top_row.add_child(card)
var content := VBoxContainer.new()
content.mouse_filter = Control.MOUSE_FILTER_IGNORE
content.add_theme_constant_override("separation", 10)
card.add_child(content)
var header := HBoxContainer.new()
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
header.add_theme_constant_override("separation", 20)
content.add_child(header)
var icon_panel := PanelContainer.new()
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon_panel.custom_minimum_size = Vector2(78, 78)
var icon_box := Brand.icon_circle_box(UIThemeService.is_dark_mode(), Brand.ROYAL_BLUE if available else Brand.PURPLE, 39)
icon_box.content_margin_left = 20
icon_box.content_margin_right = 20
icon_box.content_margin_top = 20
icon_box.content_margin_bottom = 20
icon_panel.add_theme_stylebox_override("panel", icon_box)
icon_panel.set_meta("medmind_role", "exam_icon_circle")
icon_panel.set_meta("medmind_enabled", available)
themed_controls.append(icon_panel)
header.add_child(icon_panel)
var icon := TextureRect.new()
icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
icon.texture = load("res://assets/icons/lucide/%s.svg" % icon_name)
icon.modulate = Brand.ROYAL_BLUE if available else Brand.PURPLE
icon.set_meta("medmind_role", "exam_icon")
icon.set_meta("medmind_enabled", available)
themed_controls.append(icon)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon_panel.add_child(icon)
var details := VBoxContainer.new()
details.mouse_filter = Control.MOUSE_FILTER_IGNORE
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.alignment = BoxContainer.ALIGNMENT_CENTER
details.add_theme_constant_override("separation", 8)
header.add_child(details)
var eyebrow := Label.new()
eyebrow.mouse_filter = Control.MOUSE_FILTER_IGNORE
eyebrow.text = option_name
eyebrow.add_theme_font_override("font", Brand.font)
eyebrow.add_theme_font_size_override("font_size", 16)
eyebrow.add_theme_color_override("font_color", Brand.ROYAL_BLUE)
details.add_child(eyebrow)
var title := Label.new()
title.mouse_filter = Control.MOUSE_FILTER_IGNORE
title.text = title_text
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title.add_theme_font_override("font", Brand.font)
title.add_theme_font_size_override("font_size", 22)
details.add_child(title)
var status := Label.new()
status.mouse_filter = Control.MOUSE_FILTER_IGNORE
status.text = "Available" if available else "Coming Soon"
status.add_theme_font_override("font", Brand.font)
status.add_theme_font_size_override("font_size", 14)
status.add_theme_color_override("font_color", Brand.SUCCESS if available else Brand.PURPLE)
status.add_theme_stylebox_override("normal", Brand.badge_box(UIThemeService.is_dark_mode(), Brand.SUCCESS if available else Brand.PURPLE))
status.set_meta("medmind_role", "exam_badge")
status.set_meta("medmind_enabled", available)
themed_controls.append(status)
details.add_child(status)
var arrow := TextureRect.new()
arrow.mouse_filter = Control.MOUSE_FILTER_IGNORE
arrow.texture = preload("res://assets/icons/lucide/chevron-right.svg")
arrow.modulate = Brand.TEAL if UIThemeService.is_dark_mode() and available else Brand.ROYAL_BLUE
arrow.set_meta("medmind_role", "exam_arrow")
arrow.set_meta("medmind_enabled", available)
themed_controls.append(arrow)
arrow.custom_minimum_size = Vector2(32, 32)
arrow.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
arrow.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
header.add_child(arrow)
card.tooltip_text = description
var action := Button.new()
action.text = ""
action.flat = true
action.disabled = not available
action.tooltip_text = "Open Step 3 study modes" if available else "Coming Soon"
action.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
if available:
action.pressed.connect(func(): _show_stage("step3"))
card.add_child(action)
func _exam_card_box(available: bool) -> StyleBoxFlat:
var box := Brand.content_card_box(UIThemeService.is_dark_mode(), available, Brand.ROYAL_BLUE)
box.content_margin_left = 24
box.content_margin_right = 24
box.content_margin_top = 20
box.content_margin_bottom = 20
return box
func refresh_theme_controls() -> void:
var dark := UIThemeService.is_dark_mode()
for control in themed_controls:
if not is_instance_valid(control):
continue
var role := str(control.get_meta("medmind_role", ""))
var accent: Color = control.get_meta("medmind_accent", Brand.ROYAL_BLUE)
var enabled := bool(control.get_meta("medmind_enabled", false))
var visual_accent := accent if dark or not enabled else Brand.ROYAL_BLUE
match role:
"mode_card": control.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent))
"exam_card": control.add_theme_stylebox_override("panel", _exam_card_box(enabled))
"icon_circle": control.add_theme_stylebox_override("panel", _circle_box(visual_accent, 36))
"arrow_circle": control.add_theme_stylebox_override("panel", _circle_box(visual_accent, 24, enabled))
"exam_icon_circle": control.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.ROYAL_BLUE if enabled else Brand.PURPLE, 39))
"badge":
var badge_accent := Brand.SUCCESS if enabled else accent
control.add_theme_stylebox_override("normal", _badge_box(badge_accent))
control.add_theme_color_override("font_color", badge_accent)
"exam_badge": control.add_theme_stylebox_override("normal", Brand.badge_box(dark, Brand.SUCCESS if enabled else Brand.PURPLE))
"muted": control.add_theme_color_override("font_color", Brand.muted_color(dark))
"muted_rich": control.add_theme_color_override("default_color", Brand.muted_color(dark))
"accent_icon": control.modulate = visual_accent
"exam_icon": control.modulate = Brand.ROYAL_BLUE if enabled else Brand.PURPLE
"arrow_icon": control.modulate = Color.WHITE if dark else Brand.ROYAL_BLUE
"exam_arrow": control.modulate = Brand.TEAL if dark and enabled else Brand.ROYAL_BLUE
"back_bar": _style_back_bar(control as Button)
for node in [exam_view, mode_view]:
if node != null:
for child in node.find_children("*", "Label", true, false):
var label := child as Label
if not label.has_meta("medmind_role"):
label.add_theme_color_override("font_color", Brand.text_color(dark))
func _show_stage(stage_name: String) -> void:
current_stage = stage_name
stage_changed.emit(stage_name)
pending_question_bank_launch = false
if exam_view != null:
exam_view.visible = stage_name == "exam"
@@ -256,6 +570,7 @@ func _select_ccs() -> void:
pending_ccs_launch = true
ccs_button.disabled = true
ccs_status_label.text = "Selecting a timed case..."
ccs_status_label.visible = true
CCSService.create_attempt("", "timed")
@@ -265,6 +580,7 @@ func _on_ccs_attempt_changed() -> void:
pending_ccs_launch = false
ccs_button.disabled = false
ccs_status_label.text = ""
ccs_status_label.visible = false
main.show_screen("ccs_player")
@@ -274,6 +590,7 @@ func _on_ccs_failed(message: String) -> void:
pending_ccs_launch = false
ccs_button.disabled = false
ccs_status_label.text = message
ccs_status_label.visible = true
func _retry_question_bank() -> void:
@@ -294,72 +611,6 @@ func _on_questions_load_failed(_message: String) -> void:
_refresh_bank_state()
func _select_simulation() -> void:
pending_simulation_launch = true
simulation_button.disabled = true
if bool(ExamSimulationService.readiness.get("hasActiveSimulation", false)):
ExamSimulationService.load_active()
else:
ExamSimulationService.create_simulation()
func _preview_simulation_results() -> void:
pending_simulation_launch = true
simulation_preview_link.disabled = true
simulation_status_label.text = "Generating simulated exam results..."
ExamSimulationService.load_results_preview()
func _refresh_simulation_preview_visibility() -> void:
if simulation_preview_link != null:
simulation_preview_link.visible = false
func _on_simulation_state_changed() -> void:
_refresh_simulation_state()
if not pending_simulation_launch or ExamSimulationService.simulation.is_empty():
return
pending_simulation_launch = false
var state := str(ExamSimulationService.simulation.get("state", ""))
if state == "block_active":
_launch_simulation_block()
elif state == "completed":
main.show_screen("simulation_results", ExamSimulationService.simulation)
else:
main.show_screen("simulation_break")
func _launch_simulation_block() -> void:
BlockSessionService.start_simulation_block(
ExamSimulationService.get_current_questions(),
int(ExamSimulationService.simulation.get("currentBlock", 0)),
int(ExamSimulationService.simulation.get("totalBlocks", 12)),
ExamSimulationService.get_block_deadline_unix())
BlockSessionService.restore_simulation_responses(ExamSimulationService.simulation.get("responses", []))
main.show_screen("player")
func _on_simulation_failed(message: String) -> void:
pending_simulation_launch = false
_refresh_simulation_state()
if simulation_preview_link != null:
simulation_preview_link.disabled = false
if simulation_status_label != null:
simulation_status_label.text = message
func _refresh_simulation_state() -> void:
if simulation_button == null:
return
var simulation_ready := bool(ExamSimulationService.readiness.get("ready", false))
var active := bool(ExamSimulationService.readiness.get("hasActiveSimulation", false)) or not ExamSimulationService.simulation.is_empty() and str(ExamSimulationService.simulation.get("state", "")) in ["between_blocks", "block_active"]
var available := int(ExamSimulationService.readiness.get("availableQuestions", 0))
simulation_button.disabled = pending_simulation_launch or (not simulation_ready and not active)
simulation_button.text = "Resume Step 3 Day 1 Simulation\nProgress is saved between blocks" if active else "Step 3 Day 1 Simulation\n232 questions | %s" % ("Available" if simulation_ready else "%d/232 ready" % available)
if simulation_status_label != null and not pending_simulation_launch:
simulation_status_label.text = "Full-length, 12-block simulation. Predicted scores are provisional and exclude CCS." if simulation_ready or active else "The simulation unlocks when at least 232 published questions are available."
func _refresh_bank_state() -> void:
if question_bank_button == null:
return
@@ -368,6 +619,7 @@ func _refresh_bank_state() -> void:
retry_button.visible = state == "failed" or state == "retrying"
retry_button.disabled = state == "retrying"
bank_error_label.text = QuestionBankService.last_error if state == "failed" or state == "retrying" else ""
bank_error_label.visible = not bank_error_label.text.is_empty()
if pending_question_bank_launch or state == "loading":
bank_status_label.text = "Preparing your question bank..."
elif state == "retrying":
@@ -378,6 +630,7 @@ func _refresh_bank_state() -> void:
bank_status_label.text = "The question bank could not be loaded."
else:
bank_status_label.text = ""
bank_status_label.visible = not bank_status_label.text.is_empty()
func _start_question_bank() -> void:
@@ -392,7 +645,7 @@ func _start_question_bank() -> void:
func _refresh_grid_columns() -> void:
var column_count := 1 if size.x < 820.0 else 2
if exam_grid != null:
exam_grid.columns = column_count
if exam_top_row != null:
exam_top_row.vertical = column_count == 1
if mode_grid != null:
mode_grid.columns = column_count
+10 -5
View File
@@ -23,12 +23,12 @@ const DARK_PALETTE := {
"red": Color("ff5f6d"), "border": Color("33415c")
}
const LIGHT_PALETTE := {
"background": Color("f4f7fb"), "surface": Color("ffffff"),
"surface_dark": Color("e8edf5"), "surface_light": Color("dce5f0"),
"text": Color("172033"), "muted": Color("59677c"),
"background": Color("f1f5f9"), "surface": Color("ffffff"),
"surface_dark": Color("f8fafc"), "surface_light": Color("eef2f7"),
"text": Color("0f172a"), "muted": Color("64748b"),
"cyan": Color("007a99"), "lime": Color("347a18"),
"magenta": Color("a32187"), "amber": Color("986400"),
"red": Color("b42335"), "border": Color("aeb9c8")
"red": Color("b42335"), "border": Color("dbe3ef")
}
const CUSTOM_DARK_TO_LIGHT := {
Color("111a2a"): Color("f8fafc"), Color("172337"): Color("fffffe"),
@@ -42,7 +42,12 @@ const CUSTOM_DARK_TO_LIGHT := {
Color("202637"): Color("e5e9ef"), Color("2c3445"): Color("c8d0dc"),
Color("232b3d"): Color("e1e7ef"), Color("303b53"): Color("b7c2d0"),
Color("596982"): Color("66758a"), Color("1f7a3a"): Color("dff4e5"),
Color("8f1d2c"): Color("f8dce1")
Color("8f1d2c"): Color("f8dce1"), Color("071224"): Color("ffffff"),
Color("1c2b49"): Color("dbe3ef"), Color("14255b"): Color("e8edff"),
Color("172d68"): Color("d8e5ff"), Color("07192c"): Color("ffffff"),
Color("11182e"): Color("f8faff"), Color("2a365a"): Color("cbd6e8"),
Color("0c1838"): Color("eef3ff"), Color("24366d"): Color("cbd6e8"),
Color("c1cbdd"): Color("53627a")
}
+24
View File
@@ -0,0 +1,24 @@
extends SceneTree
const NavigationRoutes := preload("res://scripts/ui/navigation_routes.gd")
var failures := 0
func _initialize() -> void:
_assert(NavigationRoutes.key_for_screen("selection", "exam") == "home", "exam selection should activate Home")
_assert(NavigationRoutes.key_for_screen("selection", "step3") == "practice", "Step 3 modes should activate Practice")
_assert(NavigationRoutes.key_for_screen("setup") == "practice", "block setup should activate Practice")
_assert(NavigationRoutes.key_for_screen("simulation_results") == "exams", "simulation results should activate Exams")
_assert(NavigationRoutes.key_for_screen("ccs_review") == "ccs", "CCS review should activate CCS Cases")
_assert(NavigationRoutes.key_for_screen("profile") == "performance", "profile should activate Performance")
_assert(NavigationRoutes.key_for_screen("map") == "study_plan", "study map should activate Study Plan")
_assert(NavigationRoutes.key_for_screen("settings") == "settings", "settings should activate Settings")
quit(1 if failures > 0 else 0)
func _assert(condition: bool, message: String) -> void:
if condition:
return
failures += 1
push_error(message)
+1
View File
@@ -0,0 +1 @@
uid://d1shellnavigationtest
+4
View File
@@ -9,6 +9,10 @@ func _initialize() -> void:
_assert(history._action_for_state("block_active") == "resume", "active blocks should resume")
_assert(history._action_for_state("between_blocks") == "resume", "between-block simulations should resume")
_assert(history._action_for_state("abandoned") == "none", "abandoned simulations should be read-only")
_assert(history.has_active_simulation({"hasActiveSimulation": true}, {}), "readiness should identify an active simulation")
_assert(history.has_active_simulation({}, {"state": "block_active"}), "a loaded active block should be resumable")
_assert(history.has_active_simulation({}, {"state": "between_blocks"}), "a loaded between-block simulation should be resumable")
_assert(not history.has_active_simulation({}, {"state": "completed"}), "completed simulations should not appear active")
quit(1 if failures > 0 else 0)
+7
View File
@@ -1,6 +1,7 @@
extends SceneTree
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
var failures := 0
@@ -21,6 +22,12 @@ func _initialize() -> void:
_assert(root.theme.get_color("default_color", "RichTextLabel").is_equal_approx(Style.TEXT), "rich text should use the light text color")
var light_box := panel.get_theme_stylebox("panel") as StyleBoxFlat
_assert(light_box.bg_color.is_equal_approx(Color("fffffe")), "custom panel surface should switch to light mode")
_assert(Brand.background_color(false).is_equal_approx(Color("f1f5f9")), "MedMind light background should match the style guide")
var light_card := Brand.content_card_box(false, true, Brand.ROYAL_BLUE)
_assert(light_card.bg_color.is_equal_approx(Color.WHITE), "MedMind light cards should use white surfaces")
_assert(light_card.border_color.is_equal_approx(Brand.ROYAL_BLUE), "active light cards should use the primary blue border")
var light_field := Brand.field_box(false)
_assert(light_field.border_color.is_equal_approx(Brand.LIGHT_BORDER), "light fields should use the semantic cool-gray border")
Style.set_dark_mode(true)
Style.refresh_tree(root, false)