Timeline UI changes.

This commit is contained in:
Nick Beckley 2026-06-02 21:09:30 +01:00
parent 305446126e
commit 92aff32155
33 changed files with 429 additions and 88 deletions

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTS2/cNhD+K8KcRZkvUaJuxjpFF27iRbJIi94ozigmLFMLiXISGPvfA2o3SNxbLwQx4DfzPYavEN0zQQe3p9O7F4ppgRJS2EqSS8O4YVwcRdvptuNNVSvBTV3/CyWEe/oOHTTae+EtMUWGmEZes14NniGSFVwgl7XOPd2XBbpXcKFCegmeqmn5TPMSpggd/B0iTl/zbBcqP04rVvM00j4uyUWfyXzY7+7ZX7eH48Ph8mqhJYOrgNBBzxVy6QdmOCqmB+9Zq5qaIdaNEo3vNZoLLMREc3RjteDTr/k4pUjJd7KSquJMWjiXgC65TLl3Cx2/nzKLzaK7XC+38nbtXuGFZuhk+dPMS7sbP4ab56Vfw4g3lzO55SmrPM3TieYUaPNkkzmOLoUpFtdJuzFQTFDCw6fiMLo0TPPzG6M+XfTv/4f8Y57+7hv5NRHupjUm6EBoCSW8d/4xRCr2d9ABFyRRtcQGKQzTLTrmamWZMcoMJLxtpIASdlNMIa7TuhT7mOjLvAmADv5w40IX6rezfwyJfFrnrOofk3fhME+4+lT88l/wileK8wxa02lNxUfCMJNPlPUd5/Xa7z8QaeQG2q3zTDEVB5ceiz/d8ggdWNd64ayzzhnvB6o5DRy5M7xBMmgItcQGtdVotOXeaNcLja4W/ZArUALluIttw5TwaBvnGSlEpt3gWK/QMGGUxMYZMTTqjY/Fw5hxzrdcEypmaUCmrXDMaqUYqto10gxNI4ffLLmmL3g2GOnl8lc2BgaxJTSCSS1qpq0VrLV1w/qm7W1vWqp95nxPc6TxN6PeBz9PyzSk4ro8xRvrPq4x//diW6SvIbJvW0Z3k3+iucgZuxDzel9jPZ/P5x8AAAD//wMAk4+m1jYEAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxU227jNhD9FYGvFRXeRIl6S50saqRJjLXRFn0bkaOYjUwZFJXsIsi/F5KTzWVRoC+CMCTnnDlzZp5IgAOShpwfj5cPGNJIcpL8EhJMaMo0ZXzH60bVDauKUiqmZf03yYm/wu+kIZWylluDVKJGqhwraSs7S51Dwxl3TJRqzgl3I2meCPjC4YO3WAzjHxhHPwTSkD99cMPjjA2+sP0wuSIOPa7DmCDYmczNenVFfz/f7G43p1sjjvPjwjvSkJZJx4TtqGZOUtVZS2tZldS5spK8sq1y+vTMh4QxQF+M7v4N3w0pYLKNKIQsGBWGPOfEQYKZcgsj7r4fZxaLRBdzPF/Cy2/zRB4wkkbkr2Ke0p3Z3p8dxnbyvTtbviQnxzgcMSaPixq32+yNBGcFK4QWjJGcXIPd+4DZ+iK77ecSwdZMoZPUYOeoMhyoUVJSJ0uohO6qSnQkJ1cYA/bvsl57G4dx6FL2InL2AWd7vroM0PY4Y3yBfkSSk/W4TRAc9EPAy29op3TKtYvTfLwaQvJhGqYxW4eEdxFezl/fr6YYMaRsA2mf/QbjnjTEQG05GDAA2toOS4Ydcww0qxxqp9Ep4SqnjHJaGWa1gpYrByVvuzlCcvLrrOF2shbH8Y3NYpK+XzhkL31a9R5D+iAjaQjjKJyskXaCa6pqBxRKaajWUnfIrakEf4XZQbzDRBrS1W3bMmtboQy3IAxI1zHpdGeEqCpQnSwrJbQuWQXWVZXgyskKLchOslqQnGzi4CabPrdaLg3YxOEftGmWijREs7KsLbSt0IrXYGpUXKtaG6yM6oApaVvgZQ1tyxmUzJpaWODagGpd2f1Q6TLc+YAXfjz28P0dbl3oQv5iAWregfp0/UuEAz4O8f7mZOPi5nJHcvJ1CvNGyNazRR59oN/0PNHXU5/8bh8RHLrrweHPPsJ5XLJlQq00bctKS23lHFXMKgraIu06KcqulBwX+14M9h5jNjsMfJin6key2212Hu3eJ7RpijO/vxYet9ts00Pqhnj4sEneFfZZgEJIzubOOHw4baOFo3auRqc5FYqXVBnDaW3KirZV3ZpW11jaWbHbKR2nlH1F5yPatFT84sUFc7VHe/+zGK8ueLEoZ7PXtqcttkj7v5bYc04OCOMU8bBs6+aJrEPAuCBfTKdRXIdr3/d+RDsEN5KmVlwVRtfqheF/36tUwStdPj8/P/8LAAD//wMAb1GgshsGAAA=

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUS2/bOBD+KwLPokJKFCUK2IPhtKiRpjESd3fRG0UOYyKyaFCU0yDwf19Qlmun6GFvwmhe32P4jnq5A9SgxX7/6QB9GFCKgp1COck5JhwTuqF1U5KG0IzzvMgp+YFSZO/gDTWoYkpRJQAXwAEzTUrcFkZhrUFQQjXJSxZ7yucBNe9I2kzDwSrI3PA3+MG6HjXoH9tr9xpnS5upzo06866DVT8E2au4zLfV8g5/Xaw3D+tT1gBDLM6sRg3SRcu1AIIZlCVmbcWx4DnBFFrFqlJKWdWnMtsH8L3sskG/XOZrF3oIqsmzvMgIzgU6pkjLIOPKrRxg87aPW0wU3cZ4OoWnz+YdHcCjJk/PZJ7a3ajO3uyGdrSdvgnSP0MwXu7g1fkXOMgOpWjv3R58sDBx8/CUXFaiJCNZznNCUIrupdraHpLVbfLQRcBS1YSBLrAAozETVGLBigLropRVzk1V5Qal6A58D91V13urvBucCclMefJhztL1wfajG4dk1Qd49jKc6j7LbgCUovXYdnbYPoLUbxv3OMZ//dhFLMvRe+hDspZhm3yRwxY1SMhaUSmkkJIrZaAkYIgmkpNKA9ccNMt1pZlgmjNBFGeypUzLkrYmRlCKHsawH8OZ/59XS2y83e1AXzbYTByvOxmM87uVhj5YY6M054zJUF03oUrmnsvOQh8+kIwaRCjkuqgBm5xyzGotsSwLgTkvuAGqRJXTE2HGPo+/eCLGaFaYsq55K1pKIDdVzUhlWqKVKZTQZQtVXpGqhlooQtqS0boVimigJdMRnXd6VOF3KxSTQI9jH68zWUXYr7bHPzm7MLL2LjjluiiY7eDpbQiw+5NqS7fbu8GGiN/M0n4f4N718e98/zNnc/HChUsM4h0k0+mJmkrWmhqXVOSY6aLENSE1NoSxkiopeFGhFN069QI+iQaTtp80OXvq4SlZeLW1AVQYfZz87wTqJOfn88lc+Mi+fdosnYfFfp/O0b8OkaRTs7MBPrwr34eYvnVDuII8Q3uCzpwXu7KThsPpqbIxyLWuQXOKc0ZLzISguBZlhduqbkXLayjVxa7JI2jrQYWp38aPcdrM7J98ufDBGqnCEI/nq1OTnWZ/zim/L3kl26/q0/Ar6WYrzZ0oiZZ9Oj2ck4P+17t5PB6P/wEAAP//AwD7D0aXKAYAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/jNhCG/4rAs6mQFEWJuqVOFzWy2RibdFv0NiJHDhGZNCgq2yDIf19QdhBnFz30Joz4zsczL/lCPOyRdOTycPj9CX2ayIokt4QEE4oyRRm/521Xs47xUqi6Ebz+h6yIu8Zn0pFGGsONRlqhQiotq2lfDYZai5ozbpmoZc4Ju4l0LwRcafHJGSzD9A3j5IInHfnLeRu+59rgSjOG2ZYxjLjxUwJvcjNfNutr+vlye3+7PZ6acMri0lnSEVv1ympkVGJdU9k3imolGOXYG9nUANC0R5nzCaOHsZzs43t9G5LHZDpRiqpkVGjyuiIWEuSWe5jw/vmQu1gQXeX4agkvn90LecJIOrF6g3lMd2FGd7Gf+tmN9mIMu53zu3Xwg9vNEVIuvCKHGA4Yk8MFzm/OQ3z+HHa7nI98gnFCsiL3GPfOw3j8cYUDzGO6C3NcyEz28b/OkI7AnAJZkYXkOC5li9Mw69GhT79ozwrf3hXbEdIQ4v7Dku6O7Df/A/0NmAfnsdhckY4wjsJWLdJBcEVla4FCXWmqVKUG5EY3gpMVWQefnJ/DPBUbn3B3ovahv8toHlxCk+aYZ/pbZbNtY7CzScX7gjkrWVkxlkVzOsyp+IrWRTQJ8xD3cT7l+0kilFhEn9yIv+BZzzGiT8UW0kPxB0wPpCMaWsNBgwZQxgxYMxyYZaBYY1FZhVYK21ippVVSM6Mk9FxaqHk/5AhZEcwmKxZfa2xQClFTJQWnUgPQvmUVBcaYwmFQCu0HuMXtmHVgWibRVlTjYKnUHKiWVUVtVUMj1NA0YjjjdDIEZ5n6uQn/nNCevPTlaO232S0+He/x0qeytkWrOBWS11RqzWmr64b2TdvrXrVYmzzZOvgpvIH8hrEPk0v5Eblx3u1h/PnI+2auMXocz7Zz40wMUxhScbJl8WFfX2efX7Fiseh35+m/izGugnnEWGRjgfNny3x9fX39AQAA//8DAMAt750MBQAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOiN5IxsIVpqQVGbBsb+94DaDRL31psw0DszfOadeSPRHZD05PZ4/OWEMS+kJnncQoIJTZmmjO9517esZ7zh1mgm5T+kJuMjfiU9MSoEHixSiRqpAtZSL4dAAdByxoGJVpWc7mUh/RtxYwN4GgM28/IJ0zLOkfTkrzHC/KXUdmMTpnmFJs0TPsQluxhKM38+3D3SP253+6fd5a8FlyJuRiA9Aek1WGRUYdtS5Y2mVgtGOfqgTOucM91FNsaMKbqpWeDzj/ow54g59KIRsmFUWHKuCbjsSsveLbj/eixdbIjuS7zewttn/0ZOmEgv6u8wL+luwjTe5Pk44Qmno0sLppswHw4uAqnJMc1HTHnEDcv20mlyeZxjdS12N40YM6nJ03O1m1we5nR4x+r5guDhfxA4YfKkJ5K1UhuDSkrprPaCIRgdtBY+OGYGL5AHZK1VxnLDuLSCWdlq0xlwzAZSk5dp9m76fdn4WcF8B8wILVChFUprGxjKDofWIAOntdddsCA7I1QHnXTWOW49mmB1KAb54MLrGLF6uCc9YRwFyA7pILimqgNHXSst1VrqAXmwRnBSk7s55jGu87pUDzHjS9r4kZ786qYFL+RuU3gdM4a8pgL1b12K7dIMa8jVDwdw1rBGMlZEaz6uufqIMCYMGQvefVqv+f4jEVpsors1JYy52rn8Wv3mltdCxXWBO1ueqkMYsGU4MGBOMwOoQSMoAQaUVaCVZUEr57kC13I/lAipCRbDVZvHmWfDgIhU29ZSZb2gvms1tQDAEGHwSr/jWD1NRedCxxSCpBYHoMpyR62SkoJsnRF6MEYMPyG5mo+zAhjwdNnWrQMN0CFoToXiLVXWctrZ1lBvOm+97rANpedHTBGnn0B9GEOal3nI1dW71Tt0H9dYLk61+fjLGOm/24zu5/AZU1Vm7MZYFuw61nNNDuiWNeFhO1j9G9mV7ar2290SupFc6Jo8Z5fyeryGpW2U7OT5fD5/AwAA//8DAD65h/X1BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUT2/cNhDFv4rAsyjzP0XdDLtFDTfNIjbaorcRZxQL0UoLito0MPzdC2o3SN1bb8JAb/j4mzd8ZTMciXXs9nT66UxzXlnN8riXlFCOC8eFfJZtZ0UnZKO0dVqrv1jNxkf6xjrmTYwyBuKaHHGDwvJeD5EjUpBColDWlJ7weWXdK4OxQTqPkZpl/Z3SOi4z69gf44zL13I2jE2clg2btEz0MK8Z5ljM/PZw98h/vT08fzxc/lppLeJmRNYx1L3DQIIbspab3jsenBJcUh+NtwDg24tsnDOlGaZmxS8/zsclz5RjpxqlG8FVYG81Q8hQLPew0vO3U3GxI7ov9Xov75/dKztTYp2qv8O8tLuJ03izbv1EZ5pOkFZKN3E5HmFGVrNTWk6U8kg7ljOlflnHXIjG6KFtpcQWvLEeW22V9qgFYSu8xYABnHGDitYO2LfBm8GgkgYdGdN7VrMd3DRBHpe5unq/m0aaM6vZx6fqMEEelnR8h/7pQvThfwAttlnHtLDaeU9Gaw3B9UoQehedU30E4YdekYwkbDA+SC+kDkoEbZ1vPYIIkdXsA8SXcabq4Z51TEhSqFvig5KOmxaBg9WBO6fdQDIGrySr2d0y53Helm2tHuZMn9N+X9axn2Fa6XLT2xRfxkwxb6lA+NOVMB7SglvM1Y8ASNGIRgtRRFs+bbn6RDgmipkKjue0Xfv9R6Kc2kV3W0o05+oA+aX6BdYX1rEAbZQQIAC4GAeyggaBApzwSA4doVHo0QSDzgQRnYFeGgQr+6FUWM2o5K3aIz5I25P1hg8oem6U7zkEDBwwDLYkJPbDO47Vx6noILbCEGoeaEBuggQejNYctQWv3OC9KrrvSK5hkaIARjpflnV34BBbQie5MtJyE4LkbbCe977tQ+9asrF4fqQ00/QvUB/GmJZ1GXJ1zVr1Dt2nbS4PTrXn7us487/3Gd0v8QulqswYxrns13WsbzU7EqxbouP+XnWv7FCWq3reny3lGi2Vq9lThpS307WsQ2N0q9/e3t7+AQAA//8DAI0J9d70BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9K8KcRZmkKErUzVin6MJNvEgWadEbxRnFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4CsE+E/Rwezq9e6GQFigh+a0kudSMa8bFUXR9w3veVXXHjebiXyjB39N36KFVzglniNWkiSnkDRvq0TFEMoIL5LJRuaf9skD/CtZXSC/eUTUvnykufg7Qw98+4Pw1z7a+ctO8YhXnifZhSTa4TObDfnfP/ro9HB8Ol1cLLRlceYQesB40GuJMUdMwNbSaGS05EzQ41TbW2ra7wHxIFIOdqgWf3ubjnAIl18tK1hVn0sC5BLTJZsqDXej4/ZRZbBbd5Xq5lbdr/wovFKGX5U8zL+1u3ORvnpdh9RPeXM5kl6es8hTnE8XkafNkkzlNNvk5FNdJu8lTSFDCw6fiMNk0zvH5N6M+XfTv/4f8Y57+7hu5NRHu5jUk6EHUBkp4b92jD1Ts76AHLkhi3REbpdBMdWiZbWrDtK71SMKZVgooYTeH5MM6r0uxD4m+xE0A9PCHnRa6UL+N7tEncmmNWdU/Ou/CIc64ulS8+S94xaua8wxa02lNxUdCH8klyvqOcb32+w9EarmBdmuMFFJxsOmx+NMuj9CDsZ0T1lhjrXZupIbTyJFbzVskjZpQSWxRGYVaGe60soNQaBsxjLkCJVCOu9g2bFCmUUYaJlupmHLcMYOETKtWtUrJpu2ytDcfi4cp46zruCKsmaERmTLCMqPqmmHd2FbqsW3l+Isl1/QFzwYjvVz+ysZAI3aEWjCpRMOUMYJ1pmnZ0HaDGXRHjcuc7ykGmn4x6r13cV7mMRXX5Sl+s+7jGvJ/L7ZF+uoD+7ZldDe7J4pFztj6kNf7Guv5fD7/AAAA//8DADYi/1M2BAAA

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUwW7cNhD9FWHOokxKFCXqZqwTdOGmXiSLtOhtRI5iwjK1ICknhrH/XlC7ReJT0QtBjPiGb9571Bt4fCYY4PZ0+vBCPkUoIbmtVPNaMa4YF0fRDy0feF81Xd8o0fwNJbh7eoUBOmmMMJpYQ4qYtLxlYzMZZi1pwYXldStzT/wWYXgDdJWlF2eoWuJXCtEtHgb403m7fM93o6vMvKy2CstMex8TepPJ/LHf3bPfbw/Hh8PlVKSYwZWzMIBtRmU1cSapbZkcO8W0qjkTNBrZtYjY9ReY84mCx7mK9unn/XZJnpIZ6qpuKs5qDecSLCbMlEeMdHw9ZRabRHe5Xm7lbTu8wQsFGOryXzEv7W7M7G6e47i62d5c1oTxKW7rhCYt4RVKOIXlRCE52vS5jZGex/n1iPHp4+VM3sYPP8isiexuWX2CAUSjoIRNoHnG5BZfXDnuZkc+bR9TcD4681+9Gijh4UtxmDFNS3h+58eXi8z7/6HyJzSPzlOxv4MBuKDaNj2xqRaKyd4iw7bRTKlGTSSM7moBJewWn5xflzUWe5/oW9hGggE+4hzpwu82mEeXyKQ15Dn/UjlXh7DY1aTip5eCV7xqOM+gNZ3WVHwm6wKZRHmIY1iv/b6+h9Sq3kC7NQTyqThgeix+w/gIA2jsjUCNGlEZM1HLaeKWo+KdJWUVWVnbzkotrZKaGyVxFNJiK8YpV6AEytEptrRKO8mubg3rOimYxNYw5NgzoaVRvBsnqd7rWDzMGYem55JswzRNlkktkGnZNMw2LXa1mrqunn6R5JoHwbPAll4u725joKztySrBailaJrUWrNdtx8auH/WoempN5nxPwdP8i1CfnAlLXKZUXBNSvJPu8+rzv6PY0vLdefZj8+huMU8UiuwxOp+fytXW8/l8/gcAAP//AwCpOTG6ggQAAA==

