Redesign study navigation and light theme
@@ -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.
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1navigationroutes
|
||||
@@ -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))
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1medmindsettings
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1shellnavigationtest
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||