diff --git a/assets/icons/lucide/LICENSE b/assets/icons/lucide/LICENSE new file mode 100644 index 0000000..718bb3f --- /dev/null +++ b/assets/icons/lucide/LICENSE @@ -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. diff --git a/assets/icons/lucide/bookmark.svg b/assets/icons/lucide/bookmark.svg new file mode 100644 index 0000000..4c62f32 --- /dev/null +++ b/assets/icons/lucide/bookmark.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/lucide/bookmark.svg.import b/assets/icons/lucide/bookmark.svg.import new file mode 100644 index 0000000..883d07f --- /dev/null +++ b/assets/icons/lucide/bookmark.svg.import @@ -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 diff --git a/assets/icons/lucide/calendar-days.svg b/assets/icons/lucide/calendar-days.svg new file mode 100644 index 0000000..36750ae --- /dev/null +++ b/assets/icons/lucide/calendar-days.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/assets/icons/lucide/calendar-days.svg.import b/assets/icons/lucide/calendar-days.svg.import new file mode 100644 index 0000000..9a06303 --- /dev/null +++ b/assets/icons/lucide/calendar-days.svg.import @@ -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 diff --git a/assets/icons/lucide/chart-no-axes-column-increasing.svg b/assets/icons/lucide/chart-no-axes-column-increasing.svg new file mode 100644 index 0000000..32ae477 --- /dev/null +++ b/assets/icons/lucide/chart-no-axes-column-increasing.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/lucide/chart-no-axes-column-increasing.svg.import b/assets/icons/lucide/chart-no-axes-column-increasing.svg.import new file mode 100644 index 0000000..3056f3c --- /dev/null +++ b/assets/icons/lucide/chart-no-axes-column-increasing.svg.import @@ -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 diff --git a/assets/icons/lucide/chevron-down.svg b/assets/icons/lucide/chevron-down.svg new file mode 100644 index 0000000..e997774 --- /dev/null +++ b/assets/icons/lucide/chevron-down.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/lucide/chevron-down.svg.import b/assets/icons/lucide/chevron-down.svg.import new file mode 100644 index 0000000..61bc15c --- /dev/null +++ b/assets/icons/lucide/chevron-down.svg.import @@ -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 diff --git a/assets/icons/lucide/chevron-right.svg b/assets/icons/lucide/chevron-right.svg new file mode 100644 index 0000000..c2bb019 --- /dev/null +++ b/assets/icons/lucide/chevron-right.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/lucide/chevron-right.svg.import b/assets/icons/lucide/chevron-right.svg.import new file mode 100644 index 0000000..725cd02 --- /dev/null +++ b/assets/icons/lucide/chevron-right.svg.import @@ -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 diff --git a/assets/icons/lucide/circle-check.svg b/assets/icons/lucide/circle-check.svg new file mode 100644 index 0000000..89c038a --- /dev/null +++ b/assets/icons/lucide/circle-check.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/circle-check.svg.import b/assets/icons/lucide/circle-check.svg.import new file mode 100644 index 0000000..1d7d4b6 --- /dev/null +++ b/assets/icons/lucide/circle-check.svg.import @@ -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 diff --git a/assets/icons/lucide/clipboard-list.svg b/assets/icons/lucide/clipboard-list.svg new file mode 100644 index 0000000..4e7dc0a --- /dev/null +++ b/assets/icons/lucide/clipboard-list.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/assets/icons/lucide/clipboard-list.svg.import b/assets/icons/lucide/clipboard-list.svg.import new file mode 100644 index 0000000..19571bc --- /dev/null +++ b/assets/icons/lucide/clipboard-list.svg.import @@ -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 diff --git a/assets/icons/lucide/clipboard-plus.svg b/assets/icons/lucide/clipboard-plus.svg new file mode 100644 index 0000000..e8eacfe --- /dev/null +++ b/assets/icons/lucide/clipboard-plus.svg @@ -0,0 +1,16 @@ + + + + + + diff --git a/assets/icons/lucide/clipboard-plus.svg.import b/assets/icons/lucide/clipboard-plus.svg.import new file mode 100644 index 0000000..c27140d --- /dev/null +++ b/assets/icons/lucide/clipboard-plus.svg.import @@ -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 diff --git a/assets/icons/lucide/clock-3.svg b/assets/icons/lucide/clock-3.svg new file mode 100644 index 0000000..aebda41 --- /dev/null +++ b/assets/icons/lucide/clock-3.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/clock-3.svg.import b/assets/icons/lucide/clock-3.svg.import new file mode 100644 index 0000000..becb887 --- /dev/null +++ b/assets/icons/lucide/clock-3.svg.import @@ -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 diff --git a/assets/icons/lucide/copy.svg b/assets/icons/lucide/copy.svg new file mode 100644 index 0000000..3566f7f --- /dev/null +++ b/assets/icons/lucide/copy.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/copy.svg.import b/assets/icons/lucide/copy.svg.import new file mode 100644 index 0000000..ad0d162 --- /dev/null +++ b/assets/icons/lucide/copy.svg.import @@ -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 diff --git a/assets/icons/lucide/house.svg b/assets/icons/lucide/house.svg new file mode 100644 index 0000000..77310af --- /dev/null +++ b/assets/icons/lucide/house.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/house.svg.import b/assets/icons/lucide/house.svg.import new file mode 100644 index 0000000..1454a0d --- /dev/null +++ b/assets/icons/lucide/house.svg.import @@ -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 diff --git a/assets/icons/lucide/log-out.svg b/assets/icons/lucide/log-out.svg new file mode 100644 index 0000000..b9d6055 --- /dev/null +++ b/assets/icons/lucide/log-out.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/lucide/log-out.svg.import b/assets/icons/lucide/log-out.svg.import new file mode 100644 index 0000000..4e8efe3 --- /dev/null +++ b/assets/icons/lucide/log-out.svg.import @@ -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 diff --git a/assets/icons/lucide/menu.svg b/assets/icons/lucide/menu.svg new file mode 100644 index 0000000..fabb944 --- /dev/null +++ b/assets/icons/lucide/menu.svg @@ -0,0 +1,15 @@ + + + + + diff --git a/assets/icons/lucide/menu.svg.import b/assets/icons/lucide/menu.svg.import new file mode 100644 index 0000000..4702bec --- /dev/null +++ b/assets/icons/lucide/menu.svg.import @@ -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 diff --git a/assets/icons/lucide/message-square.svg b/assets/icons/lucide/message-square.svg new file mode 100644 index 0000000..1edb436 --- /dev/null +++ b/assets/icons/lucide/message-square.svg @@ -0,0 +1,13 @@ + + + diff --git a/assets/icons/lucide/message-square.svg.import b/assets/icons/lucide/message-square.svg.import new file mode 100644 index 0000000..11b1aa5 --- /dev/null +++ b/assets/icons/lucide/message-square.svg.import @@ -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 diff --git a/assets/icons/lucide/microscope.svg b/assets/icons/lucide/microscope.svg new file mode 100644 index 0000000..855f8fd --- /dev/null +++ b/assets/icons/lucide/microscope.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/assets/icons/lucide/microscope.svg.import b/assets/icons/lucide/microscope.svg.import new file mode 100644 index 0000000..adcc8ae --- /dev/null +++ b/assets/icons/lucide/microscope.svg.import @@ -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 diff --git a/assets/icons/lucide/notebook-pen.svg b/assets/icons/lucide/notebook-pen.svg new file mode 100644 index 0000000..1820541 --- /dev/null +++ b/assets/icons/lucide/notebook-pen.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/assets/icons/lucide/notebook-pen.svg.import b/assets/icons/lucide/notebook-pen.svg.import new file mode 100644 index 0000000..caeb4c8 --- /dev/null +++ b/assets/icons/lucide/notebook-pen.svg.import @@ -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 diff --git a/assets/icons/lucide/pencil.svg b/assets/icons/lucide/pencil.svg new file mode 100644 index 0000000..9a0c8cc --- /dev/null +++ b/assets/icons/lucide/pencil.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/pencil.svg.import b/assets/icons/lucide/pencil.svg.import new file mode 100644 index 0000000..42e59f5 --- /dev/null +++ b/assets/icons/lucide/pencil.svg.import @@ -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 diff --git a/assets/icons/lucide/settings.svg b/assets/icons/lucide/settings.svg new file mode 100644 index 0000000..b371652 --- /dev/null +++ b/assets/icons/lucide/settings.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/settings.svg.import b/assets/icons/lucide/settings.svg.import new file mode 100644 index 0000000..13a0c46 --- /dev/null +++ b/assets/icons/lucide/settings.svg.import @@ -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 diff --git a/assets/icons/lucide/stethoscope.svg b/assets/icons/lucide/stethoscope.svg new file mode 100644 index 0000000..092e3f9 --- /dev/null +++ b/assets/icons/lucide/stethoscope.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/assets/icons/lucide/stethoscope.svg.import b/assets/icons/lucide/stethoscope.svg.import new file mode 100644 index 0000000..d147fa4 --- /dev/null +++ b/assets/icons/lucide/stethoscope.svg.import @@ -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 diff --git a/assets/icons/lucide/user-round.svg b/assets/icons/lucide/user-round.svg new file mode 100644 index 0000000..b8fd246 --- /dev/null +++ b/assets/icons/lucide/user-round.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/user-round.svg.import b/assets/icons/lucide/user-round.svg.import new file mode 100644 index 0000000..825f47d --- /dev/null +++ b/assets/icons/lucide/user-round.svg.import @@ -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 diff --git a/assets/icons/lucide/x.svg b/assets/icons/lucide/x.svg new file mode 100644 index 0000000..c8422eb --- /dev/null +++ b/assets/icons/lucide/x.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/lucide/x.svg.import b/assets/icons/lucide/x.svg.import new file mode 100644 index 0000000..35ddf0c --- /dev/null +++ b/assets/icons/lucide/x.svg.import @@ -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 diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 9277316..2602226 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -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: diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index d9197e1..f48f405 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -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: diff --git a/scripts/ui/medmind_style.gd b/scripts/ui/medmind_style.gd index 83dc1d7..6663323 100644 --- a/scripts/ui/medmind_style.gd +++ b/scripts/ui/medmind_style.gd @@ -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 diff --git a/scripts/ui/navigation_routes.gd b/scripts/ui/navigation_routes.gd new file mode 100644 index 0000000..d1bf1c9 --- /dev/null +++ b/scripts/ui/navigation_routes.gd @@ -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 diff --git a/scripts/ui/navigation_routes.gd.uid b/scripts/ui/navigation_routes.gd.uid new file mode 100644 index 0000000..16494ca --- /dev/null +++ b/scripts/ui/navigation_routes.gd.uid @@ -0,0 +1 @@ +uid://d1navigationroutes diff --git a/scripts/ui/settings_screen.gd b/scripts/ui/settings_screen.gd new file mode 100644 index 0000000..c225dc7 --- /dev/null +++ b/scripts/ui/settings_screen.gd @@ -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)) diff --git a/scripts/ui/settings_screen.gd.uid b/scripts/ui/settings_screen.gd.uid new file mode 100644 index 0000000..6885984 --- /dev/null +++ b/scripts/ui/settings_screen.gd.uid @@ -0,0 +1 @@ +uid://d1medmindsettings diff --git a/scripts/ui/simulation_history.gd b/scripts/ui/simulation_history.gd index a0dd327..81d1a1a 100644 --- a/scripts/ui/simulation_history.gd +++ b/scripts/ui/simulation_history.gd @@ -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"] diff --git a/scripts/ui/study_selector.gd b/scripts/ui/study_selector.gd index c4223df..98cea7b 100644 --- a/scripts/ui/study_selector.gd +++ b/scripts/ui/study_selector.gd @@ -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 diff --git a/scripts/ui/ui_style.gd b/scripts/ui/ui_style.gd index 73b142e..4ca2033 100644 --- a/scripts/ui/ui_style.gd +++ b/scripts/ui/ui_style.gd @@ -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") } diff --git a/tests/shell_navigation_test.gd b/tests/shell_navigation_test.gd new file mode 100644 index 0000000..0097d9a --- /dev/null +++ b/tests/shell_navigation_test.gd @@ -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) diff --git a/tests/shell_navigation_test.gd.uid b/tests/shell_navigation_test.gd.uid new file mode 100644 index 0000000..1d96d0a --- /dev/null +++ b/tests/shell_navigation_test.gd.uid @@ -0,0 +1 @@ +uid://d1shellnavigationtest diff --git a/tests/simulation_history_test.gd b/tests/simulation_history_test.gd index 474e5ce..1f4425a 100644 --- a/tests/simulation_history_test.gd +++ b/tests/simulation_history_test.gd @@ -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) diff --git a/tests/ui_theme_test.gd b/tests/ui_theme_test.gd index 8b4c7dd..2f0cb41 100644 --- a/tests/ui_theme_test.gd +++ b/tests/ui_theme_test.gd @@ -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)