View File

@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFzIR9n1jehFV9VdrVujvrCS+Xv6yXrWaueks8RrMsQ1ioYP9eg4IlkpJArV6FwTvi6sf2HgK6Rn76ial48UFz8H1rNPPuD8PfcGX7lpXrGK80T7sCQILov5Z7+7529vD48Ph8urhZZMrjyynmE9GLQkuKam4XpoDbdGCS5pcLptAKDtLjQfEsUAU7Xgt9f+OKdAyfWqUnUluLLsXDKEBFnyAAs9/jxlFZtFdxkvN3g79i/smSLrVfnLzEu5Gzf5Gzc/PUHAm9EHvxxZyU5xPlFMnjY7tgmnCZKfQ3Ftsps8hcRK9vChOEyQxjk+/eHRh8vo+/8x+TtwRx+o2N+xnglJCuuO+Kik4bpD4NDUlhtTm5Gks62SrGS7OSQf1nldin1I9DVuKlnP/oZpoYu+2+iOPpFLa8zSP5ucNf3waTdjBgQr2SHOuLpUvNotRSWqWuTLhzWd1lS8J/SRXKI802Ncr+X/Q1FGbaTdGiOFVBwgHYs3sBxZzyx0ToIFC2CcG6kRNAoUYESLZNAQaoUtaqvRaCuc0TBIjdDIYcxI1p3TLbaFasmJsQXiaCxyTfXAO20sh1aia5oRNGSJr7YWD1PmgeuEJqy5pRG5thK41XXNsW6gVWZsWzX+Zsk1cSmy30jPl6+xKTCIHaGRXGnZcG2t5J1tWj603WAH01HjsuZ7ioGm34x6512cl3lMxXVhij+se7+G/L2LbXm++8B/bJHdze4bxSJHDj7kbb6mfD6fz/8CAAD//wMA1TddHSUEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUTW/bOhD8KwLPpsIvUaJugdPiGXltjNZoi94o7qoholAGSaUNAv/3grKLNKeHdyGIFWc5OzPUCwn2EUlPro/Hd08YciIbkv1aEkxoyjRl4sBN38iet7XkmstWfCcb4m/xmfSkVc5xZ5BK1EgVsIYOcnQUAA1nHJhoVOlpfyTSvxDra8An77Ce0xeMyc+B9OSrDzD/LHdbX7tpXqCO84S7kLINrpD5uNve0n+v94e7/flUwlTAtYdCFUTHJHR0YK2iCnVLDQdFXWMG7Zi0QpgzzIeMMdipTvDwej/MOWB2vaiFrBkVhpw2BGy2hfJgEx6ej4XFKtFNqW/W8rrtX8gTRtKLzR8xz+2u3OSvHtOw+Amuzmu26SGt62hdnuMz2ZBjnI8Ys8dVn+uU8HGYng82Pbw/nynb9O4XuiUjbOclZNITrjjZkFWgabLZz6G6cNxOHkNeP+boQ/Luv3oVf+4+V/vJ5nGOj2/8+HyWefc/VP5g3b0PWO1uSE8YRwGyQzoKrqnqwFLbSEO1lnpE7kwryiDbOWQflnlJ1S5k/BHXkUhP3tsp4ZnfdXT3PqPLSyxzftOF9z7OsLhcvXrJWc1qyVgBLfm45OoTgo/oMpYhDnG59PvyFiK0WEHbJUYMudrbfF/9Y9M96YmxnePWWGOtdm7EhuHIgFnNWkANGkEJaEEZBVoZ5rSyA1dgGz6MpUI2BEt0qjWtbByN0cxRK1tGVdc6OnSOU9N2nInOgjAlYK86VndTwVnXMYUgqcERqDLcUqOkpCAb2wo9tq0Y/5LkkgfOisCAT+d3tzLQAB2C5lQo3lBlDKedaVo6tN1gBt1h4wrnW4wBp7+E+uBdnNM85uqSkOqNdJ+WUP4d1ZqWnz7QX6tHN7N7wFgVj60P5alcbD2dTqffAAAA//8DAH1Y7jeCBAAA

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTTW/cOAyG/4qhs+Xoy7LlWzDZRQdpN4M2aIveaJHuCHXkgS2nLYL57wt5pki7t70JlEg+fF/qhUV4Itax29Ppr2eKaWElS2ELKaEsF5YL9ShdV+tONpVW2igjv7CShXv6yTrWGO+ld8Q1WeIGRc17PXiOSE4KiULVJteErwvrXhiECuk5eKqm5SPNS5gi69inEHH6nntDqPw4rVjN00j7uCSIPsP8s9/d87e3h8eHw+XVQktOrgJmVFSt0NjyXjSGG7INdxIN97XrrRcalHKXtBATzRHGasFvr/1xSpGS71SldCW4cuxcMoQEGbmHhR5/njLFJtFdjpdbeDt2L+yZZtap8peYl3I3fgw3fnp6gog3Q4hhObKSnebpRHMKtMmxTTiOkMIUi2uT3RgoJlayhw/FYYQ0TPPTHxp9uIy+/x+TvwN/DJGK/R3rmJCkULfEByUtNy0Ch1o7bq22A0nvGiVZyXZTTCGu07oU+5jo67xRso79DeNCF77b2R9DIp/WOaN/ttlr+hHSbsIcEKxkh3nC1afiVW4pKlFpkS8f1nRaU/GeMMzkE+WZHuf1Wv4/KcqqLWm3zjPFVBwgHYs3sBxZxxy0XoIDB2C9H6gWNAgUYEWDZNESGoUNGmfQGie8NdBLg1DLfsiRzJ3dLbaFGtABoTC8h6blpjaSO6uBSwutVVobkBnkVdbiYcx54FthCDV3NCA3TgJ3RmuOuoZG2aFp1PCbJFfHpch6Iz1fvsZGYBFbQiu5MrLmxjnJW1c3vG/a3vW2pdpn5nuaI42/CfUu+HlapiEV14Up/pDu/Rrz9y625fkeIv+xWXY3+W80F9lyCDFv89Xl8/l8/hcAAP//AwCN39GdJQQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOGmWcRGW/Q25IxiIVppQVGbBob/e0HtFol7640YcIYvn3lnXtkMR2I9uz2dfjrTnFdWszzuISWU5cJyoZ6l71vXy7ZxznSik3+xmo2P9I31zJkYZfTENVniBkXLgx4iRyQvhUShWlNqwueV9a8MxgbpPEZqlvV3Suu4zKxnf4wzLl/L2zA2cVo2bNIy0cO8ZphjEfPbw90j//X28PzxcLm10lqSmxFZz8AHMN1gOanguAEgHlwAThGJWgKpTbykjXOmNMPUrPjl+/u45Jly7FWjdCO48uytZggZiuQAKz1/OxUVO6L7Eq/38H7sX9mZEutV/S/MS7mbOI036xYmOtN0grRSuonL8Qgzspqd0nKilEfasZwphWUdcyEao4OukxI7cKZ12OlWaYdaEHbCtejRgzV2ULFtBwydd2YwqKRBS8YEx2q2g5smyOMyV1ftd9NIc2Y1+/hUHSbIw5KO79A/XYg+/A+gRTbrmRatts6R0VqDt0EJQmejtSpEEG4IimQk0XrjvHRCaq+E1611nUMQvlT6APFlnKl6uGc9E5IU6o74oKTlpkPg0GrPrdV2IBm9U5LV7G6Z8zhvy7ZWD3Omz2n/L+vZzzCtdPnpbYovY6aYt1Qg/GmLGQ9pwS3m6rsBpGhEo4UoSVs+bbn6RDgmipkKjue0Xev9J0VZtSfdbSnRnKsD5JfqF1hfWM88dFGCBw9gYxyoFTQIFGCFQ7JoCY1Ch8YbtMaLaA0EaRBaGYYSYTWj4rdqtziBH2hAy8Fp4qZTinchOC6UNcHpIAWGdxyrj9PeydgJQ6i5pwG58RK4N1pz1C04ZQfn1PADkqtZpCiAkc6XYd0VWMSO0EqujGy58V7yzreOB9cFH2xHbSyaHynNNP0A6sMY07IuQ66uXqveofu0zWXhVLvvvo4z/3vv0f0Sv1CqSo9hnMt8Xdv6VrMjwbolOu77qn9lhzJc1fO+trRopNK6Zk8ZUt5O17DVjZGqfXt7e/sHAAD//wMAlFq6YfQEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjzS6Som2G3qOumXsSLtOhtSI5sIVpqQVGbBsb+94DaDRL31psw0DszfOadeSMRDkh6cns8/nLCmBdSkzxuIcGEpkxTJvbc9q3pedsYqXkn+T+kJuMjfiU9Mcp77i1SiRqpCqylTg6ehoCWMx6YaFXJCS8L6d8IjE3A0+ixmZdPmJZxjqQnf40xzF9KbRgbP81raNI84UNcMkRfmvnz4e6R/nG72z/tLn8tuBRxMwbSE7AOVDdoisIZqgCQOuOAog+ILQKXyl9kY8yYIkzNEj7/qB/mHDH7XjRCNowKS841CZChtOxgwf3XY+liQ3Rf4vUW3j77N3LCRHpRf4d5SXfjp/Emz8cJTzgdIS2Ybvx8OEAMpCbHNB8x5RE3LNtLpwnyOMfqWuxuGjFmUpOn52o3QR7mdHjH6vmC4OF/EDhhcqQnkrVSG4NKSglWO8EwGO21Fs4DM4MTyD2y1ipjuWFcWsGsbLXpTABmS6aXaXYw/b5s/KxgrgvMCC1QoRVKa+sZyg6H1iALoLXTnbdBdkaoLnQSLAC3Do232heDfAD/OkasHu5JTxhHEWSHdBBcU9UFoNBKS7WWekDurRGc1ORujnmM67wu1UPM+JI2fqQnv8K04IXcbfKvY0af11Sg/q1LsV2aw+pz9cMBnDWskYwV0ZqPa64+YhgT+owF7z6t13z/kQgtNtHdmhLGXO0gv1a/wfJaqEDnOdjyVO39gC3DgQUGmpmAOmgMSgQTlFVBK8u8VuC4CtByN5QIqQkWw1WbxwdoUXZgqJC+parVnlqwnHZdkIDGO8b9O47V07Q5w3dMYZDU4hCoshyoVVLSIFswQg/GiOEnJFfzcVYABzxdtnXrQIfQYdCcCsVbqmwpbltDnemcdbrD1peeHzFFnH4C9WH0aV7mIVdX71bv0H1cY7k41ebjL2Ok/24zup/9Z0xVmTGMsSzYdaznmhwQljXhYTtY/RvZle2q9tvdkqzhQsqaPGdIeT1ew1o2iov2fD6fvwEAAP//AwBUXko/9QQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUzW7jNhSFX0Xg2lRIiqJE7VKngxqZTIxJOi26uyKvHCIyaVBUpkGQdx9QdhBnBl10J1zx/n3nkC/Ewx5JRy4Ph9+f0KeJrEhyS0gwoShTlIl7rru66XhdtqzmrFL/kBVx1/hMOtJIY7jRSCtUSKVlNe2rwVBrUXPGLRO1zDVhN5HuhYArLT45g2WYvmGcXPCkI385b8P33BtcacYw2zKGETd+SuBNHubLZn1NP19u72+3x1MTTjm5dJZ0BHQPsh0URdE3VAIg7ZseKBqLWCPwSppjmvMJo4exnOzje38bksdkOlGKqmRUaPK6IhYS5JF7mPD++ZCnWBBd5fhqCS+f3Qt5wkg6sXqDeSx3YUZ3sZ/62Y32Ygy7nfO7dfCD280RUm68IocYDhiTwwXOb85DfP4cdrtcj3yCcUKyIvcY987DePxxhQPMY7oLc1zITPbxv85kMnMKZEUWkuO4tC1Oy6xHhz79knvW+Pau2I6QhhD3H0S6O7Lf/A/0N2AenMdic0U6wjgKW7VIB8EVla0FCnWlqVKVGpAb3QhOVmQdfHJ+DvNUbHzC3Ynah/kuo3lwCU2aY97pb5XNto3BziYV7wJzVrKyYiwnzekwp+IrWhfRJMxL3Mf5VO+nFKHEkvTJjfgLnvUcI/pUbCE9FH/A9EA6oqE1HDRoAGXMgDXDgVkGijUWlVVopbCNlVpaJTUzSkLPpYWa90OOkBXBbLJi8TUfBGhAQ3mLNZVVjxS0MLSqGylkpfUwDB/gFrfjIoppmURbUY2DpVJzoFpWFbVVDY1QQ9OI4YzTyRCcZernJvxzQnvy0pejtd92t/h0vMfLnMraFq3iVEheU6k1p62uG9o3ba971WJt8mbr4KfwBvIbxj5MLuVH5MZ5t4fx5yPvylxj9DieqXPjTAxTGFJxsmXxQa+vs8+vWLFY9Lvz9N/FGFfBPGIssrHA+TMxX19fX38AAAD//wMAr++TXgwFAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/bOBD9KwLPokJS1AcF7MFwWqyRpjESd3exN4ocxkRk0aAop0GR/76gJNdO0cPehNF8vHnvDX+gXh4ANWh1PH46QR8GlKJgpxAjrMSkxITtqGiKqqFlxipOKaP/ohTZO3hDDaq4UlQJwDmUgLkmBW5zo7DWICihmrCCx57yeUDNDyRtpuFkFWRu+Av8YF2PGvS37bV7jbOlzVTnRp1518GmH4LsVQTzdbO+w19W293Dds4aYIjFmdWoQVK0ktemxMDaCnMpAbdVKzEoDVCApDlXc5ntA/hedtmgXy7ztQs9BNWwjOUZwUyg9xRpGWSE3MoBdm/HiGKi6DbG0yk8fTY/0Ak8alh6JnNud6M6e3MY2tF2+iZI/wzBeHmAV+df4CQ7lKKjd0fwwcLEzcNTcoFESUYyVjJCUIrupdrbHpLNbfLQTQurmnDQORZgNOaCSix4nmOdF7JipakqZlCK7sD30F11vbfKu8GZkCyUJx/mrF0fbD+6cUg2fYBnL8Nc91l2A6AUbce2s8P+EaR+27nHMf7rxy7ush69hz4kWxn2yZ9y2KMGCVkrKoUUUpZKGSgIGKKJLEmlodQlaM50pbnguuSCqJLLlnItC9qaGEEpehjDcQxn/r9fgdh5eziAviDYTRxvOxmM84eNhj5YY6M054zJUF03bZUsPdedhT58IBk1iFBgOq8BG0ZLzGstsSxygcsyLw1QJSpGZ8KMfR5/8kSM0Tw3RV2XrWgpAWaqmpPKtEQrkyuhixYqVpGqhlooQtqC07oVimigBddxO+/0qMKvVsgngR7HPl5nsolrv9oefy/5hZGtd8Ep10XBbAdPb0OAw+9UW7vD0Q02xP3NIu23Ae5dH/8u979wthSvXLjEIN5BMp2eriSYghdYVBIwNzVgCdzgGog2NSeEqAKl6NapF/BJNJi0/aTJ2VMPT8nKq70NoMLo4+R/pqVmOT+fT+bCR/b1027tPKyOx3SJ/nGKJM3Nzgb48K58G2L63g1xi+DHKx89QWfOuK7cpOE0v1Q2Bkuta9AlxYzTAnMhKK5FUeG2qlvRljUU6uLW5BG09aDC1G83T1uI/Z0tVz5YI1UY4u18cWpy02LPJeVXkFeq/ayeh18ptzhp6URJdOzT/G5OBvpfz+b7+/v7fwAAAP//AwB0fDGlJwYAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTTW/cNhD9K8KcRZmkKErUzVin6MJNvEgWadHbiBzFhGVqQVFOAmP/e0DtBk5664UgBnwz72P4CgGfCXq4PZ3evVBIC5SQ/FaSXGrGNePyKEzftL2sq7btlNT1v1CCv6fv0EOrrBXWEKtJE1OON2yoR8ucIyO4cFw2KvfELwv0r4C+cvTiLVXz8pni4ucAPfztg5u/5tnoKzvNq6viPNE+LAmDzWQ+7Hf37K/bw/HhcHm10JLBlXfQA5oBVTdqRnJomUIkNrQDMrKOqCEUtbIXmA+JYsCpWtzT23w3p0DJ9rKSdcWZNHAuwWHCTHnAhY7fT5nFZtFdrpdbebv2r/BCEXpZ/jTz0u7GTv7meRlWP7mby5lwecoqT3E+UUyeNk82mdOEyc+huE7aTZ5CghIePhWHCdM4x+ffjPp00b//H/KPefq7b2TXRG43ryFBD0I1UMJ7tI8+ULG/gx64IOnqjtgohWaqc8iwqQ3TutYjCWtaKaCE3RySD+u8LsU+JPoSNwHQwx84LXShfhvto09k0xqzqn903oVDnN1qU/Hmv+AVr2rOM2hNpzUVH8n5SDZR1neM67XffyBSyw20W2OkkIoDpsfiT1weoQeDnRVo0CBqa0dqOI3ccdS8daSdJqeka50yymlluNUKB6EcNmIYcwVKoBx3sW2YNkRcaMtka5EprQc2KNuxQQ1167BrrMzS3nwsHqYtGttxRa5mhkbHlBHIjKpr5uoGW6nHtpXjL5Zc0xc8G+zo5fJXLgyc68hpwaQSDVPGCNaZpmVD2w1m0B01NnO+pxho+sWo997GeZnHVFyXp/jNuo9ryP+92Bbpqw/s25bR3WyfKBY5Y/Qhr/c11vP5fP4BAAD//wMA//ibKjYEAAA=

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTwW7bOBD9FYFnUSEpihJ1C5xd1Ei7MdqgLXobcUY1UYUyKCptEfjfF5RdpN3b3ohHvpk37w1fWIAnYj27PZ3+eqaQFlay5DdICWW4MFyoR2n7pu1VXbWd6FohvrCS+Xv6yXrWaueks8RrMsQ1ioYP9eg4IlkpJArV6FwTvi6sf2HgK6Rn76ial48UFz8H1rNPPuD8PfcGX7lpXrGK80T7sCQILov5Z7+7529vD48Ph8urhZZMrjyynoEdQHej4aSGlmsA4kM7ACeHRA2BrLW70HxIFANM1YLfXvvjnAIl16tK1ZXgyrJzyRASZMkDLPT485RVbBbdZbzc4O3Yv7BniqxX5S8zL+Vu3ORv3Pz0BAFvRh/8cmQlO8X5RDF52uzYJpwmSH4OxbXJbvIUEivZw4fiMEEa5/j0h0cfLqPv/8fk78AdfaBif8d6JiQprDvio5KG6w6BQ1NbbkxtRpLOtkqyku3mkHxY53Up9iHR17ipZD37G6aFLvpuozv6RC6tMUv/bHLW9MOn3YwZEKxkhzjj6lLxarcUlahqkS8f1nRaU/Ge0EdyifJMj3G9lv8PRRm1kXZrjBRScYB0LN7AcmQ9s9A5CRYsgHFupEbQKFCAES2SQUOoFbaorUajrXBGwyA1QiOHMSNZd0632BYKjdC21cglNQPXUozcggROgMKOrXCmzYxXW4uHaYvDdUIT1tzSiFxbCdzquuZYN9AqM7atGn+z5Jq4FNlvpOfL19gUGMSO0EiutGy4tlbyzjYtH9pusIPpqHFZwT3FQNNvRr3zLs7LPKbiujDFH9a9X0P+3sW2PN994D+2yO5m941ikSMHH/I2X1M+n8/nfwEAAP//AwAfRvRKJQQAAA==

