diff --git a/src/common/css/lib/src/curity-theme.css b/src/common/css/lib/src/curity-theme.css index 572fdbf7..1e7aac86 100644 --- a/src/common/css/lib/src/curity-theme.css +++ b/src/common/css/lib/src/curity-theme.css @@ -27,7 +27,7 @@ --well-box-shadow: rgb(0 0 0 / 5%) 0 6px 24px 0, rgb(0 0 0 / 8%) 0 0 0 1px; --login-symbol-display: flex; - --login-symbol-size: 140px; + --login-symbol-size: 180px; --login-symbol-margin-top: 2rem; --login-symbol-margin-bottom: 2rem; --login-symbol-border-radius: 50%; diff --git a/src/login-web-app/previewer/Previewer.tsx b/src/login-web-app/previewer/Previewer.tsx index 88d30e55..b94e125b 100644 --- a/src/login-web-app/previewer/Previewer.tsx +++ b/src/login-web-app/previewer/Previewer.tsx @@ -19,6 +19,23 @@ import { formatNextStepData } from '../src/haapi-stepper/feature/stepper/data-fo import { HaapiStepperContext } from '../src/haapi-stepper/feature/stepper/HaapiStepperContext'; import { type HaapiStepperAPI } from '../src/haapi-stepper/feature/stepper/haapi-stepper.types'; import { HaapiErrorStep } from '../src/haapi-stepper/data-access'; +import { HaapiAppConfigContext } from '../src/shared/feature/app-config/HaapiAppConfigContext'; +import { HaapiAppConfig } from '../src/shared/feature/app-config/types'; + +const mockAppConfig: HaapiAppConfig = { + initialUrl: '', + haapi: { clientId: '', tokenEndpoint: '' }, + theme: { + logo: { + path: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgMTg4IDU0IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDxkZWZzPgogICAgICAgIDxwYXRoIGlkPSJwYXRoLTEiIGQ9Ik0wLDEuMDk2MzYzNjQgTDE3OS4zODgzNjcsMS4wOTYzNjM2NCBMMTc5LjM4ODM2NywzOC4xODE4MTgyIEwwLDM4LjE4MTgxODIgTDAsMS4wOTYzNjM2NCBaIj48L3BhdGg+CiAgICAgICAgPHBhdGggaWQ9InBhdGgtMyIgZD0iTTAsMS4wOTYzNjM2NCBMMTc5LjM4ODM2NywxLjA5NjM2MzY0IEwxNzkuMzg4MzY3LDM4LjE4MTgxODIgTDAsMzguMTgxODE4MiBMMCwxLjA5NjM2MzY0IFoiPjwvcGF0aD4KICAgIDwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPgogICAgICAgIDxnIGlkPSJjdXJpdHktbG9nbyIgc2tldGNoOnR5cGU9Ik1TQXJ0Ym9hcmRHcm91cCI+CiAgICAgICAgICAgIDxnIGlkPSJQYWdlLTEiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuMDAwMDAwLCA3LjAwMDAwMCkiPgogICAgICAgICAgICAgICAgPGcgaWQ9Ikdyb3VwLTMiPgogICAgICAgICAgICAgICAgICAgIDxtYXNrIGlkPSJtYXNrLTIiIHNrZXRjaDpuYW1lPSJDbGlwIDIiIGZpbGw9IndoaXRlIj4KICAgICAgICAgICAgICAgICAgICAgICAgPHVzZSB4bGluazpocmVmPSIjcGF0aC0xIj48L3VzZT4KICAgICAgICAgICAgICAgICAgICA8L21hc2s+CiAgICAgICAgICAgICAgICAgICAgPGcgaWQ9IkNsaXAtMiI+PC9nPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik01MS40NTM3MzQ3LDI0LjYxMjkwOTEgTDUwLjg3NTE2MzMsMjUuMTg3NDU0NSBDNDcuNjA3NjEyMiwyOC40MjIgNDMuNTAwNjczNSwzMC4yNzgzNjM2IDM5LjYwNDk1OTIsMzAuMjc4MzYzNiBDMzMuNDI0MzQ2OSwzMC4yNzgzNjM2IDI4LjkzOTA0MDgsMjUuODEyOTA5MSAyOC45MzkwNDA4LDE5LjY2MiBDMjguOTM5MDQwOCwxMy41MTQ3MjczIDMzLjMxNTk3OTYsOS4wNTEwOTA5MSAzOS4zNDc4MTYzLDkuMDUxMDkwOTEgQzQyLjg0MTI4NTcsOS4wNTEwOTA5MSA0Ni45Nzc2MTIyLDEwLjY2MzgxODIgNDkuODg1MTYzMywxMy4xNTQ3MjczIEw1MC40Njc0MDgyLDEzLjY1ODM2MzYgTDU2LjI0OTQ0OSw3LjY3ODM2MzY0IEw1NS41OTc0MDgyLDcuMTI3NDU0NTUgQzUxLjE1NjE4MzcsMy4zNTI5MDkwOSA0NC44NDg4MzY3LDEuMDk0NzI3MjcgMzguNzI3LDEuMDk0NzI3MjcgQzMxLjk4MjUxMDIsMS4wOTQ3MjcyNyAyNi4yNzAyNjUzLDQuMTMxMDkwOTEgMjIuODc1OTc5Niw4LjkzMjkwOTA5IEwxMi4yMjEwODE2LDguOTMyOTA5MDkgTDExLjU5ODQyODYsMTQuMTI1NjM2NCBMMjAuMzUwNDY5NCwxNC4xMjU2MzY0IEMyMC4wNjk0NDksMTUuMDYzODE4MiAxOS44NzQ3NTUxLDE2LjA0MDE4MTggMTkuNzQyNTEwMiwxNy4wNDM4MTgyIEw2LjY0NjU5MTg0LDE3LjA0MzgxODIgTDYuMDIzOTM4NzgsMjIuMjM0NzI3MyBMMTkuNzQwNjczNSwyMi4yMzQ3MjczIEMxOS44NzEwODE2LDIzLjIzODM2MzYgMjAuMDYzOTM4OCwyNC4yMTY1NDU1IDIwLjMzOTQ0OSwyNS4xNDkyNzI3IEwyMC4zMzAyNjUzLDI1LjE0OTI3MjcgTDE5LjU1MzMyNjUsMjUuMTQ5MjcyNyBMMC42MjM5Mzg3NzYsMjUuMTQ5MjcyNyBMLTAuMDAwNTUxMDIwNDA4LDMwLjM0MiBMMTkuNTUzMzI2NSwzMC4zNDIgTDIwLjMzMDI2NTMsMzAuMzQyIEwyMi44MzAwNjEyLDMwLjM0MiBDMjYuMjI2MTgzNywzNS4xODAxODE4IDMyLjAxMTg5OCwzOC4xODIgMzguOTg1OTc5NiwzOC4xODIgQzQ3Ljc0MzUzMDYsMzguMTgyIDUzLjg0ODgzNjcsMzMuNzQ3NDU0NSA1Ni43MDg2MzI3LDMxLjEwNTYzNjQgTDU3LjMxNjU5MTgsMzAuNTQzODE4MiBMNTEuNDUzNzM0NywyNC42MTI5MDkxIFoiIGlkPSJGaWxsLTEiIGZpbGw9IiM2MjZDODciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiIG1hc2s9InVybCgjbWFzay0yKSI+PC9wYXRoPgogICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTUuMTYzMDYxMjIsMTcuMDQzNjM2NCBMMS4zMDQwODE2MywxNy4wNDM2MzY0IEwwLjY4MTQyODU3MSwyMi4yMzQ1NDU1IEw0LjU0MjI0NDksMjIuMjM0NTQ1NSBMNS4xNjMwNjEyMiwxNy4wNDM2MzY0IFoiIGlkPSJGaWxsLTQiIGZpbGw9IiM2MjZDODciIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik04Ny40NjIsMjMuOTQ4OTA5MSBDODUuMzUxNTkxOCwyNS44OTggODIuMTQyODE2MywyNy42ODE2MzY0IDc4LjEyMDM2NzMsMjcuNjgxNjM2NCBDNzIuMDEzMjI0NSwyNy42ODE2MzY0IDY3Ljk4ODkzODgsMjMuMzYzNDU0NSA2Ny45ODg5Mzg4LDE4LjAxNDM2MzYgQzY3Ljk4ODkzODgsMTIuNzc4IDcyLjA0MDc3NTUsOC4zNzggNzcuOTc4OTM4OCw4LjM3OCBDODEuMzU0ODU3MSw4LjM3OCA4NC42NzU2NzM1LDkuNjg3MDkwOTEgODYuODcwNTcxNCwxMS41NTQzNjM2IEw4NC4zMzc3MTQzLDE0LjE3MDcyNzMgQzgyLjY0OTc1NTEsMTIuNzIzNDU0NSA4MC4yODU4Nzc2LDExLjgzMjU0NTUgNzguMzE2ODk4LDExLjgzMjU0NTUgQzc0LjYwMzAyMDQsMTEuODMyNTQ1NSA3Mi4yMDk3NTUxLDE0LjY0NTI3MjcgNzIuMjA5NzU1MSwxOC4wNDM0NTQ1IEM3Mi4yMDk3NTUxLDIxLjQ2ODkwOTEgNzQuNjU4MTIyNCwyNC4yNTQzNjM2IDc4LjQ1ODMyNjUsMjQuMjU0MzYzNiBDODAuOTYxNzk1OSwyNC4yNTQzNjM2IDgzLjI5NjI4NTcsMjIuOTQ1MjcyNyA4NC44OTk3NTUxLDIxLjM1OCBMODcuNDYyLDIzLjk0ODkwOTEgWiIgaWQ9IkZpbGwtNiIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTEwNy41MjMzNjcsOC45MzQ3MjcyNyBMMTA3LjUyMzM2NywyMC41NzY1NDU1IEMxMDcuNTIzMzY3LDI1LjExODM2MzYgMTAzLjk3NjYzMywyNy42ODIgOTkuMTA5Mjg1NywyNy42ODIgQzk0LjM4MTUzMDYsMjcuNjgyIDkwLjc1MjE0MjksMjUuMTE4MzYzNiA5MC43NTIxNDI5LDIwLjU3NjU0NTUgTDkwLjc1MjE0MjksOC45MzQ3MjcyNyBMOTQuODAzOTc5Niw4LjkzNDcyNzI3IEw5NC44MDM5Nzk2LDIwLjU3NjU0NTUgQzk0LjgwMzk3OTYsMjIuOTc0NzI3MyA5Ni43NDU0MDgyLDI0LjI1NDcyNzMgOTkuMTA5Mjg1NywyNC4yNTQ3MjczIEMxMDEuNjE0NTkyLDI0LjI1NDcyNzMgMTAzLjQ3MTUzMSwyMi45NzQ3MjczIDEwMy40NzE1MzEsMjAuNTc2NTQ1NSBMMTAzLjQ3MTUzMSw4LjkzNDcyNzI3IEwxMDcuNTIzMzY3LDguOTM0NzI3MjcgWiIgaWQ9IkZpbGwtOCIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPHBhdGggZD0iTTExNy4wMDQ3NzYsMTIuMzA0NzI3MyBMMTE3LjAwNDc3NiwxNy4wNjgzNjM2IEwxMjMuNTg5NDY5LDE3LjA2ODM2MzYgQzEyNC45OTY0MDgsMTcuMDY4MzYzNiAxMjYuMTQ5ODc4LDE2LjA2NDcyNzMgMTI2LjE0OTg3OCwxNC42NzM4MTgyIEMxMjYuMTQ5ODc4LDE0LjA2MTA5MDkgMTI1LjkyMzk1OSwxMy41MDI5MDkxIDEyNS41MDE1MSwxMy4wODQ3MjczIEMxMjQuODU0OTgsMTIuMzg4MzYzNiAxMjQuMDM5NDY5LDEyLjMwNDcyNzMgMTIzLjMwODQ0OSwxMi4zMDQ3MjczIEwxMTcuMDA0Nzc2LDEyLjMwNDcyNzMgWiBNMTEyLjk4MjMyNywyNy4xMjI5MDkxIEwxMTIuOTgyMzI3LDguOTMzODE4MTggTDEyMy4xNjcwMiw4LjkzMzgxODE4IEMxMjUuNzg0MzY3LDguOTMzODE4MTggMTI3LjQ5OTg3OCw5LjY4ODM2MzY0IDEyOC42ODI3MzUsMTAuOTEyIEMxMjkuNjM5NjczLDExLjkxNTYzNjQgMTMwLjIwMTcxNCwxMy4yMjQ3MjczIDEzMC4yMDE3MTQsMTQuNjczODE4MiBDMTMwLjIwMTcxNCwxNy4wMTIgMTI4LjczOTY3MywxOC44MjI5MDkxIDEyNi40ODc4MzcsMTkuNzk3NDU0NSBMMTMwLjc5MzE0MywyNy4xMjI5MDkxIEwxMjYuMTc5MjY1LDI3LjEyMjkwOTEgTDEyMi40NjM1NTEsMjAuNDM5MjcyNyBMMTE3LjAwNDc3NiwyMC40MzkyNzI3IEwxMTcuMDA0Nzc2LDI3LjEyMjkwOTEgTDExMi45ODIzMjcsMjcuMTIyOTA5MSBaIiBpZD0iRmlsbC0xMCIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCI+PC9wYXRoPgogICAgICAgICAgICAgICAgPG1hc2sgaWQ9Im1hc2stNCIgc2tldGNoOm5hbWU9IkNsaXAgMTMiIGZpbGw9IndoaXRlIj4KICAgICAgICAgICAgICAgICAgICA8dXNlIHhsaW5rOmhyZWY9IiNwYXRoLTMiPjwvdXNlPgogICAgICAgICAgICAgICAgPC9tYXNrPgogICAgICAgICAgICAgICAgPGcgaWQ9IkNsaXAtMTMiPjwvZz4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMzQuOTI4MzY3LDguOTM0NTQ1NDUgTDEzOC45NTA4MTYsOC45MzQ1NDU0NSBMMTM4Ljk1MDgxNiwyNy4xMjM2MzY0IEwxMzQuOTI4MzY3LDI3LjEyMzYzNjQgTDEzNC45MjgzNjcsOC45MzQ1NDU0NSBaIiBpZD0iRmlsbC0xMiIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCIgbWFzaz0idXJsKCNtYXNrLTQpIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTQyLjU4Mjc3Niw4LjkzNDcyNzI3IEwxNTkuOTE3ODc4LDguOTM0NzI3MjcgTDE1OS45MTc4NzgsMTIuMzg3NDU0NSBMMTUzLjI3NDQwOCwxMi4zODc0NTQ1IEwxNTMuMjc0NDA4LDI3LjEyMzgxODIgTDE0OS4yNTE5NTksMjcuMTIzODE4MiBMMTQ5LjI1MTk1OSwxMi4zODc0NTQ1IEwxNDIuNTgyNzc2LDEyLjM4NzQ1NDUgTDE0Mi41ODI3NzYsOC45MzQ3MjcyNyBaIiBpZD0iRmlsbC0xNCIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCIgbWFzaz0idXJsKCNtYXNrLTQpIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTc5LjM4NzQ0OSw4LjkzNDcyNzI3IEwxNzIuMDk5Mjg2LDE4LjgyMiBMMTcyLjA5OTI4NiwyNy4xMjM4MTgyIEwxNjguMDQ3NDQ5LDI3LjEyMzgxODIgTDE2OC4wNDc0NDksMTguODIyIEwxNjAuNzMxNzM1LDguOTM0NzI3MjcgTDE2NS43Mzg2NzMsOC45MzQ3MjcyNyBMMTcwLjIxMjk1OSwxNS4yNTgzNjM2IEwxNzQuNjYxNTMxLDguOTM0NzI3MjcgTDE3OS4zODc0NDksOC45MzQ3MjcyNyBaIiBpZD0iRmlsbC0xNSIgZmlsbD0iIzYyNkM4NyIgc2tldGNoOnR5cGU9Ik1TU2hhcGVHcm91cCIgbWFzaz0idXJsKCNtYXNrLTQpIj48L3BhdGg+CiAgICAgICAgICAgIDwvZz4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo=', + isInsideWell: false, + }, + pageSymbols: { + default: + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA5IDEwOSI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJOYW1ubMO2c1/DtnZlcnRvbmluZ18zIiB4MT0iNi40OSIgeTE9IjYyLjU5IiB4Mj0iMzAuMTgiIHkyPSI3MS4yMiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iLjEiIHN0b3AtY29sb3I9IiNkMTViOWIiIHN0b3Atb3BhY2l0eT0iLjEiLz48c3RvcCBvZmZzZXQ9Ii4zNSIgc3RvcC1jb2xvcj0iI2QxNWI5YiIgc3RvcC1vcGFjaXR5PSIuNjgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNkMTViOWIiIHN0b3Atb3BhY2l0eT0iLjAyIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9Ik5hbW5sw7ZzX8O2dmVydG9uaW5nXzMtMiIgeDE9Ijg2LjE4IiB5MT0iNjcuOTkiIHgyPSI5My42MiIgeTI9IjcwLjciIHhsaW5rOmhyZWY9IiNOYW1ubMO2c1/DtnZlcnRvbmluZ18zIi8+PGxpbmVhckdyYWRpZW50IGlkPSJOYW1ubMO2c1/DtnZlcnRvbmluZ18zLTMiIHgxPSIyNy4xMiIgeTE9IjkiIHgyPSI4NS43NSIgeTI9IjMwLjM0IiB4bGluazpocmVmPSIjTmFtbmzDtnNfw7Z2ZXJ0b25pbmdfMyIvPjwvZGVmcz48cmVjdCB4PSI2Mi4wNCIgeT0iNTguMjMiIHdpZHRoPSIxNi45IiBoZWlnaHQ9IjEwLjMzIiByeD0iMS44OCIgcnk9IjEuODgiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzYzNmQ4NyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBzdHJva2Utd2lkdGg9IjIuMTgiLz48cGF0aCBkPSJNNjUuOCw1OC4yM3YtMy43NWMwLTIuNTksMi4xLTQuNjksNC42OS00LjY5czQuNjksMi4xLDQuNjksNC42OXYzLjc1IiBmaWxsPSJub25lIiBzdHJva2U9IiM2MzZkODciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyLjE4Ii8+PGNpcmNsZSBjeD0iNTMuODciIGN5PSIzOC4xNCIgcj0iMTAuMTYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2QzNWI5YyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS13aWR0aD0iMi4xOCIvPjxwYXRoIGQ9Ik0zNS43Niw2OS4yYzAtOS4wMSw4LjExLTE2LjMxLDE4LjExLTE2LjMxLDIuMDgsMCw0LjA4LjMyLDUuOTQuOSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDM1YjljIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIyLjE4Ii8+PHJlY3QgeD0iMTQiIHk9IjM1Ljk0IiB3aWR0aD0iNi41NSIgaGVpZ2h0PSI0IiByeD0iLjczIiByeT0iLjczIiBmaWxsPSJub25lIiBzdHJva2U9IiM2NDZlODUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0xNS40NiwzNS45NHYtMS40NmMwLTEsLjgxLTEuODIsMS44Mi0xLjgyczEuODIuODEsMS44MiwxLjgydjEuNDYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzY0NmU4NSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTE3LjI1LDI4LjE1Yy0uNjQuOTUtMi40NywyLjg4LTYuOTksMi43OGwuMDMsNy43N2MuMDIuMTQuNTYsMy43NSw3LjAzLDcuMjIsNi40NS0zLjUyLDYuOTYtNy4xMyw2Ljk4LTcuMjlsLS4wMy03Ljc2Yy00LjUxLjEzLTYuMzctMS43OC03LjAyLTIuNzNaIiBmaWxsPSJub25lIiBzdHJva2U9IiM2NDZlODUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik00NC4yNCw4Ni4xOWMwLTEuNTEsMS4yMi0yLjczLDIuNzMtMi43M2gxMC45MWMxLjUxLDAsMi43MywxLjIyLDIuNzMsMi43M3YxLjgyYzAsMS41MS0xLjIyLDIuNzMtMi43MywyLjczaC0xMC45MWMtMS41MSwwLTIuNzMtMS4yMi0yLjczLTIuNzN2LTEuODJaIiBmaWxsPSJub25lIiBzdHJva2U9IiM2MjZjODciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik01Mi40Myw5OC4wMWgtNS40NmMtMS41MSwwLTIuNzMtMS4yMi0yLjczLTIuNzN2LTEuODJjMC0xLjUxLDEuMjItMi43MywyLjczLTIuNzNoOS41NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjI2Yzg3IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48Y2lyY2xlIGN4PSI0Ny44OCIgY3k9Ijg3LjUxIiByPSIxLjAxIiBmaWxsPSIjNjI2Yzg3Ii8+PGNpcmNsZSBjeD0iNDcuODgiIGN5PSI5NC41NSIgcj0iMS4wMSIgZmlsbD0iIzYyNmM4NyIvPjxwYXRoIGQ9Ik01NS4zOCw5Ni4zMWw0LjE0LDEuNzJjLjEzLjA1LjI3LjA1LjQsMGw0LjE0LTEuNzJjLjI2LS4xMS4zOS0uNDEuMjgtLjY4LS4wNS0uMTMtLjE1LS4yMi0uMjgtLjI4bC00LjE0LTEuNzJjLS4xMy0uMDUtLjI3LS4wNS0uNCwwbC00LjE0LDEuNzJjLS4yNi4xMS0uMzkuNDEtLjI4LjY4LjA1LjEzLjE1LjIyLjI4LjI4WiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNjQ2ZTg1IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48cGF0aCBkPSJNNTQuOTcsOTcuNTlsNC41MywxLjg5Yy4xNC4wNi4zLjA2LjQ0LDBsNC41My0xLjg5IiBmaWxsPSJub25lIiBzdHJva2U9IiM2NDZlODUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik01NC45Nyw5OS4wNGw0LjUzLDEuODljLjE0LjA2LjMuMDYuNDQsMGw0LjUzLTEuODkiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzY0NmU4NSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTk2LjgzLDM2LjlsMS45Mi0xLjkyLTEuMzYtMS4zNmMtLjc3LjQ5LTEuOC40LTIuNDgtLjI3cy0uNzYtMS43LS4yNy0yLjQ4bC0xLjM2LTEuMzYtOS4zNyw5LjM3Yy0uMy4zLS4zLjc5LDAsMS4wOWw0LjM5LDQuMzljLjMuMy43OS4zLDEuMDksMGw4LjM0LTguMzQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzYyNmM4NyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PGNpcmNsZSBjeD0iOTIuMjQiIGN5PSIzMy44NCIgcj0iLjM1IiBmaWxsPSIjNjI2Yzg3Ii8+PGNpcmNsZSBjeD0iOTMuMjkiIGN5PSIzNC44OSIgcj0iLjM1IiBmaWxsPSIjNjI2Yzg3Ii8+PGNpcmNsZSBjeD0iOTQuMzMiIGN5PSIzNS45NCIgcj0iLjM1IiBmaWxsPSIjNjI2Yzg3Ii8+PHBhdGggZD0iTTI1LjMzLDgyLjc4Yy04LjYtNy44Ni0xMy45OS0xOS4xNy0xMy45OS0zMS43NCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI05hbW5sw7ZzX8O2dmVydG9uaW5nXzMpIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMS4yMSIvPjxwYXRoIGQ9Ik05Ny4zMiw1Mi44OWMtLjU2LDEzLjExLTYuOTksMjQuNzEtMTYuNzMsMzIuMjIiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNOYW1ubMO2c1/DtnZlcnRvbmluZ18zLTIpIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMS4yMSIvPjxwYXRoIGQ9Ik0yMy4zLDIxLjI3YzcuODMtOC4xNiwxOC44NC0xMy4yNSwzMS4wNS0xMy4yNSwxMy4zNiwwLDI1LjMsNi4wOSwzMy4xOSwxNS42NSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ1cmwoI05hbW5sw7ZzX8O2dmVydG9uaW5nXzMtMykiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIxLjIxIi8+PC9zdmc+', + }, + }, +}; enum Page { START = 'start', @@ -56,10 +73,12 @@ export function Previewer() { }; return ( - - setCurrentPage(page as Page)} currentPage={currentPage}> - {renderPreview()} - - + + + setCurrentPage(page as Page)} currentPage={currentPage}> + {renderPreview()} + + + ); } diff --git a/src/login-web-app/previewer/shared/ui/preview/Preview.tsx b/src/login-web-app/previewer/shared/ui/preview/Preview.tsx index 62aca9ad..a480e033 100644 --- a/src/login-web-app/previewer/shared/ui/preview/Preview.tsx +++ b/src/login-web-app/previewer/shared/ui/preview/Preview.tsx @@ -16,6 +16,11 @@ import { JsonRepresentation } from '../json-representation/JsonRepresentation'; import { Main } from '../main/Main'; import { Header } from '../page-header/PageHeader'; import { PreviewLayout } from '../preview-layout/PreviewLayout'; +import { PageSymbol } from '../../../../src/shared/ui/PageSymbol'; +import { Well } from '../../../../src/haapi-stepper/ui/well/Well'; +import { useHaapiAppConfig } from '../../../../src/shared/feature/app-config/HaapiAppConfigHook'; +import { Logo } from '../../../../src/shared/ui/Logo'; +import styles from './preview.module.css'; interface PreviewProps { title: string; @@ -25,6 +30,8 @@ interface PreviewProps { export function Preview({ title, step, onErrorToggle }: PreviewProps) { const [, setHasError] = useState(false); + const currentPage = step.metadata?.viewName ?? 'Unknown view'; + const { isInsideWell } = useHaapiAppConfig().theme.logo ?? {}; const handleErrorToggle = (hasError: boolean) => { setHasError(hasError); @@ -35,7 +42,14 @@ export function Preview({ title, step, onErrorToggle }: PreviewProps) {
- +
+ {!isInsideWell && } + + {isInsideWell && } + + + +
diff --git a/src/login-web-app/previewer/shared/ui/preview/preview.module.css b/src/login-web-app/previewer/shared/ui/preview/preview.module.css new file mode 100644 index 00000000..7b734d03 --- /dev/null +++ b/src/login-web-app/previewer/shared/ui/preview/preview.module.css @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +/* + * Groups the logo and the well into a single grid item so that the surrounding + * `Main` grid always sees exactly two children (this app view + the JSON + * representation), regardless of whether the logo renders inside or outside the + * well. Mirrors the production layout where the logo stacks above the well. + */ +.appView { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-3); + width: 100%; +} diff --git a/src/login-web-app/src/haapi-stepper/README.md b/src/login-web-app/src/haapi-stepper/README.md index cd5461e8..2710a4be 100644 --- a/src/login-web-app/src/haapi-stepper/README.md +++ b/src/login-web-app/src/haapi-stepper/README.md @@ -104,10 +104,10 @@ This is the default behavior and covers the vast majority of deployments (the LW When the `HaapiStepper` is consumed as a library — e.g. embedded in a third-party app or any context that doesn't set `window.__CONFIG__` — the consumer supplies the bootstrap configuration explicitly via the `config.bootstrap` prop: ```tsx -import { HaapiStepper } from '@curity/login-web-app/haapi-stepper'; -import type { BootstrapConfiguration } from '@curity/login-web-app/haapi-stepper'; +import { HaapiStepper } from './feature'; +import type { HaapiAppConfig } from '../shared/feature/app-config/types'; -const bootstrapConfig: BootstrapConfiguration = { +const bootstrapConfig: HaapiAppConfig = { initialUrl: 'https://idsvr.example.com/oauth/v2/oauth-authorize/...', haapi: { /* HAAPI web-driver config */ }, theme: { logo: { path: '/logo.svg', isInsideWell: true } }, diff --git a/src/login-web-app/src/shared/feature/app-config/types.ts b/src/login-web-app/src/shared/feature/app-config/types.ts index 6d1e7d91..3253ebed 100644 --- a/src/login-web-app/src/shared/feature/app-config/types.ts +++ b/src/login-web-app/src/shared/feature/app-config/types.ts @@ -6,5 +6,15 @@ export interface HaapiAppConfig extends HaapiStepperBootstrapConfig { path: string; isInsideWell: boolean; }; + pageSymbols?: PageSymbols; }; } + +export interface PageSymbols { + /** Map of full HAAPI viewName -> symbol path. Highest precedence. */ + views?: Record; + /** Map of plugin implementation type (e.g. `html-form`, `bankid`) -> symbol path. */ + plugins?: Record; + /** Fallback symbol path used when no per-view / per-plugin entry matches. */ + default?: string; +} diff --git a/src/login-web-app/src/shared/ui/Layout.spec.tsx b/src/login-web-app/src/shared/ui/Layout.spec.tsx index aed78de9..fcc43fac 100644 --- a/src/login-web-app/src/shared/ui/Layout.spec.tsx +++ b/src/login-web-app/src/shared/ui/Layout.spec.tsx @@ -9,73 +9,116 @@ * For further information, please contact Curity AB. */ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { render } from '@testing-library/react'; import { Layout } from './Layout'; +import { HaapiStepperContext } from '../../haapi-stepper/feature/stepper/HaapiStepperContext'; +import type { PageSymbols } from '../feature/app-config/types'; +import type { HaapiStepperAPI, HaapiStepperStep } from '../../haapi-stepper/feature/stepper/haapi-stepper.types'; +import type { HaapiAppConfig } from '../feature/app-config/types'; import { HaapiAppConfigContext } from '../feature/app-config/HaapiAppConfigContext'; -import { HaapiAppConfig } from '../feature/app-config/types'; -const renderLayout = (isInsideWell: boolean) => { - const config: HaapiAppConfig = { - initialUrl: 'https://example/start', - haapi: {} as HaapiAppConfig['haapi'], - theme: { logo: { path: '/assets/logo.svg', isInsideWell } }, - }; - return render( - - -
- - - ); -}; +describe('Layout', () => { + describe('Logo placement', () => { + it('renders the logo inside the well when theme.logo.isInsideWell is true', () => { + const { container } = renderLayout({ isInsideWell: true }); + const well = container.querySelector('.haapi-stepper-well'); + const logo = container.querySelector('img.haapi-stepper-logo'); + + expect(well).not.toBeNull(); + expect(logo).not.toBeNull(); + expect(well?.contains(logo)).toBe(true); + }); + + it('renders the logo before the well (not inside it) when theme.logo.isInsideWell is false', () => { + const { container } = renderLayout({ isInsideWell: false }); + const main = container.querySelector('main.app-layout'); + const well = container.querySelector('.haapi-stepper-well'); + const logo = container.querySelector('img.haapi-stepper-logo'); + + expect(main).not.toBeNull(); + expect(well).not.toBeNull(); + expect(logo).not.toBeNull(); -describe('Layout — logo placement', () => { - it('renders the logo inside the well when theme.logo.isInsideWell is true', () => { - const { container } = renderLayout(true); - const well = container.querySelector('.haapi-stepper-well'); - const logo = container.querySelector('img.haapi-stepper-logo'); + // Logo is a sibling of the well, not a descendant. + expect(well?.contains(logo)).toBe(false); + expect(main?.contains(logo)).toBe(true); - expect(well).not.toBeNull(); - expect(logo).not.toBeNull(); - expect(well?.contains(logo)).toBe(true); + // Logo appears before the well in document order. + expect(logo!.compareDocumentPosition(well!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); + + it('renders children and the well but no logo element when theme.logo is not configured', () => { + const { container, getByTestId } = renderLayout({ withoutLogo: true }); + expect(getByTestId('content')).toBeInTheDocument(); + expect(container.querySelector('.haapi-stepper-well')).not.toBeNull(); + expect(container.querySelector('img.haapi-stepper-logo')).toBeNull(); + }); }); - it('renders the logo before the well (not inside it) when theme.logo.isInsideWell is false', () => { - const { container } = renderLayout(false); - const main = container.querySelector('main.app-layout'); - const well = container.querySelector('.haapi-stepper-well'); - const logo = container.querySelector('img.haapi-stepper-logo'); + describe('Page symbol', () => { + it('renders the resolved page symbol above the children for the current step', () => { + const { container } = renderLayout({ + pageSymbols: { plugins: { 'html-form': '/symbols/html-form.svg' } }, + currentStep: stepWithViewName('authenticator/html-form/index'), + }); + + const pageSymbol = container.querySelector('img.haapi-stepper-page-symbol-image'); + const content = container.querySelector('[data-testid="content"]'); - expect(main).not.toBeNull(); - expect(well).not.toBeNull(); - expect(logo).not.toBeNull(); + expect(pageSymbol).not.toBeNull(); + expect(pageSymbol).toHaveAttribute('src', '/symbols/html-form.svg'); + // Page symbol appears before the children in document order. + expect(pageSymbol!.compareDocumentPosition(content!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); - // Logo is a sibling of the well, not a descendant. - expect(well?.contains(logo)).toBe(false); - expect(main?.contains(logo)).toBe(true); + it('renders nothing for the page symbol when theme.pageSymbols is absent', () => { + const { container } = renderLayout({ + currentStep: stepWithViewName('authenticator/html-form/index'), + }); - // Logo appears before the well in document order. - expect(logo!.compareDocumentPosition(well!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(container.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); + }); }); +}); - it('renders children and the well but no logo element when theme.logo is not configured', () => { - const configWithoutLogo: HaapiAppConfig = { - initialUrl: 'https://example/start', - haapi: {} as HaapiAppConfig['haapi'], - theme: {}, - }; +const emptyStepperAPI: HaapiStepperAPI = { + loading: false, + history: [], + nextStep: vi.fn(), + currentStep: null, + error: null, +}; + +const stepWithViewName = (viewName: string): HaapiStepperStep => + ({ metadata: { templateArea: 'lwa', viewName } }) as unknown as HaapiStepperStep; - const { container, getByTestId } = render( - +interface LayoutHarnessOptions { + isInsideWell?: boolean; + pageSymbols?: PageSymbols; + currentStep?: HaapiStepperStep | null; + withoutLogo?: boolean; +} + +const renderLayout = ({ + isInsideWell = false, + pageSymbols, + currentStep = null, + withoutLogo = false, +}: LayoutHarnessOptions = {}) => { + const config: HaapiAppConfig = { + initialUrl: 'https://example/start', + haapi: {} as HaapiAppConfig['haapi'], + theme: { logo: withoutLogo ? undefined : { path: '/assets/logo.svg', isInsideWell }, pageSymbols }, + }; + const stepper: HaapiStepperAPI = { ...emptyStepperAPI, currentStep }; + return render( + +
- - ); - - expect(getByTestId('content')).toBeInTheDocument(); - expect(container.querySelector('.haapi-stepper-well')).not.toBeNull(); - expect(container.querySelector('img.haapi-stepper-logo')).toBeNull(); - }); -}); + + + ); +}; diff --git a/src/login-web-app/src/shared/ui/Layout.tsx b/src/login-web-app/src/shared/ui/Layout.tsx index a1388075..693cfa8c 100644 --- a/src/login-web-app/src/shared/ui/Layout.tsx +++ b/src/login-web-app/src/shared/ui/Layout.tsx @@ -13,8 +13,11 @@ import { ReactNode } from 'react'; import { Well } from '../../haapi-stepper/ui/well/Well'; import { useHaapiAppConfig } from '../feature/app-config/HaapiAppConfigHook'; import { Logo } from './Logo'; +import { PageSymbol } from './PageSymbol'; +import { useHaapiStepper } from '../../haapi-stepper/feature'; export const Layout = ({ children }: { children: ReactNode }) => { + const { currentStep } = useHaapiStepper(); const { isInsideWell } = useHaapiAppConfig().theme.logo ?? {}; return ( @@ -23,7 +26,10 @@ export const Layout = ({ children }: { children: ReactNode }) => { {!isInsideWell && } {isInsideWell && } -
{children}
+
+ + {children} +
diff --git a/src/login-web-app/src/shared/ui/PageSymbol.spec.tsx b/src/login-web-app/src/shared/ui/PageSymbol.spec.tsx new file mode 100644 index 00000000..5dd1757e --- /dev/null +++ b/src/login-web-app/src/shared/ui/PageSymbol.spec.tsx @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { describe, expect, it } from 'vitest'; +import { render } from '@testing-library/react'; +import { PageSymbol } from './PageSymbol'; +import { HaapiAppConfigContext } from '../feature/app-config/HaapiAppConfigContext'; +import type { HaapiAppConfig, PageSymbols } from '../feature/app-config/types'; + +const buildConfig = (pageSymbols?: PageSymbols): HaapiAppConfig => ({ + initialUrl: 'https://example/start', + haapi: {} as HaapiAppConfig['haapi'], + theme: { + logo: { path: '/assets/logo.svg', isInsideWell: false }, + pageSymbols, + }, +}); + +const renderPageSymbol = (viewName: string | undefined, pageSymbols?: PageSymbols) => + render( + + + + ); + +describe('PageSymbol', () => { + const pageSymbols: PageSymbols = { + views: { + 'authenticator/html-form/create-account/post': '/symbols/create-account.svg', + 'authentication-action/email-verifier/confirm': '/symbols/email-verifier-confirm.svg', + 'consentor/scope-consent/review': '/symbols/scope-consent-review.svg', + }, + plugins: { + 'html-form': '/symbols/html-form.svg', + 'email-verifier': '/symbols/email-verifier.svg', + 'scope-consent': '/symbols/scope-consent.svg', + }, + default: '/symbols/default.svg', + }; + + it.each([ + ['authenticator', 'authenticator/html-form/create-account/post', '/symbols/create-account.svg'], + ['authentication-action', 'authentication-action/email-verifier/confirm', '/symbols/email-verifier-confirm.svg'], + ['consentor', 'consentor/scope-consent/review', '/symbols/scope-consent-review.svg'], + ])( + 'renders the exact `views` entry for the %s category even when a plugin or default would also match', + (_, viewName, expected) => { + renderPageSymbol(viewName, pageSymbols); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute( + 'src', + expected + ); + } + ); + + it.each([ + ['authenticator', 'authenticator/html-form/index', '/symbols/html-form.svg'], + ['authentication-action', 'authentication-action/email-verifier/verify', '/symbols/email-verifier.svg'], + ['consentor', 'consentor/scope-consent/consent', '/symbols/scope-consent.svg'], + ])( + 'falls back to the plugin-type entry for the %s category when no `views` entry matches', + (_, viewName, expected) => { + renderPageSymbol(viewName, pageSymbols); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute( + 'src', + expected + ); + } + ); + + it('falls back to `default` when neither `views` nor `plugins` matches', () => { + renderPageSymbol('authenticator/unknown-plugin/index', pageSymbols); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute( + 'src', + '/symbols/default.svg' + ); + }); + + it('falls back to `default` when the viewName is outside the three plugin categories', () => { + renderPageSymbol('views/select-authenticator/index', pageSymbols); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toHaveAttribute( + 'src', + '/symbols/default.svg' + ); + }); + + it('renders nothing when nothing resolves and no `default` is configured', () => { + renderPageSymbol('authenticator/unknown-plugin/index', { + plugins: { 'html-form': '/symbols/html-form.svg' }, + }); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); + }); + + it('renders nothing when pageSymbols is absent', () => { + renderPageSymbol('authenticator/html-form/index', undefined); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); + }); + + it('renders nothing when pageSymbols is empty', () => { + renderPageSymbol('authenticator/html-form/index', {}); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); + }); + + it('renders nothing when viewName is undefined', () => { + renderPageSymbol(undefined, pageSymbols); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); + }); + + it('renders nothing when viewName is an empty string', () => { + renderPageSymbol('', pageSymbols); + expect(document.querySelector('img.haapi-stepper-page-symbol-image')).toBeNull(); + }); +}); diff --git a/src/login-web-app/src/shared/ui/PageSymbol.tsx b/src/login-web-app/src/shared/ui/PageSymbol.tsx new file mode 100644 index 00000000..222015d3 --- /dev/null +++ b/src/login-web-app/src/shared/ui/PageSymbol.tsx @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import { useHaapiAppConfig } from '../feature/app-config/HaapiAppConfigHook'; +import { resolvePageSymbol } from '../util/resolve-page-symbol'; + +interface PageSymbolProps { + viewName: string | undefined; +} + +/** + * Renders the page symbol icon associated with the current step's HAAPI `viewName`. + * + * The mapping comes from `theme.pageSymbols` in the bootstrap configuration and is resolved by + * {@link resolvePageSymbol}. When `theme.pageSymbols` is absent, when `viewName` is absent, or when + * no entry resolves, this component renders nothing. + */ +export const PageSymbol = ({ viewName }: PageSymbolProps) => { + const { theme } = useHaapiAppConfig(); + const src = resolvePageSymbol(viewName, theme.pageSymbols); + + if (!src) { + return null; + } + + return ( + + ); +}; diff --git a/src/login-web-app/src/shared/util/css/styles.css b/src/login-web-app/src/shared/util/css/styles.css index feea940e..7cb364d4 100644 --- a/src/login-web-app/src/shared/util/css/styles.css +++ b/src/login-web-app/src/shared/util/css/styles.css @@ -248,6 +248,25 @@ svg { margin-block: 0 var(--space-2); } +.haapi-stepper-page-symbol { + text-align: center; + width: var(--login-symbol-size); + height: var(--login-symbol-size); + display: var(--login-symbol-display); + margin: 0 auto; + justify-content: center; + align-items: center; + margin-top: var(--login-symbol-margin-top); + margin-bottom: var(--login-symbol-margin-bottom); + border-radius: var(--login-symbol-border-radius); +} + +.haapi-stepper-page-symbol-image { + width: var(--login-symbol-size); + height: var(--login-symbol-size); + object-fit: contain; +} + .haapi-stepper-links { @extend .center, .py3, .flex, .flex-column; } diff --git a/src/login-web-app/src/shared/util/resolve-page-symbol.ts b/src/login-web-app/src/shared/util/resolve-page-symbol.ts new file mode 100644 index 00000000..353874cf --- /dev/null +++ b/src/login-web-app/src/shared/util/resolve-page-symbol.ts @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2026 Curity AB. All rights reserved. + * + * The contents of this file are the property of Curity AB. + * You may not copy or use this file, in either source code + * or executable form, except in compliance with terms + * set by Curity AB. + * + * For further information, please contact Curity AB. + */ + +import type { PageSymbols } from '../feature/app-config/types'; + +/** + * Resolves the page symbol image path for a given HAAPI step `viewName` against the + * `theme.pageSymbols` configuration delivered by the server bootstrap. + * + * Resolution order: + * 1. Exact match in `pageSymbols.views`. + * 2. Plugin-type match (extracted from `viewName` via {@link PLUGIN_TYPE_FROM_VIEW_NAME}) in `pageSymbols.plugins`. + * 3. `pageSymbols.default`. + * 4. `undefined` when no rule resolves — callers should render nothing. + * + * Returns `undefined` for any falsy input (no `viewName`, no `pageSymbols`). + */ +export const resolvePageSymbol = ( + viewName: string | undefined, + pageSymbols: PageSymbols | undefined +): string | undefined => { + /** + * Extracts the plugin implementation type from a HAAPI `viewName` of the form + * `//`, where category is `authenticator`, `authentication-action` + * or `consentor`. + */ + const PLUGIN_TYPE_FROM_VIEW_NAME = /^(?:authenticator|authentication-action|consentor)\/([^/]+)\/.*/; + + if (!viewName || !pageSymbols) { + return undefined; + } + + const exactMatch = pageSymbols.views?.[viewName]; + if (exactMatch) { + return exactMatch; + } + + const pluginType = PLUGIN_TYPE_FROM_VIEW_NAME.exec(viewName)?.[1]; + const pluginMatch = pluginType ? pageSymbols.plugins?.[pluginType] : undefined; + if (pluginMatch) { + return pluginMatch; + } + + return pageSymbols.default; +};