unspaghettit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/client/_app/immutable/assets/0.DSctqr5I.css +1 -0
- package/build/client/_app/immutable/assets/0.DSctqr5I.css.br +0 -0
- package/build/client/_app/immutable/assets/0.DSctqr5I.css.gz +0 -0
- package/build/client/_app/immutable/assets/BehaviorGraph.Bk0xQRZk.css +1 -0
- package/build/client/_app/immutable/assets/BehaviorGraph.Bk0xQRZk.css.br +0 -0
- package/build/client/_app/immutable/assets/BehaviorGraph.Bk0xQRZk.css.gz +0 -0
- package/build/client/_app/immutable/chunks/9nXQ5qrY2.js +1 -0
- package/build/client/_app/immutable/chunks/9nXQ5qrY2.js.br +0 -0
- package/build/client/_app/immutable/chunks/9nXQ5qrY2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B439_FLv.js +1 -0
- package/build/client/_app/immutable/chunks/B439_FLv.js.br +0 -0
- package/build/client/_app/immutable/chunks/B439_FLv.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BCEY79Dw.js +1 -0
- package/build/client/_app/immutable/chunks/BCEY79Dw.js.br +2 -0
- package/build/client/_app/immutable/chunks/BCEY79Dw.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DBJWcC6Y.js → BYIrIC5L.js} +1 -1
- package/build/client/_app/immutable/chunks/BYIrIC5L.js.br +0 -0
- package/build/client/_app/immutable/chunks/BYIrIC5L.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B_9TWPrx2.js +1 -0
- package/build/client/_app/immutable/chunks/B_9TWPrx2.js.br +0 -0
- package/build/client/_app/immutable/chunks/B_9TWPrx2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BvOhVtZg.js +1 -0
- package/build/client/_app/immutable/chunks/BvOhVtZg.js.br +1 -0
- package/build/client/_app/immutable/chunks/BvOhVtZg.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CY3em1ma2.js +1 -0
- package/build/client/_app/immutable/chunks/CY3em1ma2.js.br +0 -0
- package/build/client/_app/immutable/chunks/CY3em1ma2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CgdRZPgI.js +1 -0
- package/build/client/_app/immutable/chunks/CgdRZPgI.js.br +0 -0
- package/build/client/_app/immutable/chunks/CgdRZPgI.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D5speDV82.js +908 -0
- package/build/client/_app/immutable/chunks/D5speDV82.js.br +0 -0
- package/build/client/_app/immutable/chunks/D5speDV82.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DkxwAIfJ2.js +1 -0
- package/build/client/_app/immutable/chunks/DkxwAIfJ2.js.br +0 -0
- package/build/client/_app/immutable/chunks/DkxwAIfJ2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DatGSObE.js → OJscNS3T.js} +1 -1
- package/build/client/_app/immutable/chunks/OJscNS3T.js.br +0 -0
- package/build/client/_app/immutable/chunks/OJscNS3T.js.gz +0 -0
- package/build/client/_app/immutable/chunks/U9p9CtKG2.js +2 -0
- package/build/client/_app/immutable/chunks/U9p9CtKG2.js.br +0 -0
- package/build/client/_app/immutable/chunks/U9p9CtKG2.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.Cd4S3giu.js +2 -0
- package/build/client/_app/immutable/entry/app.Cd4S3giu.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Cd4S3giu.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.C3xXQVkq.js +1 -0
- package/build/client/_app/immutable/entry/start.C3xXQVkq.js.br +0 -0
- package/build/client/_app/immutable/entry/start.C3xXQVkq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.dIOlQ-0y.js +4 -0
- package/build/client/_app/immutable/nodes/0.dIOlQ-0y.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.dIOlQ-0y.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.KYdA6ppX.js → 1.Dyte3Ggf.js} +1 -1
- package/build/client/_app/immutable/nodes/1.Dyte3Ggf.js.br +2 -0
- package/build/client/_app/immutable/nodes/1.Dyte3Ggf.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.ivxAosDg.js +2 -0
- package/build/client/_app/immutable/nodes/10.ivxAosDg.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.ivxAosDg.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.CMW6a2Lg.js → 11.wvMfJKC2.js} +1 -1
- package/build/client/_app/immutable/nodes/11.wvMfJKC2.js.br +0 -0
- package/build/client/_app/immutable/nodes/11.wvMfJKC2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.DBz20KgG.js → 2.CmPPom9Z.js} +1 -1
- package/build/client/_app/immutable/nodes/2.CmPPom9Z.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CmPPom9Z.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.D-iCGCEx.js +1 -0
- package/build/client/_app/immutable/nodes/3.D-iCGCEx.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.D-iCGCEx.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{4.BvMzqBJj.js → 4.DbfAvO8Z.js} +1 -1
- package/build/client/_app/immutable/nodes/4.DbfAvO8Z.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DbfAvO8Z.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.CC5Q7lVw.js +42 -0
- package/build/client/_app/immutable/nodes/5.CC5Q7lVw.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.CC5Q7lVw.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.CHIjlzpO.js +1 -0
- package/build/client/_app/immutable/nodes/6.CHIjlzpO.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.CHIjlzpO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.BOHISqs-.js → 7.Ejs18ZUc.js} +1 -1
- package/build/client/_app/immutable/nodes/7.Ejs18ZUc.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.Ejs18ZUc.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.CemgNJfw.js → 8.B-HweAc8.js} +1 -1
- package/build/client/_app/immutable/nodes/8.B-HweAc8.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.B-HweAc8.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.CKPeM6tx.js +5 -0
- package/build/client/_app/immutable/nodes/9.CKPeM6tx.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.CKPeM6tx.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/client/lyriks_logo.svg +148 -0
- package/build/client/lyriks_logo.svg.br +0 -0
- package/build/client/lyriks_logo.svg.gz +0 -0
- package/build/server/chunks/{0-C_o0oz-N.js → 0-Co8kcANG.js} +4 -4
- package/build/server/chunks/{0-C_o0oz-N.js.map → 0-Co8kcANG.js.map} +1 -1
- package/build/server/chunks/1-BSUItTig.js +9 -0
- package/build/server/chunks/{1-DPpKAKXV.js.map → 1-BSUItTig.js.map} +1 -1
- package/build/server/chunks/10-BygvxrZp.js +9 -0
- package/build/server/chunks/10-BygvxrZp.js.map +1 -0
- package/build/server/chunks/11-DRx0tRx2.js +9 -0
- package/build/server/chunks/11-DRx0tRx2.js.map +1 -0
- package/build/server/chunks/{2-AlfFqtL1.js → 2-BQT3m1vc.js} +3 -3
- package/build/server/chunks/{2-AlfFqtL1.js.map → 2-BQT3m1vc.js.map} +1 -1
- package/build/server/chunks/{3-vbjUt_51.js → 3-DPZ9BquJ.js} +3 -3
- package/build/server/chunks/{3-vbjUt_51.js.map → 3-DPZ9BquJ.js.map} +1 -1
- package/build/server/chunks/{4-BNow4x6D.js → 4-DHo47YX6.js} +3 -3
- package/build/server/chunks/{4-BNow4x6D.js.map → 4-DHo47YX6.js.map} +1 -1
- package/build/server/chunks/5-Cp9evBAG.js +9 -0
- package/build/server/chunks/5-Cp9evBAG.js.map +1 -0
- package/build/server/chunks/6-DiBq3bOV.js +9 -0
- package/build/server/chunks/6-DiBq3bOV.js.map +1 -0
- package/build/server/chunks/{6-QQ7r8Rd5.js → 7-C4hmS0dG.js} +3 -3
- package/build/server/chunks/{6-QQ7r8Rd5.js.map → 7-C4hmS0dG.js.map} +1 -1
- package/build/server/chunks/{7-CbPLGaIG.js → 8-CFFuDzBC.js} +4 -4
- package/build/server/chunks/{7-CbPLGaIG.js.map → 8-CFFuDzBC.js.map} +1 -1
- package/build/server/chunks/9-nhhKZJrs.js +9 -0
- package/build/server/chunks/9-nhhKZJrs.js.map +1 -0
- package/build/server/chunks/BehaviorGraph-m5kYj5HH.js +757 -0
- package/build/server/chunks/BehaviorGraph-m5kYj5HH.js.map +1 -0
- package/build/server/chunks/{FeatureCard-BQOY6gJQ.js → FeatureCard-CfbXNYe8.js} +2 -2
- package/build/server/chunks/{FeatureCard-BQOY6gJQ.js.map → FeatureCard-CfbXNYe8.js.map} +1 -1
- package/build/server/chunks/{ProgressBar-CfhccQ83.js → ProgressBar-DDoQJ_C9.js} +2 -2
- package/build/server/chunks/ProgressBar-DDoQJ_C9.js.map +1 -0
- package/build/server/chunks/{ProjectsIndex-CoDrvRya.js → ProjectsIndex-DUVJ3hyL.js} +2 -2
- package/build/server/chunks/{ProjectsIndex-CoDrvRya.js.map → ProjectsIndex-DUVJ3hyL.js.map} +1 -1
- package/build/server/chunks/TransitionCatalog-B8zHs-2E.js +271 -0
- package/build/server/chunks/TransitionCatalog-B8zHs-2E.js.map +1 -0
- package/build/server/chunks/{_layout.svelte-BREws55o.js → _layout.svelte-CLTmk0xU.js} +66 -15
- package/build/server/chunks/_layout.svelte-CLTmk0xU.js.map +1 -0
- package/build/server/chunks/{_page.svelte-De508ek8.js → _page.svelte-B1nG3PKn.js} +4 -4
- package/build/server/chunks/{_page.svelte-De508ek8.js.map → _page.svelte-B1nG3PKn.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-Zf9H4KOP.js → _page.svelte-BW_nbAAH.js} +11 -315
- package/build/server/chunks/_page.svelte-BW_nbAAH.js.map +1 -0
- package/build/server/chunks/{_page.svelte-BqSC-1vK.js → _page.svelte-Caq7J0jU.js} +91 -47
- package/build/server/chunks/_page.svelte-Caq7J0jU.js.map +1 -0
- package/build/server/chunks/{_page.svelte-B7hT3P8E.js → _page.svelte-Cham-dsM.js} +4 -4
- package/build/server/chunks/{_page.svelte-B7hT3P8E.js.map → _page.svelte-Cham-dsM.js.map} +1 -1
- package/build/server/chunks/_page.svelte-Dhwjwph_.js +41 -0
- package/build/server/chunks/_page.svelte-Dhwjwph_.js.map +1 -0
- package/build/server/chunks/{_page.svelte-BKKCa9H5.js → _page.svelte-DlFVT40-.js} +3 -3
- package/build/server/chunks/{_page.svelte-BKKCa9H5.js.map → _page.svelte-DlFVT40-.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-BtI2zZ_Z.js → _page.svelte-NVT2dzpG.js} +176 -327
- package/build/server/chunks/_page.svelte-NVT2dzpG.js.map +1 -0
- package/build/server/chunks/{_page.svelte-BKTveFAj.js → _page.svelte-Oj-W7G5s.js} +5 -5
- package/build/server/chunks/{_page.svelte-BKTveFAj.js.map → _page.svelte-Oj-W7G5s.js.map} +1 -1
- package/build/server/chunks/_page.svelte-Z_kK2lHY.js +68 -0
- package/build/server/chunks/_page.svelte-Z_kK2lHY.js.map +1 -0
- package/build/server/chunks/{builderModeStore.svelte-ihupr-3p.js → builderModeStore.svelte-BpRIU_zP.js} +217 -6
- package/build/server/chunks/builderModeStore.svelte-BpRIU_zP.js.map +1 -0
- package/build/server/chunks/client-DeX3TC3s.js +51 -0
- package/build/server/chunks/{client-DfpLcAZ9.js.map → client-DeX3TC3s.js.map} +1 -1
- package/build/server/chunks/{error.svelte-C35KOpru.js → error.svelte-Cdjeq3L2.js} +4 -4
- package/build/server/chunks/{error.svelte-C35KOpru.js.map → error.svelte-Cdjeq3L2.js.map} +1 -1
- package/build/server/chunks/featureStore.svelte-DIYgPBVm.js +161 -0
- package/build/server/chunks/featureStore.svelte-DIYgPBVm.js.map +1 -0
- package/build/server/chunks/{hooks.server-Rv301GTB.js → hooks.server-y3jdg_sB.js} +6 -2
- package/build/server/chunks/hooks.server-y3jdg_sB.js.map +1 -0
- package/build/server/chunks/{internal-BPKrFkK1.js → internal-KYK0WpL7.js} +4 -4
- package/build/server/chunks/{internal-BPKrFkK1.js.map → internal-KYK0WpL7.js.map} +1 -1
- package/build/server/chunks/projectFeaturesStore.svelte-2o-72_vr.js +313 -0
- package/build/server/chunks/projectFeaturesStore.svelte-2o-72_vr.js.map +1 -0
- package/build/server/chunks/{reconcile-Dv7jS3C8.js → reconcile-B5xqb6-s.js} +3 -272
- package/build/server/chunks/reconcile-B5xqb6-s.js.map +1 -0
- package/build/server/chunks/registry-DqAn_hVE.js +21 -0
- package/build/server/chunks/registry-DqAn_hVE.js.map +1 -0
- package/build/server/chunks/{state-CpLVNZq7.js → state-DBjl9lhV.js} +2 -2
- package/build/server/chunks/{state-CpLVNZq7.js.map → state-DBjl9lhV.js.map} +1 -1
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +33 -17
- package/build/server/manifest.js.map +1 -1
- package/cli/commands/dashboard.ts +14 -0
- package/cli/commands/init.ts +26 -0
- package/cli/commands/theme.ts +62 -0
- package/cli/unspa.ts +38 -2
- package/cli/util/context-files.ts +5 -0
- package/cli/util/theme.ts +34 -0
- package/mcp-server/sync-notifier.ts +88 -35
- package/package.json +2 -1
- package/src/app.css +187 -0
- package/src/app.html +15 -1
- package/src/features/behavior-model/domain/services/BehaviorGraphModel.ts +531 -0
- package/src/features/behavior-model/presentation/adapters/VisBehaviorGraphRenderer.ts +492 -0
- package/src/features/behavior-model/presentation/components/BehaviorGraph.svelte +370 -0
- package/src/features/behavior-model/presentation/components/FeatureHeader.svelte +13 -5
- package/src/features/behavior-model/presentation/view-models/BehaviorGraphTheme.ts +43 -0
- package/src/features/builder-mode/domain/BuilderModeDashboard.ts +7 -1
- package/src/features/builder-mode/presentation/components/BuilderModeDashboard.svelte +78 -16
- package/src/features/builder-mode/presentation/components/BuilderTagChips.svelte +25 -0
- package/src/features/builder-mode/presentation/stores/builderModeStore.svelte.ts +247 -3
- package/src/features/projects/presentation/components/ProjectEditor.svelte +7 -0
- package/src/features/simulator/application/use-cases/RunScenarios.ts +15 -1
- package/src/hooks.server.ts +11 -1
- package/src/lib/theme/registry.ts +77 -0
- package/src/lib/theme/themeStore.svelte.ts +64 -0
- package/src/routes/+layout.svelte +184 -30
- package/src/routes/features/[id]/graph/+page.svelte +34 -0
- package/src/routes/projects/[id]/graph/+page.svelte +79 -0
- package/src/shared/presentation/components/ProgressBar.svelte +1 -1
- package/src/shared/presentation/toast/SyncToast.svelte +14 -3
- package/src/shared/presentation/toast/viewLinkResolver.ts +32 -0
- package/static/lyriks_logo.svg +148 -0
- package/build/client/_app/immutable/assets/0.DFMDYAU9.css +0 -1
- package/build/client/_app/immutable/assets/0.DFMDYAU9.css.br +0 -0
- package/build/client/_app/immutable/assets/0.DFMDYAU9.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BO66rBOa2.js +0 -1
- package/build/client/_app/immutable/chunks/BO66rBOa2.js.br +0 -0
- package/build/client/_app/immutable/chunks/BO66rBOa2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DBJWcC6Y.js.br +0 -0
- package/build/client/_app/immutable/chunks/DBJWcC6Y.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DHoA038D.js +0 -1
- package/build/client/_app/immutable/chunks/DHoA038D.js.br +0 -2
- package/build/client/_app/immutable/chunks/DHoA038D.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DatGSObE.js.br +0 -0
- package/build/client/_app/immutable/chunks/DatGSObE.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DjWKKtqp.js +0 -1
- package/build/client/_app/immutable/chunks/DjWKKtqp.js.br +0 -1
- package/build/client/_app/immutable/chunks/DjWKKtqp.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Dq0DUAz1.js +0 -1
- package/build/client/_app/immutable/chunks/Dq0DUAz1.js.br +0 -0
- package/build/client/_app/immutable/chunks/Dq0DUAz1.js.gz +0 -0
- package/build/client/_app/immutable/chunks/iQu0D9Ux.js +0 -1
- package/build/client/_app/immutable/chunks/iQu0D9Ux.js.br +0 -0
- package/build/client/_app/immutable/chunks/iQu0D9Ux.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CLgh6Mx_.js +0 -2
- package/build/client/_app/immutable/entry/app.CLgh6Mx_.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CLgh6Mx_.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.D5GPCQZD.js +0 -1
- package/build/client/_app/immutable/entry/start.D5GPCQZD.js.br +0 -0
- package/build/client/_app/immutable/entry/start.D5GPCQZD.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.BOoI-hsu.js +0 -4
- package/build/client/_app/immutable/nodes/0.BOoI-hsu.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.BOoI-hsu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.KYdA6ppX.js.br +0 -2
- package/build/client/_app/immutable/nodes/1.KYdA6ppX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DBz20KgG.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DBz20KgG.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.19DIoFtw.js +0 -1
- package/build/client/_app/immutable/nodes/3.19DIoFtw.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.19DIoFtw.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.BvMzqBJj.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.BvMzqBJj.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.Dq6obSGG.js +0 -42
- package/build/client/_app/immutable/nodes/5.Dq6obSGG.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.Dq6obSGG.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.BOHISqs-.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.BOHISqs-.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.CemgNJfw.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.CemgNJfw.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.DejSfIYh.js +0 -5
- package/build/client/_app/immutable/nodes/8.DejSfIYh.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.DejSfIYh.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.CMW6a2Lg.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.CMW6a2Lg.js.gz +0 -0
- package/build/server/chunks/1-DPpKAKXV.js +0 -9
- package/build/server/chunks/5-D7OCJpxD.js +0 -9
- package/build/server/chunks/5-D7OCJpxD.js.map +0 -1
- package/build/server/chunks/8-CVO-E-sf.js +0 -9
- package/build/server/chunks/8-CVO-E-sf.js.map +0 -1
- package/build/server/chunks/9-DxT1baO5.js +0 -9
- package/build/server/chunks/9-DxT1baO5.js.map +0 -1
- package/build/server/chunks/ProgressBar-CfhccQ83.js.map +0 -1
- package/build/server/chunks/_layout.svelte-BREws55o.js.map +0 -1
- package/build/server/chunks/_page.svelte-BqSC-1vK.js.map +0 -1
- package/build/server/chunks/_page.svelte-BtI2zZ_Z.js.map +0 -1
- package/build/server/chunks/_page.svelte-Zf9H4KOP.js.map +0 -1
- package/build/server/chunks/builderModeStore.svelte-ihupr-3p.js.map +0 -1
- package/build/server/chunks/client-DfpLcAZ9.js +0 -24
- package/build/server/chunks/hooks.server-Rv301GTB.js.map +0 -1
- package/build/server/chunks/reconcile-Dv7jS3C8.js.map +0 -1
- /package/build/client/_app/immutable/assets/{9.nv0I59TU.css → 11.nv0I59TU.css} +0 -0
- /package/build/client/_app/immutable/assets/{9.nv0I59TU.css.br → 11.nv0I59TU.css.br} +0 -0
- /package/build/client/_app/immutable/assets/{9.nv0I59TU.css.gz → 11.nv0I59TU.css.gz} +0 -0
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
onRemove,
|
|
100
100
|
onRename,
|
|
101
101
|
typeOptions = [],
|
|
102
|
+
collapsible = false,
|
|
102
103
|
class: className = ''
|
|
103
104
|
}: {
|
|
104
105
|
readonly tags: readonly Tag[];
|
|
@@ -108,9 +109,15 @@
|
|
|
108
109
|
readonly onRemove?: (tag: Tag) => void | Promise<void>;
|
|
109
110
|
readonly onRename?: (from: Tag, to: Tag) => void | Promise<void>;
|
|
110
111
|
readonly typeOptions?: readonly string[];
|
|
112
|
+
// When true (the Builder cards), tags collapse to a compact tag-icon
|
|
113
|
+
// toggle by default and only reveal the chips + add/edit affordances on
|
|
114
|
+
// click — they were eating too much vertical space on every card.
|
|
115
|
+
readonly collapsible?: boolean;
|
|
111
116
|
readonly class?: string;
|
|
112
117
|
} = $props();
|
|
113
118
|
|
|
119
|
+
let expanded = $state(false);
|
|
120
|
+
|
|
114
121
|
const datalistId = `buildertags-types-${++nextDatalistSerial}`;
|
|
115
122
|
|
|
116
123
|
// Mix the palette color toward a deep slate for the dark builder plate (the
|
|
@@ -185,6 +192,23 @@
|
|
|
185
192
|
{#if tags.length > 0 || onAdd}
|
|
186
193
|
<div class={className} use:animateHeight={adding || editingKey !== null}>
|
|
187
194
|
<div class="flex flex-wrap items-center gap-1.5">
|
|
195
|
+
{#if collapsible}
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
class="inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium text-slate-400 transition hover:text-brand-300 {expanded ? 'bg-slate-800/60 text-slate-200' : ''}"
|
|
199
|
+
onclick={() => (expanded = !expanded)}
|
|
200
|
+
aria-expanded={expanded}
|
|
201
|
+
aria-label={expanded ? 'Hide tags' : 'Show tags'}
|
|
202
|
+
title={expanded ? 'Hide tags' : tags.length > 0 ? `Show ${tags.length} tag${tags.length === 1 ? '' : 's'}` : 'Add a tag'}
|
|
203
|
+
>
|
|
204
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" class="size-3.5" aria-hidden="true">
|
|
205
|
+
<path d="M20.59 13.41 11 3.83A2 2 0 0 0 9.59 3H4a1 1 0 0 0-1 1v5.59A2 2 0 0 0 3.59 11l9.58 9.59a2 2 0 0 0 2.83 0l4.59-4.59a2 2 0 0 0 0-2.83z" />
|
|
206
|
+
<circle cx="7.5" cy="7.5" r="1.1" fill="currentColor" stroke="none" />
|
|
207
|
+
</svg>
|
|
208
|
+
{#if !expanded && tags.length > 0}<span>{tags.length}</span>{/if}
|
|
209
|
+
</button>
|
|
210
|
+
{/if}
|
|
211
|
+
{#if !collapsible || expanded}
|
|
188
212
|
{#each tags as tag (tagKey(tag))}
|
|
189
213
|
{#if editingKey === tagKey(tag)}
|
|
190
214
|
<form class="inline-flex items-center gap-1" onsubmit={(e) => submitEdit(e, tag)}>
|
|
@@ -276,6 +300,7 @@
|
|
|
276
300
|
>+</button>
|
|
277
301
|
{/if}
|
|
278
302
|
{/if}
|
|
303
|
+
{/if}
|
|
279
304
|
|
|
280
305
|
{#if typeOptions.length > 0}
|
|
281
306
|
<datalist id={datalistId}>
|
|
@@ -55,6 +55,25 @@ class BuilderModeStore {
|
|
|
55
55
|
|
|
56
56
|
loading = $state(true);
|
|
57
57
|
dashboard = $state<BuilderModeDashboard>({ projects: [] });
|
|
58
|
+
// True once a full read of every project has happened. A deep-linked open
|
|
59
|
+
// (`?project=<id>`) loads ONLY that project (see `loadProjectOnly`), leaving
|
|
60
|
+
// this false until the user returns to the all-projects view, which fills in
|
|
61
|
+
// the rest via `ensureFullyLoaded`. Avoids reading + scoring the whole hub
|
|
62
|
+
// just to render one deep-linked project.
|
|
63
|
+
fullyLoaded = $state(false);
|
|
64
|
+
// Total projects in the hub, tracked apart from `dashboard.projects` because
|
|
65
|
+
// a deep-linked open only loads one. Cheap to fetch (project docs, no feature
|
|
66
|
+
// scoring), so the "All projects (N)" label shows the true total even while
|
|
67
|
+
// only one project is loaded.
|
|
68
|
+
totalProjectCount = $state(0);
|
|
69
|
+
// The count to show: the true total once known, else however many are loaded.
|
|
70
|
+
projectCount = $derived(Math.max(this.totalProjectCount, this.dashboard.projects.length));
|
|
71
|
+
// How many project cards are still streaming in — drives placeholder
|
|
72
|
+
// skeletons in the all-projects grid while the progressive load runs. Zero
|
|
73
|
+
// once fully loaded.
|
|
74
|
+
pendingProjectCount = $derived(
|
|
75
|
+
this.fullyLoaded ? 0 : Math.max(0, this.totalProjectCount - this.dashboard.projects.length)
|
|
76
|
+
);
|
|
58
77
|
// Explicit selection model: `null` means "no filter" (show the whole list,
|
|
59
78
|
// detail pane prompts the user to pick). Selecting a project/core feature
|
|
60
79
|
// filters to it; the UI floats it to the top of its list and pins it sticky.
|
|
@@ -311,6 +330,35 @@ class BuilderModeStore {
|
|
|
311
330
|
return byRelevance(coreFeature.features, (feature) => this.featureScore(feature, q));
|
|
312
331
|
});
|
|
313
332
|
|
|
333
|
+
/**
|
|
334
|
+
* The selected core feature's features grouped by their surface, in surface
|
|
335
|
+
* (declaration) order. A surface is the higher-level grouping a feature lives
|
|
336
|
+
* on (Feature › Surface › action-level feature); the Builder lists features
|
|
337
|
+
* under a surface heading rather than one flat list. Honors the same search
|
|
338
|
+
* filter as `visibleSelectedFeatures`; empty surfaces are dropped.
|
|
339
|
+
*/
|
|
340
|
+
visibleSelectedSurfaces = $derived.by<
|
|
341
|
+
readonly { surfaceId: string; surfaceName: string; features: readonly BuilderModeFeature[] }[]
|
|
342
|
+
>(() => {
|
|
343
|
+
const coreFeature = this.selectedCoreFeature;
|
|
344
|
+
if (!coreFeature) return [];
|
|
345
|
+
const q = this.searchQuery;
|
|
346
|
+
const showAll = !q || this.matchesText(q, coreFeature.name, coreFeature.description);
|
|
347
|
+
const groups: { surfaceId: string; surfaceName: string; features: BuilderModeFeature[] }[] = [];
|
|
348
|
+
const byId = new Map<string, (typeof groups)[number]>();
|
|
349
|
+
for (const feature of coreFeature.features) {
|
|
350
|
+
if (!showAll && this.featureScore(feature, q) === 0) continue;
|
|
351
|
+
let group = byId.get(feature.surfaceId);
|
|
352
|
+
if (!group) {
|
|
353
|
+
group = { surfaceId: feature.surfaceId, surfaceName: feature.surfaceName, features: [] };
|
|
354
|
+
byId.set(feature.surfaceId, group);
|
|
355
|
+
groups.push(group);
|
|
356
|
+
}
|
|
357
|
+
group.features.push(feature);
|
|
358
|
+
}
|
|
359
|
+
return groups;
|
|
360
|
+
});
|
|
361
|
+
|
|
314
362
|
setSearch(query: string): void {
|
|
315
363
|
this.search = query;
|
|
316
364
|
}
|
|
@@ -368,10 +416,60 @@ class BuilderModeStore {
|
|
|
368
416
|
);
|
|
369
417
|
}
|
|
370
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Load the full project list PROGRESSIVELY: read the cheap project list
|
|
421
|
+
* first, then load + score each project independently and stream it into the
|
|
422
|
+
* dashboard as it resolves — so cards appear one-by-one instead of the whole
|
|
423
|
+
* grid blocking on the slowest project. Cards are ordered by the project
|
|
424
|
+
* list regardless of which finishes first. The skeleton shows until the
|
|
425
|
+
* first card is ready (so the empty-state never flashes mid-load).
|
|
426
|
+
*/
|
|
371
427
|
async refresh(): Promise<void> {
|
|
372
428
|
this.loading = true;
|
|
373
|
-
|
|
374
|
-
|
|
429
|
+
this.error = null;
|
|
430
|
+
try {
|
|
431
|
+
const container = await this.host.data.getContainer();
|
|
432
|
+
const summaries = await container.useCases.listProjects();
|
|
433
|
+
this.totalProjectCount = summaries.length;
|
|
434
|
+
if (summaries.length === 0) {
|
|
435
|
+
this.dashboard = { projects: [] };
|
|
436
|
+
this.fullyLoaded = true;
|
|
437
|
+
this.loading = false;
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const order = new Map(summaries.map((summary, index) => [String(summary.id), index]));
|
|
441
|
+
const loadProject = getBuilderModeProjectUseCase({
|
|
442
|
+
projects: container.projectRepository,
|
|
443
|
+
features: container.repository,
|
|
444
|
+
statuses: container.statusRepository
|
|
445
|
+
});
|
|
446
|
+
const loaded: BuilderModeProject[] = [];
|
|
447
|
+
await Promise.all(
|
|
448
|
+
summaries.map(async (summary) => {
|
|
449
|
+
const project = await loadProject(asProjectId(String(summary.id)));
|
|
450
|
+
if (!project) return;
|
|
451
|
+
loaded.push(project);
|
|
452
|
+
// Keep the visible order stable (project-list order), independent of
|
|
453
|
+
// which load finishes first.
|
|
454
|
+
loaded.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
|
|
455
|
+
this.dashboard = { projects: [...loaded] };
|
|
456
|
+
this.loading = false; // reveal the grid as soon as one card is ready
|
|
457
|
+
})
|
|
458
|
+
);
|
|
459
|
+
this.fullyLoaded = true;
|
|
460
|
+
this.loading = false;
|
|
461
|
+
if (
|
|
462
|
+
this.selectedProjectId &&
|
|
463
|
+
!this.dashboard.projects.some((project) => project.id === this.selectedProjectId)
|
|
464
|
+
) {
|
|
465
|
+
this.selectedProjectId = null;
|
|
466
|
+
this.selectedCoreFeatureId = null;
|
|
467
|
+
}
|
|
468
|
+
await this.refreshQueue();
|
|
469
|
+
} catch (e) {
|
|
470
|
+
this.error = (e as Error).message;
|
|
471
|
+
this.loading = false;
|
|
472
|
+
}
|
|
375
473
|
}
|
|
376
474
|
|
|
377
475
|
async refreshSilent(): Promise<void> {
|
|
@@ -383,6 +481,8 @@ class BuilderModeStore {
|
|
|
383
481
|
statuses: container.statusRepository
|
|
384
482
|
});
|
|
385
483
|
this.dashboard = await loadDashboard();
|
|
484
|
+
this.fullyLoaded = true;
|
|
485
|
+
this.totalProjectCount = this.dashboard.projects.length;
|
|
386
486
|
if (
|
|
387
487
|
this.selectedProjectId &&
|
|
388
488
|
!this.dashboard.projects.some((project) => project.id === this.selectedProjectId)
|
|
@@ -397,17 +497,74 @@ class BuilderModeStore {
|
|
|
397
497
|
}
|
|
398
498
|
}
|
|
399
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Load ONLY the given project (its features, scores, queue) into the
|
|
502
|
+
* dashboard — used on a deep-linked open so we don't read and score every
|
|
503
|
+
* project in the hub just to show one. The rest of the list stays unloaded
|
|
504
|
+
* (`fullyLoaded=false`) until `ensureFullyLoaded` runs. Falls back to a full
|
|
505
|
+
* read when the id can't be resolved alone, so an unknown/stale link still
|
|
506
|
+
* lands the user on a usable list.
|
|
507
|
+
*/
|
|
508
|
+
async loadProjectOnly(projectId: string): Promise<void> {
|
|
509
|
+
this.loading = true;
|
|
510
|
+
// Fetch the real total in parallel so "All projects (N)" is right.
|
|
511
|
+
void this.refreshProjectCount();
|
|
512
|
+
try {
|
|
513
|
+
const container = await this.host.data.getContainer();
|
|
514
|
+
const loadProject = getBuilderModeProjectUseCase({
|
|
515
|
+
projects: container.projectRepository,
|
|
516
|
+
features: container.repository,
|
|
517
|
+
statuses: container.statusRepository
|
|
518
|
+
});
|
|
519
|
+
const project = await loadProject(asProjectId(projectId));
|
|
520
|
+
if (!project) {
|
|
521
|
+
await this.refreshSilent();
|
|
522
|
+
} else {
|
|
523
|
+
this.dashboard = { projects: [project] };
|
|
524
|
+
this.fullyLoaded = false;
|
|
525
|
+
this.error = null;
|
|
526
|
+
await this.refreshQueue();
|
|
527
|
+
}
|
|
528
|
+
} catch (e) {
|
|
529
|
+
this.error = (e as Error).message;
|
|
530
|
+
} finally {
|
|
531
|
+
this.loading = false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Ensure the whole project list is loaded (for the all-projects view). A
|
|
536
|
+
* no-op once any full read has happened, so returning from a deep-linked
|
|
537
|
+
* project triggers exactly one full load. */
|
|
538
|
+
async ensureFullyLoaded(): Promise<void> {
|
|
539
|
+
if (this.fullyLoaded) return;
|
|
540
|
+
await this.refresh();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** Fetch the true total project count (cheap — project docs only, no feature
|
|
544
|
+
* scoring) so the "All projects (N)" label is correct even when a single
|
|
545
|
+
* project was deep-loaded. Decorative: failures are ignored. */
|
|
546
|
+
async refreshProjectCount(): Promise<void> {
|
|
547
|
+
try {
|
|
548
|
+
const container = await this.host.data.getContainer();
|
|
549
|
+
this.totalProjectCount = (await container.useCases.listProjects()).length;
|
|
550
|
+
} catch {
|
|
551
|
+
// Count is decorative; leave the previous value.
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
400
555
|
selectProject(id: string): void {
|
|
401
556
|
this.selectedProjectId = id;
|
|
402
557
|
this.selectedCoreFeatureId = null;
|
|
403
558
|
void this.refreshQueue();
|
|
404
559
|
}
|
|
405
560
|
|
|
406
|
-
/** Cancel the project filter.
|
|
561
|
+
/** Cancel the project filter. Returning to the all-projects view fills the
|
|
562
|
+
* list if a deep-linked open only loaded one project. */
|
|
407
563
|
deselectProject(): void {
|
|
408
564
|
this.selectedProjectId = null;
|
|
409
565
|
this.selectedCoreFeatureId = null;
|
|
410
566
|
this.queue = [];
|
|
567
|
+
void this.ensureFullyLoaded();
|
|
411
568
|
}
|
|
412
569
|
|
|
413
570
|
/** Toggle a core feature filter (click the selected one again to clear). */
|
|
@@ -415,6 +572,27 @@ class BuilderModeStore {
|
|
|
415
572
|
this.selectedCoreFeatureId = this.selectedCoreFeatureId === id ? null : id;
|
|
416
573
|
}
|
|
417
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Set the full project + core-feature selection in ONE synchronous pass.
|
|
577
|
+
* Used when restoring from the URL (deep-link / refresh-persist, and the
|
|
578
|
+
* activity-toast "View" links that target the Builder while it's already
|
|
579
|
+
* open). Doing both at once avoids the project change transiently clearing
|
|
580
|
+
* the core and the URL writer racing to drop the `core` param before it's
|
|
581
|
+
* applied. Idempotent and order-independent for the URL ↔ store sync.
|
|
582
|
+
*/
|
|
583
|
+
applySelection(projectId: string | null, coreFeatureId: string | null): void {
|
|
584
|
+
if (this.selectedProjectId !== projectId) {
|
|
585
|
+
this.selectedProjectId = projectId;
|
|
586
|
+
this.selectedCoreFeatureId = null;
|
|
587
|
+
this.queue = [];
|
|
588
|
+
void this.refreshQueue();
|
|
589
|
+
// Clearing the project (back to the all-projects view) needs the full
|
|
590
|
+
// list if we only deep-loaded one project.
|
|
591
|
+
if (!projectId) void this.ensureFullyLoaded();
|
|
592
|
+
}
|
|
593
|
+
this.selectedCoreFeatureId = coreFeatureId;
|
|
594
|
+
}
|
|
595
|
+
|
|
418
596
|
deselectCoreFeature(): void {
|
|
419
597
|
this.selectedCoreFeatureId = null;
|
|
420
598
|
}
|
|
@@ -651,6 +829,72 @@ class BuilderModeStore {
|
|
|
651
829
|
if (this.selectedProjectId) await this.refreshProject(this.selectedProjectId);
|
|
652
830
|
}
|
|
653
831
|
|
|
832
|
+
private hasProject(projectId: string): boolean {
|
|
833
|
+
return this.dashboard.projects.some((project) => project.id === projectId);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** The project whose core features include `featureId`, or null. A model
|
|
837
|
+
* Feature maps to one Builder "core feature", so a feature/status event
|
|
838
|
+
* touches exactly one project's card. */
|
|
839
|
+
private projectIdOwningFeature(featureId: string): string | null {
|
|
840
|
+
for (const project of this.dashboard.projects) {
|
|
841
|
+
if (project.coreFeatures.some((coreFeature) => coreFeature.id === featureId)) {
|
|
842
|
+
return project.id;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return null;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Apply one out-of-band sync event (MCP write) with the SMALLEST refresh
|
|
850
|
+
* that keeps the view correct — the Builder's mirror of the Expert view's
|
|
851
|
+
* refetchOne/syncWith. A `project` event rebuilds just that project's card
|
|
852
|
+
* in place; a `feature`/`implementation-status` event rebuilds only the
|
|
853
|
+
* owning project. Both reuse the keyed lists, so scroll, selection, queue
|
|
854
|
+
* widget, and expanded descriptions survive instead of the whole dashboard
|
|
855
|
+
* blanking and re-running its entrance animations. We fall back to a full
|
|
856
|
+
* silent refresh ONLY when the event names something we don't have yet (a
|
|
857
|
+
* brand-new project, or a feature not attached to any loaded project) —
|
|
858
|
+
* that's the one case a scoped rebuild can't surface.
|
|
859
|
+
*/
|
|
860
|
+
async applySyncEvent(event: SyncEvent): Promise<void> {
|
|
861
|
+
if (event.kind === 'project') {
|
|
862
|
+
if (this.hasProject(event.id)) {
|
|
863
|
+
await this.refreshProject(event.id);
|
|
864
|
+
// Queue lives on the project; an enqueue/reorder/target write rides
|
|
865
|
+
// the same 'project' event, so refresh it when the open project moved.
|
|
866
|
+
if (event.id === this.selectedProjectId) await this.refreshQueue();
|
|
867
|
+
} else {
|
|
868
|
+
await this.refreshSilent();
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// 'feature' and 'implementation-status' both carry a feature id.
|
|
873
|
+
const projectId = this.projectIdOwningFeature(event.id);
|
|
874
|
+
if (projectId) {
|
|
875
|
+
await this.refreshProject(projectId);
|
|
876
|
+
if (projectId === this.selectedProjectId) await this.refreshQueue();
|
|
877
|
+
} else {
|
|
878
|
+
await this.refreshSilent();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* A Builder deep-link for an entity touched by a sync event, or null when it
|
|
884
|
+
* isn't shown in the Builder (so the activity-toast falls back to the Expert
|
|
885
|
+
* route). A `project` event targets the project card; `feature` /
|
|
886
|
+
* `implementation-status` (both carry a feature id) target that feature's
|
|
887
|
+
* core card via `?project=&core=`.
|
|
888
|
+
*/
|
|
889
|
+
deepLinkFor(kind: SyncEvent['kind'], id: string): string | null {
|
|
890
|
+
if (kind === 'project') {
|
|
891
|
+
return this.hasProject(id) ? `/builder-mode?project=${encodeURIComponent(id)}` : null;
|
|
892
|
+
}
|
|
893
|
+
const projectId = this.projectIdOwningFeature(id);
|
|
894
|
+
if (!projectId) return null;
|
|
895
|
+
return `/builder-mode?project=${encodeURIComponent(projectId)}&core=${encodeURIComponent(id)}`;
|
|
896
|
+
}
|
|
897
|
+
|
|
654
898
|
/** Drag-and-drop reorder: drop `itemId` at absolute position `targetIndex`. */
|
|
655
899
|
async moveQueueTo(itemId: QueueItemId, targetIndex: number): Promise<void> {
|
|
656
900
|
const projectId = this.selectedProjectId;
|
|
@@ -173,6 +173,13 @@
|
|
|
173
173
|
<span aria-hidden="true">←</span>
|
|
174
174
|
Back to projects
|
|
175
175
|
</a>
|
|
176
|
+
<a
|
|
177
|
+
href={`/projects/${project.id}/graph`}
|
|
178
|
+
class="inline-flex items-center gap-1.5 rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:border-brand-300 hover:bg-cyan-50 hover:text-brand-800"
|
|
179
|
+
title="Open the whole project behavior graph"
|
|
180
|
+
>
|
|
181
|
+
Graph
|
|
182
|
+
</a>
|
|
176
183
|
<button
|
|
177
184
|
type="button"
|
|
178
185
|
class="inline-flex items-center gap-1.5 rounded-md border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition hover:border-brand-300 hover:bg-cyan-50 hover:text-brand-800 disabled:opacity-50"
|
|
@@ -158,13 +158,27 @@ const evaluateAssertions = (
|
|
|
158
158
|
...(a.description ? { description: a.description } : {})
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
|
+
// Guard a malformed assertion that carries no state path (untyped scenario
|
|
162
|
+
// data can deserialize with `path: undefined`). Evaluating it would call
|
|
163
|
+
// `readPath(undefined)` → `undefined.split('.')` and crash the WHOLE
|
|
164
|
+
// feature's run. Report it as not-held with a clear path string instead, so
|
|
165
|
+
// one bad assertion fails just its own scenario rather than the run.
|
|
166
|
+
const rawPath: unknown = a.path;
|
|
167
|
+
if (typeof rawPath !== 'string' || rawPath.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
path: String(a.path),
|
|
170
|
+
operator: a.operator,
|
|
171
|
+
held: false,
|
|
172
|
+
...(a.description ? { description: a.description } : {})
|
|
173
|
+
};
|
|
174
|
+
}
|
|
161
175
|
const held = evaluateCondition(
|
|
162
176
|
{ left: a.path, operator: a.operator, right: a.value },
|
|
163
177
|
snapshot,
|
|
164
178
|
params
|
|
165
179
|
);
|
|
166
180
|
return {
|
|
167
|
-
path:
|
|
181
|
+
path: rawPath,
|
|
168
182
|
operator: a.operator,
|
|
169
183
|
held,
|
|
170
184
|
...(a.description ? { description: a.description } : {})
|
package/src/hooks.server.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Handle } from '@sveltejs/kit';
|
|
2
|
+
import { env } from '$env/dynamic/public';
|
|
2
3
|
import {
|
|
3
4
|
checkOrigin,
|
|
4
5
|
checkRequestAuth,
|
|
5
6
|
isAuthEnabled,
|
|
6
7
|
isOriginCheckEnabled
|
|
7
8
|
} from '$lib/server/security/auth';
|
|
9
|
+
import { parseThemeId } from '$lib/theme/registry';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Global request gate for the dashboard. When `UNSPA_AUTH_TOKEN` is
|
|
@@ -35,5 +37,13 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
35
37
|
});
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
|
-
|
|
40
|
+
// Seed the initial colour theme into the server-rendered <html data-theme>
|
|
41
|
+
// so the CLI default (PUBLIC_UNSPA_THEME) paints with no flash. The inline
|
|
42
|
+
// head script in app.html may still override this from localStorage when the
|
|
43
|
+
// user picked a theme via the in-app switcher. Inert for HTML responses that
|
|
44
|
+
// don't carry the placeholder (e.g. the /api JSON above already returned).
|
|
45
|
+
const theme = parseThemeId(env.PUBLIC_UNSPA_THEME);
|
|
46
|
+
return resolve(event, {
|
|
47
|
+
transformPageChunk: ({ html }) => html.replace('%unspa.theme%', theme)
|
|
48
|
+
});
|
|
39
49
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard theme registry.
|
|
3
|
+
*
|
|
4
|
+
* A "theme" is a purely cosmetic skin over the design system — it swaps the
|
|
5
|
+
* `@theme` color-token *values* (the brand ramp, the canvas, the hairline) and
|
|
6
|
+
* a little chrome (the header/shell background). It never adds, removes, or
|
|
7
|
+
* moves a feature: every surface, action, and control is identical between
|
|
8
|
+
* themes; only the colours change.
|
|
9
|
+
*
|
|
10
|
+
* Default is the always-present base look (the teal/cyan brand). Every other
|
|
11
|
+
* theme is opt-in, chosen at runtime by `PUBLIC_UNSPA_THEME` (the CLI default,
|
|
12
|
+
* via `unspa theme set` / `unspa dashboard --theme`) and overridable live in
|
|
13
|
+
* the browser by the header switcher (persisted in localStorage).
|
|
14
|
+
*
|
|
15
|
+
* This module is pure (no `$env`, no Svelte, no DOM) so it's unit-testable; the
|
|
16
|
+
* runtime wiring lives in `themeStore.svelte.ts` (browser) and `hooks.server.ts`
|
|
17
|
+
* (initial server-rendered attribute).
|
|
18
|
+
*/
|
|
19
|
+
export type DashboardThemeId = 'default' | 'lyriks';
|
|
20
|
+
|
|
21
|
+
export interface DashboardTheme {
|
|
22
|
+
readonly id: DashboardThemeId;
|
|
23
|
+
readonly label: string;
|
|
24
|
+
readonly description: string;
|
|
25
|
+
/** A representative CSS `background` value, shown as a swatch in the switcher. */
|
|
26
|
+
readonly swatch: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The base theme. Always available; the look you get with no theme set. */
|
|
30
|
+
const DEFAULT: DashboardTheme = {
|
|
31
|
+
id: 'default',
|
|
32
|
+
label: 'Default',
|
|
33
|
+
description: 'The standard Unspaghettit look — teal/cyan brand on a soft canvas.',
|
|
34
|
+
swatch: 'linear-gradient(135deg,#22d3ee,#06b6d4,#0e7490)'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Opt-in themes, keyed by the id used in `PUBLIC_UNSPA_THEME`. */
|
|
38
|
+
const OPTIONAL_THEMES: Readonly<Record<string, DashboardTheme>> = {
|
|
39
|
+
lyriks: {
|
|
40
|
+
id: 'lyriks',
|
|
41
|
+
label: 'Lyriks',
|
|
42
|
+
description: 'Lyriks.io brand skin — a violet→fuchsia gradient header over a cool canvas.',
|
|
43
|
+
swatch: 'linear-gradient(90deg,#6d28d9,#a21caf,#db2777)'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Default first, then the opt-in themes, in registration order. */
|
|
48
|
+
export const ALL_THEMES: readonly DashboardTheme[] = [DEFAULT, ...Object.values(OPTIONAL_THEMES)];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse a raw `PUBLIC_UNSPA_THEME` (or localStorage / CLI) value into a known
|
|
52
|
+
* theme id. Trims + lowercases; anything unrecognised (including the literal
|
|
53
|
+
* "default" and the unreplaced `%unspa.theme%` placeholder) falls back to the
|
|
54
|
+
* default theme, so a bad value never blanks the UI.
|
|
55
|
+
*/
|
|
56
|
+
export const parseThemeId = (raw: string | undefined): DashboardThemeId => {
|
|
57
|
+
const id = (raw ?? '').trim().toLowerCase();
|
|
58
|
+
return id in OPTIONAL_THEMES ? (id as DashboardThemeId) : 'default';
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Resolve a raw value into its full theme definition. */
|
|
62
|
+
export const resolveTheme = (raw: string | undefined): DashboardTheme => {
|
|
63
|
+
const id = parseThemeId(raw);
|
|
64
|
+
return id === 'default' ? DEFAULT : OPTIONAL_THEMES[id]!;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The opt-in themes (everything except the always-on default base). */
|
|
68
|
+
export const optionalThemes = (): readonly DashboardTheme[] => Object.values(OPTIONAL_THEMES);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether `id` names a known theme — including the always-valid "default", so
|
|
72
|
+
* the CLI accepts `unspa theme set default` as an explicit revert.
|
|
73
|
+
*/
|
|
74
|
+
export const isThemeId = (id: string): boolean => {
|
|
75
|
+
const normalized = id.trim().toLowerCase();
|
|
76
|
+
return normalized === 'default' || Object.prototype.hasOwnProperty.call(OPTIONAL_THEMES, normalized);
|
|
77
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active dashboard theme. Purely cosmetic — it only decides which colour skin
|
|
3
|
+
* the design system renders with (see `registry.ts`); no feature ever depends
|
|
4
|
+
* on it.
|
|
5
|
+
*
|
|
6
|
+
* Resolution order, highest priority first:
|
|
7
|
+
* 1. The browser choice made via the header switcher (localStorage).
|
|
8
|
+
* 2. The CLI default (`PUBLIC_UNSPA_THEME`, set by `unspa theme set` /
|
|
9
|
+
* `unspa dashboard --theme`).
|
|
10
|
+
* 3. The default theme.
|
|
11
|
+
*
|
|
12
|
+
* The initial value is seeded from `PUBLIC_UNSPA_THEME` so server-side
|
|
13
|
+
* rendering paints the CLI-default chrome with no flash. On the client,
|
|
14
|
+
* `init()` re-reads the `data-theme` attribute on <html> — which the inline
|
|
15
|
+
* head script in app.html has already overridden from localStorage when the
|
|
16
|
+
* user has a saved choice — and mirrors it into reactive state so the chrome in
|
|
17
|
+
* the layout (header gradient, canvas) tracks it. `setTheme` updates the
|
|
18
|
+
* attribute (re-skinning every token-driven colour) and persists to
|
|
19
|
+
* localStorage, all live, no reload.
|
|
20
|
+
*
|
|
21
|
+
* Browser-only side effects are guarded so the store is SSR-safe.
|
|
22
|
+
*/
|
|
23
|
+
import { env } from '$env/dynamic/public';
|
|
24
|
+
import { parseThemeId, type DashboardThemeId } from './registry';
|
|
25
|
+
|
|
26
|
+
const STORAGE_KEY = 'unspa.theme';
|
|
27
|
+
|
|
28
|
+
class ThemeStore {
|
|
29
|
+
/** The currently applied theme id. */
|
|
30
|
+
current = $state<DashboardThemeId>(parseThemeId(env.PUBLIC_UNSPA_THEME));
|
|
31
|
+
private initialized = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Hydrate from the `data-theme` attribute already on <html> (server default,
|
|
35
|
+
* possibly overridden by the inline head script from localStorage). Idempotent.
|
|
36
|
+
*/
|
|
37
|
+
init(): void {
|
|
38
|
+
if (this.initialized) return;
|
|
39
|
+
this.initialized = true;
|
|
40
|
+
if (typeof document === 'undefined') return;
|
|
41
|
+
this.current = parseThemeId(document.documentElement.dataset.theme);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Apply a theme live: reactive state + the <html> attribute + localStorage. */
|
|
45
|
+
setTheme(next: DashboardThemeId): void {
|
|
46
|
+
const id = parseThemeId(next);
|
|
47
|
+
this.current = id;
|
|
48
|
+
if (typeof document !== 'undefined') {
|
|
49
|
+
document.documentElement.dataset.theme = id;
|
|
50
|
+
}
|
|
51
|
+
if (typeof localStorage === 'undefined') return;
|
|
52
|
+
// Only persist a non-default choice; clearing the key lets the CLI default
|
|
53
|
+
// (PUBLIC_UNSPA_THEME) win again on the next load.
|
|
54
|
+
if (id === 'default') localStorage.removeItem(STORAGE_KEY);
|
|
55
|
+
else localStorage.setItem(STORAGE_KEY, id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Convenience flag for the layout's chrome conditionals. */
|
|
59
|
+
get isLyriks(): boolean {
|
|
60
|
+
return this.current === 'lyriks';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const themeStore = new ThemeStore();
|