View File

@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUwW7cNhCGX0XgeSmTFEWJuhl2gi7c1IvESIveRpxRTFimFiTlxDD23Qtqt0h8KnohiBFn+M/3D/XGAjwTG9j18fjhhUJObMey30JKKMOF4UI9SDu03aCauuta06j2b7Zj/o5e2cA67Zx0lnhDhrhG0fKxmRxHJCuFRKFaXWrCt8SGNwa+RnrxjuolfaWY/BLYwP70AZfv5W7wtZuXFeu4zLQPKUNwRcwf+5s7/vv14eH+cD6VKJXk2iMbGNgRdD8ZTmrsuAYgPnYjcHJI1BLIRrtzmg+ZYoC5Tvj0835ccqDsBlWrphZcWXbaMYQMRfIIiR5ej0XFhui2xHdbeNsOb+yFIhvU7l+Y53JXbvZXz2lc/YxX5zVDekrbOoHLS3xlO3aMy5Fi9rTxuU6Jnsf59QHS08fzmbJNH36QWzPhzbKGzAYmtWQ7tgGaZ8h+CdVF483sKeTtY44+JO/+q1bx5/5LdZghT0t8fufHlzPm/f+g/Ancow9U7W/ZwIQkhU1PfFLScN0jcGgby41pzETS2U6VRm6WkH1YlzVV+5DpW9xaYgP7CHOis77r6B59JpfXWPr8yxTdh7jg6nL100spalE3QpSkNR/XXH0m9JFcptLEQ1wv9b6+T1FGbUk3a4wUcnWA/Fj9BumRDcxC7yRYsADGuYlaQZNAAUZ0SAYNoVbYobYajbbCGQ2j1AitHKcSYTtGZXSqbVrbroHWTJr3I3VcS7QcJBreCbI4WWnHtn/HsbqfN/6uF5qw4ZYm5NpK4FY3DcemhU6ZqevU9AuSyzxIUQAjvZzf3abAIPaERnKlZcu1tZL3tu342PWjHU1PrSua7ygGmn8B9cm7uKRlytVlQqp36D6vofw7qm1avvvAf2we3S7uiWJVPAYfylO52Ho6nU7/AAAA//8DAIN+pxyCBAAA

