Phase 3C Help System

This commit is contained in:
Nick Beckley 2026-06-04 20:19:52 +01:00
parent bbd7c548e0
commit eaacd3d27f
28 changed files with 723 additions and 43 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPpkJRFEUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaN5fY/hD9SrPaAGLQ+HT0fo44AWKLopRAnlmHBM2LaQDSmbkueUl7yk9b9ogdwdvKEG1UzrQkvAJXDAzJAKt6XV2BiQBSkMoRVLPdXzgJofSLncwNFpyP3wF4TB+R416G/XG/+aZiuX686PJg++g3U/RNXrtMzX9eoOf1lutg+bU9YAQyrOnUENaklBK2EsJrbUmNWCYlEzhRVRtZCqNqD1qcz1EUKvunwwL5f5xsceom5oTsucYCrR+wIZFVVauVUDbN8OaYuJotsUX0zh6bP5gY4QUEMXZzJP7W505272Qzu6ztxEFZ4h2qD28OrDCxxVhxboEPwBQnQwcfPwlF1WKkhOcsopIWiB7pXeuR6y9W320CXASgvCwJRYgjWYyUJhycoSm7JSNeW2rqlFC3QHoYfuquu908EP3sZspjz7MGfl++j60Y9Dtu4jPAcVT3WfVTcAWqDN2HZu2D2CMm9b/zimf/3YJSyrMQToY7ZRcZf9qYYdapBUQhdKKqkU19pCRcASQxQntQFuOBhGTW2YZIYzSTRnqi2YUVXR2hRBC/QwxsMYz/x/v1piG9x+D+aywXbieNOpaH3Yrw300VmXpDlnTIbquglVNvdcdQ76+IFk1CBSADWlAGxpwTETRmFVlRJzXnILhZY1LU6EWfc8/uSJWGtYaSsheCvbggC1tWCkti0x2pZamqqFmtakFiCkJqStWCFaqYmBomImoQvejDr+aoVyEuhx7NN1ZusE+9X1+DtnF0Y2wUevfZcEcx08vQ0R9r9TbeX3Bz+4mPDbWdpvA9z7Pv2d73/mbC5e+niJQbqDbDo9ynlBRFVi1gqRiCJYtFpio0qgsqJCq4Tq1usXCFkymHL9pMnZUw9P2TLonYug4xjS5H8mUCc5P59P5sJH/vXTduUDLA+HxRz945hIOjU7G+DDu/JtSOk7P8QryDO0J+jsebErOxk4np4ql4LcGAGGF5iyosJMygILWdW4rUUrWy6g0he7Zo9gXAAdp37bMKZpM7O/8+UyRGeVjkM6ni9eT3aa/Tmn/LrklWw/q0/Dr6SbrTR3Kkiy7NPp4Zwc9L/ezff39/f/AAAA//8DABQY6yIoBgAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUy27cOBD8FYFnUeZLlKibMU6wA2/Wg2SQDXJrka2YsEwNSMqJYfjfF9TMwvFpsReCaLGa1VVFvZAAj0gGcn06fXjCkBOpSfZbSTChKdOUqSM3A5ODahspBDe9+k5q4m/xmQykU9Zya5BK1EiVYy0d5WSpc2g4446JVpWe8COR4YWAbxw+eYvNkr5iTH4JZCB/++CWn+Vu8I2dl9U1cZlxH1KGYAuZv/a7W/rn9eF4dzifSpgKuPGODGRkXLS9myibpKWq6wXtOwUUGHS9gc6htWeYDxljgLlJ7uHtfrfkgNkOohGyYVQY8loTBxkK5RESHp9PhcUm0U2p11t52w4v5AkjGUT9r5jndld29lePaVz97K7Oa4b0kLZ1ApuX+ExqcorLCWP2uOlznRI+jvPzEdLDx/OZsk0ffqFdM7rdsoZMBsJlT2qyCTTPkP0SqgvH3ewx5O1jjj4kb/+rlyQ1uftSHWbI0xIf3/nx5Szz/n+o/AnsvQ9Y7W/IQBhH4WSPdBJcU9U7oNBKQ7WWekJuTSc4qcluCdmHdVlTtQ8Zf8RtJDKQjzAnPPO7jvbeZ7R5jWXOb7rk6hAXt9pcvXnJWcMayVgBrfm05uozOh/RZixDHON66ff1PURosYF2a4wYcnWAfF/9AemeDMRAbzkYMADa2glbhhNzDDTrHGqn0SnhOqeMcloZZrWCkSsHLR+nUiE1wRKdaksrSJBawETbTnKqADk1wiLlE/S87ZkzWMx907G6mzec7ZlCJ6nByVFlOFCjpKROttAJPXWdmH6T5JIHzorADp/O725joJ3r0WlOheItVcZw2pu2o2PXj2bUPba2cL7FGHD+TahP3sYlLVOuLgmp3kn3eQ3l31FtafnpA/21eXSz2AeMVfEYfChP5WLr6+vr6z8AAAD//wMA6TtuZ4IEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUy27kNhD8FYHXiDLfEnVzbC8ycPzAziAJcmuRLZuxTA0oyt6F4X8PpLHXj0WAXAShSXZVV1f3E4lwj6Qlx/v92QPGPJGS5LCGBBOGMkOZ2nHbMtkqXUlhLBf136Qk4Ry/k5bUyjnuLFKJBqnyTNNO9o56j5Yz7pnQaskJNxNpnwiEyuNDcFiN0x+YpjBG0pI/Q/Tj44INoXLDOPsqjQNu4pQhuoXM5ebknP5+fL27uj7cmnBaHlfBk5Z0jAvd+J6yXjqq6kbQplZAgUHdWKg9Ond4FmLGFGGoJn/3hu/HHDG7VlRCVowKS55L4iHDQrmDCXff9wuLVaLTJV6u4fW3fSIPmEgrylcxD+mO3BCO7qduDoM/Wr+kJPs07jHlgKsaV9vijQRnFauEEYyRklyAuw0Ri81pcTUsJYJrmEIvqcXeU2U5UKukpF5qqIXp61r0pCTnmCIO77JeBJfGaexz8SJy8QFne3xyFqEbcMH4AsOEpCSbaZshehjGiGff0M35kGuX5uX4ZIw5xHmcp2ITM94keDl/fX8yp4QxF9eQb4vfYLolLbHQOA4WLIBxrkfNsGeegWG1R+MNeiV87ZVV3ijLnFHQceVB865fIqQkvy4abmfncJre2KwmGYaVQ/HSp5MhYMwfZCQtYRyFlw3SXnBDVeOBgpaWGiNNj9zZWvBXmB2kG8ykJX3TdR1zrhPKcgfCgvQ9k970Voi6BtVLXSthjGY1OF/Xgisva3Qge8kaQUpynUY/u/y51XJtwHUa/0GXF6lISwzTunHQdcIo3oBtUHGjGmOxtqoHpqTrgOsGuo4z0MzZRjjgxoLqvO5/qHQWb0LE0zDtB/j+DrepTCV/cQAN70F9uv4lwT0+junu8mDj6vJsR0rydY7LRig2i0UeQ6TfzDLRF/OQw+42IXj0F6PHn32Ey7gU64TqvuMcvadWa06VkoJahoxabhvtmGWNl6Qkp6O7w1QsDoMQl6n6kexqWxwndxsyujynhd9fK4+rbXE9QO7HdP9hk7wr7LMAlZCcLZ3x+HDYRitH432D3nAqFNdUWctpY3VNu7rpbGca1G5R7GrO+zkXX9GHhC6vFb94ccU8uUV397MYry54sShni9e2hy22Svu/lthzSe4Rpjnh/bqt2yeyiRHTinw6H0ZxEy/CMIQJ3Rj9RFqra1tpyeoXhv95zxhbKa758/Pz878AAAD//wMAVM4TJxsGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/UMBD9K5HP69R2HCfOrdqCWBXoClaAuE08E2o1dVaOU0DV/nfk7KICNy6WNfKbeR/jZxbgkVjHro/HV08U0sw2LPm1pIQyXBgu9EHaTlSdrstKaW1s85VtmL+ln6xjjXZOOku8IkNco6h5Xw2OI5KVQqJQtc494dvMumcGvkR68o7Kaf5EcfZTYB377ANO3/Ns8KUbpwXLOI20C3OC4DKZ97vtLX97vT/c7c+vZpozuPTIOtYLqeoWBy6GynHdtIq3jQYOAprWQoPk3BnmQ6IYYCxnfHiZj1MKlFynSlWVgivLThuGkCBT7mGmw89jZrFadJPrm7W8Xrtn9kSRdWrz28xzuys3+qvHuV/8iFfnM8H8kFUe43SkmDytnqwyxxGSn0JxmbQdPYXENuzuY7EfIQ1TfPzLqI9n/bv/kH/I01/9ILckwu20hMQ6JrVkG/YO3L0PVOxuWMeEJIVVS3xQ0nDdInCoK8uNqcxA0tlGZcx2CsmHZVrmYhcSfYurANax1zDOdKZ+Hd29T+TSErOqLybvwj5OuLhUvPgvRSnKSogMWtJxScUHQh/JJcr6DnG59PsHooxaQdslRgqp2EO6L97AfM86ZqF1EixYAOPcQLWgQaAAIxokg4ZQK2xQW41GW+GMhl5qhFr2Q66wDaMcd3HeMF1bixK4A2O4HmTDe6CeOyd6YRXUVZ2JvPhY3I0ZB64VmrDilgbk2krgVlcVx6qGRpmhadTwhyWX9KXIBiM9nf/KysAgtoRGcqVlzbW1kre2bnjftL3tTUu1y5xvKQYa/zDqnXdxmqchFZflKf6y7sMS8n8v1kX67gP/sWZ0M7kHikXOGHzI632J9XQ6nX4BAAD//wMAdbxn5zYEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFzoR2l7Ufe6qWpldd01X1jJ/D39ZD1rtXPSWeI1GeIaRcOHenQckawUEoVqdK4JXxfWvzDwFdKzd1TNy0eKi58D69knH3D+nnuDr9w0r1jFeaJ9WBIEl8X8s9/d87e3h8eHw+XVQksmVx5ZzwYhVdPhyMVYO67bTvGu1cBBQNtZaJGcu9B8SBQDTNWC317745wCJderStWV4Mqyc8kQEmTJAyz0+POUVWwW3WW83ODt2L+wZ4qsV+UvMy/lbtzkb9z89AQBb0Yf/HJkJTvF+UQxedrs2CacJkh+DsW1yW7yFBIr2cOH4jBBGuf49IdHHy6j7//H5O/AHX2gYn/HeiYkKaw74qOShusOgUNTW25MbUaSzrZKspLt5pB8WOd1KfYh0de4qWQ9+xumhS76bqM7+kQurTFL/2xy1vTDp92MGRCsZIc44+pS8Wq3FJWoapEvH9Z0WlPxntBHconyTI9xvZb/D0UZtZF2a4wUUnGAdCzewHJkPbPQOQkWLIBxbqRG0ChQgBEtkkFDqBW2qK1Go61wRsMgNUIjhzEjWXdOt9gWquukEyQH3mDbcT2g46C7kTsYZTM0zhiVd+DV1uJhyjxwndCENbc0ItdWAre6rjnWDbTKjG2rxt8suSYuRfYb6fnyNTYFBrEjNJIrLRuurZW8s03Lh7Yb7GA6alzWfE8x0PSbUe+8i/Myj6m4Lkzxh3Xv15C/d7Etz3cf+I8tsrvZfaNY5MjBh7zN15TP5/P5XwAAAP//AwCa+TscJQQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTXW/UMBD8K5Gfz6m/4sR5q64gTgV6ohUg3hzvhrNI7ZPjFFB1/x05dwjKG2/W2LM7O7N+JsE+IunJ9fH46glDnsmGZL9CgglNmaZMPXDTc9VLXTPTtVKZL2RD/C3+JD1plXPcGaQSNVIFrKGDHB0FQMMZByYaVWrarzPpn4n1NeCTd1jH+SOm2cdAevLJB4jfS2/razfFBeoUJ9yFOdvgipj3u+0tfXu9f7jbn1/NOBdy7YH0BDrduhEYNVw5qrjQ1CpgVA+d1oNpG9mxM82HjCnYqZ7h25/+EHPA7HpRC1kzKgw5bQjYbIvkwc748PNYVKwW3RR8s8LrsX8mT5hILza/zTyXu3KTv3Lx8dEGuBp98POBbMgxxSOm7HG1Y51wmmz2MVSXJtvJY8hkQ+7uq/1k8xjT4wuP7s+j7/5j8nfWHXzAandDesI4CpAd0lFwTVUHltpGGqq11CNyZ1rByYZsY8g+LHGZq13I+DWtKklPXttpxrO+6+QOPqPLSyrSP+uSNf7weRuhAKX3PkVYXK7+2M1ZzWrJyuXdko9Lrj4g+IQuY5npIS2X8v9QhBYrabukhCFXe5sP1Rs7H0hPjO0ct8Yaa7VzIzYMRwbMatYCatAISkALyijQyjCnlR24AtvwYSxI0V3SrdaFErIVIxeSSmUFVVYDHTpnKRrUAkcxtqp7YWt1NxWedR1TCJIaHIEqwy01SkoKsrGt0GPbivEvSy6Jc1b8Bnw6f41VgQboEDSnQvGGKmM47UzT0qHtBjPoDhtXNN9iCjj9ZdQ771Kc45iry8JUL6z7sITyvat1eb77QH+skd1E9w1TVSK3PpRtvqR8Op1OvwAAAP//AwD5Qg2QJQQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/UMBD9K9Gc16ntOE6cW7UFsSrQFawAcXM8E2o1dVaOU0DV/nfk7KICNy6WNfKbeR/jZwj2kaCD6+Px1ROFNMMGkl9LkkvNuGZcHYTphOoqXXJTq1aor7ABf0s/oYNGOSecIVaRJqaQ16yvBscQyQgukMta5Z722wzdM1hfIj15R+U0f6I4+ylAB599wOl7nm196cZpwTJOI+3CnGxwmcz73faWvb3eH+7251czzRlceoQOsNWNG5AzI5RjSkjNrELOdN9q3Zumrlp+hvmQKAY7ljM+vMzHKQVKrpOlrErOpIHTBtAmmyn3dqbDz2NmsVp0k+ubtbxeu2d4ogid3Pw289zuyo3+6nHuFz/i1flMdn7IKo9xOlJMnlZPVpnjaJOfQnGZtB09hQQbuPtY7Eebhik+/mXUx7P+3X/IP+Tpr36QWxLhdlpCgg6EErCBd9bd+0DF7gY64IIkVi2xQQrNVIuW2boyTOtKDyScaWTGbKeQfFimZS52IdG3uAqADl7bcaYz9evo7n0il5aYVX3ReRf2ccLFpeLFf8FLXlY8k7xb0nFJxQdCH8klyvoOcbn0+wcitVxB2yVGCqnY23RfvLHzPXRgbOuENdZYq50bqOY0cORW8wZJoyZUEhtURqFWhjutbC8U2lr0Q67ABijHXawbJslVrRKC9QMhU1hr1pJSrKqHpjLOCYuZ4ouPxd2Ycda1XBFWzNCATBlhmVFVxbCqbSP10DRy+MOSS/qCZ4ORns5/ZWWgEVtCLZhUombKGMFaUzesb9re9Lql2mXOtxQDjX8Y9c67OM3TkIrL8hR/WfdhCfm/F+siffeB/VgzupncA8UiZ2x9yOt9ifV0Op1+AQAA//8DABxPTfk2BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLNshtSI5sIVpqQVGbBsb+94LaDRL31psw0My8fOadeSMRDkh6cns8/nLCmBdSkzxuIcGEpkxTpvbc9rztmW5aLRTn3WdSk/ERv5GeGOU99xapRI1UBdZSJwdPQ0DLGQ9MtKrUhJeF9G8ExibgafTYzMtfmJZxjqQnf48xzF9LbxgbP81raNI84UNcMkRfxPz5cPdI/7jd7Z92l78WXEpyMwbSE0AupeWCAnpXRAzUWc6ps7qTA+/YMMhL2hgzpghTs4QvP/qHOUfMvheNkA2jwpJzTQJkKJIdLLj/diwqNkT3JV5v4e2zfyMnTKQX9XeYl3I3fhpv8nyc8ITTEdKC6cbPhwPEQGpyTPMRUx5xw7K9dJogj3Osrs3uphFjJjV5eq52E+RhTod3rJ4vCB7+B4ETJkd6IlkrtTGopJRgtRMMg9Fea+E8MDM4gdwja60ylhvGpRXMylabzgRg1pOavEyzg+n3ZeNnBXNdYEZogQqtUFpbz1B2OLQGWQCtne68DbIzQnWhk2ABuHVovNW+GOQD+NcxYvVwT3rCOIogO6SD4JqqLgCFVlqqtdQDcm+N4KQmd3PMY1zndakeYsaXtPEjPfkVpgUv5G6Tfx0z+rymAvWTLs12aQ6rz9UPB3DWsEYyVpLWfFxz9RHDmNBnLHj3ab3W+0+K0GJLultTwpirHeTX6jdYXgsV6DwHW56qvR+wZTiwwEAzE1AHjUGJYIKyKmhlmdcKHFcBWu6GEiE1wWK4avO4ZiwIC54C1x1VHRrqHB8oSKcGbpyT5j3H6mnanOE7pjBIanEIVFkO1CopaZAtGKEHY8TwE5Kr+TgrgAOeLtt6URBCh0FzKhRvqbKW0862hjrTOet0h60vmh8xRZx+AvVh9Gle5iFXV+9W79B9XGO5ONXm469jpP9sM7qf/RdMVZkxjLEs2HWs55ocEJY14WE7WP0b2ZXtqvbb3ZKsUaxVNXnOkPJ6/B62jVJSnc/n878AAAD//wMAIo9cfPUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUzW7jNhSFX0XgWlRISqJE7VKngxqZTIyJOy26uyKvHCIyaVBUpkHgdx9QdhBnBl10J1zx/n3nkK/EwR5JR64Ph9+f0cWJ5CTaJSSYkJRJyqotVx2vOyYLKRQva/EPyYm9xRfSkabSmmuFtESJtDKspn05aGoMKs64YaKuUk3YTaR7JWALg89WY+Gnbxgm6x3pyF/WGf899QZb6NHPpgh+xLWbIjidhvmyXt3Sz9eb7f3mdGrCKSUX1pCOAPKyVFxQQN2nIQbaK85pr2RbDrxlw1Ce0qyLGByMxWSe3vsbHx1G3YlClAWjQpFjTgxESCP3MOH25ZCmWBDdpHi+hJfP7pU8YyCdyN9gnspd6dFe7ad+tqO5Gv1uZ91u5d1gd3OAmBrn5BD8AUO0uMD5zToIL5/9bpfqkU8wTkhyssWwtw7G048bHGAe44Ofw0JmMk//dSaRmaMnOVlIjuPSNjsvsxotuvhL7kXj+4dsM0IcfNh/EOnhxH79P9DfgX60DrP1DekI4yhM2SIdBJe0ag1QqEtFpSzlgFyrRnCSk5V30brZz1O2dhF3Z2of5rsO+tFG1HEOaae/ZTLbJngz65i9C8xZwYqSsZQ0x8Mcs69obEAdMS2xDfO53k8pQool6ZMd8Rc8qzkEdDHbQHzM/oDpkXREQas5KFAAUusBa4YDMwwkawxKI9FUwjSmUpWRlWJaVtDzykDN+yFFSE4wmSxbfM1MpZlhgkomG1oZ2VOoq5I2te4BBmhMLz/Aze7HRRTdsgpNSRUOhlaKA1VVWVJT1tAIOTSNGC44nQ3BWaJ+acI/JzRnL305Wfttd4PPp3u8zCmNadFITkXFa1opxWmr6ob2TdurXrZY67TZyrvJv4H8hqH3k43pEbmzzu5h/PnIuzK3GByOF+rcWR385IeYnW2ZfdDr6+zSK5YtFv1uHf13McaN108YsmQssO5CzOPxePwBAAD//wMAg6nKKwwFAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G36MJNvYgXadEbxRnFRLTUgqI2DYz97wW1GyTurTdiwBm+fOadeSPRHZH05P50+uWMMS+kJjlsIcGEpkxTpg7c9rztmW5a23WW2b9JTcITfiM9Mcp77i1SiRqpAtbSQY6eAqDljAMTrSo13eeF9G/EhQbwHDw28/IJ0xLmSHryZ4gwfy1vu9D4aV6hSfOEu7hkF30R88fu4Yn+fr8/PO+vtxZcSnITgPTEIZfSckEd+qGIGOlgOaeD1Z0cecfGUV7TQsyYopuaBb78eB/mHDH7XjRCNowKSy41AZddkTy4BQ/fTkXFhuixxOstvB37N3LGRHpRf4d5LXfnp3C3rMOEZ5xOLi2Y7vx8PLoIpCanNJ8w5YAbljOmYV5CLkS9N67rOIfOGdUa6GQrpAHJEDpmWrBgnVZ6FL5tRxg6a9SoQHAFGpUaDKnJBm6aXA5zrG7aH6aAMZOaPL9U+8nlcU7Hd+hfrkR3/wNokU16IlkrtTGopJTO6kEwBKO91mLwjplxEMg9stYqY7lhXFrBrGy16Qw4Zj2pyQfnX0PEavdIesI4CpAd0lFwTVUHjrpWWqq11CNyb43gpCYPc8whrvO6VLuY8XPa/kt68qubFrz+9D7515DR5zUVCH/pYsZ9mmH1ufphAM4a1kjGStKaT2uuPiKEhD5jwXFI663ef1KEFlvSw5oSxlztXX6tfnPLK+mJdZ3nzjrrnPZ+xJbhyIA5zQygBo2gBBhQVoFWlnmt3MAVuJYPY4mQmmDxW7VZHDohtbCWemUHqrR31HnZUaYQFPfathzecayep62TvitXJLU4AlWWO2qVlBRk64zQozFi/AnJzSycFcCA5+uwbgo0QIegORWKt1RZy2lnW0MH0w120B22vmh+whRx+gnUh+DTvMxjrm5eq96h+7jGsnCqzXdfQ6T/bD16nP0XTFXpsQuxzNetrZeaHNEta8Ljtq/6N7Ivw1UdtrUlWaNYq2rykl3K6+l72DZKSXW5XC7/AgAA//8DAL+PBmf0BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPpkJSFCUK2IPhtFgjTWMk7u5ibxQ5jInIokFRToMi/31BWa6dooe9CaP5eO/NG/5AvdoDatDycPh0hD4OaIGim0KMMIGJwIRvqWxo2RCRSyloJfi/aIHcHbyhBlVca6ol4AIEYG5IidvCamwMSEqoIazkqad6HlDzAymXGzg6Dbkf/oIwON+jBv3teuNf02zlct350eTBd7Duh6h6ncB8Xa/u8JflZvuwOWUNMKTi3BnUIAW0KCRlWIFuEwiLW0kpbqWoC0trYm1xKnN9hNCrLh/My2W+8bGHqBuWsyInmEn0vkBGRZUgt2qA7dshoZgkuk3xxRSePpsf6AgBNWxxFvPU7kZ37mY/tKPrzE1U4RmiDWoPrz68wFF1aIEOwR8gRAeTNg9P2QUSJTnJmWCEoAW6V3rnesjWt9lDNxHWNeFgCizBGswlVVjyosCmKFXFhK0qZtEC3UHoobvqeu908IO3MZslzz7MWfk+un7045Ct+wjPQcVT3WfVDYAWaDO2nRt2j6DM29Y/julfP3aJy2oMAfqYbVTcZX+qYYcaJFWtqZJKKiW0tlASsMQQJUhlQBgBhjNTGS65EVwSLbhqKTeqpK1NEbRAD2M8jPGs//crENvg9nswFwTbSeNNp6L1Yb820EdnXVrNOWMyVNdNrLK556pz0McPIqMGEQrMFDVgy6jAvDYKq7KQWIhCWKBaVoyeBLPuefypE7HW8MKWdS1a2VICzFY1J5VtidG20NKULVSsIlUNtdSEtCWndSs1MUBLbhK74M2o469WKKYFPY59us5snWi/uh5/F/yiyCb46LXv0sJcB09vQ4T977a28vuDH1xM/O282m8D3Ps+/Z3vf9ZsLl76eIlBuoNsOj3gpWC6BMwIB8yZVrhWpMUVE5KaWouWC7RAt16/QMiSwZTrp52cPfXwlC2D3rkIOo4hTf5nInVa5+fzyVz0yL9+2q58gOXhsJijfxyTSKdmZwN8eFe+DSl954d4RXmm9gSdPQO7spOB4+mpcikojKnBCIoZpyXmUlJcy7LCbVW3shU1lPpi1+wRjAug49RvG8Y0bVb2d75chuis0nFIx/PF68lOsz/nlF9BXq3tZ/Vp+NXqZivNnShJln06PZyTg/7Xu/n+/v7+HwAAAP//AwA2OBCbKAYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTT2/cNhDFv4owZ1HmP1GibsY6RRdu4kWySIveKHIYE5apBUU5CYz97gG1GzjprRdBGGhm3vu90StE84wwwO3p9O4FY16ghhy2EqdcEaoIlUemB9YOtGsU7Rjv1L9QQ7jH7zBAJ61lViMRqJBIR1syCm+Jc6gZZY7yVpaZ5ssCwyuY0Dh8CRabefmMaQlzhAH+DtHNX8tuExo7zatr0jzhPi7ZRFvEfNjv7slft4fjw+Hy1YJLaW6CgwEMMiE048SgHYsIT0bNGBm16oVnPfVeXNpCzJiimZrFPb3td3OOmO3AGy4aSriGcw3OZFMkj2bB4/dTUbEhuiv1eitvr8MrvGCCgdc/YV7G3dgp3Dwv4xomd3N5ZrM8FZenNJ8w5YAbk83mNJkc5lhdN+2mgDFDDQ+fqsNksp/T82+gPl387/+H/WPZ/u4b2jWj281rzDAAEz3U8N7YxxCx2t/BAJQhd6JH4jlTRPbOENMKTZQSyiOzuuMMatjNMYe4zutS7WPGL2kzAAP8YaYFL9Jvk30MGW1eU3H1jyq3cEizW22u3vgz2tBGUFqa1nxac/URXUhoMxZ/x7Re5/2nhSu+Ne3WlDDm6mDyY/WnWR5hAG16y4w22hhlrceWoqeOGkU7h8opdJK7zkktnZKaWiXNyKQzLRt9qUANWOKutgvrhdej7zRxHFsi+wJbYEckdaOWGqk0hckbx+ph2qKxPZXoBNHoHZGaGaKlEMSJ1nRc+a7j/hck1/QZLcMcvlz+lU2Bcq5HpxjhkrVEas1Ir9uOjF0/6lH12Nqi+R5TxOkXUO+DTfMy+1xdj6f6Dd3HNZb/vdoO6WuI5NuW0d1snzBVJWMTYjnva6zn8/n8AwAA//8DAIHwA5s2BAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227jNhD9FYGvFRXeRIl6S50saqRJjLXRFn0bkaOYjUwZFJXsIsi/F5KTzWVRoC+CMCTnnDlzZp5IgAOShpwfj5cPGNJIcpL8EhJMaMo0ZWrHTcPLhlWFZkZqZv4mOfFX+J00pFLWcmuQStRIlWMlbWVnqXNoOOOOiVLNOeFuJM0TAV84fPAWi2H8A+Poh0Aa8qcPbnicscEXth8mV8Shx3UYEwQ7k7lZr67o7+eb3e3mdGvEcX5ceEcaAsilNFxQQNvOJDraGs5pa3QtO16zrpOnZz4kjAH6YnT3b/huSAGTbUQhZMGoMOQ5Jw4SzJRbGHH3/TizWCS6mOP5El5+myfygJE0In8V85TuzPb+7DC2k+/d2fIlOTnG4YgxeVzUuN1mbyQ4K1ghtGCM5OQa7N4HzNYX2W2/lGhrptBJarBzVBkO1CgpqZMlVEJ3VSU6kpMrjAH7d1mvvY3DOHQpexE5+4CzPV9dBmh7nDG+QD8iycl63CYIDvoh4OU3tFM65drFaT5eDSH5MA3TmK1DwrsIL+ev71dTjBhStoG0z36DcU8aYqC2HAwYAG1thyXDjjkGmlUOtdPolHCVU0Y5rQyzWkHLlYOSt90cITn5ddZwO1mL4/jGZjFJ3y8cspc+rXqPIX2QkTSEcRRO1kg7wTVVtQMKpTRUa6k75NZUgr/C7CDeYSIN6eq2bZm1rVCGWxAGpOuYdLozQlQVqE6WlRJal6wC66pKcOVkhRZkJ1ktSE42cXCTTZ9bLZcGbOLwD9o0S0UaollZ1hbaVmjFazA1Kq5VrQ1WRnXAlLQt8LKGtuUMSmZNLSxwbUC1rux+qHQZ7nzACz8ee/j+DrcudCF/sQA170B9uv4lwgEfh3h/c7JxcXO5Izn5OoV5I2Tr2SKPPtBvep7o66lPfrePCA7d9eDwZx/hPC7ZMqGd0qXVraF16TRVBktaKzC0dBUKq/RcLMnJxWDvMWazw8CHeap+JLvdZufR7n1Cm6Y48/tr4XG7zTY9pG6Ihw+b5F1hnwUohORs7ozDh9M2Wjhq52p0mlOheEmVMZzWpqxoW9WtaXWNpZ053k7pOKXsKzof0aal4hcvLpirPdr7n8V4dcGLRTmbvbY9bbFF2v+1xJ5zckAYp4iHZVs3T2QdAsYF+WI6jeI6XPu+9yPaIbiRNMawomaKvxD8r2ucKVHoSqnn5+fnfwEAAP//AwCMO3OQGgYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTy27cOBD8FYFnUeZLlKibMd5FBk7Wg8RIFnuj2K0MEZkcUJSTwJh/X1AzQeLcciOKrO7qquYLCfYJyUBuT6e/njHkhdQk+w0STGjKNGXqkZuBtwPrGs2MYUr+R2ri7/E7GUinnOPOIJWokSpgLR3l5CgAGs44MNGqUtN+XsjwQqxvAJ+9wyYuHzEtPgYykE8+QPxaelvfuDmu0KQ44z4s2QZXxPyz393Tt7eHx4fD5dWCSyE3HshALHIpDRfUohuLiImOhnM6Gt3LifdsmuSF5kPGFOzcLPDlZ3+IOWB2g2iEbBgVhpxrAjbbInm0Cz5+PxUVm0V3Ba83eDsOL+QZExlE/cPMS7kbN/sbF5+ebICbyQe/HElNTimeMGWPmx3bhPNss4+hujbZzR5DJjV5+FAdZpunmJ5eefThMvr+DyZ/Z93RB6z2d2QgjKMA2SOdBNdU9WCpbaWhWks9IXemE5zUZBdD9mGN61LtQ8bPaVNJBvK3nRe86LtN7ugzurymIv1fXbLGbz7vIhSAkZocUoTV5eqn3Zw1rJGsXD6s+bTm6j2CT+gylpke03ot/xtFaLGRdmtKGHJ1sPlYvbHLkQzE2N5xa6yxVjs3YctwYsCsZh2gBo2gBHSgjAKtDHNa2ZErsC0fp4IU3SXd6rJQWnDmeqCGY0uVVJz2E7cUjJNWasNY176ytXqYN57rmUKQ1OAEVBluqVFSUpCt7YSeuk5Mv1hyTZyz4jfg8+VrbAo0QI+gORWKt1QZw2lv2o6OXT+aUffYuqL5HlPA+Rej3nmX4hKnXF0Xpnpl3fs1lO9dbcvz1Qf6bYvsLrovmKoSufWhbPM15fP5fP4fAAD//wMA0tpm8SUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUUW/cKBDHv4o1z4sDGGPjt2jT6la5Xlbtqne6NwxDg+LgFeC0UbTf/YS9pzZPp3tBaMwM//nNH79B0M8IA9yezx9eMOQEO8h+DXHKJaGSUHFiamDtQLta0pa3lP4NO/D3+AoDdMIYZhSSBiUSYWlLxsYZYi0qRpmlvBWlpv6WYHgD7WuLL95gPaevGJOfAwzwpw92/l7u1r4207zYOs4THkLKOpgi5o/D/p78fns8PRy3UwlTSa69hQE0sqZRjBONZiwiHBkVY2RUsm8c66lzzZbmQ8YY9FQn+/TzfjvngNkMvOZNTQlXcNmB1VkXyaNOeHo9FxUrorsS363hdTu8wQtGGPjuX5hbuRsz+ZvnNC5+sjfbmnV6SuvqtMlzfIUdnON8xpg9rnxuU8LncXo96fT0cTtTtunDDzRLRrufl5BhANa0sIMV0DTp7OdQXTXuJ48hrx9z9CF581+1CpqHL9Vx0tnN8fndPL5smA//g/InbR59wOpwBwNQhtw2PRLHmSSit5rotlFEykY6ZEZ1nMEO9nPIPizzkqpDyPgtri3BAB/1lHDTdxvNo89o8hJLn3/J4qtjnO1icvVzlozWtG4oLUlLPi+5+ozWRzQZSxOnuFzrfX2fwiVfk/ZLjBhyddT5sfpNp0cYQOneMK200loa47Cl6KilWtLOorQSreC2s0IJK4WiRgo9MmF1y0ZXIrADLNapVrfKzkjndEfa0RkirKGk55YRrloje8lFj+Ydx+phWvmbngq0DVHoLBGKaaJE0xDbtLrj0nUdd78gufqB0QLY4sv27jYF1vZoJSNcsJYIpRjpVduRsetHNcoeW1M032MMOP0C6pM3cU6zy9XVIdU7dJ+XUP4d1eqW7z6QH+uM7mbzhLEqM9Y+lKdyHevlcrn8AwAA//8DANrekCGCBAAA

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,56 @@
using Microsoft.AspNetCore.Mvc;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class HelpDrawerController(
IHelpService help,
IHelpMarkdownRenderer markdown,
IWebHostEnvironment environment) : Controller
{
public IActionResult Article(string key)
{
var article = help.GetByContextKey(key);
return PartialView("_HelpDrawerArticle", new HelpDrawerArticleViewModel
{
Article = article,
RequestedContextKey = key,
RenderedHtml = article is null ? string.Empty : markdown.Render(article.MarkdownContent),
RelatedArticles = article is null ? [] : help.GetRelatedArticles(article)
});
}
public IActionResult Test()
{
return View();
}
public IActionResult Image(string file)
{
if (string.IsNullOrWhiteSpace(file) || file.Contains("..") || Path.IsPathRooted(file))
{
return NotFound();
}
var imageRoot = Path.Combine(environment.ContentRootPath, "Help", "Images");
var fullPath = Path.GetFullPath(Path.Combine(imageRoot, file));
var safeRoot = Path.GetFullPath(imageRoot).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (!fullPath.StartsWith(safeRoot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(fullPath))
{
return NotFound();
}
var contentType = Path.GetExtension(fullPath).ToLowerInvariant() switch
{
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".webp" => "image/webp",
".svg" => "image/svg+xml",
_ => "application/octet-stream"
};
return PhysicalFile(fullPath, contentType);
}
}

View File

@ -64,6 +64,7 @@ public class Program
builder.Services.AddScoped<IAcceptanceService, AcceptanceService>(); builder.Services.AddScoped<IAcceptanceService, AcceptanceService>();
builder.Services.AddScoped<IImportService, ImportService>(); builder.Services.AddScoped<IImportService, ImportService>();
builder.Services.AddSingleton<IHelpService, HelpService>(); builder.Services.AddSingleton<IHelpService, HelpService>();
builder.Services.AddSingleton<IHelpMarkdownRenderer, HelpMarkdownRenderer>();
var app = builder.Build(); var app = builder.Build();

View File

@ -0,0 +1,164 @@
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
namespace PlotLine.Services;
public interface IHelpMarkdownRenderer
{
string Render(string markdown);
}
public sealed partial class HelpMarkdownRenderer : IHelpMarkdownRenderer
{
public string Render(string markdown)
{
if (string.IsNullOrWhiteSpace(markdown))
{
return "<p class=\"muted\">This help article has no body content yet.</p>";
}
var lines = markdown.Replace("\r\n", "\n").Split('\n');
var html = new StringBuilder();
var paragraph = new List<string>();
var listItems = new List<string>();
var inTable = false;
void FlushParagraph()
{
if (paragraph.Count == 0) return;
html.Append("<p>").Append(RenderInline(string.Join(" ", paragraph))).AppendLine("</p>");
paragraph.Clear();
}
void FlushList()
{
if (listItems.Count == 0) return;
html.AppendLine("<ul>");
foreach (var item in listItems)
{
html.Append("<li>").Append(RenderInline(item)).AppendLine("</li>");
}
html.AppendLine("</ul>");
listItems.Clear();
}
void CloseTable()
{
if (!inTable) return;
html.AppendLine("</tbody></table>");
inTable = false;
}
for (var index = 0; index < lines.Length; index++)
{
var line = lines[index].Trim();
if (string.IsNullOrWhiteSpace(line))
{
FlushParagraph();
FlushList();
CloseTable();
continue;
}
if (line.StartsWith("#", StringComparison.Ordinal))
{
FlushParagraph();
FlushList();
CloseTable();
var level = Math.Clamp(line.TakeWhile(ch => ch == '#').Count(), 1, 4);
var text = line[level..].Trim();
html.Append("<h").Append(level).Append(">")
.Append(RenderInline(text))
.Append("</h").Append(level).AppendLine(">");
continue;
}
if (line.StartsWith("- ", StringComparison.Ordinal) || line.StartsWith("* ", StringComparison.Ordinal))
{
FlushParagraph();
CloseTable();
listItems.Add(line[2..].Trim());
continue;
}
if (LooksLikeTableRow(line))
{
FlushParagraph();
FlushList();
if (index + 1 < lines.Length && LooksLikeTableSeparator(lines[index + 1].Trim()))
{
if (inTable) CloseTable();
html.AppendLine("<table class=\"table table-sm help-markdown-table\"><thead><tr>");
foreach (var cell in SplitTableCells(line))
{
html.Append("<th>").Append(RenderInline(cell)).AppendLine("</th>");
}
html.AppendLine("</tr></thead><tbody>");
inTable = true;
index++;
continue;
}
if (inTable)
{
html.AppendLine("<tr>");
foreach (var cell in SplitTableCells(line))
{
html.Append("<td>").Append(RenderInline(cell)).AppendLine("</td>");
}
html.AppendLine("</tr>");
continue;
}
}
paragraph.Add(line);
}
FlushParagraph();
FlushList();
CloseTable();
return html.ToString();
}
private static bool LooksLikeTableRow(string line) => line.Contains('|');
private static bool LooksLikeTableSeparator(string line) =>
line.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.All(cell => cell.Length > 0 && cell.All(ch => ch is '-' or ':'));
private static IReadOnlyList<string> SplitTableCells(string line) =>
line.Trim('|').Split('|', StringSplitOptions.TrimEntries).ToList();
private static string RenderInline(string value)
{
var encoded = WebUtility.HtmlEncode(value);
encoded = ImageRegex().Replace(encoded, match =>
{
var alt = match.Groups["alt"].Value;
var url = match.Groups["url"].Value;
return $"<img src=\"/HelpDrawer/Image?file={WebUtility.UrlEncode(url)}\" alt=\"{alt}\" class=\"help-article-image\" />";
});
encoded = LinkRegex().Replace(encoded, match =>
{
var text = match.Groups["text"].Value;
var url = match.Groups["url"].Value;
return $"<a href=\"{url}\" target=\"_blank\" rel=\"noopener\">{text}</a>";
});
encoded = StrongRegex().Replace(encoded, "<strong>$1</strong>");
encoded = EmphasisRegex().Replace(encoded, "<em>$1</em>");
return encoded;
}
[GeneratedRegex(@"!\[(?<alt>[^\]]*)\]\((?<url>[^)]+)\)")]
private static partial Regex ImageRegex();
[GeneratedRegex(@"\[(?<text>[^\]]+)\]\((?<url>[^)]+)\)")]
private static partial Regex LinkRegex();
[GeneratedRegex(@"\*\*([^*]+)\*\*")]
private static partial Regex StrongRegex();
[GeneratedRegex(@"\*([^*]+)\*")]
private static partial Regex EmphasisRegex();
}

View File

@ -8,6 +8,7 @@ public interface IHelpService
IReadOnlyList<HelpArticle> GetAllArticles(); IReadOnlyList<HelpArticle> GetAllArticles();
HelpArticle? GetByContextKey(string contextKey); HelpArticle? GetByContextKey(string contextKey);
HelpArticle? GetByRelativePath(string relativePath); HelpArticle? GetByRelativePath(string relativePath);
IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5);
IReadOnlyList<HelpCoverageItem> GetCoverage(); IReadOnlyList<HelpCoverageItem> GetCoverage();
} }
@ -51,6 +52,25 @@ public sealed class HelpService(IWebHostEnvironment environment) : IHelpService
return GetAllArticles().FirstOrDefault(x => string.Equals(x.RelativePath, normalisedPath, StringComparison.OrdinalIgnoreCase)); return GetAllArticles().FirstOrDefault(x => string.Equals(x.RelativePath, normalisedPath, StringComparison.OrdinalIgnoreCase));
} }
public IReadOnlyList<HelpArticle> GetRelatedArticles(HelpArticle article, int maxItems = 5)
{
var tags = article.Tags.Select(x => x.ToLowerInvariant()).ToHashSet();
return GetAllArticles()
.Where(x => !string.Equals(x.ContextKey, article.ContextKey, StringComparison.OrdinalIgnoreCase))
.Select(x => new
{
Article = x,
Score = (string.Equals(x.Category, article.Category, StringComparison.OrdinalIgnoreCase) ? 2 : 0)
+ x.Tags.Count(tag => tags.Contains(tag.ToLowerInvariant()))
})
.Where(x => x.Score > 0)
.OrderByDescending(x => x.Score)
.ThenBy(x => x.Article.Title)
.Take(maxItems)
.Select(x => x.Article)
.ToList();
}
public IReadOnlyList<HelpCoverageItem> GetCoverage() => public IReadOnlyList<HelpCoverageItem> GetCoverage() =>
GetAllArticles() GetAllArticles()
.Select(x => new HelpCoverageItem .Select(x => new HelpCoverageItem

View File

@ -15,3 +15,11 @@ public sealed class HelpArticleDiagnosticViewModel
{ {
public HelpArticle Article { get; set; } = new(); public HelpArticle Article { get; set; } = new();
} }
public sealed class HelpDrawerArticleViewModel
{
public HelpArticle? Article { get; set; }
public string RenderedHtml { get; set; } = string.Empty;
public IReadOnlyList<HelpArticle> RelatedArticles { get; set; } = [];
public string RequestedContextKey { get; set; } = string.Empty;
}

View File

@ -0,0 +1,21 @@
@{
ViewData["Title"] = "Help Drawer Test";
}
<div class="page-heading">
<div>
<p class="eyebrow">Developer validation</p>
<h1>Help Drawer Test</h1>
<p class="lead-text">Temporary controls for testing the contextual help drawer.</p>
</div>
</div>
<section class="plain-card">
<h2>Drawer controls</h2>
<div class="button-row">
<button class="btn btn-primary" type="button" data-help-open="timeline.plotLines">Open Plot Lines help</button>
<button class="btn btn-outline-secondary" type="button" data-help-open="missing.example">Open missing help</button>
<help-icon key="timeline.plotLines" />
</div>
<p class="muted mt-3">Use the drawer splitter to resize it, then reload this page to confirm the width is restored. Pin the drawer and navigate to another page to confirm it reopens.</p>
</section>

View File

@ -0,0 +1,41 @@
@model HelpDrawerArticleViewModel
@if (Model.Article is null)
{
<article class="help-drawer-article">
<p class="eyebrow">Help</p>
<h2>Help for this feature has not yet been written.</h2>
@if (!string.IsNullOrWhiteSpace(Model.RequestedContextKey))
{
<p class="muted">Context key: @Model.RequestedContextKey</p>
}
</article>
}
else
{
<article class="help-drawer-article">
<p class="eyebrow">@Model.Article.Category</p>
<h2>@Model.Article.Title</h2>
@if (!string.IsNullOrWhiteSpace(Model.Article.QuickSummary))
{
<p class="lead-text">@Model.Article.QuickSummary</p>
}
<div class="help-markdown-body">
@Html.Raw(Model.RenderedHtml)
</div>
@if (Model.RelatedArticles.Any())
{
<section class="help-related-section">
<h3>Related articles</h3>
<ul>
@foreach (var related in Model.RelatedArticles)
{
<li>
<button type="button" data-help-open="@related.ContextKey">@related.Title</button>
</li>
}
</ul>
</section>
}
</article>
}

View File

@ -58,22 +58,46 @@
</div> </div>
</nav> </nav>
</header> </header>
<div class="container-fluid plotline-shell @ViewData["ShellClass"]"> <div class="plotweaver-help-layout" data-help-layout>
<main role="main" class="pb-3"> <div class="plotweaver-help-main">
@if (TempData["ArchiveMessage"] is string archiveMessage) <div class="container-fluid plotline-shell @ViewData["ShellClass"]">
{ <main role="main" class="pb-3">
<div class="alert alert-info">@archiveMessage</div> @if (TempData["ArchiveMessage"] is string archiveMessage)
} {
@if (TempData["ArchiveError"] is string archiveError) <div class="alert alert-info">@archiveMessage</div>
{ }
<div class="alert alert-warning">@archiveError</div> @if (TempData["ArchiveError"] is string archiveError)
} {
@if (TempData["ImportMessage"] is string importMessage) <div class="alert alert-warning">@archiveError</div>
{ }
<div class="alert alert-info">@importMessage</div> @if (TempData["ImportMessage"] is string importMessage)
} {
@RenderBody() <div class="alert alert-info">@importMessage</div>
</main> }
@RenderBody()
</main>
</div>
</div>
<div class="plotweaver-help-splitter" data-help-splitter role="separator" aria-orientation="vertical" aria-label="Resize help drawer"></div>
<aside class="plotweaver-help-drawer" data-help-drawer aria-label="Help drawer">
<div class="plotweaver-help-collapsed-strip" data-help-expand role="button" tabindex="0" aria-label="Expand help drawer">Help</div>
<div class="plotweaver-help-drawer-panel">
<header class="plotweaver-help-drawer-header">
<div>
<p class="eyebrow mb-0">Contextual help</p>
<strong data-help-drawer-heading>Help</strong>
</div>
<div class="btn-group btn-group-sm" role="group" aria-label="Help drawer controls">
<button class="btn btn-outline-secondary" type="button" data-help-pin aria-pressed="false">Pin</button>
<button class="btn btn-outline-secondary" type="button" data-help-collapse>Collapse</button>
<button class="btn btn-outline-secondary" type="button" data-help-close>Close</button>
</div>
</header>
<div class="plotweaver-help-drawer-content" data-help-content>
<p class="muted">Choose More from a help popover to open the full guide.</p>
</div>
</div>
</aside>
</div> </div>
<div class="modal fade" id="plotlineConfirmModal" tabindex="-1" aria-labelledby="plotlineConfirmModalTitle" aria-hidden="true"> <div class="modal fade" id="plotlineConfirmModal" tabindex="-1" aria-labelledby="plotlineConfirmModalTitle" aria-hidden="true">

View File

@ -1453,6 +1453,148 @@ a:focus-visible {
--bs-alert-border-color: #705229; --bs-alert-border-color: #705229;
} }
.plotweaver-help-layout {
--help-drawer-width: 25%;
display: flex;
align-items: stretch;
width: 100%;
min-width: 0;
transition: gap 0.18s ease;
}
.plotweaver-help-main {
flex: 1 1 auto;
min-width: 0;
transition: flex-basis 0.18s ease;
}
.plotweaver-help-splitter,
.plotweaver-help-drawer {
display: none;
}
.plotweaver-help-layout.help-open .plotweaver-help-splitter {
display: block;
flex: 0 0 8px;
cursor: col-resize;
background: linear-gradient(90deg, transparent, rgba(107, 114, 128, 0.22), transparent);
}
.plotweaver-help-layout.help-open .plotweaver-help-drawer {
display: block;
flex: 0 0 clamp(20%, var(--help-drawer-width), 50%);
min-width: 20%;
max-width: 50%;
border-left: 1px solid rgba(107, 114, 128, 0.22);
background: rgba(255, 253, 248, 0.96);
transition: flex-basis 0.18s ease;
}
.plotweaver-help-layout.help-collapsed .plotweaver-help-splitter {
display: none;
}
.plotweaver-help-layout.help-collapsed .plotweaver-help-drawer {
flex-basis: 2.35rem;
min-width: 2.35rem;
max-width: 2.35rem;
}
.plotweaver-help-layout.help-collapsed .plotweaver-help-drawer-panel {
display: none;
}
.plotweaver-help-collapsed-strip {
display: none;
}
.plotweaver-help-layout.help-collapsed .plotweaver-help-collapsed-strip {
display: flex;
align-items: center;
justify-content: center;
width: 2.35rem;
min-height: calc(100vh - 5rem);
color: #5f6875;
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
writing-mode: vertical-rl;
cursor: pointer;
}
.plotweaver-help-drawer-panel {
position: sticky;
top: 0;
display: flex;
flex-direction: column;
max-height: calc(100vh - 1rem);
}
.plotweaver-help-drawer-header {
display: flex;
justify-content: space-between;
gap: 0.75rem;
align-items: flex-start;
padding: 1rem;
border-bottom: 1px solid rgba(107, 114, 128, 0.18);
}
.plotweaver-help-drawer-content {
min-height: 12rem;
padding: 1rem;
overflow: auto;
}
.help-drawer-article h2 {
margin-bottom: 0.5rem;
}
.help-markdown-body {
display: grid;
gap: 0.75rem;
}
.help-markdown-body h1,
.help-markdown-body h2,
.help-markdown-body h3,
.help-markdown-body h4 {
margin: 0.5rem 0 0;
}
.help-markdown-body p,
.help-markdown-body ul {
margin-bottom: 0;
}
.help-article-image {
display: block;
max-width: 100%;
height: auto;
border: 1px solid rgba(107, 114, 128, 0.18);
border-radius: 0.45rem;
}
.help-related-section {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid rgba(107, 114, 128, 0.18);
}
.help-related-section button {
padding: 0;
color: #2f6f63;
background: none;
border: 0;
font-weight: 700;
text-align: left;
}
.help-resizing {
cursor: col-resize;
user-select: none;
}
.plotweaver-help-icon { .plotweaver-help-icon {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -1502,6 +1644,20 @@ a:focus-visible {
border-color: #f1c38f; border-color: #f1c38f;
} }
[data-theme="dark"] .plotweaver-help-layout.help-open .plotweaver-help-drawer {
border-left-color: rgba(255, 255, 255, 0.12);
background: rgba(22, 22, 27, 0.97);
}
[data-theme="dark"] .plotweaver-help-drawer-header,
[data-theme="dark"] .help-related-section {
border-color: rgba(255, 255, 255, 0.12);
}
[data-theme="dark"] .plotweaver-help-collapsed-strip {
color: #d8d0c4;
}
.relationship-map-controls { .relationship-map-controls {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(150px, 1fr)) auto auto auto; grid-template-columns: repeat(4, minmax(150px, 1fr)) auto auto auto;

View File

@ -218,6 +218,170 @@
}); });
})(); })();
(() => {
const layout = document.querySelector("[data-help-layout]");
const drawer = document.querySelector("[data-help-drawer]");
const content = document.querySelector("[data-help-content]");
const splitter = document.querySelector("[data-help-splitter]");
const closeButton = document.querySelector("[data-help-close]");
const collapseButton = document.querySelector("[data-help-collapse]");
const expandStrip = document.querySelector("[data-help-expand]");
const pinButton = document.querySelector("[data-help-pin]");
const heading = document.querySelector("[data-help-drawer-heading]");
if (!layout || !drawer || !content) {
return;
}
const storage = {
width: "plotweaver.helpDrawer.width",
pinned: "plotweaver.helpDrawer.pinned",
key: "plotweaver.helpDrawer.key",
collapsed: "plotweaver.helpDrawer.collapsed"
};
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
const readBool = (key) => {
try {
return localStorage.getItem(key) === "true";
} catch {
return false;
}
};
const writeValue = (key, value) => {
try {
localStorage.setItem(key, value);
} catch {
// Help drawer preferences are conveniences; blocked storage should not break help.
}
};
const setWidth = (value) => {
const width = `${clamp(Number.parseFloat(value) || 25, 20, 50)}%`;
layout.style.setProperty("--help-drawer-width", width);
writeValue(storage.width, width);
};
const setPinned = (isPinned) => {
writeValue(storage.pinned, String(isPinned));
pinButton?.setAttribute("aria-pressed", String(isPinned));
if (pinButton) {
pinButton.textContent = isPinned ? "Unpin" : "Pin";
}
};
const setCollapsed = (isCollapsed) => {
layout.classList.toggle("help-collapsed", isCollapsed);
writeValue(storage.collapsed, String(isCollapsed));
if (collapseButton) {
collapseButton.textContent = isCollapsed ? "Expand" : "Collapse";
}
};
const openDrawer = async (key, options = {}) => {
const contextKey = key || "";
layout.classList.add("help-open");
if (!options.keepCollapsed) {
setCollapsed(false);
}
writeValue(storage.key, contextKey);
if (heading) {
heading.textContent = contextKey || "Help";
}
content.innerHTML = "<p class=\"muted\">Loading help...</p>";
try {
const response = await fetch(`/HelpDrawer/Article?key=${encodeURIComponent(contextKey)}`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
});
content.innerHTML = response.ok
? await response.text()
: "<p class=\"muted\">Help for this feature has not yet been written.</p>";
const title = content.querySelector(".help-drawer-article h2")?.textContent?.trim();
if (heading && title) {
heading.textContent = title;
}
} catch {
content.innerHTML = "<p class=\"muted\">Help for this feature has not yet been written.</p>";
}
};
const closeDrawer = () => {
layout.classList.remove("help-open", "help-collapsed");
setPinned(false);
};
setWidth(localStorage.getItem(storage.width) || "25%");
setPinned(readBool(storage.pinned));
setCollapsed(readBool(storage.collapsed));
document.addEventListener("plotweaver:help-more", (event) => {
const key = event.detail?.key || event.target?.dataset?.helpKey || "";
document.querySelectorAll("[data-help-icon]").forEach((icon) => {
window.bootstrap?.Popover?.getInstance(icon)?.hide();
});
openDrawer(key);
});
document.addEventListener("click", (event) => {
const opener = event.target instanceof Element ? event.target.closest("[data-help-open]") : null;
if (!opener) {
return;
}
event.preventDefault();
openDrawer(opener.dataset.helpOpen || "");
});
closeButton?.addEventListener("click", closeDrawer);
collapseButton?.addEventListener("click", () => setCollapsed(!layout.classList.contains("help-collapsed")));
expandStrip?.addEventListener("click", () => setCollapsed(false));
expandStrip?.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setCollapsed(false);
}
});
pinButton?.addEventListener("click", () => setPinned(!readBool(storage.pinned)));
splitter?.addEventListener("pointerdown", (event) => {
if (!layout.classList.contains("help-open") || layout.classList.contains("help-collapsed")) {
return;
}
event.preventDefault();
document.body.classList.add("help-resizing");
splitter.setPointerCapture?.(event.pointerId);
const onMove = (moveEvent) => {
const rect = layout.getBoundingClientRect();
const widthPercent = ((rect.right - moveEvent.clientX) / rect.width) * 100;
setWidth(widthPercent);
};
const onUp = () => {
document.body.classList.remove("help-resizing");
document.removeEventListener("pointermove", onMove);
document.removeEventListener("pointerup", onUp);
};
document.addEventListener("pointermove", onMove);
document.addEventListener("pointerup", onUp);
});
const pinnedKey = (() => {
try {
return localStorage.getItem(storage.key) || "";
} catch {
return "";
}
})();
if (readBool(storage.pinned) && pinnedKey) {
openDrawer(pinnedKey, { keepCollapsed: true });
}
})();
(() => { (() => {
const roots = document.querySelectorAll(".scene-inspector-root"); const roots = document.querySelectorAll(".scene-inspector-root");
if (!roots.length) { if (!roots.length) {