@theokit/ui 0.13.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/CHANGELOG.md +1325 -0
- package/DESIGN.md +456 -0
- package/LICENSE +201 -0
- package/NOTICE +38 -0
- package/README.md +467 -0
- package/dist/chunk-27ENTTY7.js +146 -0
- package/dist/chunk-27ENTTY7.js.map +1 -0
- package/dist/chunk-2H6TQELG.js +33 -0
- package/dist/chunk-2H6TQELG.js.map +1 -0
- package/dist/chunk-2L6MRJD4.js +120 -0
- package/dist/chunk-2L6MRJD4.js.map +1 -0
- package/dist/chunk-2Y5V2PAL.js +80 -0
- package/dist/chunk-2Y5V2PAL.js.map +1 -0
- package/dist/chunk-34NAFDVL.js +46 -0
- package/dist/chunk-34NAFDVL.js.map +1 -0
- package/dist/chunk-36KJGXEK.js +112 -0
- package/dist/chunk-36KJGXEK.js.map +1 -0
- package/dist/chunk-3BMYYNN6.js +124 -0
- package/dist/chunk-3BMYYNN6.js.map +1 -0
- package/dist/chunk-3OHV7EEI.js +34 -0
- package/dist/chunk-3OHV7EEI.js.map +1 -0
- package/dist/chunk-3QKTS6F5.js +88 -0
- package/dist/chunk-3QKTS6F5.js.map +1 -0
- package/dist/chunk-3TBXLYNM.js +42 -0
- package/dist/chunk-3TBXLYNM.js.map +1 -0
- package/dist/chunk-4AM2HSXU.js +67 -0
- package/dist/chunk-4AM2HSXU.js.map +1 -0
- package/dist/chunk-4BCGKM65.js +44 -0
- package/dist/chunk-4BCGKM65.js.map +1 -0
- package/dist/chunk-4D3JILQX.js +145 -0
- package/dist/chunk-4D3JILQX.js.map +1 -0
- package/dist/chunk-4EJU2GBG.js +48 -0
- package/dist/chunk-4EJU2GBG.js.map +1 -0
- package/dist/chunk-4WKO3G5C.js +110 -0
- package/dist/chunk-4WKO3G5C.js.map +1 -0
- package/dist/chunk-53XPKI7Q.js +97 -0
- package/dist/chunk-53XPKI7Q.js.map +1 -0
- package/dist/chunk-55TDVDPG.js +58 -0
- package/dist/chunk-55TDVDPG.js.map +1 -0
- package/dist/chunk-56BJLFW7.js +26 -0
- package/dist/chunk-56BJLFW7.js.map +1 -0
- package/dist/chunk-5HOQLE6Y.js +35 -0
- package/dist/chunk-5HOQLE6Y.js.map +1 -0
- package/dist/chunk-5TY3NYF5.js +144 -0
- package/dist/chunk-5TY3NYF5.js.map +1 -0
- package/dist/chunk-5VOSCJKQ.js +92 -0
- package/dist/chunk-5VOSCJKQ.js.map +1 -0
- package/dist/chunk-65NVO6TK.js +171 -0
- package/dist/chunk-65NVO6TK.js.map +1 -0
- package/dist/chunk-6A5TPCKP.js +64 -0
- package/dist/chunk-6A5TPCKP.js.map +1 -0
- package/dist/chunk-6CO4LEXZ.js +41 -0
- package/dist/chunk-6CO4LEXZ.js.map +1 -0
- package/dist/chunk-6FVUPNPG.js +56 -0
- package/dist/chunk-6FVUPNPG.js.map +1 -0
- package/dist/chunk-76YWTIWK.js +106 -0
- package/dist/chunk-76YWTIWK.js.map +1 -0
- package/dist/chunk-7EI7424P.js +78 -0
- package/dist/chunk-7EI7424P.js.map +1 -0
- package/dist/chunk-AHTVYOPQ.js +26 -0
- package/dist/chunk-AHTVYOPQ.js.map +1 -0
- package/dist/chunk-AJTJNHKK.js +85 -0
- package/dist/chunk-AJTJNHKK.js.map +1 -0
- package/dist/chunk-AMT3CPMC.js +155 -0
- package/dist/chunk-AMT3CPMC.js.map +1 -0
- package/dist/chunk-AX5EH73R.js +59 -0
- package/dist/chunk-AX5EH73R.js.map +1 -0
- package/dist/chunk-B3VAJSZ2.js +35 -0
- package/dist/chunk-B3VAJSZ2.js.map +1 -0
- package/dist/chunk-B4CQMQ64.js +25 -0
- package/dist/chunk-B4CQMQ64.js.map +1 -0
- package/dist/chunk-BMRZXT5T.js +115 -0
- package/dist/chunk-BMRZXT5T.js.map +1 -0
- package/dist/chunk-BYZ6OFH4.js +73 -0
- package/dist/chunk-BYZ6OFH4.js.map +1 -0
- package/dist/chunk-C55VUQ7N.js +156 -0
- package/dist/chunk-C55VUQ7N.js.map +1 -0
- package/dist/chunk-D4GEAV4C.js +91 -0
- package/dist/chunk-D4GEAV4C.js.map +1 -0
- package/dist/chunk-DC43CHAM.js +152 -0
- package/dist/chunk-DC43CHAM.js.map +1 -0
- package/dist/chunk-DKCRLN35.js +92 -0
- package/dist/chunk-DKCRLN35.js.map +1 -0
- package/dist/chunk-DN5BUDBI.js +86 -0
- package/dist/chunk-DN5BUDBI.js.map +1 -0
- package/dist/chunk-DOLKDYMS.js +88 -0
- package/dist/chunk-DOLKDYMS.js.map +1 -0
- package/dist/chunk-DW34WXCG.js +28 -0
- package/dist/chunk-DW34WXCG.js.map +1 -0
- package/dist/chunk-DZAAKHGZ.js +135 -0
- package/dist/chunk-DZAAKHGZ.js.map +1 -0
- package/dist/chunk-E4IRSSHO.js +116 -0
- package/dist/chunk-E4IRSSHO.js.map +1 -0
- package/dist/chunk-E67WQXBV.js +104 -0
- package/dist/chunk-E67WQXBV.js.map +1 -0
- package/dist/chunk-EG6IHP3H.js +128 -0
- package/dist/chunk-EG6IHP3H.js.map +1 -0
- package/dist/chunk-EO7LOXG2.js +82 -0
- package/dist/chunk-EO7LOXG2.js.map +1 -0
- package/dist/chunk-EWDN56AS.js +24 -0
- package/dist/chunk-EWDN56AS.js.map +1 -0
- package/dist/chunk-F5P5P2SC.js +141 -0
- package/dist/chunk-F5P5P2SC.js.map +1 -0
- package/dist/chunk-FAWPRZTM.js +79 -0
- package/dist/chunk-FAWPRZTM.js.map +1 -0
- package/dist/chunk-FGYJ2WPX.js +36 -0
- package/dist/chunk-FGYJ2WPX.js.map +1 -0
- package/dist/chunk-GBG3I5I5.js +46 -0
- package/dist/chunk-GBG3I5I5.js.map +1 -0
- package/dist/chunk-GDMCDW66.js +19 -0
- package/dist/chunk-GDMCDW66.js.map +1 -0
- package/dist/chunk-H6HSQCOW.js +80 -0
- package/dist/chunk-H6HSQCOW.js.map +1 -0
- package/dist/chunk-HDM4RCIF.js +111 -0
- package/dist/chunk-HDM4RCIF.js.map +1 -0
- package/dist/chunk-HNTOGGVD.js +77 -0
- package/dist/chunk-HNTOGGVD.js.map +1 -0
- package/dist/chunk-HQW2ABO4.js +28 -0
- package/dist/chunk-HQW2ABO4.js.map +1 -0
- package/dist/chunk-HRDRGZ2Y.js +76 -0
- package/dist/chunk-HRDRGZ2Y.js.map +1 -0
- package/dist/chunk-HUOVA7SF.js +83 -0
- package/dist/chunk-HUOVA7SF.js.map +1 -0
- package/dist/chunk-ITA3SNOR.js +133 -0
- package/dist/chunk-ITA3SNOR.js.map +1 -0
- package/dist/chunk-IYNUPG2G.js +61 -0
- package/dist/chunk-IYNUPG2G.js.map +1 -0
- package/dist/chunk-JJ65ZI4P.js +199 -0
- package/dist/chunk-JJ65ZI4P.js.map +1 -0
- package/dist/chunk-JRBGZ6NI.js +106 -0
- package/dist/chunk-JRBGZ6NI.js.map +1 -0
- package/dist/chunk-K45OO62F.js +108 -0
- package/dist/chunk-K45OO62F.js.map +1 -0
- package/dist/chunk-KDTKA667.js +67 -0
- package/dist/chunk-KDTKA667.js.map +1 -0
- package/dist/chunk-KI7KZBSN.js +142 -0
- package/dist/chunk-KI7KZBSN.js.map +1 -0
- package/dist/chunk-KOJ7XOPZ.js +87 -0
- package/dist/chunk-KOJ7XOPZ.js.map +1 -0
- package/dist/chunk-KQTHJ22B.js +82 -0
- package/dist/chunk-KQTHJ22B.js.map +1 -0
- package/dist/chunk-KRC43RZR.js +77 -0
- package/dist/chunk-KRC43RZR.js.map +1 -0
- package/dist/chunk-LJQOEGQ2.js +116 -0
- package/dist/chunk-LJQOEGQ2.js.map +1 -0
- package/dist/chunk-LKRNUSKZ.js +149 -0
- package/dist/chunk-LKRNUSKZ.js.map +1 -0
- package/dist/chunk-LLL7QQ52.js +76 -0
- package/dist/chunk-LLL7QQ52.js.map +1 -0
- package/dist/chunk-LQ4B5X4Y.js +56 -0
- package/dist/chunk-LQ4B5X4Y.js.map +1 -0
- package/dist/chunk-M3FSLEHQ.js +76 -0
- package/dist/chunk-M3FSLEHQ.js.map +1 -0
- package/dist/chunk-M5G3O6H6.js +57 -0
- package/dist/chunk-M5G3O6H6.js.map +1 -0
- package/dist/chunk-M6JIC5PU.js +81 -0
- package/dist/chunk-M6JIC5PU.js.map +1 -0
- package/dist/chunk-N2HJ3SLS.js +186 -0
- package/dist/chunk-N2HJ3SLS.js.map +1 -0
- package/dist/chunk-NGZWBFTP.js +45 -0
- package/dist/chunk-NGZWBFTP.js.map +1 -0
- package/dist/chunk-OAKCXT35.js +34 -0
- package/dist/chunk-OAKCXT35.js.map +1 -0
- package/dist/chunk-OSD3U3HT.js +54 -0
- package/dist/chunk-OSD3U3HT.js.map +1 -0
- package/dist/chunk-OUXESQ2R.js +42 -0
- package/dist/chunk-OUXESQ2R.js.map +1 -0
- package/dist/chunk-OY2LJHMJ.js +43 -0
- package/dist/chunk-OY2LJHMJ.js.map +1 -0
- package/dist/chunk-OYEZR4CN.js +221 -0
- package/dist/chunk-OYEZR4CN.js.map +1 -0
- package/dist/chunk-P57HUMAE.js +66 -0
- package/dist/chunk-P57HUMAE.js.map +1 -0
- package/dist/chunk-P6Y2PI6L.js +82 -0
- package/dist/chunk-P6Y2PI6L.js.map +1 -0
- package/dist/chunk-PA7TDXUQ.js +51 -0
- package/dist/chunk-PA7TDXUQ.js.map +1 -0
- package/dist/chunk-PPBGGNPV.js +112 -0
- package/dist/chunk-PPBGGNPV.js.map +1 -0
- package/dist/chunk-PRH4HKND.js +48 -0
- package/dist/chunk-PRH4HKND.js.map +1 -0
- package/dist/chunk-PSPAZJUQ.js +32 -0
- package/dist/chunk-PSPAZJUQ.js.map +1 -0
- package/dist/chunk-Q5G5CGZ2.js +170 -0
- package/dist/chunk-Q5G5CGZ2.js.map +1 -0
- package/dist/chunk-QDAF3LP7.js +89 -0
- package/dist/chunk-QDAF3LP7.js.map +1 -0
- package/dist/chunk-QGVIGNJ3.js +37 -0
- package/dist/chunk-QGVIGNJ3.js.map +1 -0
- package/dist/chunk-QNUITYSY.js +68 -0
- package/dist/chunk-QNUITYSY.js.map +1 -0
- package/dist/chunk-QSWVN3RT.js +116 -0
- package/dist/chunk-QSWVN3RT.js.map +1 -0
- package/dist/chunk-QTLQZ7OJ.js +110 -0
- package/dist/chunk-QTLQZ7OJ.js.map +1 -0
- package/dist/chunk-QYAMLIG2.js +84 -0
- package/dist/chunk-QYAMLIG2.js.map +1 -0
- package/dist/chunk-REILH4XF.js +128 -0
- package/dist/chunk-REILH4XF.js.map +1 -0
- package/dist/chunk-S6SSK6QX.js +80 -0
- package/dist/chunk-S6SSK6QX.js.map +1 -0
- package/dist/chunk-SA7ED3PN.js +68 -0
- package/dist/chunk-SA7ED3PN.js.map +1 -0
- package/dist/chunk-SIJOEM4N.js +55 -0
- package/dist/chunk-SIJOEM4N.js.map +1 -0
- package/dist/chunk-SLOKAAH2.js +70 -0
- package/dist/chunk-SLOKAAH2.js.map +1 -0
- package/dist/chunk-TR6NPSMX.js +85 -0
- package/dist/chunk-TR6NPSMX.js.map +1 -0
- package/dist/chunk-TSZ5DEAT.js +106 -0
- package/dist/chunk-TSZ5DEAT.js.map +1 -0
- package/dist/chunk-TUNVF45W.js +127 -0
- package/dist/chunk-TUNVF45W.js.map +1 -0
- package/dist/chunk-TXOBNSQ5.js +63 -0
- package/dist/chunk-TXOBNSQ5.js.map +1 -0
- package/dist/chunk-U44DRLMM.js +88 -0
- package/dist/chunk-U44DRLMM.js.map +1 -0
- package/dist/chunk-U4THNRV5.js +114 -0
- package/dist/chunk-U4THNRV5.js.map +1 -0
- package/dist/chunk-UAZOFC4W.js +72 -0
- package/dist/chunk-UAZOFC4W.js.map +1 -0
- package/dist/chunk-UGKI466V.js +12 -0
- package/dist/chunk-UGKI466V.js.map +1 -0
- package/dist/chunk-VM4RMQQN.js +11 -0
- package/dist/chunk-VM4RMQQN.js.map +1 -0
- package/dist/chunk-VQ37VLAS.js +54 -0
- package/dist/chunk-VQ37VLAS.js.map +1 -0
- package/dist/chunk-VT7VSYH5.js +73 -0
- package/dist/chunk-VT7VSYH5.js.map +1 -0
- package/dist/chunk-VTIRUCLZ.js +57 -0
- package/dist/chunk-VTIRUCLZ.js.map +1 -0
- package/dist/chunk-VVBAEYKI.js +202 -0
- package/dist/chunk-VVBAEYKI.js.map +1 -0
- package/dist/chunk-WHFIQUCC.js +120 -0
- package/dist/chunk-WHFIQUCC.js.map +1 -0
- package/dist/chunk-WPSESV5Z.js +74 -0
- package/dist/chunk-WPSESV5Z.js.map +1 -0
- package/dist/chunk-WXEXCHEN.js +51 -0
- package/dist/chunk-WXEXCHEN.js.map +1 -0
- package/dist/chunk-X2DDPD3D.js +113 -0
- package/dist/chunk-X2DDPD3D.js.map +1 -0
- package/dist/chunk-X7VIMKLD.js +127 -0
- package/dist/chunk-X7VIMKLD.js.map +1 -0
- package/dist/chunk-XJ3EG6XY.js +30 -0
- package/dist/chunk-XJ3EG6XY.js.map +1 -0
- package/dist/chunk-XOT5HWSF.js +23 -0
- package/dist/chunk-XOT5HWSF.js.map +1 -0
- package/dist/chunk-Y72IP43U.js +117 -0
- package/dist/chunk-Y72IP43U.js.map +1 -0
- package/dist/chunk-YD6FLXBV.js +61 -0
- package/dist/chunk-YD6FLXBV.js.map +1 -0
- package/dist/chunk-YEQQGYYO.js +1022 -0
- package/dist/chunk-YEQQGYYO.js.map +1 -0
- package/dist/chunk-YYW6AEIT.js +46 -0
- package/dist/chunk-YYW6AEIT.js.map +1 -0
- package/dist/chunk-ZEVGXKRU.js +104 -0
- package/dist/chunk-ZEVGXKRU.js.map +1 -0
- package/dist/chunk-ZKSMMLDP.js +74 -0
- package/dist/chunk-ZKSMMLDP.js.map +1 -0
- package/dist/chunk-ZU6IM6PK.js +101 -0
- package/dist/chunk-ZU6IM6PK.js.map +1 -0
- package/dist/chunk-ZUS5KZGO.js +714 -0
- package/dist/chunk-ZUS5KZGO.js.map +1 -0
- package/dist/chunk-ZVS2GOT2.js +58 -0
- package/dist/chunk-ZVS2GOT2.js.map +1 -0
- package/dist/chunk-ZXPDS6DH.js +3 -0
- package/dist/chunk-ZXPDS6DH.js.map +1 -0
- package/dist/chunk-ZZQQJX5Z.js +173 -0
- package/dist/chunk-ZZQQJX5Z.js.map +1 -0
- package/dist/components.css +2 -0
- package/dist/composites/account-menu/index.js +6 -0
- package/dist/composites/account-menu/index.js.map +1 -0
- package/dist/composites/agent-composer/index.js +7 -0
- package/dist/composites/agent-composer/index.js.map +1 -0
- package/dist/composites/agent-editor/index.js +10 -0
- package/dist/composites/agent-editor/index.js.map +1 -0
- package/dist/composites/agent-stream/index.js +12 -0
- package/dist/composites/agent-stream/index.js.map +1 -0
- package/dist/composites/agent-timeline/index.js +5 -0
- package/dist/composites/agent-timeline/index.js.map +1 -0
- package/dist/composites/approval-card/index.js +5 -0
- package/dist/composites/approval-card/index.js.map +1 -0
- package/dist/composites/chat-composer/index.js +6 -0
- package/dist/composites/chat-composer/index.js.map +1 -0
- package/dist/composites/chat-message/index.js +6 -0
- package/dist/composites/chat-message/index.js.map +1 -0
- package/dist/composites/code-block/index.js +5 -0
- package/dist/composites/code-block/index.js.map +1 -0
- package/dist/composites/command-palette/index.js +5 -0
- package/dist/composites/command-palette/index.js.map +1 -0
- package/dist/composites/confirm-dialog/index.js +7 -0
- package/dist/composites/confirm-dialog/index.js.map +1 -0
- package/dist/composites/cron-jobs-list/index.js +5 -0
- package/dist/composites/cron-jobs-list/index.js.map +1 -0
- package/dist/composites/data-table/index.js +10 -0
- package/dist/composites/data-table/index.js.map +1 -0
- package/dist/composites/deployment-row/index.js +5 -0
- package/dist/composites/deployment-row/index.js.map +1 -0
- package/dist/composites/domain-config/index.js +7 -0
- package/dist/composites/domain-config/index.js.map +1 -0
- package/dist/composites/env-var-editor/index.js +7 -0
- package/dist/composites/env-var-editor/index.js.map +1 -0
- package/dist/composites/mcp-server-list/index.js +5 -0
- package/dist/composites/mcp-server-list/index.js.map +1 -0
- package/dist/composites/page-shell/index.js +7 -0
- package/dist/composites/page-shell/index.js.map +1 -0
- package/dist/composites/permission-modal/index.js +6 -0
- package/dist/composites/permission-modal/index.js.map +1 -0
- package/dist/composites/preview-env-card/index.js +6 -0
- package/dist/composites/preview-env-card/index.js.map +1 -0
- package/dist/composites/preview-panel/index.js +5 -0
- package/dist/composites/preview-panel/index.js.map +1 -0
- package/dist/composites/project-card/index.js +6 -0
- package/dist/composites/project-card/index.js.map +1 -0
- package/dist/composites/rollback-ui/index.js +6 -0
- package/dist/composites/rollback-ui/index.js.map +1 -0
- package/dist/composites/rule-editor/index.js +11 -0
- package/dist/composites/rule-editor/index.js.map +1 -0
- package/dist/composites/skill-editor/index.js +11 -0
- package/dist/composites/skill-editor/index.js.map +1 -0
- package/dist/composites/skills-list/index.js +5 -0
- package/dist/composites/skills-list/index.js.map +1 -0
- package/dist/composites/stability-bundle-viewer/index.js +4 -0
- package/dist/composites/stability-bundle-viewer/index.js.map +1 -0
- package/dist/composites/task-header/index.js +5 -0
- package/dist/composites/task-header/index.js.map +1 -0
- package/dist/composites/usage-meter/index.js +5 -0
- package/dist/composites/usage-meter/index.js.map +1 -0
- package/dist/fonts/LICENSE-GEIST.txt +92 -0
- package/dist/fonts/geist-400.woff2 +0 -0
- package/dist/fonts/geist-500.woff2 +0 -0
- package/dist/fonts/geist-600.woff2 +0 -0
- package/dist/fonts/geist-mono-400.woff2 +0 -0
- package/dist/fonts/geist-mono-500.woff2 +0 -0
- package/dist/fonts/geist-mono-600.woff2 +0 -0
- package/dist/fonts-cdn.css +28 -0
- package/dist/fonts.css +75 -0
- package/dist/index.d.ts +4621 -0
- package/dist/index.js +1338 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-D5xmXqYb.d.ts +172 -0
- package/dist/preset-v3-legacy.d.ts +35 -0
- package/dist/preset-v3-legacy.js +159 -0
- package/dist/preset-v3-legacy.js.map +1 -0
- package/dist/preset.css +27 -0
- package/dist/primitives/action-bar/index.js +4 -0
- package/dist/primitives/action-bar/index.js.map +1 -0
- package/dist/primitives/agent-error-card/index.js +5 -0
- package/dist/primitives/agent-error-card/index.js.map +1 -0
- package/dist/primitives/agent-event/index.js +4 -0
- package/dist/primitives/agent-event/index.js.map +1 -0
- package/dist/primitives/agent-handoff/index.js +4 -0
- package/dist/primitives/agent-handoff/index.js.map +1 -0
- package/dist/primitives/agent-profile/index.js +4 -0
- package/dist/primitives/agent-profile/index.js.map +1 -0
- package/dist/primitives/agent-starting-state/index.js +5 -0
- package/dist/primitives/agent-starting-state/index.js.map +1 -0
- package/dist/primitives/agent-streaming/index.js +5 -0
- package/dist/primitives/agent-streaming/index.js.map +1 -0
- package/dist/primitives/alert/index.js +4 -0
- package/dist/primitives/alert/index.js.map +1 -0
- package/dist/primitives/artifact-preview/index.js +4 -0
- package/dist/primitives/artifact-preview/index.js.map +1 -0
- package/dist/primitives/attachment-chip/index.js +4 -0
- package/dist/primitives/attachment-chip/index.js.map +1 -0
- package/dist/primitives/audit-log-entry/index.js +4 -0
- package/dist/primitives/audit-log-entry/index.js.map +1 -0
- package/dist/primitives/auto-compact-notice/index.js +5 -0
- package/dist/primitives/auto-compact-notice/index.js.map +1 -0
- package/dist/primitives/avatar/index.js +4 -0
- package/dist/primitives/avatar/index.js.map +1 -0
- package/dist/primitives/badge/index.js +4 -0
- package/dist/primitives/badge/index.js.map +1 -0
- package/dist/primitives/branch-indicator/index.js +4 -0
- package/dist/primitives/branch-indicator/index.js.map +1 -0
- package/dist/primitives/browser-controls/index.js +4 -0
- package/dist/primitives/browser-controls/index.js.map +1 -0
- package/dist/primitives/build-log-stream/index.js +5 -0
- package/dist/primitives/build-log-stream/index.js.map +1 -0
- package/dist/primitives/button/index.js +4 -0
- package/dist/primitives/button/index.js.map +1 -0
- package/dist/primitives/capability-indicator/index.js +4 -0
- package/dist/primitives/capability-indicator/index.js.map +1 -0
- package/dist/primitives/card/index.js +4 -0
- package/dist/primitives/card/index.js.map +1 -0
- package/dist/primitives/channel-card/index.js +4 -0
- package/dist/primitives/channel-card/index.js.map +1 -0
- package/dist/primitives/chat-thread/index.js +5 -0
- package/dist/primitives/chat-thread/index.js.map +1 -0
- package/dist/primitives/checkbox/index.js +4 -0
- package/dist/primitives/checkbox/index.js.map +1 -0
- package/dist/primitives/context-card/index.js +4 -0
- package/dist/primitives/context-card/index.js.map +1 -0
- package/dist/primitives/context-window-bar/index.js +4 -0
- package/dist/primitives/context-window-bar/index.js.map +1 -0
- package/dist/primitives/copy-button/index.js +4 -0
- package/dist/primitives/copy-button/index.js.map +1 -0
- package/dist/primitives/cost-meter/index.js +4 -0
- package/dist/primitives/cost-meter/index.js.map +1 -0
- package/dist/primitives/created-files-card/index.js +4 -0
- package/dist/primitives/created-files-card/index.js.map +1 -0
- package/dist/primitives/cron-job-card/index.js +4 -0
- package/dist/primitives/cron-job-card/index.js.map +1 -0
- package/dist/primitives/danger-zone/index.js +4 -0
- package/dist/primitives/danger-zone/index.js.map +1 -0
- package/dist/primitives/dialog/index.js +4 -0
- package/dist/primitives/dialog/index.js.map +1 -0
- package/dist/primitives/diff-viewer/index.js +4 -0
- package/dist/primitives/diff-viewer/index.js.map +1 -0
- package/dist/primitives/dropdown-menu/index.js +4 -0
- package/dist/primitives/dropdown-menu/index.js.map +1 -0
- package/dist/primitives/empty-state/index.js +4 -0
- package/dist/primitives/empty-state/index.js.map +1 -0
- package/dist/primitives/export-chat-dialog/index.js +4 -0
- package/dist/primitives/export-chat-dialog/index.js.map +1 -0
- package/dist/primitives/folder-context-card/index.js +4 -0
- package/dist/primitives/folder-context-card/index.js.map +1 -0
- package/dist/primitives/folder-selector/index.js +4 -0
- package/dist/primitives/folder-selector/index.js.map +1 -0
- package/dist/primitives/form-field/index.js +4 -0
- package/dist/primitives/form-field/index.js.map +1 -0
- package/dist/primitives/gateway-status-indicator/index.js +4 -0
- package/dist/primitives/gateway-status-indicator/index.js.map +1 -0
- package/dist/primitives/hook-config/index.js +4 -0
- package/dist/primitives/hook-config/index.js.map +1 -0
- package/dist/primitives/hook-event-log/index.js +4 -0
- package/dist/primitives/hook-event-log/index.js.map +1 -0
- package/dist/primitives/input/index.js +4 -0
- package/dist/primitives/input/index.js.map +1 -0
- package/dist/primitives/intent-selector/index.js +4 -0
- package/dist/primitives/intent-selector/index.js.map +1 -0
- package/dist/primitives/label/index.js +4 -0
- package/dist/primitives/label/index.js.map +1 -0
- package/dist/primitives/lane-board/index.js +4 -0
- package/dist/primitives/lane-board/index.js.map +1 -0
- package/dist/primitives/login-split/index.js +4 -0
- package/dist/primitives/login-split/index.js.map +1 -0
- package/dist/primitives/mcp-server-card/index.js +4 -0
- package/dist/primitives/mcp-server-card/index.js.map +1 -0
- package/dist/primitives/memory-editor/index.js +4 -0
- package/dist/primitives/memory-editor/index.js.map +1 -0
- package/dist/primitives/mention-menu/index.js +4 -0
- package/dist/primitives/mention-menu/index.js.map +1 -0
- package/dist/primitives/metrics-panel/index.js +4 -0
- package/dist/primitives/metrics-panel/index.js.map +1 -0
- package/dist/primitives/model-card/index.js +4 -0
- package/dist/primitives/model-card/index.js.map +1 -0
- package/dist/primitives/model-selector/index.js +4 -0
- package/dist/primitives/model-selector/index.js.map +1 -0
- package/dist/primitives/pagination/index.js +4 -0
- package/dist/primitives/pagination/index.js.map +1 -0
- package/dist/primitives/permission-matrix/index.js +4 -0
- package/dist/primitives/permission-matrix/index.js.map +1 -0
- package/dist/primitives/pin-input/index.js +4 -0
- package/dist/primitives/pin-input/index.js.map +1 -0
- package/dist/primitives/plan-badge/index.js +4 -0
- package/dist/primitives/plan-badge/index.js.map +1 -0
- package/dist/primitives/progress/index.js +4 -0
- package/dist/primitives/progress/index.js.map +1 -0
- package/dist/primitives/progress-checklist/index.js +4 -0
- package/dist/primitives/progress-checklist/index.js.map +1 -0
- package/dist/primitives/project-switcher/index.js +4 -0
- package/dist/primitives/project-switcher/index.js.map +1 -0
- package/dist/primitives/quick-action-chips/index.js +4 -0
- package/dist/primitives/quick-action-chips/index.js.map +1 -0
- package/dist/primitives/radio-group/index.js +4 -0
- package/dist/primitives/radio-group/index.js.map +1 -0
- package/dist/primitives/recent-folders-list/index.js +4 -0
- package/dist/primitives/recent-folders-list/index.js.map +1 -0
- package/dist/primitives/rule-card/index.js +4 -0
- package/dist/primitives/rule-card/index.js.map +1 -0
- package/dist/primitives/run-stats/index.js +4 -0
- package/dist/primitives/run-stats/index.js.map +1 -0
- package/dist/primitives/run-status-pill/index.js +4 -0
- package/dist/primitives/run-status-pill/index.js.map +1 -0
- package/dist/primitives/running-tasks-panel/index.js +4 -0
- package/dist/primitives/running-tasks-panel/index.js.map +1 -0
- package/dist/primitives/scroll-area/index.js +4 -0
- package/dist/primitives/scroll-area/index.js.map +1 -0
- package/dist/primitives/select/index.js +4 -0
- package/dist/primitives/select/index.js.map +1 -0
- package/dist/primitives/session-list-item/index.js +4 -0
- package/dist/primitives/session-list-item/index.js.map +1 -0
- package/dist/primitives/session-timeline/index.js +4 -0
- package/dist/primitives/session-timeline/index.js.map +1 -0
- package/dist/primitives/sheet/index.js +4 -0
- package/dist/primitives/sheet/index.js.map +1 -0
- package/dist/primitives/sidebar/index.js +4 -0
- package/dist/primitives/sidebar/index.js.map +1 -0
- package/dist/primitives/skeleton/index.js +5 -0
- package/dist/primitives/skeleton/index.js.map +1 -0
- package/dist/primitives/skill-card/index.js +4 -0
- package/dist/primitives/skill-card/index.js.map +1 -0
- package/dist/primitives/social-auth-row/index.js +4 -0
- package/dist/primitives/social-auth-row/index.js.map +1 -0
- package/dist/primitives/stat-tile/index.js +4 -0
- package/dist/primitives/stat-tile/index.js.map +1 -0
- package/dist/primitives/status-dot/index.js +4 -0
- package/dist/primitives/status-dot/index.js.map +1 -0
- package/dist/primitives/steps-rail/index.js +4 -0
- package/dist/primitives/steps-rail/index.js.map +1 -0
- package/dist/primitives/sub-agent-dispatch/index.js +4 -0
- package/dist/primitives/sub-agent-dispatch/index.js.map +1 -0
- package/dist/primitives/switch/index.js +4 -0
- package/dist/primitives/switch/index.js.map +1 -0
- package/dist/primitives/system-prompt-editor/index.js +4 -0
- package/dist/primitives/system-prompt-editor/index.js.map +1 -0
- package/dist/primitives/table/index.js +4 -0
- package/dist/primitives/table/index.js.map +1 -0
- package/dist/primitives/tabs/index.js +4 -0
- package/dist/primitives/tabs/index.js.map +1 -0
- package/dist/primitives/task-plan/index.js +4 -0
- package/dist/primitives/task-plan/index.js.map +1 -0
- package/dist/primitives/terminal-panel/index.js +5 -0
- package/dist/primitives/terminal-panel/index.js.map +1 -0
- package/dist/primitives/textarea/index.js +4 -0
- package/dist/primitives/textarea/index.js.map +1 -0
- package/dist/primitives/thinking-level-selector/index.js +4 -0
- package/dist/primitives/thinking-level-selector/index.js.map +1 -0
- package/dist/primitives/timestamp/index.js +4 -0
- package/dist/primitives/timestamp/index.js.map +1 -0
- package/dist/primitives/toast/index.js +4 -0
- package/dist/primitives/toast/index.js.map +1 -0
- package/dist/primitives/token-usage-chart/index.js +4 -0
- package/dist/primitives/token-usage-chart/index.js.map +1 -0
- package/dist/primitives/tool-call/index.js +4 -0
- package/dist/primitives/tool-call/index.js.map +1 -0
- package/dist/primitives/tool-call-card/index.js +4 -0
- package/dist/primitives/tool-call-card/index.js.map +1 -0
- package/dist/primitives/tool-result/index.js +4 -0
- package/dist/primitives/tool-result/index.js.map +1 -0
- package/dist/primitives/tools-list/index.js +4 -0
- package/dist/primitives/tools-list/index.js.map +1 -0
- package/dist/primitives/tooltip/index.js +4 -0
- package/dist/primitives/tooltip/index.js.map +1 -0
- package/dist/primitives/topnav/index.js +4 -0
- package/dist/primitives/topnav/index.js.map +1 -0
- package/dist/primitives/update-banner/index.js +4 -0
- package/dist/primitives/update-banner/index.js.map +1 -0
- package/dist/slide/index.d.ts +212 -0
- package/dist/slide/index.js +3 -0
- package/dist/slide/index.js.map +1 -0
- package/dist/slide/plugins/emoji/index.d.ts +29 -0
- package/dist/slide/plugins/emoji/index.js +157 -0
- package/dist/slide/plugins/emoji/index.js.map +1 -0
- package/dist/slide/plugins/math/index.d.ts +13 -0
- package/dist/slide/plugins/math/index.js +145 -0
- package/dist/slide/plugins/math/index.js.map +1 -0
- package/dist/slide/plugins/mermaid/index.d.ts +55 -0
- package/dist/slide/plugins/mermaid/index.js +218 -0
- package/dist/slide/plugins/mermaid/index.js.map +1 -0
- package/dist/slide/plugins/shiki/index.d.ts +18 -0
- package/dist/slide/plugins/shiki/index.js +87 -0
- package/dist/slide/plugins/shiki/index.js.map +1 -0
- package/dist/slide/themes/default.css +256 -0
- package/dist/slide/themes/layouts.css +143 -0
- package/dist/slide/themes/violet-forge.css +256 -0
- package/dist/slide-deck/index.css +52 -0
- package/dist/slide-deck/index.css.map +1 -0
- package/dist/slide-deck/index.d.ts +377 -0
- package/dist/slide-deck/index.js +1111 -0
- package/dist/slide-deck/index.js.map +1 -0
- package/dist/styles-v3-legacy.css +88 -0
- package/dist/styles.css +137 -0
- package/dist/tokens-v4.css +187 -0
- package/dist/tokens.css +230 -0
- package/dist/vite-plugin.d.ts +29 -0
- package/dist/vite-plugin.js +76 -0
- package/dist/vite-plugin.js.map +1 -0
- package/dist/whiteboard/index.d.ts +258 -0
- package/dist/whiteboard/index.js +738 -0
- package/dist/whiteboard/index.js.map +1 -0
- package/llms.txt +273 -0
- package/package.json +800 -0
- package/registry/index.json +856 -0
- package/registry/r/account-menu.json +24 -0
- package/registry/r/action-bar.json +22 -0
- package/registry/r/agent-composer.json +22 -0
- package/registry/r/agent-editor.json +27 -0
- package/registry/r/agent-error-card.json +22 -0
- package/registry/r/agent-event.json +24 -0
- package/registry/r/agent-handoff.json +22 -0
- package/registry/r/agent-profile.json +23 -0
- package/registry/r/agent-starting-state.json +22 -0
- package/registry/r/agent-stream.json +27 -0
- package/registry/r/agent-streaming.json +22 -0
- package/registry/r/agent-timeline.json +22 -0
- package/registry/r/agent-types.json +15 -0
- package/registry/r/alert.json +22 -0
- package/registry/r/approval-card.json +25 -0
- package/registry/r/artifact-preview.json +22 -0
- package/registry/r/attachment-chip.json +24 -0
- package/registry/r/audit-log-entry.json +23 -0
- package/registry/r/auto-compact-notice.json +22 -0
- package/registry/r/avatar.json +23 -0
- package/registry/r/badge.json +22 -0
- package/registry/r/browser-controls.json +22 -0
- package/registry/r/build-log-stream.json +19 -0
- package/registry/r/button.json +23 -0
- package/registry/r/capability-indicator.json +23 -0
- package/registry/r/card.json +22 -0
- package/registry/r/chat-composer.json +23 -0
- package/registry/r/chat-message.json +129 -0
- package/registry/r/chat-thread.json +20 -0
- package/registry/r/chat-types.json +15 -0
- package/registry/r/checkbox.json +24 -0
- package/registry/r/cn.json +19 -0
- package/registry/r/code-block.json +21 -0
- package/registry/r/command-palette.json +25 -0
- package/registry/r/confirm-dialog.json +25 -0
- package/registry/r/context-card.json +23 -0
- package/registry/r/context-window-bar.json +20 -0
- package/registry/r/copy-button.json +22 -0
- package/registry/r/cost-meter.json +22 -0
- package/registry/r/created-files-card.json +23 -0
- package/registry/r/cron-job-card.json +22 -0
- package/registry/r/cron-jobs-list.json +23 -0
- package/registry/r/danger-zone.json +20 -0
- package/registry/r/data-table.json +27 -0
- package/registry/r/deployment-row.json +23 -0
- package/registry/r/dialog.json +23 -0
- package/registry/r/diff-viewer.json +20 -0
- package/registry/r/domain-config.json +25 -0
- package/registry/r/dropdown-menu.json +23 -0
- package/registry/r/empty-state.json +20 -0
- package/registry/r/env-var-editor.json +25 -0
- package/registry/r/folder-context-card.json +23 -0
- package/registry/r/folder-selector.json +22 -0
- package/registry/r/form-field.json +23 -0
- package/registry/r/hook-config.json +22 -0
- package/registry/r/hook-event-log.json +22 -0
- package/registry/r/input.json +22 -0
- package/registry/r/intent-selector.json +24 -0
- package/registry/r/label.json +22 -0
- package/registry/r/lane-board.json +20 -0
- package/registry/r/live-region-context.json +16 -0
- package/registry/r/login-split.json +20 -0
- package/registry/r/mcp-server-card.json +22 -0
- package/registry/r/mcp-server-list.json +23 -0
- package/registry/r/memory-editor.json +23 -0
- package/registry/r/mention-menu.json +23 -0
- package/registry/r/metrics-panel.json +22 -0
- package/registry/r/mode-types.json +15 -0
- package/registry/r/model-card.json +23 -0
- package/registry/r/model-selector.json +23 -0
- package/registry/r/page-shell.json +25 -0
- package/registry/r/pagination.json +22 -0
- package/registry/r/permission-matrix.json +22 -0
- package/registry/r/permission-modal.json +24 -0
- package/registry/r/permission-types.json +15 -0
- package/registry/r/pin-input.json +20 -0
- package/registry/r/plan-badge.json +20 -0
- package/registry/r/preview-env-card.json +25 -0
- package/registry/r/preview-panel.json +21 -0
- package/registry/r/progress-checklist.json +23 -0
- package/registry/r/progress.json +20 -0
- package/registry/r/project-card.json +25 -0
- package/registry/r/project-switcher.json +22 -0
- package/registry/r/quick-action-chips.json +21 -0
- package/registry/r/radio-group.json +23 -0
- package/registry/r/recent-folders-list.json +22 -0
- package/registry/r/rollback-ui.json +24 -0
- package/registry/r/rule-card.json +23 -0
- package/registry/r/rule-editor.json +28 -0
- package/registry/r/rule-types.json +18 -0
- package/registry/r/run-stats.json +22 -0
- package/registry/r/running-tasks-panel.json +22 -0
- package/registry/r/safe-href.json +16 -0
- package/registry/r/scroll-area.json +22 -0
- package/registry/r/select.json +24 -0
- package/registry/r/session-list-item.json +20 -0
- package/registry/r/session-timeline.json +22 -0
- package/registry/r/sheet.json +24 -0
- package/registry/r/sidebar.json +19 -0
- package/registry/r/skeleton.json +19 -0
- package/registry/r/skill-card.json +24 -0
- package/registry/r/skill-editor.json +28 -0
- package/registry/r/skills-list.json +23 -0
- package/registry/r/slide-deck.json +130 -0
- package/registry/r/slide-plugin-emoji.json +28 -0
- package/registry/r/slide-plugin-math.json +24 -0
- package/registry/r/slide-plugin-mermaid.json +23 -0
- package/registry/r/slide-plugin-shiki.json +23 -0
- package/registry/r/slide.json +123 -0
- package/registry/r/social-auth-row.json +21 -0
- package/registry/r/stat-tile.json +22 -0
- package/registry/r/status-dot.json +20 -0
- package/registry/r/steps-rail.json +20 -0
- package/registry/r/sub-agent-dispatch.json +22 -0
- package/registry/r/switch.json +23 -0
- package/registry/r/system-prompt-editor.json +22 -0
- package/registry/r/table.json +22 -0
- package/registry/r/tabs.json +22 -0
- package/registry/r/tailwind-preset.json +19 -0
- package/registry/r/task-header.json +24 -0
- package/registry/r/task-plan.json +22 -0
- package/registry/r/task-types.json +15 -0
- package/registry/r/terminal-panel.json +22 -0
- package/registry/r/textarea.json +22 -0
- package/registry/r/theme-provider.json +59 -0
- package/registry/r/theme-script.json +18 -0
- package/registry/r/theo-ui-provider.json +20 -0
- package/registry/r/timestamp.json +20 -0
- package/registry/r/toast.json +30 -0
- package/registry/r/token-usage-chart.json +20 -0
- package/registry/r/tokens.json +21 -0
- package/registry/r/tool-call-card.json +23 -0
- package/registry/r/tool-call.json +22 -0
- package/registry/r/tool-result.json +20 -0
- package/registry/r/tools-list.json +23 -0
- package/registry/r/tooltip.json +22 -0
- package/registry/r/topnav.json +22 -0
- package/registry/r/types.json +15 -0
- package/registry/r/usage-meter.json +21 -0
- package/registry/r/whiteboard.json +101 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "slide-deck",
|
|
4
|
+
"type": "registry:block",
|
|
5
|
+
"title": "SlideDeck",
|
|
6
|
+
"description": "Composite engine that orchestrates N <Slide> primitives. Keyboard / touch / hash routing, thumbnails, presenter view, fullscreen, CSS transitions, Marpit-style fragments, PDF export. Subpath-isolated bundle in @theokit/ui.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"zod"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/slide.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/composites/slide-deck/slide-deck.tsx",
|
|
17
|
+
"type": "registry:block",
|
|
18
|
+
"target": "components/blocks/slide-deck/slide-deck.tsx",
|
|
19
|
+
"content": "import {\n type FC,\n type ReactNode,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n/**\n * `<SlideDeck>` — composite engine orchestrating N `<Slide>` primitives.\n *\n * Subpath-isolated at `@theokit/ui/slide-deck` (ADR D1). Imports `<Slide>` via\n * the public package path so consumer's installed peer-deps are reused.\n *\n * See RFC 0003 and the plan in\n * `.claude/knowledge-base/plans/slide-deck-composite-plan.md`.\n *\n * Two render modes:\n * - DEFAULT layout: no children → renders canonical chrome (Slides + Controls\n * + ProgressBar + SlideNumber + PresenterView + buttons).\n * - HEADLESS: children provided → consumer composes own layout from\n * `<SlideDeck.X>` sub-components and a shared DeckContext.\n *\n * SSR-safe: parses markdown only inside `useEffect`. Hash routing uses lazy\n * `initFromHash` (D17) to avoid hydration mismatch. Reducer clamps\n * `currentIndex` whenever `slides.length` changes (EC-4 reconciliation).\n */\nimport { Slide, type SlidePlugin } from \"@/components/ui/slide/index\";\nimport { DeckContext, type DeckContextValue, useDeckContext } from \"@/components/blocks/slide-deck/context\";\nimport { Controls } from \"@/components/blocks/slide-deck/controls\";\nimport { countFragmentsInMarkdown } from \"@/components/blocks/slide-deck/fragments\";\nimport { PresenterView } from \"@/components/blocks/slide-deck/presenter-view\";\nimport { printDeck } from \"@/components/blocks/slide-deck/print-styles\";\nimport { ProgressBar } from \"@/components/blocks/slide-deck/progress-bar\";\nimport type { SlideDeckInput, SlideDeckSlide, SlideDeckTransition } from \"@/components/blocks/slide-deck/schema\";\nimport { SlideNumber } from \"@/components/blocks/slide-deck/slide-number\";\nimport { splitDeck } from \"@/components/blocks/slide-deck/split-deck\";\nimport { Thumbnails } from \"@/components/blocks/slide-deck/thumbnails\";\nimport { readInitialHash, useDeckHashRouting } from \"@/components/blocks/slide-deck/use-deck-hash-routing\";\nimport { useDeckKeyboard } from \"@/components/blocks/slide-deck/use-deck-keyboard\";\nimport { useDeckState } from \"@/components/blocks/slide-deck/use-deck-state\";\nimport { useDeckSwipe } from \"@/components/blocks/slide-deck/use-deck-swipe\";\nimport { useFullscreen } from \"@/components/blocks/slide-deck/use-fullscreen\";\n\nimport \"./transitions.css\";\n\nexport interface SlideDeckProps {\n /** Markdown string (auto-split) or pre-parsed slide array. */\n slides: SlideDeckInput;\n /** Initial slide index (0-based). Default 0. */\n initialIndex?: number;\n /** Transition preset. Default \"fade\". */\n transition?: SlideDeckTransition;\n /** Enable keyboard navigation. Default true. */\n enableKeyboard?: boolean;\n /** Enable touch/pointer swipe navigation. Default true. */\n enableTouch?: boolean;\n /** Enable hash routing (#/N). Default true. */\n enableHashRouting?: boolean;\n /** Optional unique deck id (auto-generated UUID if absent). */\n deckId?: string;\n /** Called after each navigation. */\n onIndexChange?: (index: number, slide: SlideDeckSlide | undefined) => void;\n /** Headless mode: render custom chrome using `<SlideDeck.X>` sub-components. */\n children?: ReactNode;\n /** Outer className for the deck root. */\n className?: string;\n /** Accessible label for the deck region. Defaults to \"Slide deck\". */\n \"aria-label\"?: string;\n /**\n * Rich-content plugins relayed to every inner `<Slide>` (D15 / RFC 0004).\n * Pass MEMOIZED arrays to avoid re-parses on every render.\n */\n plugins?: SlidePlugin[];\n}\n\nfunction generateDeckId(): string {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n return crypto.randomUUID();\n }\n return `deck-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nconst SlideDeckBase: FC<SlideDeckProps> = ({\n slides: slidesInput,\n initialIndex = 0,\n transition = \"fade\",\n enableKeyboard = true,\n enableTouch = true,\n enableHashRouting = true,\n deckId: deckIdProp,\n onIndexChange,\n children,\n className,\n \"aria-label\": ariaLabel = \"Slide deck\",\n plugins,\n}) => {\n const generatedId = useId();\n const deckId = deckIdProp ?? generatedId ?? generateDeckId();\n const rootRef = useRef<HTMLDivElement>(null);\n\n const [parsedSlides, setParsedSlides] = useState<SlideDeckSlide[]>(() => {\n return Array.isArray(slidesInput) ? slidesInput : [];\n });\n\n // Async parse when string markdown is passed. Re-run when prop changes.\n useEffect(() => {\n if (Array.isArray(slidesInput)) {\n setParsedSlides(slidesInput);\n return;\n }\n let cancelled = false;\n splitDeck(slidesInput).then(\n (result) => {\n if (!cancelled) setParsedSlides(result);\n },\n () => {\n if (!cancelled) setParsedSlides([]);\n },\n );\n return () => {\n cancelled = true;\n };\n }, [slidesInput]);\n\n const [state, dispatch] = useDeckState({\n initialIndex,\n totalSlides: parsedSlides.length,\n initFromHash: enableHashRouting ? readInitialHash : undefined,\n });\n\n // EC-4: reconcile when slides.length changes.\n const previousLengthRef = useRef(0);\n useEffect(() => {\n const length = parsedSlides.length;\n dispatch({ type: \"UPDATE_TOTAL_SLIDES\", total: length });\n // First non-empty parse → honour `initialIndex` (or hash) which was clamped\n // to 0 during initial mount when totalSlides was still 0.\n if (previousLengthRef.current === 0 && length > 0) {\n let target = initialIndex;\n if (enableHashRouting) {\n const hashIdx = readInitialHash();\n if (typeof hashIdx === \"number\") target = hashIdx;\n }\n if (target > 0) {\n dispatch({ type: \"JUMP_TO\", index: target });\n }\n }\n previousLengthRef.current = length;\n }, [parsedSlides.length, dispatch, initialIndex, enableHashRouting]);\n\n // Update totalFragmentsInCurrent when slide content changes.\n useEffect(() => {\n const current = parsedSlides[state.currentIndex];\n const count = current ? countFragmentsInMarkdown(current.markdown) : 0;\n dispatch({ type: \"UPDATE_TOTAL_FRAGMENTS\", total: count });\n }, [parsedSlides, state.currentIndex, dispatch]);\n\n const fullscreen = useFullscreen(rootRef);\n\n // Sync state.fullscreen with browser API. When user presses Esc on native UI.\n useEffect(() => {\n dispatch({ type: \"SET_FULLSCREEN\", value: fullscreen.isFullscreen });\n }, [fullscreen.isFullscreen, dispatch]);\n\n // EC-3 / D16: transition timeout fallback so rapid nav doesn't leave state stuck.\n useEffect(() => {\n if (state.transitionDirection === \"none\") return;\n const t = setTimeout(() => dispatch({ type: \"TRANSITION_END\" }), 300);\n return () => clearTimeout(t);\n }, [state.transitionDirection, dispatch]);\n\n const onPrint = useCallback(() => {\n printDeck();\n }, []);\n\n useDeckKeyboard(dispatch, {\n enabled: enableKeyboard,\n totalSlides: parsedSlides.length,\n onToggleFullscreen: fullscreen.toggle,\n onPrint,\n });\n\n useDeckSwipe(rootRef, dispatch, { enabled: enableTouch });\n\n useDeckHashRouting(dispatch, {\n enabled: enableHashRouting,\n totalSlides: parsedSlides.length,\n currentIndex: state.currentIndex,\n });\n\n // Fire onIndexChange callback when currentIndex changes.\n const lastIndexRef = useRef(state.currentIndex);\n useEffect(() => {\n if (lastIndexRef.current !== state.currentIndex) {\n lastIndexRef.current = state.currentIndex;\n onIndexChange?.(state.currentIndex, parsedSlides[state.currentIndex]);\n }\n }, [state.currentIndex, parsedSlides, onIndexChange]);\n\n const contextValue: DeckContextValue = useMemo(\n () => ({\n state,\n dispatch,\n slides: parsedSlides,\n transition,\n deckId,\n plugins,\n toggleFullscreen: fullscreen.toggle,\n print: onPrint,\n }),\n [state, dispatch, parsedSlides, transition, deckId, plugins, fullscreen.toggle, onPrint],\n );\n\n return (\n <DeckContext.Provider value={contextValue}>\n <div\n ref={rootRef}\n aria-roledescription=\"slide deck\"\n aria-label={ariaLabel}\n data-theo-slide-deck\n data-theo-slide-deck-fullscreen={state.fullscreen ? \"true\" : undefined}\n className={[\"theo-slide-deck\", className].filter(Boolean).join(\" \")}\n style={{ position: \"relative\", width: \"100%\", height: \"100%\" }}\n >\n {/* Live region for screen readers announcing slide changes. */}\n {/* biome-ignore lint/a11y/useSemanticElements: ARIA live region needs explicit role for screen reader announcement; no native HTML equivalent for status updates from non-form, non-output content. */}\n <div\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n style={{\n position: \"absolute\",\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: \"hidden\",\n clip: \"rect(0,0,0,0)\",\n whiteSpace: \"nowrap\",\n border: 0,\n }}\n >\n {parsedSlides.length > 0\n ? `Slide ${state.currentIndex + 1} of ${parsedSlides.length}`\n : \"Empty deck\"}\n </div>\n {children ?? <DefaultDeckLayout />}\n {/* Hidden print container — visible only during @media print. */}\n <PrintContainer slides={parsedSlides} plugins={plugins} />\n </div>\n </DeckContext.Provider>\n );\n};\n\nconst SlidesView: FC<{ className?: string }> = ({ className }) => {\n const { state, slides, transition, plugins } = useDeckContext();\n const current = slides[state.currentIndex];\n return (\n <div\n className={[\"theo-slide-deck-slide-frame\", className].filter(Boolean).join(\" \")}\n data-theo-slide-deck-transition={transition}\n data-theo-slide-deck-direction={state.transitionDirection}\n style={{\n position: \"relative\",\n width: \"100%\",\n height: \"100%\",\n minHeight: 0,\n overflow: \"hidden\",\n }}\n >\n {current ? (\n <div\n key={state.currentIndex}\n data-theo-slide-deck-slide-state=\"incoming\"\n style={{ position: \"absolute\", inset: 0 }}\n >\n <Slide\n markdown={current.markdown}\n plugins={plugins}\n aria-label={`Slide ${state.currentIndex + 1}`}\n />\n </div>\n ) : (\n <div\n data-theo-slide-deck-empty\n style={{\n display: \"grid\",\n placeItems: \"center\",\n height: \"100%\",\n opacity: 0.6,\n fontSize: 14,\n }}\n >\n Empty deck\n </div>\n )}\n </div>\n );\n};\n\nconst PresenterButton: FC<{ className?: string }> = ({ className }) => {\n const { state, dispatch } = useDeckContext();\n return (\n <button\n type=\"button\"\n onClick={() => dispatch({ type: \"TOGGLE_PRESENTER\" })}\n aria-pressed={state.presenterMode}\n aria-label={state.presenterMode ? \"Close presenter view\" : \"Open presenter view\"}\n className={[\"theo-slide-deck-presenter-button\", className].filter(Boolean).join(\" \")}\n >\n {state.presenterMode ? \"Close presenter\" : \"Presenter\"}\n </button>\n );\n};\n\nconst FullscreenButton: FC<{ className?: string }> = ({ className }) => {\n const { state, toggleFullscreen } = useDeckContext();\n return (\n <button\n type=\"button\"\n onClick={() => void toggleFullscreen()}\n aria-pressed={state.fullscreen}\n aria-label={state.fullscreen ? \"Exit fullscreen\" : \"Enter fullscreen\"}\n className={[\"theo-slide-deck-fullscreen-button\", className].filter(Boolean).join(\" \")}\n >\n {state.fullscreen ? \"Exit fullscreen\" : \"Fullscreen\"}\n </button>\n );\n};\n\nconst PrintButton: FC<{ className?: string }> = ({ className }) => {\n const { print } = useDeckContext();\n return (\n <button\n type=\"button\"\n onClick={() => print()}\n aria-label=\"Print or save deck as PDF\"\n className={[\"theo-slide-deck-print-button\", className].filter(Boolean).join(\" \")}\n >\n Print\n </button>\n );\n};\n\nconst PrintContainer: FC<{ slides: SlideDeckSlide[]; plugins?: SlidePlugin[] }> = ({\n slides,\n plugins,\n}) => {\n return (\n <div\n className=\"theo-slide-deck-print-container\"\n aria-hidden=\"true\"\n style={{\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n visibility: \"hidden\",\n }}\n >\n {slides.map((slide, index) => (\n <div\n key={`print-${slide.id ?? index}`}\n className=\"theo-slide-deck-print-slide\"\n style={{ width: 1280, height: 720 }}\n >\n <Slide\n markdown={slide.markdown}\n plugins={plugins}\n aria-label={`Print slide ${index + 1}`}\n />\n </div>\n ))}\n </div>\n );\n};\n\nconst DefaultDeckLayout: FC = () => {\n return (\n <div\n className=\"theo-slide-deck-default-layout\"\n style={{\n display: \"grid\",\n gridTemplateColumns: \"1fr\",\n gridTemplateRows: \"1fr auto\",\n gap: 8,\n height: \"100%\",\n }}\n >\n <SlidesView />\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n gap: 8,\n padding: \"4px 8px\",\n flexWrap: \"wrap\",\n }}\n >\n <Controls />\n <ProgressBar />\n <div style={{ display: \"flex\", gap: 4 }}>\n <PresenterButton />\n <FullscreenButton />\n <PrintButton />\n </div>\n </div>\n <SlideNumber />\n <PresenterView />\n </div>\n );\n};\n\ntype SlideDeckComponent = FC<SlideDeckProps> & {\n Slides: FC<{ className?: string }>;\n Controls: typeof Controls;\n ProgressBar: typeof ProgressBar;\n SlideNumber: typeof SlideNumber;\n Thumbnails: typeof Thumbnails;\n PresenterView: typeof PresenterView;\n PresenterButton: FC<{ className?: string }>;\n FullscreenButton: FC<{ className?: string }>;\n PrintButton: FC<{ className?: string }>;\n};\n\nconst SlideDeck = SlideDeckBase as SlideDeckComponent;\nSlideDeck.Slides = SlidesView;\nSlideDeck.Controls = Controls;\nSlideDeck.ProgressBar = ProgressBar;\nSlideDeck.SlideNumber = SlideNumber;\nSlideDeck.Thumbnails = Thumbnails;\nSlideDeck.PresenterView = PresenterView;\nSlideDeck.PresenterButton = PresenterButton;\nSlideDeck.FullscreenButton = FullscreenButton;\nSlideDeck.PrintButton = PrintButton;\n\nexport { SlideDeck };\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "components/composites/slide-deck/context.tsx",
|
|
23
|
+
"type": "registry:block",
|
|
24
|
+
"target": "components/blocks/slide-deck/context.tsx",
|
|
25
|
+
"content": "/**\n * DeckContext — internal Context shared between `<SlideDeck>` and its\n * dot-namespace sub-components (`<SlideDeck.Controls>`, etc.). ADR D14.\n */\nimport { type Dispatch, createContext, useContext } from \"react\";\nimport type { SlidePlugin } from \"@/components/ui/slide/index\";\nimport type { SlideDeckSlide, SlideDeckTransition } from \"@/components/blocks/slide-deck/schema\";\nimport type { DeckAction, DeckState } from \"@/components/blocks/slide-deck/use-deck-state\";\n\nexport interface DeckContextValue {\n state: DeckState;\n dispatch: Dispatch<DeckAction>;\n slides: SlideDeckSlide[];\n transition: SlideDeckTransition;\n deckId: string;\n /** Rich-content plugins relayed to every inner `<Slide>` (D15). */\n plugins?: SlidePlugin[];\n /** Toggle browser fullscreen on the deck root. Safe to call when unsupported. */\n toggleFullscreen: () => void | Promise<void>;\n /** Trigger native print dialog with deck-specific @page CSS. */\n print: () => void;\n}\n\nexport const DeckContext = createContext<DeckContextValue | null>(null);\n\nexport function useDeckContext(): DeckContextValue {\n const ctx = useContext(DeckContext);\n if (!ctx) {\n throw new Error(\n \"SlideDeck sub-components must be used inside <SlideDeck>. \" +\n \"Wrap them in <SlideDeck slides={...}>.\",\n );\n }\n return ctx;\n}\n"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "components/composites/slide-deck/controls.tsx",
|
|
29
|
+
"type": "registry:block",
|
|
30
|
+
"target": "components/blocks/slide-deck/controls.tsx",
|
|
31
|
+
"content": "/**\n * `<SlideDeck.Controls>` — prev/next buttons + slide indicator (\"3 / 12\").\n */\nimport type { FC } from \"react\";\nimport { useDeckContext } from \"@/components/blocks/slide-deck/context\";\n\nexport interface ControlsProps {\n className?: string;\n}\n\nexport const Controls: FC<ControlsProps> = ({ className }) => {\n const { state, dispatch } = useDeckContext();\n const atStart = state.currentIndex <= 0;\n const atEnd = state.currentIndex >= state.totalSlides - 1;\n return (\n <div\n className={[\"theo-slide-deck-controls\", className].filter(Boolean).join(\" \")}\n data-theo-slide-deck-controls\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n }}\n >\n <button\n type=\"button\"\n aria-label=\"Previous slide\"\n disabled={atStart}\n onClick={() => dispatch({ type: \"PREV_SLIDE\" })}\n className=\"theo-slide-deck-controls-prev\"\n >\n ←\n </button>\n <span\n aria-live=\"polite\"\n className=\"theo-slide-deck-controls-indicator\"\n style={{ fontVariantNumeric: \"tabular-nums\", minWidth: 64, textAlign: \"center\" }}\n >\n {state.totalSlides === 0 ? \"0 / 0\" : `${state.currentIndex + 1} / ${state.totalSlides}`}\n </span>\n <button\n type=\"button\"\n aria-label=\"Next slide\"\n disabled={atEnd}\n onClick={() => dispatch({ type: \"NEXT_SLIDE\" })}\n className=\"theo-slide-deck-controls-next\"\n >\n →\n </button>\n </div>\n );\n};\n"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "components/composites/slide-deck/fragments.ts",
|
|
35
|
+
"type": "registry:block",
|
|
36
|
+
"target": "components/blocks/slide-deck/fragments.ts",
|
|
37
|
+
"content": "/**\n * Progressive fragments detector (ADR D12).\n *\n * Marpit convention: lists with `*` (asterisco) marker — instead of `-` or `+`\n * — become fragment-revealed lists. Each item is a step.\n *\n * Implementation: regex pre-pass on raw markdown counts top-of-line `* ` markers.\n * Decision (EC-9): mixed `*` and `-` in the same list is rare; we count ONLY\n * `* ` items as fragments. Plain `- item` lists remain non-fragmented.\n *\n * v0.4: counts only. CSS attribute application happens at render time inside\n * `<SlideDeck.Slides>`, post-parse, by walking the rendered DOM.\n */\n\nconst FRAGMENT_MARKER_RE = /^\\s*\\*\\s+\\S/gm;\n\n/**\n * Count fragment markers in raw markdown.\n *\n * Detects only `*` at start of a line followed by space + non-whitespace.\n * Avoids matching `**bold**`, `*italic*`, or `_ * _` patterns.\n */\nexport function countFragmentsInMarkdown(markdown: string): number {\n if (!markdown) return 0;\n // Strip fenced code blocks first to avoid counting * inside code.\n const stripped = markdown.replace(/```[\\s\\S]*?```/g, \"\");\n const matches = stripped.match(FRAGMENT_MARKER_RE);\n return matches ? matches.length : 0;\n}\n"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"path": "components/composites/slide-deck/notes.ts",
|
|
41
|
+
"type": "registry:block",
|
|
42
|
+
"target": "components/blocks/slide-deck/notes.ts",
|
|
43
|
+
"content": "/**\n * Speaker notes extractor (ADR D11).\n *\n * Sintaxe: HTML comments `<!-- notes: ... -->` ou `<!-- note: ... -->`.\n * Aceita ambos singular e plural. Texto interno é plain (sem markdown nesting\n * em v0.4 — pode vir em v0.5).\n *\n * Returns the body with comments removed and the aggregated notes string.\n */\n\nconst NOTES_RE = /<!--\\s*notes?:\\s*([\\s\\S]*?)\\s*-->/gi;\n\nexport interface ExtractNotesResult {\n body: string;\n notes: string | undefined;\n}\n\nexport function extractNotes(md: string): ExtractNotesResult {\n const matches = [...md.matchAll(NOTES_RE)];\n if (matches.length === 0) {\n return { body: md, notes: undefined };\n }\n const notes = matches\n .map((m) => (m[1] ?? \"\").trim())\n .filter((s) => s.length > 0)\n .join(\"\\n\\n\");\n const body = md.replace(NOTES_RE, \"\").trim();\n return { body, notes: notes.length > 0 ? notes : undefined };\n}\n"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"path": "components/composites/slide-deck/presenter-view.tsx",
|
|
47
|
+
"type": "registry:block",
|
|
48
|
+
"target": "components/blocks/slide-deck/presenter-view.tsx",
|
|
49
|
+
"content": "/**\n * `<SlideDeck.PresenterView>` — inline split-screen panel.\n *\n * Pragmatic v0.4 scope: renders an in-page panel (current slide + next slide +\n * speaker notes + timer) when `state.presenterMode === true`. The separate\n * window via `window.open` + `BroadcastChannel` is deferred to v0.5 (consumer\n * demand will trigger the upgrade — D6 of the plan is reduced to inline panel).\n *\n * Toggling presenter mode is dispatched via hotkey (n/N/p/P, see useDeckKeyboard).\n */\nimport { type FC, useEffect, useRef, useState } from \"react\";\nimport { Slide } from \"@/components/ui/slide/index\";\nimport { useDeckContext } from \"@/components/blocks/slide-deck/context\";\n\nexport interface PresenterViewProps {\n className?: string;\n}\n\nfunction formatElapsed(ms: number): string {\n const s = Math.floor(ms / 1000);\n const mm = Math.floor(s / 60)\n .toString()\n .padStart(2, \"0\");\n const ss = (s % 60).toString().padStart(2, \"0\");\n return `${mm}:${ss}`;\n}\n\nexport const PresenterView: FC<PresenterViewProps> = ({ className }) => {\n const { state, slides } = useDeckContext();\n const startedAt = useRef<number | null>(null);\n const [elapsed, setElapsed] = useState(0);\n\n // Initialize the timer when presenter mode first opens; reset when closed.\n useEffect(() => {\n if (state.presenterMode) {\n if (startedAt.current === null) {\n startedAt.current = Date.now();\n }\n const interval = window.setInterval(() => {\n if (startedAt.current !== null) {\n setElapsed(Date.now() - startedAt.current);\n }\n }, 1000);\n return () => window.clearInterval(interval);\n }\n startedAt.current = null;\n setElapsed(0);\n return undefined;\n }, [state.presenterMode]);\n\n if (!state.presenterMode) return null;\n\n const current = slides[state.currentIndex];\n const next = slides[state.currentIndex + 1];\n const notes = current?.notes;\n\n return (\n <aside\n className={[\"theo-slide-deck-presenter\", className].filter(Boolean).join(\" \")}\n data-theo-slide-deck-presenter\n aria-label=\"Presenter view\"\n style={{\n display: \"grid\",\n gridTemplateColumns: \"1fr 1fr\",\n gridTemplateRows: \"auto 1fr\",\n gap: 16,\n padding: 16,\n background: \"color-mix(in srgb, currentColor 6%, transparent)\",\n borderRadius: 8,\n }}\n >\n <header\n style={{\n gridColumn: \"1 / -1\",\n display: \"flex\",\n justifyContent: \"space-between\",\n alignItems: \"center\",\n }}\n >\n <h3 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>Presenter view</h3>\n <span\n aria-label=\"Elapsed time\"\n style={{ fontVariantNumeric: \"tabular-nums\", fontSize: 14 }}\n >\n {formatElapsed(elapsed)}\n </span>\n </header>\n <section aria-label=\"Current slide preview\">\n <h4 style={{ margin: \"0 0 8px\", fontSize: 12, opacity: 0.7 }}>Current</h4>\n <div\n style={{\n aspectRatio: \"16 / 9\",\n overflow: \"hidden\",\n border: \"1px solid rgba(127,127,127,0.3)\",\n borderRadius: 6,\n }}\n >\n {current ? <Slide markdown={current.markdown} aria-label=\"Current slide\" /> : null}\n </div>\n </section>\n <section aria-label=\"Next slide preview\">\n <h4 style={{ margin: \"0 0 8px\", fontSize: 12, opacity: 0.7 }}>Next</h4>\n <div\n style={{\n aspectRatio: \"16 / 9\",\n overflow: \"hidden\",\n border: \"1px solid rgba(127,127,127,0.3)\",\n borderRadius: 6,\n opacity: next ? 1 : 0.4,\n }}\n >\n {next ? (\n <Slide markdown={next.markdown} aria-label=\"Next slide\" />\n ) : (\n <div style={{ padding: 16, fontSize: 14, opacity: 0.6 }}>End of deck</div>\n )}\n </div>\n </section>\n {notes ? (\n <section\n aria-label=\"Speaker notes\"\n style={{\n gridColumn: \"1 / -1\",\n background: \"color-mix(in srgb, currentColor 8%, transparent)\",\n padding: 12,\n borderRadius: 6,\n fontSize: 14,\n whiteSpace: \"pre-wrap\",\n }}\n >\n <strong>Notes: </strong>\n {notes}\n </section>\n ) : null}\n </aside>\n );\n};\n"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"path": "components/composites/slide-deck/print-styles.ts",
|
|
53
|
+
"type": "registry:block",
|
|
54
|
+
"target": "components/blocks/slide-deck/print-styles.ts",
|
|
55
|
+
"content": "/**\n * Print CSS injection (ADR D7).\n *\n * Injects a `<style id=\"theo-slide-deck-print\">` element into the document\n * head with `@page` + `@media print` rules that render `.theo-slide-deck-print-container`\n * with one slide per page. Calls `window.print()`. Removes the style on the\n * `afterprint` event (cleanup runs regardless of print cancel/complete).\n */\n\nconst STYLE_ID = \"theo-slide-deck-print-styles\";\nconst PRINT_CSS = `\n@media print {\n @page {\n size: 1280px 720px;\n margin: 0;\n }\n body > * {\n visibility: hidden;\n }\n .theo-slide-deck-print-container,\n .theo-slide-deck-print-container * {\n visibility: visible;\n }\n .theo-slide-deck-print-container {\n position: absolute;\n inset: 0;\n }\n .theo-slide-deck-print-slide {\n page-break-after: always;\n break-after: page;\n width: 1280px;\n height: 720px;\n overflow: hidden;\n }\n .theo-slide-deck-print-slide:last-child {\n page-break-after: auto;\n break-after: auto;\n }\n}\n`;\n\nexport function injectPrintStyles(): HTMLStyleElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"injectPrintStyles requires a document (browser env)\");\n }\n let style = document.getElementById(STYLE_ID) as HTMLStyleElement | null;\n if (!style) {\n style = document.createElement(\"style\");\n style.id = STYLE_ID;\n style.textContent = PRINT_CSS;\n document.head.appendChild(style);\n }\n return style;\n}\n\nexport function removePrintStyles(): void {\n if (typeof document === \"undefined\") return;\n const style = document.getElementById(STYLE_ID);\n if (style) style.remove();\n}\n\nexport interface PrintDeckOptions {\n /** Optional callback fired after print dialog closes (success or cancel). */\n onAfterPrint?: () => void;\n}\n\n/**\n * Trigger native print dialog with deck-specific CSS injected.\n *\n * The `afterprint` event listener is cleaned up automatically. Idempotent —\n * calling twice in quick succession is safe (style is reused).\n */\nexport function printDeck(opts: PrintDeckOptions = {}): void {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n injectPrintStyles();\n const cleanup = (): void => {\n removePrintStyles();\n window.removeEventListener(\"afterprint\", cleanup);\n opts.onAfterPrint?.();\n };\n window.addEventListener(\"afterprint\", cleanup);\n window.print();\n}\n"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"path": "components/composites/slide-deck/progress-bar.tsx",
|
|
59
|
+
"type": "registry:block",
|
|
60
|
+
"target": "components/blocks/slide-deck/progress-bar.tsx",
|
|
61
|
+
"content": "/**\n * `<SlideDeck.ProgressBar>` — horizontal HTML5 progress element.\n */\nimport type { FC } from \"react\";\nimport { useDeckContext } from \"@/components/blocks/slide-deck/context\";\n\nexport interface ProgressBarProps {\n className?: string;\n}\n\nexport const ProgressBar: FC<ProgressBarProps> = ({ className }) => {\n const { state } = useDeckContext();\n // Edge case: totalSlides=0 → 0% safely.\n const value = state.totalSlides === 0 ? 0 : state.currentIndex + 1;\n const max = Math.max(1, state.totalSlides);\n return (\n <progress\n className={[\"theo-slide-deck-progress\", className].filter(Boolean).join(\" \")}\n data-theo-slide-deck-progress\n value={value}\n max={max}\n aria-label=\"Slide progress\"\n />\n );\n};\n"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"path": "components/composites/slide-deck/schema.ts",
|
|
65
|
+
"type": "registry:block",
|
|
66
|
+
"target": "components/blocks/slide-deck/schema.ts",
|
|
67
|
+
"content": "/**\n * Zod schema for `<SlideDeck>` input.\n *\n * Two shapes accepted (ADR D4):\n * - `string` — full markdown, split internally by `splitDeck` (top-level\n * `thematicBreak`, see ADR D3 / D12 of Slide).\n * - `SlideDeckSlide[]` — pre-parsed array (CMS/DB consumers).\n *\n * See `.claude/knowledge-base/plans/slide-deck-composite-plan.md` §16.3.\n */\nimport { z } from \"zod\";\n\nexport const slideDeckSlide = z.object({\n /** Markdown content of this slide. Capped at 50 KB (same as Slide body). */\n markdown: z.string().max(50_000),\n /** Optional id for hash routing (defaults to numeric index, 1-based). */\n id: z\n .string()\n .regex(/^[a-z0-9-]+$/, \"id must be lowercase kebab-case\")\n .max(64)\n .optional(),\n /** Speaker notes (plain text extracted from <!-- notes: ... --> comments). */\n notes: z.string().max(5_000).optional(),\n});\n\n/** Composed input — `slides` prop accepts either form. */\nexport const slideDeckInput = z.union([z.string().max(500_000), z.array(slideDeckSlide).max(500)]);\n\nexport type SlideDeckSlide = z.infer<typeof slideDeckSlide>;\nexport type SlideDeckInput = z.infer<typeof slideDeckInput>;\n\n/** Transition presets. ADR D8. */\nexport const slideDeckTransition = z.enum([\"none\", \"fade\", \"slide\"]);\nexport type SlideDeckTransition = z.infer<typeof slideDeckTransition>;\n"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"path": "components/composites/slide-deck/slide-number.tsx",
|
|
71
|
+
"type": "registry:block",
|
|
72
|
+
"target": "components/blocks/slide-deck/slide-number.tsx",
|
|
73
|
+
"content": "/**\n * `<SlideDeck.SlideNumber>` — decorative \"N / Total\" overlay.\n *\n * aria-hidden because the live indicator in <Controls> already announces the\n * current position; this is purely visual chrome.\n */\nimport type { FC } from \"react\";\nimport { useDeckContext } from \"@/components/blocks/slide-deck/context\";\n\nexport interface SlideNumberProps {\n className?: string;\n}\n\nexport const SlideNumber: FC<SlideNumberProps> = ({ className }) => {\n const { state } = useDeckContext();\n if (state.totalSlides === 0) return null;\n return (\n <div\n className={[\"theo-slide-deck-slide-number\", className].filter(Boolean).join(\" \")}\n data-theo-slide-deck-slide-number\n aria-hidden=\"true\"\n style={{\n position: \"absolute\",\n bottom: 12,\n right: 16,\n fontVariantNumeric: \"tabular-nums\",\n fontSize: 14,\n opacity: 0.6,\n }}\n >\n {state.currentIndex + 1} / {state.totalSlides}\n </div>\n );\n};\n"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"path": "components/composites/slide-deck/split-deck.ts",
|
|
77
|
+
"type": "registry:block",
|
|
78
|
+
"target": "components/blocks/slide-deck/split-deck.ts",
|
|
79
|
+
"content": "/**\n * Split a markdown string into individual slides at top-level `thematicBreak`s.\n *\n * Mirrors the Slide primitive's `detectMultiSlide` algorithm (D12 of RFC 0002),\n * but returns ALL slides instead of truncating.\n *\n * ADR D15: strips global frontmatter FIRST so the leading `---\\n...\\n---\\n`\n * delimiter is not parsed as a `thematicBreak` (which would yield a phantom\n * empty slide #0).\n *\n * The first slide may have its own frontmatter (kept attached); subsequent\n * slides do not parse frontmatter again (consumer's `<Slide markdown>` does\n * per-slide validation).\n */\nimport { extractFrontmatter } from \"@/components/ui/slide/index\";\nimport { extractNotes } from \"@/components/blocks/slide-deck/notes\";\nimport type { SlideDeckSlide } from \"@/components/blocks/slide-deck/schema\";\n\nexport async function splitDeck(markdown: string): Promise<SlideDeckSlide[]> {\n // D15: strip leading global frontmatter so its `---` delimiters are not\n // mistaken for slide separators.\n const { body: bodyAfterFM } = extractFrontmatter(markdown);\n\n if (bodyAfterFM.trim().length === 0) {\n return [];\n }\n\n const { fromMarkdown } = await import(\"mdast-util-from-markdown\");\n const tree = fromMarkdown(bodyAfterFM);\n\n const breakOffsets: number[] = [];\n for (const node of tree.children) {\n if (node.type === \"thematicBreak\") {\n const start = node.position?.start.offset;\n const end = node.position?.end.offset;\n if (typeof start === \"number\" && typeof end === \"number\") {\n breakOffsets.push(start);\n breakOffsets.push(end);\n }\n }\n }\n\n if (breakOffsets.length === 0) {\n // Single-slide deck.\n const { body, notes } = extractNotes(bodyAfterFM);\n if (body.trim().length === 0 && !notes) return [];\n return [{ markdown: body, notes }];\n }\n\n // Build ranges between (or excluding) the `---` tokens.\n const slides: SlideDeckSlide[] = [];\n const positions: number[] = [0];\n // breakOffsets is [start0, end0, start1, end1, ...]. We want to skip the\n // `---` itself but include the content between them.\n for (let i = 0; i < breakOffsets.length; i += 2) {\n const start = breakOffsets[i];\n const end = breakOffsets[i + 1];\n if (typeof start === \"number\") positions.push(start);\n if (typeof end === \"number\") positions.push(end);\n }\n positions.push(bodyAfterFM.length);\n\n // Pair adjacent positions to form slide ranges (skip the `---` segments).\n for (let i = 0; i < positions.length - 1; i += 2) {\n const start = positions[i];\n const end = positions[i + 1];\n if (typeof start !== \"number\" || typeof end !== \"number\") continue;\n const chunk = bodyAfterFM.slice(start, end).trim();\n if (chunk.length === 0) continue;\n const { body, notes } = extractNotes(chunk);\n if (body.trim().length === 0 && !notes) continue;\n slides.push({ markdown: body, notes });\n }\n\n return slides;\n}\n"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"path": "components/composites/slide-deck/thumbnails.tsx",
|
|
83
|
+
"type": "registry:block",
|
|
84
|
+
"target": "components/blocks/slide-deck/thumbnails.tsx",
|
|
85
|
+
"content": "/**\n * `<SlideDeck.Thumbnails>` — sidebar with mini Slide instances.\n *\n * Performance strategy:\n * - Each thumbnail renders the actual `<Slide>` inside a scaled wrapper (~0.18×).\n * - IntersectionObserver lazy-loads thumbnails — off-screen ones show a\n * placeholder skeleton instead of parsing the markdown.\n * - EC-13: when IntersectionObserver is undefined (legacy env / SSR), falls\n * back to eager render. Acceptable for decks ≤ 50 slides.\n *\n * Click handler dispatches JUMP_TO. Auto-scroll keeps current thumbnail visible.\n */\nimport { type FC, useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Slide } from \"@/components/ui/slide/index\";\nimport { useDeckContext } from \"@/components/blocks/slide-deck/context\";\n\nexport interface ThumbnailsProps {\n className?: string;\n /** Scale of each thumbnail. Default 0.18. */\n scale?: number;\n}\n\nconst CANVAS_W = 1280;\nconst CANVAS_H = 720;\n\ninterface ThumbnailItemProps {\n markdown: string;\n index: number;\n isCurrent: boolean;\n scale: number;\n eager: boolean;\n onSelect: (index: number) => void;\n registerRef: (index: number, el: HTMLElement | null) => void;\n}\n\nconst ThumbnailItem: FC<ThumbnailItemProps> = ({\n markdown,\n index,\n isCurrent,\n scale,\n eager,\n onSelect,\n registerRef,\n}) => {\n const [revealed, setRevealed] = useState(eager);\n\n const setRef = useCallback(\n (el: HTMLElement | null) => {\n registerRef(index, el);\n if (eager) return;\n if (!el) return;\n if (typeof IntersectionObserver === \"undefined\") {\n // EC-13 fallback: no IO, render eagerly.\n setRevealed(true);\n return;\n }\n const io = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n setRevealed(true);\n io.disconnect();\n break;\n }\n }\n },\n { rootMargin: \"200px\" },\n );\n io.observe(el);\n },\n [index, eager, registerRef],\n );\n\n const w = Math.round(CANVAS_W * scale);\n const h = Math.round(CANVAS_H * scale);\n return (\n <button\n ref={setRef}\n type=\"button\"\n onClick={() => onSelect(index)}\n data-theo-slide-deck-thumbnail\n data-current={isCurrent || undefined}\n aria-label={`Slide ${index + 1}`}\n aria-current={isCurrent ? \"page\" : undefined}\n className=\"theo-slide-deck-thumbnail\"\n style={{\n width: w,\n height: h,\n padding: 0,\n border: isCurrent ? \"2px solid currentColor\" : \"1px solid rgba(127,127,127,0.3)\",\n borderRadius: 6,\n overflow: \"hidden\",\n position: \"relative\",\n cursor: \"pointer\",\n flexShrink: 0,\n background: \"transparent\",\n }}\n >\n <div\n style={{\n width: CANVAS_W,\n height: CANVAS_H,\n transform: `scale(${scale})`,\n transformOrigin: \"top left\",\n pointerEvents: \"none\",\n }}\n >\n {revealed ? (\n <Slide markdown={markdown} aria-label={`Thumbnail ${index + 1}`} />\n ) : (\n <div\n data-theo-slide-deck-thumbnail-placeholder\n style={{\n width: \"100%\",\n height: \"100%\",\n background: \"rgba(127,127,127,0.08)\",\n }}\n />\n )}\n </div>\n </button>\n );\n};\n\nexport const Thumbnails: FC<ThumbnailsProps> = ({ className, scale = 0.18 }) => {\n const { state, dispatch, slides } = useDeckContext();\n const refs = useRef<Map<number, HTMLElement>>(new Map());\n\n const registerRef = useCallback((index: number, el: HTMLElement | null) => {\n if (el) {\n refs.current.set(index, el);\n } else {\n refs.current.delete(index);\n }\n }, []);\n\n const onSelect = useCallback(\n (index: number) => {\n dispatch({ type: \"JUMP_TO\", index });\n },\n [dispatch],\n );\n\n // Auto-scroll current into view.\n useEffect(() => {\n const el = refs.current.get(state.currentIndex);\n if (el && \"scrollIntoView\" in el) {\n el.scrollIntoView({ behavior: \"smooth\", block: \"nearest\", inline: \"nearest\" });\n }\n }, [state.currentIndex]);\n\n // EC-13: eager mode when IO is absent.\n const eagerAll = useMemo(() => typeof IntersectionObserver === \"undefined\", []);\n\n return (\n <ul\n className={[\"theo-slide-deck-thumbnails\", className].filter(Boolean).join(\" \")}\n data-theo-slide-deck-thumbnails\n aria-label=\"Slide thumbnails\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: 8,\n overflowY: \"auto\",\n padding: 8,\n listStyle: \"none\",\n margin: 0,\n }}\n >\n {slides.map((slide, index) => (\n <li key={`${slide.id ?? index}-${index}`}>\n <ThumbnailItem\n markdown={slide.markdown}\n index={index}\n isCurrent={index === state.currentIndex}\n scale={scale}\n eager={eagerAll || index < 3 /* first 3 always eager for snappy first paint */}\n onSelect={onSelect}\n registerRef={registerRef}\n />\n </li>\n ))}\n </ul>\n );\n};\n"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"path": "components/composites/slide-deck/transitions.css",
|
|
89
|
+
"type": "registry:block",
|
|
90
|
+
"target": "components/blocks/slide-deck/transitions.css",
|
|
91
|
+
"content": "/*\n * Slide deck transitions (ADR D8).\n *\n * Three presets via the `data-theo-slide-deck-transition` attribute on the\n * deck root: \"none\" | \"fade\" | \"slide\".\n *\n * Active transition direction is signaled via `data-theo-slide-deck-direction`\n * (\"none\" | \"next\" | \"prev\"). CSS animations are 250ms, GPU-accelerated.\n *\n * Respects `prefers-reduced-motion: reduce` — all durations collapse to 0ms,\n * effectively becoming \"none\" regardless of the prop.\n */\n\n.theo-slide-deck-slide-frame {\n --theo-slide-deck-transition-duration: 250ms;\n position: absolute;\n inset: 0;\n}\n\n/* fade preset */\n.theo-slide-deck-slide-frame[data-theo-slide-deck-transition=\"fade\"]\n > [data-theo-slide-deck-slide-state=\"incoming\"] {\n animation: theo-slide-deck-fade-in var(--theo-slide-deck-transition-duration) ease-out;\n}\n\n@keyframes theo-slide-deck-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n/* slide preset */\n.theo-slide-deck-slide-frame[data-theo-slide-deck-transition=\"slide\"][data-theo-slide-deck-direction=\"next\"]\n > [data-theo-slide-deck-slide-state=\"incoming\"] {\n animation: theo-slide-deck-slide-in-right var(--theo-slide-deck-transition-duration) ease-out;\n}\n\n.theo-slide-deck-slide-frame[data-theo-slide-deck-transition=\"slide\"][data-theo-slide-deck-direction=\"prev\"]\n > [data-theo-slide-deck-slide-state=\"incoming\"] {\n animation: theo-slide-deck-slide-in-left var(--theo-slide-deck-transition-duration) ease-out;\n}\n\n@keyframes theo-slide-deck-slide-in-right {\n from {\n transform: translateX(30%);\n opacity: 0;\n }\n to {\n transform: translateX(0);\n opacity: 1;\n }\n}\n\n@keyframes theo-slide-deck-slide-in-left {\n from {\n transform: translateX(-30%);\n opacity: 0;\n }\n to {\n transform: translateX(0);\n opacity: 1;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .theo-slide-deck-slide-frame,\n .theo-slide-deck-slide-frame * {\n animation-duration: 0ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0ms !important;\n }\n}\n"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"path": "components/composites/slide-deck/use-deck-hash-routing.ts",
|
|
95
|
+
"type": "registry:block",
|
|
96
|
+
"target": "components/blocks/slide-deck/use-deck-hash-routing.ts",
|
|
97
|
+
"content": "/**\n * Hash routing hook (ADR D13).\n *\n * Pattern `#/N` (1-based). Bidirectional sync:\n * - Initial state: read via `useDeckState`'s lazy `initFromHash` (D17 SSR-safe).\n * - hashchange event → JUMP_TO.\n * - currentIndex change → `history.replaceState` (does NOT trigger hashchange,\n * so no infinite loop — verified in test EC-10).\n */\nimport { type Dispatch, useEffect } from \"react\";\nimport type { DeckAction } from \"@/components/blocks/slide-deck/use-deck-state\";\n\nexport interface UseDeckHashRoutingOptions {\n enabled?: boolean;\n totalSlides: number;\n currentIndex: number;\n}\n\n/** Read hash → return 0-based index, or undefined if not present/invalid. */\nexport function readHashIndex(hash: string): number | undefined {\n if (!hash || hash === \"#\" || hash === \"#/\") return undefined;\n const match = hash.match(/^#\\/(\\d+)/);\n if (!match) return undefined;\n const oneBased = Number.parseInt(match[1] ?? \"\", 10);\n if (!Number.isFinite(oneBased) || oneBased < 1) return undefined;\n return oneBased - 1;\n}\n\n/** SSR-safe wrapper for initial hash read (D17). */\nexport function readInitialHash(): number | undefined {\n if (typeof window === \"undefined\") return undefined;\n return readHashIndex(window.location.hash);\n}\n\n/** Format index → hash string. */\nexport function formatHash(zeroBasedIndex: number): string {\n return `#/${zeroBasedIndex + 1}`;\n}\n\nexport function useDeckHashRouting(\n dispatch: Dispatch<DeckAction>,\n opts: UseDeckHashRoutingOptions,\n): void {\n const { enabled = true, totalSlides, currentIndex } = opts;\n\n // Listen for hashchange (back/forward, manual edit, shared link click).\n useEffect(() => {\n if (!enabled) return;\n if (typeof window === \"undefined\") return;\n const handler = (): void => {\n const idx = readHashIndex(window.location.hash);\n if (typeof idx !== \"number\") return;\n const clamped = Math.max(0, Math.min(idx, Math.max(0, totalSlides - 1)));\n dispatch({ type: \"JUMP_TO\", index: clamped });\n };\n window.addEventListener(\"hashchange\", handler);\n return () => window.removeEventListener(\"hashchange\", handler);\n }, [enabled, totalSlides, dispatch]);\n\n // Sync currentIndex → hash via replaceState (silent, no hashchange fired).\n useEffect(() => {\n if (!enabled) return;\n if (typeof window === \"undefined\") return;\n const targetHash = formatHash(currentIndex);\n if (window.location.hash === targetHash) return;\n // replaceState does NOT trigger hashchange — verified in test EC-10.\n window.history.replaceState(null, \"\", targetHash);\n }, [enabled, currentIndex]);\n}\n"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"path": "components/composites/slide-deck/use-deck-keyboard.ts",
|
|
101
|
+
"type": "registry:block",
|
|
102
|
+
"target": "components/blocks/slide-deck/use-deck-keyboard.ts",
|
|
103
|
+
"content": "/**\n * Keyboard navigation hook (ADR D9).\n *\n * Hardcoded bindings (no remap in v0.4):\n * ArrowRight, Space, PageDown → NEXT_SLIDE\n * ArrowLeft, PageUp → PREV_SLIDE\n * Home → JUMP_TO 0\n * End → JUMP_TO last\n * Escape → SET_FULLSCREEN false\n * f / F → toggleFullscreen callback\n * n / N / p / P → TOGGLE_PRESENTER\n * Ctrl+P / Meta+P → onPrint callback (preventDefault)\n *\n * Guards: ignora events quando target é INPUT, TEXTAREA, ou contentEditable\n * (consumer pode ter inputs em modais sem conflito).\n */\nimport { type Dispatch, useEffect } from \"react\";\nimport type { DeckAction } from \"@/components/blocks/slide-deck/use-deck-state\";\n\nexport interface UseDeckKeyboardOptions {\n enabled?: boolean;\n totalSlides: number;\n onToggleFullscreen?: () => void;\n onPrint?: () => void;\n}\n\nfunction isEditableTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n if (tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\") return true;\n if (target.isContentEditable) return true;\n return false;\n}\n\nexport function useDeckKeyboard(\n dispatch: Dispatch<DeckAction>,\n opts: UseDeckKeyboardOptions,\n): void {\n const { enabled = true, totalSlides, onToggleFullscreen, onPrint } = opts;\n useEffect(() => {\n if (!enabled) return;\n const handler = (event: KeyboardEvent): void => {\n if (isEditableTarget(event.target)) return;\n\n const key = event.key;\n const isPrintCombo = (event.ctrlKey || event.metaKey) && (key === \"p\" || key === \"P\");\n\n if (isPrintCombo) {\n event.preventDefault();\n onPrint?.();\n return;\n }\n\n switch (key) {\n case \"ArrowRight\":\n case \" \":\n case \"Spacebar\":\n case \"PageDown\":\n dispatch({ type: \"NEXT_SLIDE\" });\n break;\n case \"ArrowLeft\":\n case \"PageUp\":\n dispatch({ type: \"PREV_SLIDE\" });\n break;\n case \"Home\":\n dispatch({ type: \"JUMP_TO\", index: 0 });\n break;\n case \"End\":\n dispatch({ type: \"JUMP_TO\", index: Math.max(0, totalSlides - 1) });\n break;\n case \"Escape\":\n dispatch({ type: \"SET_FULLSCREEN\", value: false });\n break;\n case \"f\":\n case \"F\":\n onToggleFullscreen?.();\n break;\n case \"n\":\n case \"N\":\n case \"p\":\n case \"P\":\n dispatch({ type: \"TOGGLE_PRESENTER\" });\n break;\n default:\n return;\n }\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [enabled, totalSlides, dispatch, onToggleFullscreen, onPrint]);\n}\n"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"path": "components/composites/slide-deck/use-deck-state.ts",
|
|
107
|
+
"type": "registry:block",
|
|
108
|
+
"target": "components/blocks/slide-deck/use-deck-state.ts",
|
|
109
|
+
"content": "/**\n * Deck state machine (ADR D5).\n *\n * Single useReducer governs all deck-level state transitions: currentIndex,\n * currentFragment, presenterMode, fullscreen, transitionDirection.\n *\n * NEXT_SLIDE advances fragment first if `currentFragment < totalFragmentsInCurrent`,\n * else advances slide. PREV_SLIDE mirrors. JUMP_TO clamps to valid range.\n *\n * ADR D17: lazy init reads hash via `initFromHash` callback when provided —\n * SSR-safe (callback guards `typeof window !== \"undefined\"`).\n */\nimport { type Dispatch, useReducer } from \"react\";\n\nexport interface DeckState {\n currentIndex: number;\n currentFragment: number;\n presenterMode: boolean;\n fullscreen: boolean;\n transitionDirection: \"none\" | \"next\" | \"prev\";\n totalSlides: number;\n totalFragmentsInCurrent: number;\n}\n\nexport type DeckAction =\n | { type: \"NEXT_SLIDE\" }\n | { type: \"PREV_SLIDE\" }\n | { type: \"JUMP_TO\"; index: number }\n | { type: \"NEXT_FRAGMENT\" }\n | { type: \"PREV_FRAGMENT\" }\n | { type: \"RESET_FRAGMENTS\" }\n | { type: \"TOGGLE_PRESENTER\" }\n | { type: \"SET_FULLSCREEN\"; value: boolean }\n | { type: \"UPDATE_TOTAL_SLIDES\"; total: number }\n | { type: \"UPDATE_TOTAL_FRAGMENTS\"; total: number }\n | { type: \"TRANSITION_END\" };\n\nexport function deckReducer(state: DeckState, action: DeckAction): DeckState {\n switch (action.type) {\n case \"NEXT_SLIDE\": {\n // Advance fragment first when there are remaining fragments.\n if (state.currentFragment < state.totalFragmentsInCurrent) {\n return { ...state, currentFragment: state.currentFragment + 1 };\n }\n const next = Math.min(state.currentIndex + 1, Math.max(0, state.totalSlides - 1));\n if (next === state.currentIndex) return state;\n return {\n ...state,\n currentIndex: next,\n currentFragment: 0,\n transitionDirection: \"next\",\n };\n }\n case \"PREV_SLIDE\": {\n if (state.currentFragment > 0) {\n return { ...state, currentFragment: state.currentFragment - 1 };\n }\n const prev = Math.max(state.currentIndex - 1, 0);\n if (prev === state.currentIndex) return state;\n return {\n ...state,\n currentIndex: prev,\n currentFragment: 0,\n transitionDirection: \"prev\",\n };\n }\n case \"JUMP_TO\": {\n const clamped = Math.max(0, Math.min(action.index, Math.max(0, state.totalSlides - 1)));\n if (clamped === state.currentIndex) return state;\n return {\n ...state,\n currentIndex: clamped,\n currentFragment: 0,\n transitionDirection: \"none\",\n };\n }\n case \"NEXT_FRAGMENT\": {\n if (state.currentFragment >= state.totalFragmentsInCurrent) return state;\n return { ...state, currentFragment: state.currentFragment + 1 };\n }\n case \"PREV_FRAGMENT\": {\n if (state.currentFragment <= 0) return state;\n return { ...state, currentFragment: state.currentFragment - 1 };\n }\n case \"RESET_FRAGMENTS\":\n return { ...state, currentFragment: 0 };\n case \"TOGGLE_PRESENTER\":\n return { ...state, presenterMode: !state.presenterMode };\n case \"SET_FULLSCREEN\":\n return state.fullscreen === action.value ? state : { ...state, fullscreen: action.value };\n case \"UPDATE_TOTAL_SLIDES\": {\n const next = Math.max(0, action.total);\n const clampedIdx = Math.max(0, Math.min(state.currentIndex, Math.max(0, next - 1)));\n return { ...state, totalSlides: next, currentIndex: clampedIdx };\n }\n case \"UPDATE_TOTAL_FRAGMENTS\":\n return { ...state, totalFragmentsInCurrent: Math.max(0, action.total) };\n case \"TRANSITION_END\":\n return state.transitionDirection === \"none\"\n ? state\n : { ...state, transitionDirection: \"none\" };\n }\n}\n\nexport interface UseDeckStateOptions {\n initialIndex?: number;\n totalSlides: number;\n /** Lazy initializer hook (D17): returns initial index, e.g. parsing hash. */\n initFromHash?: () => number | undefined;\n}\n\nfunction initDeckState(init: UseDeckStateOptions): DeckState {\n const total = Math.max(0, init.totalSlides);\n let idx = init.initialIndex ?? 0;\n if (init.initFromHash) {\n const fromHash = init.initFromHash();\n if (typeof fromHash === \"number\" && Number.isFinite(fromHash)) {\n idx = fromHash;\n }\n }\n const clamped = Math.max(0, Math.min(idx, Math.max(0, total - 1)));\n return {\n currentIndex: clamped,\n currentFragment: 0,\n presenterMode: false,\n fullscreen: false,\n transitionDirection: \"none\",\n totalSlides: total,\n totalFragmentsInCurrent: 0,\n };\n}\n\nexport function useDeckState(\n opts: UseDeckStateOptions,\n): readonly [DeckState, Dispatch<DeckAction>] {\n const [state, dispatch] = useReducer(deckReducer, opts, initDeckState);\n return [state, dispatch] as const;\n}\n"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"path": "components/composites/slide-deck/use-deck-swipe.ts",
|
|
113
|
+
"type": "registry:block",
|
|
114
|
+
"target": "components/blocks/slide-deck/use-deck-swipe.ts",
|
|
115
|
+
"content": "/**\n * Touch swipe hook (ADR D10).\n *\n * Detecta swipe horizontal via Pointer Events nativos:\n * - threshold: 50px de deslocamento\n * - velocity: > 0.3 px/ms\n * - direction guard: |dx| > 2*|dy| (bloqueia swipe vertical / scroll)\n *\n * Swipe left (dx < 0) → NEXT_SLIDE\n * Swipe right (dx > 0) → PREV_SLIDE\n *\n * EC-6: pointercancel limpa o tracking state (mobile browser pode cancelar mid-swipe).\n * EC-7: tracking limita ao primeiro pointerId (multi-touch é ignorado).\n */\nimport { type Dispatch, type RefObject, useEffect } from \"react\";\nimport type { DeckAction } from \"@/components/blocks/slide-deck/use-deck-state\";\n\nexport interface UseDeckSwipeOptions {\n enabled?: boolean;\n}\n\ninterface TrackedPointer {\n pointerId: number;\n startX: number;\n startY: number;\n startedAt: number;\n}\n\nconst SWIPE_THRESHOLD_PX = 50;\nconst SWIPE_VELOCITY_PX_PER_MS = 0.3;\n\nexport function useDeckSwipe(\n ref: RefObject<HTMLElement | null>,\n dispatch: Dispatch<DeckAction>,\n opts: UseDeckSwipeOptions = {},\n): void {\n const { enabled = true } = opts;\n useEffect(() => {\n if (!enabled) return;\n const el = ref.current;\n if (!el) return;\n\n let tracked: TrackedPointer | null = null;\n\n const onPointerDown = (e: PointerEvent): void => {\n // EC-7: ignore secondary pointers (multi-touch).\n if (tracked !== null) return;\n tracked = {\n pointerId: e.pointerId,\n startX: e.clientX,\n startY: e.clientY,\n startedAt: e.timeStamp,\n };\n };\n\n const onPointerUp = (e: PointerEvent): void => {\n if (!tracked || e.pointerId !== tracked.pointerId) return;\n const dx = e.clientX - tracked.startX;\n const dy = e.clientY - tracked.startY;\n const dt = Math.max(1, e.timeStamp - tracked.startedAt);\n tracked = null;\n\n const absDx = Math.abs(dx);\n const absDy = Math.abs(dy);\n const velocity = absDx / dt;\n\n // Direction guard: horizontal-dominant swipe only.\n if (absDx < absDy * 2) return;\n // Threshold + velocity guard.\n if (absDx < SWIPE_THRESHOLD_PX) return;\n if (velocity < SWIPE_VELOCITY_PX_PER_MS) return;\n\n if (dx < 0) {\n dispatch({ type: \"NEXT_SLIDE\" });\n } else if (dx > 0) {\n dispatch({ type: \"PREV_SLIDE\" });\n }\n };\n\n const onPointerCancel = (e: PointerEvent): void => {\n // EC-6: clear tracking on cancel (browser back gesture, system interruption).\n if (tracked && e.pointerId === tracked.pointerId) {\n tracked = null;\n }\n };\n\n el.addEventListener(\"pointerdown\", onPointerDown);\n el.addEventListener(\"pointerup\", onPointerUp);\n el.addEventListener(\"pointercancel\", onPointerCancel);\n return () => {\n el.removeEventListener(\"pointerdown\", onPointerDown);\n el.removeEventListener(\"pointerup\", onPointerUp);\n el.removeEventListener(\"pointercancel\", onPointerCancel);\n };\n }, [enabled, ref, dispatch]);\n}\n"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"path": "components/composites/slide-deck/use-fullscreen.ts",
|
|
119
|
+
"type": "registry:block",
|
|
120
|
+
"target": "components/blocks/slide-deck/use-fullscreen.ts",
|
|
121
|
+
"content": "/**\n * Cross-browser fullscreen hook (ADR D6 area / EC-8).\n *\n * Wraps `Element.requestFullscreen()` + Safari `webkit*` prefix. Listens\n * `fullscreenchange` (+ webkit) to sync state when user presses Esc via the\n * native UI.\n *\n * EC-8: iOS Safari < 16 doesn't expose fullscreen on arbitrary elements (only\n * `<video>`). Feature-detect — when unavailable, hook is a no-op and `toggle`\n * is safe to call.\n */\nimport { type RefObject, useCallback, useEffect, useState } from \"react\";\n\ninterface VendoredElement extends HTMLElement {\n webkitRequestFullscreen?: () => Promise<void>;\n}\n\ninterface VendoredDocument extends Document {\n webkitFullscreenElement?: Element | null;\n webkitExitFullscreen?: () => Promise<void>;\n}\n\nfunction isFullscreenSupported(): boolean {\n if (typeof document === \"undefined\") return false;\n const doc = document as VendoredDocument;\n const el = document.documentElement as VendoredElement;\n return (\n Boolean(el.requestFullscreen ?? el.webkitRequestFullscreen) &&\n Boolean(doc.exitFullscreen ?? doc.webkitExitFullscreen)\n );\n}\n\nfunction currentFullscreenElement(): Element | null {\n if (typeof document === \"undefined\") return null;\n const doc = document as VendoredDocument;\n return document.fullscreenElement ?? doc.webkitFullscreenElement ?? null;\n}\n\nexport interface UseFullscreenResult {\n isFullscreen: boolean;\n toggle: () => Promise<void>;\n isSupported: boolean;\n}\n\nexport function useFullscreen(ref: RefObject<HTMLElement | null>): UseFullscreenResult {\n const supported = isFullscreenSupported();\n const [isFullscreen, setFullscreen] = useState(false);\n\n useEffect(() => {\n if (!supported) return;\n const handler = (): void => {\n setFullscreen(currentFullscreenElement() !== null);\n };\n document.addEventListener(\"fullscreenchange\", handler);\n document.addEventListener(\"webkitfullscreenchange\", handler);\n return () => {\n document.removeEventListener(\"fullscreenchange\", handler);\n document.removeEventListener(\"webkitfullscreenchange\", handler);\n };\n }, [supported]);\n\n const toggle = useCallback(async (): Promise<void> => {\n if (!supported) return;\n const el = ref.current as VendoredElement | null;\n if (!el) return;\n const doc = document as VendoredDocument;\n try {\n if (currentFullscreenElement()) {\n await (doc.exitFullscreen?.() ?? doc.webkitExitFullscreen?.());\n } else {\n await (el.requestFullscreen?.() ?? el.webkitRequestFullscreen?.());\n }\n } catch {\n // User denied or API unavailable — silent fallback. Defensive: never throw.\n }\n }, [ref, supported]);\n\n return { isFullscreen, toggle, isSupported: supported };\n}\n"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"path": "components/composites/slide-deck/index.ts",
|
|
125
|
+
"type": "registry:block",
|
|
126
|
+
"target": "components/blocks/slide-deck/index.ts",
|
|
127
|
+
"content": "/**\n * Public surface of `@theokit/ui/slide-deck`. Subpath-isolated composite engine.\n */\nexport { SlideDeck } from \"@/components/blocks/slide-deck/slide-deck\";\nexport type { SlideDeckProps } from \"@/components/blocks/slide-deck/slide-deck\";\nexport {\n slideDeckInput,\n slideDeckSlide,\n slideDeckTransition,\n type SlideDeckInput,\n type SlideDeckSlide,\n type SlideDeckTransition,\n} from \"@/components/blocks/slide-deck/schema\";\nexport { splitDeck } from \"@/components/blocks/slide-deck/split-deck\";\nexport { extractNotes } from \"@/components/blocks/slide-deck/notes\";\nexport { countFragmentsInMarkdown } from \"@/components/blocks/slide-deck/fragments\";\nexport {\n formatHash,\n readHashIndex,\n readInitialHash,\n useDeckHashRouting,\n} from \"@/components/blocks/slide-deck/use-deck-hash-routing\";\nexport {\n deckReducer,\n useDeckState,\n type DeckAction,\n type DeckState,\n} from \"@/components/blocks/slide-deck/use-deck-state\";\nexport { useDeckKeyboard } from \"@/components/blocks/slide-deck/use-deck-keyboard\";\nexport { useDeckSwipe } from \"@/components/blocks/slide-deck/use-deck-swipe\";\nexport { useFullscreen } from \"@/components/blocks/slide-deck/use-fullscreen\";\nexport {\n injectPrintStyles,\n printDeck,\n removePrintStyles,\n} from \"@/components/blocks/slide-deck/print-styles\";\nexport { Controls } from \"@/components/blocks/slide-deck/controls\";\nexport { ProgressBar } from \"@/components/blocks/slide-deck/progress-bar\";\nexport { SlideNumber } from \"@/components/blocks/slide-deck/slide-number\";\nexport { Thumbnails } from \"@/components/blocks/slide-deck/thumbnails\";\nexport { PresenterView } from \"@/components/blocks/slide-deck/presenter-view\";\nexport {\n DeckContext,\n useDeckContext,\n type DeckContextValue,\n} from \"@/components/blocks/slide-deck/context\";\n"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "slide-plugin-emoji",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Slide plugin · Emoji",
|
|
6
|
+
"description": "Tier 2 Slide plugin that replaces :shortcode: with Unicode emoji. Ancestor-aware: skips replacement inside <code> / <pre> to preserve type hints and YAML keys. Zero peer-deps.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"hast"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/slide.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/slide/plugins/emoji/index.ts",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/slide/plugins/emoji/index.ts",
|
|
19
|
+
"content": "/**\n * Slide rich-content plugin — Unicode emoji shortcodes.\n *\n * Zero peer-deps for runtime (only unist-util-visit-parents which is part of\n * the existing slide stack tree). Substitutes `:shortcode:` patterns with the\n * matching Unicode emoji from the embedded map.\n *\n * EC-6: uses `visitParents` with an ancestor check so shortcodes inside\n * `<code>` / `<pre>` are NEVER replaced. This prevents corruption of code\n * samples that legitimately use `:colon:syntax` (Python type hints, YAML\n * keys, Ruby symbols, JSX self-closing, etc.).\n *\n * Unknown shortcodes pass through unchanged (`:foo:` stays as text).\n */\nimport type { Root as HastRoot } from \"hast\";\nimport type { SlidePlugin } from \"@/components/ui/slide/plugin\";\nimport { EMOJI_MAP } from \"@/components/ui/slide/plugins/emoji/map\";\n\nconst SHORTCODE_RE = /:([a-z0-9_+-]+):/g;\n\n/**\n * Return true when any ancestor element is `<code>` or `<pre>`. Used to skip\n * emoji replacement inside code samples (EC-6).\n */\nfunction isInsideCodeOrPre(ancestors: ReadonlyArray<{ type: string; tagName?: string }>): boolean {\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const a = ancestors[i];\n if (!a || a.type !== \"element\") continue;\n if (a.tagName === \"code\" || a.tagName === \"pre\") return true;\n }\n return false;\n}\n\nexport interface EmojiPluginOptions {\n /** Override or extend the default emoji map. Merged on top of the built-in 100. */\n extra?: Record<string, string>;\n}\n\nexport function emojiPlugin(opts: EmojiPluginOptions = {}): SlidePlugin {\n const map = { ...EMOJI_MAP, ...(opts.extra ?? {}) };\n return {\n name: \"emoji\",\n async hastTransform(tree: HastRoot): Promise<HastRoot> {\n const { visitParents } = await import(\"unist-util-visit-parents\");\n visitParents(\n tree,\n \"text\",\n (node: { value: string }, ancestors: Array<{ type: string; tagName?: string }>) => {\n if (isInsideCodeOrPre(ancestors)) return;\n node.value = node.value.replace(SHORTCODE_RE, (match, code: string) => {\n const replacement = map[code.toLowerCase()];\n return replacement ?? match;\n });\n },\n );\n return tree;\n },\n };\n}\n\nexport { EMOJI_MAP };\n"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"path": "components/primitives/slide/plugins/emoji/map.ts",
|
|
23
|
+
"type": "registry:ui",
|
|
24
|
+
"target": "components/ui/slide/plugins/emoji/map.ts",
|
|
25
|
+
"content": "/**\n * 100 emoji shortcodes commonly used in tech presentations.\n *\n * Selection rationale: covers the top ~80% of GitHub markdown usage in code\n * docs, release notes, slides. Unicode-native (no external font dependency)\n * so the appearance follows the OS emoji font (Apple Color Emoji on macOS,\n * Segoe UI Emoji on Windows, Noto Color Emoji on Linux/Android).\n *\n * Adding entries: keep keys lowercase, kebab/underscore allowed, single Unicode\n * scalar value (sequences like 👨💻 are also OK).\n *\n * Twemoji variant: out of scope — `slide/plugins/emoji-twemoji` is a v0.5\n * candidate if demand emerges.\n */\nexport const EMOJI_MAP: Record<string, string> = {\n // Faces & reactions\n smile: \"\\u{1F600}\",\n grin: \"\\u{1F601}\",\n joy: \"\\u{1F602}\",\n rofl: \"\\u{1F923}\",\n wink: \"\\u{1F609}\",\n sweat_smile: \"\\u{1F605}\",\n blush: \"\\u{1F60A}\",\n thinking: \"\\u{1F914}\",\n neutral_face: \"\\u{1F610}\",\n expressionless: \"\\u{1F611}\",\n no_mouth: \"\\u{1F636}\",\n heart_eyes: \"\\u{1F60D}\",\n sunglasses: \"\\u{1F60E}\",\n cry: \"\\u{1F622}\",\n sob: \"\\u{1F62D}\",\n scream: \"\\u{1F631}\",\n fearful: \"\\u{1F628}\",\n flushed: \"\\u{1F633}\",\n zzz: \"\\u{1F4A4}\",\n\n // Hand gestures\n thumbsup: \"\\u{1F44D}\",\n thumbs_up: \"\\u{1F44D}\",\n thumbsdown: \"\\u{1F44E}\",\n thumbs_down: \"\\u{1F44E}\",\n ok_hand: \"\\u{1F44C}\",\n clap: \"\\u{1F44F}\",\n wave: \"\\u{1F44B}\",\n raised_hands: \"\\u{1F64C}\",\n pray: \"\\u{1F64F}\",\n muscle: \"\\u{1F4AA}\",\n point_right: \"\\u{1F449}\",\n point_left: \"\\u{1F448}\",\n point_up: \"\\u{1F446}\",\n point_down: \"\\u{1F447}\",\n\n // Signals & icons\n check: \"✅\",\n white_check_mark: \"✅\",\n x: \"❌\",\n heavy_check_mark: \"✔\",\n ballot_box_with_check: \"☑️\",\n warning: \"⚠️\",\n no_entry: \"⛔\",\n no_entry_sign: \"\\u{1F6AB}\",\n question: \"❓\",\n exclamation: \"❗\",\n bangbang: \"‼️\",\n information_source: \"ℹ️\",\n zap: \"⚡\",\n bell: \"\\u{1F514}\",\n no_bell: \"\\u{1F515}\",\n\n // Tech\n rocket: \"\\u{1F680}\",\n computer: \"\\u{1F4BB}\",\n desktop_computer: \"\\u{1F5A5}️\",\n keyboard: \"⌨️\",\n bulb: \"\\u{1F4A1}\",\n hammer: \"\\u{1F528}\",\n wrench: \"\\u{1F527}\",\n gear: \"⚙️\",\n lock: \"\\u{1F512}\",\n unlock: \"\\u{1F513}\",\n key: \"\\u{1F511}\",\n package: \"\\u{1F4E6}\",\n link: \"\\u{1F517}\",\n paperclip: \"\\u{1F4CE}\",\n scissors: \"✂️\",\n hourglass: \"⏳\",\n alarm_clock: \"⏰\",\n stopwatch: \"⏱️\",\n hash: \"#️⃣\",\n\n // Files & docs\n page_facing_up: \"\\u{1F4C4}\",\n pencil: \"✏️\",\n memo: \"\\u{1F4DD}\",\n book: \"\\u{1F4D6}\",\n books: \"\\u{1F4DA}\",\n bookmark: \"\\u{1F516}\",\n clipboard: \"\\u{1F4CB}\",\n chart_with_upwards_trend: \"\\u{1F4C8}\",\n chart_with_downwards_trend: \"\\u{1F4C9}\",\n bar_chart: \"\\u{1F4CA}\",\n\n // Status / energy\n fire: \"\\u{1F525}\",\n sparkles: \"✨\",\n star: \"⭐\",\n star2: \"\\u{1F31F}\",\n tada: \"\\u{1F389}\",\n confetti_ball: \"\\u{1F38A}\",\n boom: \"\\u{1F4A5}\",\n rainbow: \"\\u{1F308}\",\n sunny: \"☀️\",\n cloud: \"☁️\",\n snowflake: \"❄️\",\n hot_face: \"\\u{1F975}\",\n cold_face: \"\\u{1F976}\",\n\n // Affection & symbols\n heart: \"❤️\",\n broken_heart: \"\\u{1F494}\",\n hearts: \"\\u{1F495}\",\n fist: \"✊\",\n trophy: \"\\u{1F3C6}\",\n medal: \"\\u{1F396}️\",\n hundred: \"\\u{1F4AF}\",\n eye: \"\\u{1F441}️\",\n eyes: \"\\u{1F440}\",\n\n // Coffee & food (slide signature)\n coffee: \"☕\",\n tea: \"\\u{1F375}\",\n beer: \"\\u{1F37A}\",\n pizza: \"\\u{1F355}\",\n\n // Arrows\n arrow_right: \"➡️\",\n arrow_left: \"⬅️\",\n arrow_up: \"⬆️\",\n arrow_down: \"⬇️\",\n arrow_upper_right: \"↗️\",\n arrow_lower_right: \"↘️\",\n};\n"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "slide-plugin-math",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Slide plugin · Math",
|
|
6
|
+
"description": "Tier 2 Slide plugin that renders $inline$ and $$block$$ math via KaTeX. Peer-deps katex + hast-util-from-html are opt-in.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"hast",
|
|
9
|
+
"hast-util-from-html",
|
|
10
|
+
"katex"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"https://usetheodev.github.io/theo-ui/r/slide.json",
|
|
14
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
{
|
|
18
|
+
"path": "components/primitives/slide/plugins/math/index.ts",
|
|
19
|
+
"type": "registry:ui",
|
|
20
|
+
"target": "components/ui/slide/plugins/math/index.ts",
|
|
21
|
+
"content": "/**\n * Slide rich-content plugin — KaTeX math rendering.\n *\n * Peer-deps (optional): `katex`, `hast-util-from-html`, `unist-util-visit`.\n * Lazy + opt-in (ADR D10 / D13 of the rich-content plan).\n *\n * Detects `$inline$` and `$$block$$` patterns in hast text nodes and replaces\n * them with KaTeX-rendered HTML. Block math (display mode) is rendered with\n * `displayMode: true` for proper line height + numbering.\n *\n * EC-2: every peer-dep is loaded via dynamic `import()` wrapped in try/catch;\n * a missing peer-dep surfaces via the D16 path (PLUGIN_ERROR) and the math\n * source stays as plain text — slide doesn't break.\n *\n * EC-4: `sanitizeSchemaExtension` lists ~30 MathML tags + KaTeX-specific\n * attributes so the rendered output survives the security barrier (D17).\n *\n * Consumer setup:\n * import \"katex/dist/katex.min.css\";\n * <Slide markdown={md} plugins={[mathPlugin()]} />\n */\nimport type { Root as HastRoot } from \"hast\";\nimport type { SlidePlugin } from \"@/components/ui/slide/plugin\";\n\nexport interface MathPluginOptions {\n /** KaTeX render options forwarded to `renderToString`. */\n katexOptions?: Record<string, unknown>;\n}\n\n// EC-4: complete MathML core tag list — KaTeX emits these for fraction, sqrt,\n// matrix, sub/super, accents, etc. Plus `<span>`/`<div>` for layout chrome.\nconst MATHML_TAG_NAMES = [\n \"span\",\n \"div\",\n \"math\",\n \"semantics\",\n \"annotation\",\n \"annotation-xml\",\n // token elements\n \"mtext\",\n \"mn\",\n \"mo\",\n \"mi\",\n \"ms\",\n \"mglyph\",\n // general layout\n \"mrow\",\n \"mfrac\",\n \"msqrt\",\n \"mroot\",\n \"mstyle\",\n \"merror\",\n \"mpadded\",\n \"mphantom\",\n \"menclose\",\n \"mspace\",\n // scripts & limits\n \"msub\",\n \"msup\",\n \"msubsup\",\n \"munder\",\n \"mover\",\n \"munderover\",\n \"mmultiscripts\",\n \"mprescripts\",\n // tables (matrices)\n \"mtable\",\n \"mtr\",\n \"mtd\",\n \"mlabeledtr\",\n];\n\nconst MATHML_ATTRIBUTES: Record<string, string[]> = {\n \"*\": [\"style\", \"className\", \"ariaHidden\", \"ariaLabel\"],\n span: [\"style\", \"className\"],\n math: [\"xmlns\", \"display\"],\n annotation: [\"encoding\"],\n \"annotation-xml\": [\"encoding\"],\n mfrac: [\"linethickness\"],\n mspace: [\"width\", \"height\", \"depth\"],\n mover: [\"accent\"],\n munder: [\"accentunder\"],\n mo: [\"fence\", \"form\", \"lspace\", \"rspace\", \"stretchy\", \"symmetric\"],\n};\n\nconst INLINE_RE = /\\$([^$\\n]+)\\$/g;\nconst BLOCK_RE = /\\$\\$([\\s\\S]+?)\\$\\$/g;\n\nexport function mathPlugin(opts: MathPluginOptions = {}): SlidePlugin {\n let peerDepMissing = false;\n\n return {\n name: \"math\",\n sanitizeSchemaExtension: {\n tagNames: MATHML_TAG_NAMES,\n attributes: MATHML_ATTRIBUTES,\n },\n async hastTransform(tree: HastRoot): Promise<HastRoot> {\n if (peerDepMissing) return tree;\n // EC-2: peer-dep guard. Throw on missing import — D16 absorbs into errors[].\n // biome-ignore lint/suspicious/noExplicitAny: katex default export untyped here\n let katex: any;\n // biome-ignore lint/suspicious/noExplicitAny: hast-util-from-html fromHtml untyped here\n let fromHtml: any;\n // biome-ignore lint/suspicious/noExplicitAny: unist-util-visit\n let visit: any;\n try {\n katex = (await import(\"katex\")).default;\n fromHtml = (await import(\"hast-util-from-html\")).fromHtml;\n visit = (await import(\"unist-util-visit\")).visit;\n } catch (e) {\n peerDepMissing = true;\n throw new Error(\n `[slide/plugins/math] peer-deps missing (katex / hast-util-from-html). Run: pnpm add katex hast-util-from-html. Math formulas remain as plain text. Error: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n\n // Walk text nodes, find $..$ and $$..$$ patterns, replace with rendered HTML.\n visit(\n tree,\n \"text\",\n (\n node: { value: string },\n idx: number | undefined,\n parent: { children: unknown[]; tagName?: string } | undefined,\n ) => {\n if (!parent || idx === undefined) return;\n // Skip if the text is inside a code / pre block — math shouldn't\n // accidentally trigger on `$amount = 100$` inside a code sample.\n if (parent.tagName === \"code\" || parent.tagName === \"pre\") return;\n const value = node.value;\n const replacements: Array<{\n start: number;\n end: number;\n html: string;\n }> = [];\n // Block math first ($$..$$) so we can mask its ranges before inline scan.\n for (const m of value.matchAll(BLOCK_RE)) {\n if (m.index === undefined) continue;\n try {\n replacements.push({\n start: m.index,\n end: m.index + m[0].length,\n html: katex.renderToString(m[1], {\n ...(opts.katexOptions ?? {}),\n displayMode: true,\n throwOnError: false,\n }),\n });\n } catch {\n // Malformed TeX — KaTeX may throw with throwOnError:true. Skip.\n }\n }\n for (const m of value.matchAll(INLINE_RE)) {\n if (m.index === undefined) continue;\n // Avoid re-processing inline matches that fall inside a block range.\n if (replacements.some((r) => (m.index ?? 0) >= r.start && (m.index ?? 0) < r.end))\n continue;\n try {\n replacements.push({\n start: m.index,\n end: m.index + m[0].length,\n html: katex.renderToString(m[1], {\n ...(opts.katexOptions ?? {}),\n displayMode: false,\n throwOnError: false,\n }),\n });\n } catch {\n // skip\n }\n }\n if (replacements.length === 0) return;\n replacements.sort((a, b) => a.start - b.start);\n const newChildren: unknown[] = [];\n let cursor = 0;\n for (const r of replacements) {\n if (r.start > cursor) {\n newChildren.push({ type: \"text\", value: value.slice(cursor, r.start) });\n }\n const fragment = fromHtml(r.html, { fragment: true });\n newChildren.push(...fragment.children);\n cursor = r.end;\n }\n if (cursor < value.length) {\n newChildren.push({ type: \"text\", value: value.slice(cursor) });\n }\n parent.children.splice(idx, 1, ...newChildren);\n return idx + newChildren.length;\n },\n );\n return tree;\n },\n };\n}\n"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "slide-plugin-mermaid",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Slide plugin · Mermaid",
|
|
6
|
+
"description": "Tier 2 Slide plugin that converts <code class=\"language-mermaid\"> blocks into Mermaid SVG diagrams (lazy-loaded). Peer-dep mermaid is opt-in.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"hast",
|
|
9
|
+
"mermaid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/slide.json",
|
|
13
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "components/primitives/slide/plugins/mermaid/index.tsx",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/ui/slide/plugins/mermaid/index.tsx",
|
|
20
|
+
"content": "import type { Element, Root as HastRoot } from \"hast\";\n/**\n * Slide rich-content plugin — Mermaid diagrams.\n *\n * Peer-dep: `mermaid` (optional, ~370 KB raw — lazy + opt-in mandatory).\n * Render is client-only because Mermaid measures DOM nodes during layout;\n * SSR / static render emits a labelled placeholder + source-code fallback.\n *\n * Pipeline contribution (D11 of the rich-content plan):\n * - `hastTransform`: detect `<pre><code class=\"language-mermaid\">…</code></pre>`\n * and replace with `<div class=\"theo-slide-mermaid-host\" data-theo-mermaid-source=\"…\">`.\n * - `components.div`: when div has `data-theo-mermaid-source`, render the\n * `<MermaidDiagram>` React component that lazy-imports `mermaid` and\n * injects the SVG via `dangerouslySetInnerHTML` after `mermaid.render()`.\n *\n * EC-4: complete SVG tag + attribute allow-list (~30 tags) so the rendered SVG\n * survives sanitize. Critical: without these, sanitize strips the SVG entirely.\n *\n * EC-10: SSR placeholder is distinguishable from a render error. Error path\n * surfaces the Mermaid source code in a `<pre>` so consumers can debug + the\n * print path still shows the code (PDF cannot run dynamic Mermaid).\n */\nimport { type FC, useEffect, useState } from \"react\";\nimport type { SlidePlugin } from \"@/components/ui/slide/plugin\";\n\n// EC-4: comprehensive SVG tag list (mermaid 11.x output across diagram types).\nconst SVG_TAG_NAMES = [\n \"div\",\n // Custom element used to bridge into our React renderer (mapped via components).\n \"theo-mermaid\",\n // root + grouping\n \"svg\",\n \"g\",\n \"defs\",\n \"use\",\n \"symbol\",\n \"marker\",\n \"pattern\",\n \"mask\",\n \"clipPath\",\n // shapes\n \"path\",\n \"rect\",\n \"circle\",\n \"ellipse\",\n \"line\",\n \"polyline\",\n \"polygon\",\n // text\n \"text\",\n \"tspan\",\n \"textPath\",\n \"title\",\n \"desc\",\n // gradients + filters (used by some flowchart themes)\n \"linearGradient\",\n \"radialGradient\",\n \"stop\",\n \"filter\",\n \"feGaussianBlur\",\n \"feOffset\",\n \"feColorMatrix\",\n \"feComponentTransfer\",\n \"feComposite\",\n \"feMerge\",\n \"feMergeNode\",\n \"feFlood\",\n // foreign content (HTML labels)\n \"foreignObject\",\n \"span\",\n \"br\",\n];\n\nconst SVG_ATTRIBUTES: Record<string, string[]> = {\n \"*\": [\n \"id\",\n \"className\",\n \"style\",\n \"transform\",\n \"fill\",\n \"stroke\",\n \"strokeWidth\",\n \"strokeDasharray\",\n \"strokeLinecap\",\n \"strokeLinejoin\",\n \"opacity\",\n \"fillOpacity\",\n \"strokeOpacity\",\n \"ariaLabel\",\n \"ariaHidden\",\n \"role\",\n ],\n svg: [\"xmlns\", \"viewBox\", \"width\", \"height\", \"preserveAspectRatio\", \"xmlnsXlink\", \"version\"],\n path: [\"d\", \"markerEnd\", \"markerStart\", \"markerMid\"],\n rect: [\"x\", \"y\", \"width\", \"height\", \"rx\", \"ry\"],\n circle: [\"cx\", \"cy\", \"r\"],\n ellipse: [\"cx\", \"cy\", \"rx\", \"ry\"],\n line: [\"x1\", \"y1\", \"x2\", \"y2\"],\n polyline: [\"points\"],\n polygon: [\"points\"],\n text: [\n \"x\",\n \"y\",\n \"dx\",\n \"dy\",\n \"textAnchor\",\n \"dominantBaseline\",\n \"fontSize\",\n \"fontFamily\",\n \"fontWeight\",\n ],\n tspan: [\"x\", \"y\", \"dx\", \"dy\"],\n textPath: [\"xlinkHref\", \"href\", \"startOffset\"],\n marker: [\"markerUnits\", \"markerWidth\", \"markerHeight\", \"refX\", \"refY\", \"orient\", \"viewBox\"],\n use: [\"xlinkHref\", \"href\", \"x\", \"y\", \"width\", \"height\"],\n linearGradient: [\"x1\", \"y1\", \"x2\", \"y2\", \"gradientUnits\"],\n radialGradient: [\"cx\", \"cy\", \"r\", \"fx\", \"fy\", \"gradientUnits\"],\n stop: [\"offset\", \"stopColor\", \"stopOpacity\"],\n foreignObject: [\"x\", \"y\", \"width\", \"height\"],\n div: [\"data-state\", \"data-theo-mermaid-source\", \"className\"],\n \"theo-mermaid\": [\"source\"],\n};\n\nexport interface MermaidPluginOptions {\n /** Mermaid theme name. Defaults to `\"default\"`. */\n theme?: \"default\" | \"forest\" | \"dark\" | \"neutral\" | \"base\";\n}\n\n/**\n * React component that renders Mermaid lazily on mount.\n *\n * SSR / pre-load: renders the source as `<pre>` + `role=\"img\"` for a11y.\n * Client: lazy-loads `mermaid`, calls `mermaid.render()`, then injects the\n * resulting SVG via `dangerouslySetInnerHTML` so React owns the DOM\n * subtree (avoids reconciliation crashes when state flips back to the\n * placeholder — `ref.innerHTML` would mutate under React's feet).\n *\n * The Mermaid output is a TRUSTED string from the `mermaid` lib itself (not\n * user-controlled markup) — the user-supplied source is the DSL, not raw HTML.\n * Using dangerouslySetInnerHTML here is a controlled escape hatch.\n *\n * Peer-dep guard: if `import(\"mermaid\")` fails, falls back to the same\n * pre-formatted source view + an `aria-label` hint (EC-10).\n */\nexport const MermaidDiagram: FC<{ source: string; theme?: string }> = ({ source, theme }) => {\n const [svg, setSvg] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n // Reset state when source/theme changes so the placeholder shows again\n // while the new diagram renders.\n setSvg(null);\n setError(null);\n (async () => {\n try {\n // biome-ignore lint/suspicious/noExplicitAny: mermaid default export untyped\n const mermaid: any = (await import(\"mermaid\")).default;\n if (cancelled) return;\n mermaid.initialize({ startOnLoad: false, theme: theme ?? \"default\" });\n const id = `theo-mmd-${Math.random().toString(36).slice(2, 10)}`;\n const result = await mermaid.render(id, source);\n if (!cancelled) {\n setSvg(result.svg);\n }\n } catch (e) {\n if (cancelled) return;\n const msg = e instanceof Error ? e.message : String(e);\n // Recognize the various module-resolution failure messages emitted by\n // Node, Vite, Rollup, Webpack, Vitest, and browser dynamic-import.\n const moduleNotFound =\n msg.includes(\"Cannot find module\") ||\n msg.includes(\"Could not resolve\") ||\n msg.includes(\"Failed to fetch\") ||\n msg.includes(\"Failed to resolve module\") ||\n msg.includes(\"ERR_MODULE_NOT_FOUND\");\n if (moduleNotFound) {\n setError(\"Mermaid not installed. Run: pnpm add mermaid\");\n } else {\n setError(`Mermaid render failed: ${msg}`);\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [source, theme]);\n\n if (error) {\n return (\n <div\n data-theo-slide-mermaid\n data-state=\"error\"\n role=\"img\"\n aria-label={error}\n className=\"theo-slide-mermaid-host\"\n >\n <pre style={{ fontSize: \"0.8em\", opacity: 0.7, whiteSpace: \"pre-wrap\" }}>{source}</pre>\n </div>\n );\n }\n if (svg) {\n // SVG is React-owned via dangerouslySetInnerHTML so the next state change\n // (e.g. new `source` prop → back to placeholder) can unmount cleanly.\n return (\n <div\n data-theo-slide-mermaid\n data-state=\"ready\"\n role=\"img\"\n aria-label=\"Mermaid diagram\"\n className=\"theo-slide-mermaid-host\"\n // biome-ignore lint/security/noDangerouslySetInnerHtml: mermaid.render() output is trusted (lib-generated SVG, not user markup)\n dangerouslySetInnerHTML={{ __html: svg }}\n />\n );\n }\n return (\n <div\n data-theo-slide-mermaid\n data-state=\"loading\"\n role=\"img\"\n aria-label=\"Mermaid diagram\"\n className=\"theo-slide-mermaid-host\"\n >\n <pre style={{ fontSize: \"0.7em\", opacity: 0.4, whiteSpace: \"pre-wrap\" }}>{source}</pre>\n </div>\n );\n};\n\nexport function mermaidPlugin(opts: MermaidPluginOptions = {}): SlidePlugin {\n return {\n name: \"mermaid\",\n sanitizeSchemaExtension: {\n tagNames: SVG_TAG_NAMES,\n attributes: SVG_ATTRIBUTES,\n },\n components: {\n // The hastTransform below replaces mermaid <pre> with a `<div>` carrying\n // `data-theo-mermaid-source`. We can't override `div` globally — instead,\n // we use a custom tagName `theo-mermaid` that the React renderer maps.\n \"theo-mermaid\": ((props: { source?: string }) => (\n <MermaidDiagram source={props.source ?? \"\"} theme={opts.theme} />\n )) as FC<unknown>,\n },\n async hastTransform(tree: HastRoot): Promise<HastRoot> {\n // unist-util-visit is a peer-dep of the slide stack — should always be\n // present. Guard anyway per EC-2.\n // biome-ignore lint/suspicious/noExplicitAny: unist-util-visit untyped at runtime\n let visit: any;\n try {\n visit = (await import(\"unist-util-visit\")).visit;\n } catch (e) {\n throw new Error(`[slide/plugins/mermaid] peer-dep 'unist-util-visit' missing: ${e}`);\n }\n visit(\n tree,\n \"element\",\n (\n node: Element,\n _idx: number | undefined,\n parent: { tagName?: string; type?: string; children?: unknown[] } | undefined,\n ) => {\n if (node.tagName !== \"code\") return;\n const className = (node.properties?.className as string[] | undefined) ?? [];\n if (!className.includes(\"language-mermaid\")) return;\n if (!parent || parent.tagName !== \"pre\") return;\n const codeNode = node.children?.[0];\n const source = codeNode && codeNode.type === \"text\" ? codeNode.value : \"\";\n // Replace the entire <pre> with our custom element. hast-util-to-jsx-runtime\n // maps element tagName to a React component if found in `components` map.\n Object.assign(parent, {\n type: \"element\",\n tagName: \"theo-mermaid\",\n // Note: `source` is passed as a non-standard property; the React\n // component picks it up. We avoid `data-*` attributes here so we\n // don't have to whitelist them in sanitize.\n properties: { source },\n children: [],\n });\n },\n );\n return tree;\n },\n };\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "slide-plugin-shiki",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Slide plugin · Shiki",
|
|
6
|
+
"description": "Tier 2 Slide plugin that pre-renders fenced code blocks with Shiki's dual-theme highlighter. Peer-dep shiki is opt-in.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"hast",
|
|
9
|
+
"shiki"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/slide.json",
|
|
13
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "components/primitives/slide/plugins/shiki/index.ts",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/ui/slide/plugins/shiki/index.ts",
|
|
20
|
+
"content": "/**\n * Slide rich-content plugin — Shiki syntax highlighting.\n *\n * Lazy + opt-in (ADR D9, D13 of the rich-content plan). Peer-dep `shiki` is\n * declared optional in `package.json`; the plugin guards every dynamic import\n * so a missing peer-dep is reported (D16) rather than crashing the slide.\n *\n * Pipeline contribution:\n * - `hastTransform`: walks `<pre><code class=\"language-XXX\">` nodes, replaces\n * them with the pre-rendered `<pre><code>` tree emitted by Shiki.\n * - `sanitizeSchemaExtension`: allows `<span>` with `style` and `class`\n * (Shiki emits inline styles for tokens).\n *\n * Performance:\n * - Highlighter is created lazily on first use and cached.\n * - Grammars listed in `langs` are pre-loaded once; unknown langs pass-through\n * as plain `<pre><code>` (defaultSchema allows that).\n *\n * @example\n * import { shikiPlugin } from \"@theokit/ui/slide/plugins/shiki\";\n * import \"shiki\"; // installed as peer-dep\n * <Slide markdown={md} plugins={[shikiPlugin({ langs: [\"ts\",\"python\"] })]} />\n */\nimport type { Element, Root as HastRoot } from \"hast\";\nimport type { SlidePlugin } from \"@/components/ui/slide/plugin\";\n\nexport interface ShikiPluginOptions {\n /** Dual-theme map — light/dark Shiki theme names. Defaults to `github-light` / `github-dark`. */\n themes?: { light: string; dark: string };\n /** Languages to pre-load. Unknown languages pass-through. Defaults to a tech-stack baseline. */\n langs?: string[];\n}\n\nconst DEFAULT_LANGS = [\n \"ts\",\n \"tsx\",\n \"js\",\n \"jsx\",\n \"python\",\n \"go\",\n \"rust\",\n \"java\",\n \"json\",\n \"yaml\",\n \"bash\",\n \"shell\",\n \"html\",\n \"css\",\n \"sql\",\n \"markdown\",\n];\n\nexport function shikiPlugin(opts: ShikiPluginOptions = {}): SlidePlugin {\n const themes = opts.themes ?? { light: \"github-light\", dark: \"github-dark\" };\n const langs = opts.langs ?? DEFAULT_LANGS;\n // biome-ignore lint/suspicious/noExplicitAny: shiki has no exported type for `Highlighter` at the top level\n let highlighter: any = null;\n let peerDepMissing = false;\n\n // biome-ignore lint/suspicious/noExplicitAny: shiki Highlighter is untyped here\n async function getHighlighter(): Promise<any> {\n if (highlighter) return highlighter;\n if (peerDepMissing) return null;\n try {\n const shiki = await import(\"shiki\");\n highlighter = await shiki.createHighlighter({\n themes: [themes.light, themes.dark],\n langs,\n });\n return highlighter;\n } catch (e) {\n peerDepMissing = true;\n // Surface via D16 path (composePlugins catches → PLUGIN_ERROR).\n throw new Error(\n `[slide/plugins/shiki] peer-dep 'shiki' not installed or failed to load. Run: pnpm add shiki. Original: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n\n return {\n name: \"shiki\",\n sanitizeSchemaExtension: {\n tagNames: [\"span\", \"pre\", \"code\"],\n // Shiki emits inline `style` and `class` on tokens; \"*\" applies to every tag.\n attributes: {\n \"*\": [\"style\", \"className\"],\n pre: [\"style\", \"className\", \"tabIndex\"],\n code: [\"style\", \"className\"],\n span: [\"style\", \"className\"],\n },\n },\n async hastTransform(tree: HastRoot): Promise<HastRoot> {\n const hl = await getHighlighter();\n if (!hl) return tree; // peer-dep missing AND we've already reported it once\n const { visit } = await import(\"unist-util-visit\");\n const { fromHtml } = await import(\"hast-util-from-html\");\n // Walk: find <code class=\"language-XXX\">, replace its parent <pre> subtree\n // with the highlighted output (which is also a <pre><code>...</code></pre>).\n // biome-ignore lint/suspicious/noExplicitAny: hast unist-util-visit untyped here\n visit(tree, \"element\", (node: Element, _idx: number | undefined, parent: any) => {\n if (node.tagName !== \"code\") return;\n const className = (node.properties?.className as string[] | undefined) ?? [];\n const langClass = className.find((c) => c.startsWith(\"language-\"));\n if (!langClass) return;\n const lang = langClass.replace(\"language-\", \"\");\n if (!langs.includes(lang)) return;\n const first = node.children?.[0];\n const codeText = first && first.type === \"text\" ? first.value : \"\";\n let html: string;\n try {\n html = hl.codeToHtml(codeText, { lang, themes });\n } catch {\n // Shiki failed on this snippet (e.g. malformed input). Leave the plain\n // <pre><code> in place — the consumer still gets readable text.\n return;\n }\n const newTree = fromHtml(html, { fragment: true });\n const top = newTree.children[0];\n if (parent?.tagName === \"pre\" && parent.children?.length === 1 && top) {\n Object.assign(parent, top);\n }\n });\n return tree;\n },\n };\n}\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|