View File

@ -95,10 +95,10 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
AgeReferenceSceneID = (int?)null,
Height = (string?)null,
EyeColour = (string?)null,
CharacterImportance = NormalizeCharacterImportance(character.Role, character.Importance),
DefaultDescription = CombineText(
character.Description,
character.Notes,
CharacterImportMetadata(character),
ImportMarker)
},
transaction,
@ -658,12 +658,6 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
MetadataLine("Package description", package.Description)) ?? ImportMarker;
}
private static string? CharacterImportMetadata(ImportCharacterDto character)
{
var role = NormalizeCharacterRole(character.Role, character.Importance);
return role is null ? null : $"Imported character classification: {role}";
}
private static IReadOnlyList<string> GetSkippedOptionalFields(PlotLineImportPackage package)
{
var skipped = new List<string>();
@ -1059,7 +1053,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
: lookupIds.ThreadEventTypeIds.Values.First();
}
private static string? NormalizeCharacterRole(string? role, int? importance)
private static int? NormalizeCharacterImportance(string? role, int? importance)
{
var normalized = Clean(role).ToLowerInvariant();
if (normalized.Contains("protagonist", StringComparison.OrdinalIgnoreCase)
@ -1068,33 +1062,33 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) :
|| normalized.Contains("lead", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("antagonist", StringComparison.OrdinalIgnoreCase))
{
return "Major";
return 10;
}
if (normalized.Contains("minor", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("background", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("cameo", StringComparison.OrdinalIgnoreCase))
{
return "Minor";
return 2;
}
if (normalized.Contains("support", StringComparison.OrdinalIgnoreCase)
|| normalized.Contains("secondary", StringComparison.OrdinalIgnoreCase))
{
return "Supporting";
return 6;
}
if (importance >= 8)
{
return "Major";
return 10;
}
if (importance >= 5)
{
return "Supporting";
return 6;
}
return importance.HasValue ? "Minor" : null;
return importance.HasValue ? 2 : null;
}
private static int ResolveSceneDependencyTypeId(ImportLookupIds lookupIds, string? dependencyType)

View File

@ -1816,6 +1816,7 @@ public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory)
character.AgeReferenceSceneID,
character.Height,
character.EyeColour,
character.CharacterImportance,
character.DefaultDescription
},
commandType: CommandType.StoredProcedure);

View File

@ -467,6 +467,7 @@ public sealed class Character
public int? AgeReferenceSceneID { get; set; }
public string? Height { get; set; }
public string? EyeColour { get; set; }
public int? CharacterImportance { get; set; }
public string? DefaultDescription { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime UpdatedDate { get; set; }

View File

@ -1,3 +1,4 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace PlotLine.Models;
@ -52,6 +53,7 @@ public sealed class ImportCharacterDto
public string? DisplayName { get; set; }
public string? ShortName { get; set; }
public string? Role { get; set; }
[JsonConverter(typeof(ImportImportanceJsonConverter))]
public int? Importance { get; set; }
public string? Description { get; set; }
public string? Notes { get; set; }
@ -299,3 +301,65 @@ public sealed record ImportResult
public string? TechnicalDetail { get; init; }
public string Message { get; init; } = string.Empty;
}
public sealed class ImportImportanceJsonConverter : JsonConverter<int?>
{
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var numeric))
{
return Math.Clamp(numeric, 1, 10);
}
if (reader.TokenType != JsonTokenType.String)
{
return null;
}
var text = reader.GetString()?.Trim();
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
if (int.TryParse(text, out var parsed))
{
return Math.Clamp(parsed, 1, 10);
}
var normalized = text.ToLowerInvariant();
if (normalized.Contains("major") || normalized.Contains("main") || normalized.Contains("protagonist"))
{
return 10;
}
if (normalized.Contains("support") || normalized.Contains("secondary"))
{
return 6;
}
if (normalized.Contains("minor") || normalized.Contains("background") || normalized.Contains("cameo"))
{
return 2;
}
return null;
}
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
{
if (value.HasValue)
{
writer.WriteNumberValue(value.Value);
}
else
{
writer.WriteNullValue();
}
}
}

View File

@ -1349,9 +1349,8 @@ public sealed class TimelineService(
characterGroup => characterGroup.GroupBy(x => x.SceneID).ToDictionary(sceneGroup => sceneGroup.Key, sceneGroup => sceneGroup.ToList()));
return characters
// Character has no global category/importance column. Timeline grouping uses lane order:
// the first six lanes render as "Major characters", so imported classification metadata
// and strong scene roles should sort likely main characters ahead of supporting cast.
// Timeline grouping prefers CharacterImportance when present; imported classification
// metadata and strong scene roles remain as fallbacks for older projects.
.OrderByDescending(character => CharacterLanePriority(character, appearancesByCharacterAndScene.TryGetValue(character.CharacterID, out var sceneAppearances) ? sceneAppearances.Values.SelectMany(x => x) : []))
.ThenBy(x => x.CharacterName)
.Select(character =>
@ -1374,9 +1373,22 @@ public sealed class TimelineService(
private static int CharacterLanePriority(Character character, IEnumerable<SceneCharacter> appearances)
{
var description = character.DefaultDescription ?? string.Empty;
var appearanceList = appearances as IReadOnlyCollection<SceneCharacter> ?? appearances.ToList();
var priority = 0;
if (description.Contains("Imported character classification: Major", StringComparison.OrdinalIgnoreCase))
if (character.CharacterImportance >= 8)
{
priority += 1_200;
}
else if (character.CharacterImportance >= 4)
{
priority += 500;
}
else if (character.CharacterImportance.HasValue)
{
priority -= 150;
}
else if (description.Contains("Imported character classification: Major", StringComparison.OrdinalIgnoreCase))
{
priority += 1_000;
}
@ -1389,7 +1401,9 @@ public sealed class TimelineService(
priority -= 100;
}
foreach (var appearance in appearances)
priority += appearanceList.Count * 3;
foreach (var appearance in appearanceList)
{
if (string.Equals(appearance.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase))
{
@ -2904,6 +2918,7 @@ public sealed class CharacterService(IProjectRepository projects, ICharacterRepo
AgeAtSeriesStart = character.AgeAtSeriesStart,
Height = character.Height,
EyeColour = character.EyeColour,
CharacterImportance = character.CharacterImportance,
DefaultDescription = character.DefaultDescription,
Project = await projects.GetAsync(character.ProjectID)
};
@ -2947,6 +2962,7 @@ public sealed class CharacterService(IProjectRepository projects, ICharacterRepo
AgeAtSeriesStart = model.AgeAtSeriesStart,
Height = model.Height,
EyeColour = model.EyeColour,
CharacterImportance = model.CharacterImportance,
DefaultDescription = model.DefaultDescription
});

View File

@ -771,7 +771,7 @@ public sealed class ImportService(IImportRepository imports, ILogger<ImportServi
{
if (package.Characters.Any(character => character.Importance.HasValue || !string.IsNullOrWhiteSpace(character.Role)))
{
result.Warnings.Add("Character importance is not stored in a dedicated character field; the importer uses role/importance for timeline grouping where supported and preserves a clean classification note.");
result.Warnings.Add("Character role/importance will be mapped to Story importance for timeline grouping.");
}
foreach (var group in package.Characters

View File

@ -43,6 +43,7 @@ BEGIN
AgeReferenceSceneID int NULL,
Height nvarchar(50) NULL,
EyeColour nvarchar(50) NULL,
CharacterImportance int NULL,
DefaultDescription nvarchar(max) NULL,
CreatedDate datetime2 NOT NULL CONSTRAINT DF_Characters_CreatedDate DEFAULT SYSUTCDATETIME(),
UpdatedDate datetime2 NOT NULL CONSTRAINT DF_Characters_UpdatedDate DEFAULT SYSUTCDATETIME(),
@ -319,7 +320,7 @@ AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY CharacterName;
@ -332,7 +333,7 @@ AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE CharacterID = @CharacterID AND IsArchived = 0;
END;
@ -349,14 +350,15 @@ CREATE OR ALTER PROCEDURE dbo.Character_Save
@AgeReferenceSceneID int = NULL,
@Height nvarchar(50) = NULL,
@EyeColour nvarchar(50) = NULL,
@CharacterImportance int = NULL,
@DefaultDescription nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @CharacterID IS NULL OR @CharacterID = 0
BEGIN
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, DefaultDescription)
VALUES (@ProjectID, @CharacterName, @ShortName, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @DefaultDescription);
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription)
VALUES (@ProjectID, @CharacterName, @ShortName, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @CharacterImportance, @DefaultDescription);
SET @CharacterID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
@ -364,7 +366,7 @@ BEGIN
UPDATE dbo.Characters
SET CharacterName = @CharacterName, ShortName = @ShortName, Sex = @Sex, BirthDate = @BirthDate,
AgeAtSeriesStart = @AgeAtSeriesStart, AgeReferenceSceneID = @AgeReferenceSceneID,
Height = @Height, EyeColour = @EyeColour, DefaultDescription = @DefaultDescription,
Height = @Height, EyeColour = @EyeColour, CharacterImportance = @CharacterImportance, DefaultDescription = @DefaultDescription,
UpdatedDate = SYSUTCDATETIME()
WHERE CharacterID = @CharacterID;
END
@ -710,11 +712,11 @@ CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject
AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (12) CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY CharacterName;
ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,

View File

@ -10,11 +10,11 @@ AS
BEGIN
SET NOCOUNT ON;
SELECT TOP (12) CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY CharacterName;
ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,

View File

@ -0,0 +1,117 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
IF COL_LENGTH(N'dbo.Characters', N'CharacterImportance') IS NULL
BEGIN
ALTER TABLE dbo.Characters ADD CharacterImportance int NULL;
END;
GO
UPDATE dbo.Characters
SET CharacterImportance =
CASE
WHEN DefaultDescription LIKE N'%Imported character classification: Major%' THEN 10
WHEN DefaultDescription LIKE N'%Imported character classification: Supporting%' THEN 6
WHEN DefaultDescription LIKE N'%Imported character classification: Minor%' THEN 2
ELSE CharacterImportance
END
WHERE CharacterImportance IS NULL
AND DefaultDescription LIKE N'%Imported character classification:%';
GO
CREATE OR ALTER PROCEDURE dbo.Character_ListByProject
@ProjectID int
AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY CharacterName;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Character_Get
@CharacterID int
AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE CharacterID = @CharacterID AND IsArchived = 0;
END;
GO
CREATE OR ALTER PROCEDURE dbo.Character_Save
@CharacterID int = NULL,
@ProjectID int,
@CharacterName nvarchar(200),
@ShortName nvarchar(100) = NULL,
@Sex nvarchar(50) = NULL,
@BirthDate date = NULL,
@AgeAtSeriesStart int = NULL,
@AgeReferenceSceneID int = NULL,
@Height nvarchar(50) = NULL,
@EyeColour nvarchar(50) = NULL,
@CharacterImportance int = NULL,
@DefaultDescription nvarchar(max) = NULL
AS
BEGIN
SET NOCOUNT ON;
IF @CharacterID IS NULL OR @CharacterID = 0
BEGIN
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, DefaultDescription)
VALUES (@ProjectID, @CharacterName, @ShortName, @Sex, @BirthDate, @AgeAtSeriesStart, @AgeReferenceSceneID, @Height, @EyeColour, @CharacterImportance, @DefaultDescription);
SET @CharacterID = CAST(SCOPE_IDENTITY() AS int);
END
ELSE
BEGIN
UPDATE dbo.Characters
SET CharacterName = @CharacterName, ShortName = @ShortName, Sex = @Sex, BirthDate = @BirthDate,
AgeAtSeriesStart = @AgeAtSeriesStart, AgeReferenceSceneID = @AgeReferenceSceneID,
Height = @Height, EyeColour = @EyeColour, CharacterImportance = @CharacterImportance, DefaultDescription = @DefaultDescription,
UpdatedDate = SYSUTCDATETIME()
WHERE CharacterID = @CharacterID;
END
SELECT @CharacterID;
END;
GO
CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject
@ProjectID int,
@BookID int = NULL
AS
BEGIN
SET NOCOUNT ON;
SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID,
Height, EyeColour, CharacterImportance, DefaultDescription, CreatedDate, UpdatedDate, IsArchived
FROM dbo.Characters
WHERE ProjectID = @ProjectID AND IsArchived = 0
ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName;
SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart,
sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName,
sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName,
sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription,
sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle,
sc.CreatedDate, sc.UpdatedDate
FROM dbo.SceneCharacters sc
INNER JOIN dbo.Characters c ON c.CharacterID = sc.CharacterID
INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID
INNER JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID
INNER JOIN dbo.Books b ON b.BookID = ch.BookID
LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID
LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID
LEFT JOIN dbo.LocationPaths location ON location.LocationID = sc.LocationID
LEFT JOIN dbo.LocationPaths entry ON entry.LocationID = sc.EntryLocationID
LEFT JOIN dbo.LocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID
WHERE c.ProjectID = @ProjectID AND c.IsArchived = 0 AND s.IsArchived = 0 AND ch.IsArchived = 0 AND b.IsArchived = 0
AND (@BookID IS NULL OR b.BookID = @BookID)
ORDER BY b.SortOrder, ch.SortOrder, s.SortOrder, c.CharacterName;
END;
GO

View File

@ -838,9 +838,20 @@ public sealed class CharacterEditViewModel
[Display(Name = "Eye colour")]
public string? EyeColour { get; set; }
[Display(Name = "Story importance")]
public int? CharacterImportance { get; set; }
[Display(Name = "Default description")]
public string? DefaultDescription { get; set; }
public IReadOnlyList<SelectListItem> ImportanceOptions { get; set; } =
[
new("Use timeline fallback", string.Empty),
new("Major", "10"),
new("Secondary / Supporting", "6"),
new("Minor", "2")
];
public Project? Project { get; set; }
}

View File

@ -2,6 +2,13 @@
@{
ViewData["Title"] = Model.Character.CharacterName;
var firstAppearance = Model.Appearances.FirstOrDefault();
var storyImportance = Model.Character.CharacterImportance switch
{
>= 8 => "Major",
>= 4 => "Secondary / Supporting",
>= 1 => "Minor",
_ => "Timeline fallback"
};
}
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
@ -54,6 +61,7 @@
<p><strong>Age:</strong> @Model.DisplayAge</p>
<p><strong>Height:</strong> @Model.Character.Height</p>
<p><strong>Eye colour:</strong> @Model.Character.EyeColour</p>
<p><strong>Story importance:</strong> @storyImportance</p>
</article>
<article class="plain-card">
<h2>Relationships</h2>

View File

@ -44,6 +44,11 @@
<label asp-for="EyeColour" class="form-label"></label>
<input asp-for="EyeColour" class="form-control" />
</div>
<div class="col-md-6">
<label asp-for="CharacterImportance" class="form-label"></label>
<select asp-for="CharacterImportance" asp-items="Model.ImportanceOptions" class="form-select"></select>
<div class="form-text">Controls timeline grouping for major, secondary/supporting, and minor characters.</div>
</div>
<div class="col-12">
<label asp-for="DefaultDescription" class="form-label"></label>
<textarea asp-for="DefaultDescription" class="form-control" rows="5"></textarea>

View File

@ -51,6 +51,61 @@
}
int ChapterSceneColumns(ChapterTimelineViewModel chapter) => Math.Max(chapter.Scenes.Count, 1);
int BookSceneColumns(BookTimelineViewModel book) => Math.Max(book.Chapters.Any() ? book.Chapters.Sum(ChapterSceneColumns) : 1, 1);
int CharacterAppearanceCount(CharacterLaneViewModel lane) => lane.SceneSlots.Sum(slot => slot.Appearances.Count);
bool HasLeadSceneRole(CharacterLaneViewModel lane) => lane.SceneSlots
.SelectMany(slot => slot.Appearances)
.Any(appearance =>
string.Equals(appearance.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase) ||
string.Equals(appearance.RoleInSceneTypeName, "Main Participant", StringComparison.OrdinalIgnoreCase));
string CharacterGroupName(CharacterLaneViewModel lane)
{
var description = lane.Character.DefaultDescription ?? string.Empty;
// Timeline grouping prefers the editable CharacterImportance field; imported
// classification metadata remains a fallback for older imported projects.
if (lane.Character.CharacterImportance >= 8)
{
return "Major characters";
}
if (lane.Character.CharacterImportance >= 4)
{
return "Secondary / supporting characters";
}
if (lane.Character.CharacterImportance.HasValue)
{
return "Minor / mentioned characters";
}
if (description.Contains("Imported character classification: Major", StringComparison.OrdinalIgnoreCase))
{
return "Major characters";
}
if (description.Contains("Imported character classification: Supporting", StringComparison.OrdinalIgnoreCase))
{
return "Secondary / supporting characters";
}
if (description.Contains("Imported character classification: Minor", StringComparison.OrdinalIgnoreCase))
{
return "Minor / mentioned characters";
}
if (HasLeadSceneRole(lane) || CharacterAppearanceCount(lane) >= 6)
{
return "Major characters";
}
return CharacterAppearanceCount(lane) <= 1 ? "Minor / mentioned characters" : "Secondary / supporting characters";
}
int CharacterGroupOrder(string groupName) => groupName switch
{
"Major characters" => 0,
"Secondary / supporting characters" => 1,
_ => 2
};
}
<section class="context-header">
@ -106,7 +161,7 @@
</div>
<section class="timeline-control-panel" data-timeline-controls>
<details class="timeline-filter-drawer" @(hasActiveFilter ? "open" : "")>
<details class="timeline-filter-drawer">
<summary>Filters and focus</summary>
<form method="get" class="timeline-filter-grid">
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
@ -390,7 +445,10 @@
@foreach (var chapter in book.Chapters)
{
var chapterWarnings = chapter.Scenes.Sum(x => x.WarningCount);
var isActiveChapter = chapter.Scenes.Any(scene => scene.SceneID == Model.SelectedSceneID);
<button type="button"
class="@(isActiveChapter ? "is-active" : "")"
data-active-chapter="@(isActiveChapter ? "true" : "false")"
data-jump-scene="@chapter.Scenes.FirstOrDefault()?.SceneID"
style="--chapter-scenes:@Math.Max(chapter.Scenes.Count, 1); --chapter-warnings:@Math.Min(chapterWarnings, 8)"
title="Chapter @chapter.Chapter.ChapterNumber: @chapter.Chapter.ChapterTitle (@chapter.Scenes.Count scenes, @chapterWarnings warnings)">
@ -784,14 +842,19 @@ else
<div class="character-lane-board" data-timeline-section="characters" style="--scene-count:@timelineColumns.Count">
<div class="lane-board-heading character-lane-board-heading">
<button type="button" data-collapse-toggle="characters">Character appearances</button>
<small>First twelve project characters</small>
<small>All project characters, grouped by role and activity</small>
</div>
<div data-collapse-body="characters">
@foreach (var group in Model.CharacterLanes.Select((lane, index) => new { lane, group = index < 6 ? "Major characters" : "Secondary characters" }).GroupBy(x => x.group))
@foreach (var group in Model.CharacterLanes
.Select((lane, index) => new { lane, index, group = CharacterGroupName(lane) })
.GroupBy(x => x.group)
.OrderBy(group => CharacterGroupOrder(group.Key)))
{
var groupId = $"character-group-{CharacterGroupOrder(group.Key)}";
var defaultCollapsed = CharacterGroupOrder(group.Key) > 0 ? "collapsed" : null;
<div class="lane-group">
<button type="button" class="lane-group-heading" data-collapse-toggle="character-group-@group.Key.GetHashCode()">@group.Key</button>
<div data-collapse-body="character-group-@group.Key.GetHashCode()">
<button type="button" class="lane-group-heading" data-collapse-toggle="@groupId" data-collapse-default="@defaultCollapsed">@group.Key <span>@group.Count()</span></button>
<div class="character-lane-group-body" data-collapse-body="@groupId">
@foreach (var laneItem in group)
{
var lane = laneItem.lane;
@ -874,7 +937,7 @@ else
<div class="metric-graph-stack mt-2">
@foreach (var graph in Model.MetricGraphs)
{
<div class="metric-graph-row">
<div class="metric-graph-row" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
<div class="metric-graph-label">@graph.MetricName</div>
<div class="metric-graph-points" style="--scene-count:@Math.Max(timelineColumns.Count, 1)">
@foreach (var column in timelineColumns)

View File

@ -393,6 +393,7 @@ td.text-end {
.timeline-workspace {
min-width: 0;
max-width: 100%;
}
.pacing-panel {
@ -421,6 +422,11 @@ td.text-end {
.metric-graph-stack {
display: grid;
gap: 10px;
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
padding-bottom: 4px;
}
.metric-graph-row {
@ -428,6 +434,7 @@ td.text-end {
grid-template-columns: 150px minmax(260px, 1fr);
gap: 10px;
align-items: center;
min-width: max(100%, calc(150px + (var(--scene-count, 1) * 34px)));
}
.metric-graph-label {
@ -445,6 +452,7 @@ td.text-end {
border-radius: 8px;
padding: 6px;
background: linear-gradient(180deg, #f6faf8 0%, #edf4f1 100%);
min-width: calc(var(--scene-count) * 34px);
}
.metric-point {
@ -1953,6 +1961,12 @@ td.text-end {
gap: 4px;
margin: 0 0 12px;
padding: 8px;
max-width: 100%;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
scrollbar-gutter: stable;
border: 1px solid rgba(47, 111, 99, 0.18);
border-radius: 8px;
background: rgba(255, 255, 255, 0.72);
@ -1960,7 +1974,7 @@ td.text-end {
.timeline-minimap button {
position: relative;
flex: var(--chapter-scenes) 1 42px;
flex: 0 0 max(42px, calc(var(--chapter-scenes) * 18px));
min-width: 42px;
min-height: 34px;
border: 1px solid rgba(47, 111, 99, 0.18);
@ -1971,6 +1985,13 @@ td.text-end {
font-weight: 800;
}
.timeline-minimap button.is-active {
border-color: var(--plotline-accent);
color: #fff;
background: linear-gradient(180deg, var(--plotline-accent) 0%, var(--plotline-accent-dark) 100%);
box-shadow: 0 0 0 2px rgba(47, 111, 99, 0.14);
}
.timeline-minimap button::after {
content: "";
position: absolute;
@ -2157,6 +2178,11 @@ td.text-end {
gap: 7px;
}
.character-lane-group-body {
display: grid;
gap: 7px;
}
.lane-group-heading {
position: sticky;
left: 0;

View File

@ -255,19 +255,30 @@
ctx.lineWidth = 1;
ctx.font = "700 10px system-ui, -apple-system, Segoe UI, sans-serif";
let previousChapter = null;
const chapterStarts = [];
scenes.forEach((scene, index) => {
if (scene.chapterLabel === previousChapter) {
return;
if (!index || scene.chapterLabel !== scenes[index - 1].chapterLabel) {
chapterStarts.push({ scene, index });
}
});
const labelInterval = chapterStarts.length > 40 ? 5 : chapterStarts.length > 24 ? 4 : chapterStarts.length > 14 ? 2 : 1;
chapterStarts.forEach((entry, chapterIndex) => {
const { scene, index } = entry;
const x = scales.x.getPixelForValue(index);
ctx.beginPath();
ctx.moveTo(x, chartArea.top);
ctx.lineTo(x, chartArea.bottom);
ctx.stroke();
ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22);
previousChapter = scene.chapterLabel;
const chapterNumber = Number.parseInt(String(scene.chapterLabel).replace(/[^0-9]/g, ""), 10);
const shouldLabel = chapterIndex === 0 ||
chapterIndex === chapterStarts.length - 1 ||
labelInterval === 1 ||
(Number.isFinite(chapterNumber) && chapterNumber % labelInterval === 0);
if (shouldLabel) {
ctx.fillText(scene.chapterLabel, Math.min(x + 4, chartArea.right - 70), chartArea.bottom + 22);
}
});
ctx.restore();
@ -478,6 +489,7 @@
const densitySelect = document.querySelector("[data-timeline-density]");
const zoomSelect = document.querySelector("[data-timeline-zoom]");
const searchInput = document.querySelector("[data-timeline-search]");
const minimap = root.querySelector("[data-timeline-minimap]");
const jumpSelects = document.querySelectorAll("[data-timeline-jump], [data-timeline-warning-jump], [data-timeline-entity-jump]");
const sceneCards = [...root.querySelectorAll(".timeline-scene-card[data-scene-id]")];
const collapsedKey = "plotline.timeline.collapsed";
@ -494,6 +506,7 @@
}
};
const hasCollapsedPreference = localStorage.getItem(collapsedKey) !== null;
const collapsed = readSet(collapsedKey);
const applyMode = (kind, value, values) => {
@ -535,6 +548,21 @@
window.setTimeout(() => target.classList.remove("keyboard-focus"), 1800);
};
const scrollActiveChapterIntoView = (behavior = "auto") => {
const activeChapter = minimap?.querySelector("[data-active-chapter='true']");
if (!minimap || !activeChapter) {
return;
}
const hostRect = minimap.getBoundingClientRect();
const activeRect = activeChapter.getBoundingClientRect();
const activeCenter = activeRect.left + activeRect.width / 2;
const hostCenter = hostRect.left + hostRect.width / 2;
if (activeRect.left < hostRect.left || activeRect.right > hostRect.right) {
minimap.scrollBy({ left: activeCenter - hostCenter, behavior });
}
};
const saveCollapsed = () => localStorage.setItem(collapsedKey, JSON.stringify([...collapsed]));
const setCollapsed = (id, shouldCollapse) => {
@ -549,6 +577,10 @@
document.querySelectorAll("[data-collapse-toggle]").forEach((button) => {
const id = button.dataset.collapseToggle;
if (!hasCollapsedPreference && button.dataset.collapseDefault === "collapsed") {
collapsed.add(id);
}
const isCollapsed = collapsed.has(id);
setCollapsed(id, isCollapsed);
button.addEventListener("click", () => {
@ -567,6 +599,7 @@
zoomSelect?.addEventListener("change", () => applyZoom(zoomSelect.value));
applyDensity(localStorage.getItem(densityKey) || "comfortable");
applyZoom(localStorage.getItem(zoomKey) || "chapter");
window.requestAnimationFrame(() => scrollActiveChapterIntoView("auto"));
const runSearch = () => {
const query = searchInput?.value.trim().toLowerCase() || "";