@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,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "slide",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Slide",
|
|
6
|
+
"description": "View-only primitive that renders Markdown + YAML frontmatter as a themed slide surface (Marp-inspired). Sanitized hast pipeline, two built-in themes, opt-in plugin system. Subpath-isolated bundle in @theokit/ui.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"hast",
|
|
9
|
+
"hast-util-from-html",
|
|
10
|
+
"hast-util-sanitize",
|
|
11
|
+
"hast-util-to-jsx-runtime",
|
|
12
|
+
"mdast",
|
|
13
|
+
"mdast-util-from-markdown",
|
|
14
|
+
"mdast-util-gfm",
|
|
15
|
+
"mdast-util-to-hast",
|
|
16
|
+
"micromark-extension-gfm",
|
|
17
|
+
"unist-util-visit",
|
|
18
|
+
"unist-util-visit-parents",
|
|
19
|
+
"yaml",
|
|
20
|
+
"zod"
|
|
21
|
+
],
|
|
22
|
+
"registryDependencies": [
|
|
23
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
{
|
|
27
|
+
"path": "components/primitives/slide/slide.tsx",
|
|
28
|
+
"type": "registry:ui",
|
|
29
|
+
"target": "components/ui/slide/slide.tsx",
|
|
30
|
+
"content": "import {\n type FC,\n type ReactElement,\n type ReactNode,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n/**\n * `<Slide>` — view-only primitive that renders markdown + YAML frontmatter\n * into a themed, fixed-aspect surface. Lives in the isolated subpath\n * `@theokit/ui/slide`.\n *\n * See RFC 0002 (`docs/rfcs/0002-slide.md`) and the plan in\n * `.claude/knowledge-base/plans/slide-view-primitive-plan.md`.\n *\n * SSR note: initial render returns the section wrapper; the parsed React tree\n * fills in client-side via the useEffect → `parseSlide` chain. Consumers\n * wrapping in Suspense / skeleton can mitigate visible jump.\n *\n * Performance tip: pass a memoized `onValidationError` (via `useCallback`)\n * and a memoized `components` map. Inline arrows recreate on each render\n * and cause re-parses.\n */\nimport { type ParsedSlide, parseSlide } from \"@/components/ui/slide/parse\";\nimport type { SlidePlugin } from \"@/components/ui/slide/plugin\";\nimport type { SlideValidationError } from \"@/components/ui/slide/schema\";\nimport type { SlideTheme } from \"@/components/ui/slide/themes/index\";\nimport { useSlideFit } from \"@/components/ui/slide/use-slide-fit\";\n\nexport type { SlideTheme } from \"@/components/ui/slide/themes/index\";\nexport type {\n SlideValidationError,\n SlideValidationErrorCode,\n} from \"@/components/ui/slide/schema\";\nexport type { SlidePlugin } from \"@/components/ui/slide/plugin\";\n\nexport interface SlideAspectRatio {\n width: number;\n height: number;\n}\n\nexport interface SlideProps {\n /**\n * Slide markdown. CommonMark + GFM + optional YAML frontmatter delimited\n * by `---`. Top-level horizontal rules (`---` on their own line outside\n * frontmatter) imply a deck split and trigger `MULTIPLE_SLIDES`; only the\n * first slide is rendered.\n *\n * Note: `<figure>`/`<figcaption>` tags are stripped by the default\n * sanitize schema (D8). Use `<img>` directly for captionless images.\n */\n markdown: string;\n /** Theme name. Defaults to `\"default\"`. */\n theme?: SlideTheme;\n /**\n * Aspect ratio of the logical canvas. Default `\"16:9\"` → 1280×720.\n * Custom `{ width, height }` accepted; zero/negative/NaN fallback to 16:9.\n */\n aspectRatio?: \"16:9\" | \"4:3\" | SlideAspectRatio;\n /** Lower clamp for container-fit scale. Default 0.1. */\n minScale?: number;\n /** Upper clamp for container-fit scale. Default 4. */\n maxScale?: number;\n /** Best-effort callback invoked in useEffect when validation/sanitize emits errors. */\n onValidationError?: (errors: SlideValidationError[]) => void;\n /** Override individual element renderers (passed to hast-util-to-jsx-runtime). */\n // biome-ignore lint/suspicious/noExplicitAny: third-party component override map\n components?: Record<string, FC<any>>;\n /**\n * Rich-content plugins (Tier 2). Each plugin transforms the mdast/hast tree\n * or adds React component overrides. Pass MEMOIZED arrays to avoid re-parses\n * on every render (D15).\n *\n * @example\n * const plugins = useMemo(() => [emojiPlugin(), shikiPlugin()], []);\n * <Slide markdown={md} plugins={plugins} />\n */\n plugins?: SlidePlugin[];\n /** Accessible label for the slide. Defaults to `\"Slide\"`. */\n \"aria-label\"?: string;\n /** Optional class applied to the outer host element (sizing/positioning hook). */\n className?: string;\n}\n\nconst ASPECT_PRESETS: Record<\"16:9\" | \"4:3\", SlideAspectRatio> = {\n \"16:9\": { width: 1280, height: 720 },\n \"4:3\": { width: 960, height: 720 },\n};\n\n/**\n * Resolve aspectRatio prop to concrete dimensions. Invalid custom values\n * (zero, negative, non-finite) silently fall back to 16:9 — surfaced via\n * `INVALID_ASPECT_RATIO` error in `onValidationError`. ADR D14.\n */\nfunction resolveCanvas(ar: SlideProps[\"aspectRatio\"]): {\n width: number;\n height: number;\n invalid?: boolean;\n} {\n if (!ar || ar === \"16:9\") return ASPECT_PRESETS[\"16:9\"];\n if (ar === \"4:3\") return ASPECT_PRESETS[\"4:3\"];\n if (\n ar.width <= 0 ||\n ar.height <= 0 ||\n !Number.isFinite(ar.width) ||\n !Number.isFinite(ar.height)\n ) {\n return { ...ASPECT_PRESETS[\"16:9\"], invalid: true };\n }\n return ar;\n}\n\nexport const Slide: FC<SlideProps> = ({\n markdown,\n theme = \"default\",\n aspectRatio = \"16:9\",\n minScale,\n maxScale,\n onValidationError,\n components,\n plugins,\n className,\n \"aria-label\": ariaLabel = \"Slide\",\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const canvas = useMemo(() => resolveCanvas(aspectRatio), [aspectRatio]);\n const scale = useSlideFit(containerRef, canvas.width, canvas.height, {\n minScale,\n maxScale,\n });\n\n const [parsed, setParsed] = useState<ParsedSlide | null>(null);\n\n // Surface INVALID_ASPECT_RATIO immediately when the prop is invalid.\n // Independent of markdown parsing so consumers see the signal even with\n // an empty markdown payload.\n useEffect(() => {\n if (canvas.invalid && onValidationError) {\n onValidationError([\n {\n code: \"INVALID_ASPECT_RATIO\",\n path: [\"aspectRatio\"],\n message:\n \"aspectRatio width/height must be positive finite numbers; falling back to 16:9.\",\n got: aspectRatio,\n },\n ]);\n }\n }, [canvas.invalid, onValidationError, aspectRatio]);\n\n // EC-7 / version counter — prevents older parses from overwriting newer\n // ones on rapid prop changes.\n const versionRef = useRef(0);\n useEffect(() => {\n const myVersion = ++versionRef.current;\n let cancelled = false;\n parseSlide(markdown, { components, plugins }).then(\n (result) => {\n if (cancelled || myVersion !== versionRef.current) return;\n setParsed(result);\n if (result.errors.length > 0 && onValidationError) {\n onValidationError(result.errors);\n }\n },\n (err: unknown) => {\n // Defensive: parseSlide promises to never throw, but if a runtime\n // failure escapes (e.g. peer-dep missing), surface a synthetic error\n // and keep the slide visible with empty body.\n if (cancelled || myVersion !== versionRef.current) return;\n if (onValidationError) {\n onValidationError([\n {\n code: \"INVALID_FRONTMATTER\",\n path: [],\n message: err instanceof Error ? err.message : \"parseSlide rejected.\",\n },\n ]);\n }\n setParsed({\n frontmatter: {},\n tree: emptyFragment(),\n errors: [],\n truncated: false,\n });\n },\n );\n return () => {\n cancelled = true;\n };\n }, [markdown, components, plugins, onValidationError]);\n\n const treeNode: ReactNode = parsed?.tree ?? null;\n const slideThemeAttr: SlideTheme = theme;\n\n // Tier 1 — frontmatter-driven attributes & overlays.\n const fm = parsed?.frontmatter ?? {};\n const layout = fm.layout;\n // Background precedence: explicit frontmatter > Marpit  (D18 / EC-5).\n const bgUrl = fm.backgroundImage ?? parsed?.extractedBackground?.url;\n const bgModifier = parsed?.extractedBackground?.modifier;\n const bgGradient = fm.backgroundGradient;\n const headerText = fm.header;\n const footerText = fm.footer;\n const paginateValue = fm.paginate;\n const showPaginate = paginateValue === true;\n\n // Background style: gradient > image > inherited.\n const backgroundImageStyle: string | undefined = bgGradient\n ? bgGradient\n : bgUrl\n ? `url(\"${bgUrl}\")`\n : undefined;\n\n return (\n <div\n ref={containerRef}\n className={[\"theo-slide-host\", className].filter(Boolean).join(\" \")}\n data-theo-slide-host\n style={{\n position: \"relative\",\n overflow: \"hidden\",\n width: \"100%\",\n height: \"100%\",\n }}\n >\n {/*\n Section is taken out of normal flow with position:absolute so its\n 1280×720 layout box doesn't inflate the host. `translate(-50%, -50%)`\n anchors the section's center to the host's center; the scale applies\n relative to that same origin. Mirrors Reveal.js transformSlides()\n (see `.claude/knowledge-base/reference/slide.md` §4.5 / §14.2).\n */}\n <section\n aria-roledescription=\"slide\"\n aria-label={ariaLabel}\n className=\"theo-slide\"\n data-theo-slide-theme={slideThemeAttr}\n data-theo-slide-layout={layout ?? \"default\"}\n data-theo-slide-bg-modifier={bgModifier}\n style={{\n position: \"absolute\",\n top: \"50%\",\n left: \"50%\",\n width: canvas.width,\n height: canvas.height,\n transform: `translate(-50%, -50%) scale(${scale})`,\n transformOrigin: \"center\",\n padding: \"var(--theo-slide-padding, 64px)\",\n color: \"inherit\",\n // Background fills the canvas. When neither image nor gradient is set,\n // the slide inherits the parent surface (same pattern as Whiteboard).\n background: \"transparent\",\n // Only set backgroundImage + ancillary props when one is provided —\n // otherwise the shorthand `background: transparent` is preserved as-is\n // (important for the inherit-from-parent guarantee).\n ...(backgroundImageStyle\n ? {\n backgroundImage: backgroundImageStyle,\n backgroundSize: bgModifier === \"fit\" ? \"contain\" : \"cover\",\n backgroundPosition: \"center\",\n backgroundRepeat: \"no-repeat\",\n }\n : {}),\n fontFamily:\n \"var(--theo-slide-font-family, system-ui, -apple-system, 'Segoe UI', sans-serif)\",\n fontSize: \"var(--theo-slide-font-base, 28px)\",\n lineHeight: 1.5,\n boxSizing: \"border-box\",\n overflow: \"hidden\",\n }}\n >\n {headerText ? (\n <div className=\"theo-slide-header\" aria-hidden=\"true\">\n {headerText}\n </div>\n ) : null}\n {treeNode}\n {footerText ? (\n <div className=\"theo-slide-footer\" aria-hidden=\"true\">\n {footerText}\n </div>\n ) : null}\n {showPaginate ? (\n <div className=\"theo-slide-paginate\" aria-hidden=\"true\">\n 1\n </div>\n ) : null}\n </section>\n </div>\n );\n};\n\n/** Empty React fragment placeholder used when parseSlide fails fatally. */\nfunction emptyFragment(): ReactElement {\n // Cheap fallback that does not require importing Fragment from react/jsx-runtime.\n // Using a real element keeps the type as ReactElement (not ReactNode).\n return { type: \"span\", props: { children: null }, key: null } as unknown as ReactElement;\n}\n"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "components/primitives/slide/parse.ts",
|
|
34
|
+
"type": "registry:ui",
|
|
35
|
+
"target": "components/ui/slide/parse.ts",
|
|
36
|
+
"content": "import type { Root as HastRoot } from \"hast\";\n/**\n * Markdown → React tree pipeline for the Slide primitive.\n *\n * parseSlide(markdown)\n * ├─ validateSlide(markdown) → { frontmatter, body, errors? }\n * ├─ parseBody(body) → mdast Root (micromark + GFM)\n * ├─ mdastToHast(mdastTree) → hast Root (allowDangerousHtml: false)\n * ├─ sanitizeHast(hastTree) → hast Root + BANNED_TAG diff (ADR D13)\n * └─ hastToReact(hastTree) → React tree (jsx-runtime, no innerHTML)\n *\n * Every transform is composed of lazy-imported peer-deps so the slide bundle\n * stays under budget and never vendors the markdown stack into the barrel\n * (ADR D2 / D3 of the slide plan).\n */\nimport type { Root as MdastRoot } from \"mdast\";\nimport type { ReactElement } from \"react\";\nimport { detectAlerts } from \"@/components/ui/slide/alerts\";\nimport { extractMarpitBackgrounds } from \"@/components/ui/slide/marpit-bg\";\nimport { type MergedSanitizeExtensions, type SlidePlugin, composePlugins } from \"@/components/ui/slide/plugin\";\nimport { collectTagCounts, getSlideSanitizeSchema } from \"@/components/ui/slide/sanitize\";\nimport { type SlideFrontmatter, type SlideValidationError, sanitizeBgUrl } from \"@/components/ui/slide/schema\";\nimport { validateSlide } from \"@/components/ui/slide/validate\";\n\nexport interface ParseSlideOptions {\n /** Override individual element renderers (passed to hast-util-to-jsx-runtime). */\n components?: Record<string, unknown>;\n /** Rich-content plugins (Tier 2). Order matters — D13 / RFC 0004. */\n plugins?: SlidePlugin[];\n}\n\nexport interface ExtractedBackground {\n /** Sanitized URL (http/https only). */\n url: string;\n /** Optional Marpit modifier: `cover` | `fit` | `left` | `right`. */\n modifier?: \"cover\" | \"fit\" | \"left\" | \"right\";\n}\n\nexport interface ParsedSlide {\n frontmatter: SlideFrontmatter;\n /** React element renderable as the body of a slide. Always defined (may be empty Fragment). */\n tree: ReactElement;\n /** Validation + sanitize errors collected during parsing. Empty on success. */\n errors: SlideValidationError[];\n /** True when the input contained a top-level thematic break and only the first slide was rendered. */\n truncated: boolean;\n /**\n * Background image extracted from Marpit `` syntax (D18 / EC-5).\n * Precedence: `frontmatter.backgroundImage` > `extractedBackground.url`.\n */\n extractedBackground?: ExtractedBackground;\n}\n\nexport async function parseBody(body: string): Promise<MdastRoot> {\n const [{ fromMarkdown }, { gfmFromMarkdown }, { gfm }] = await Promise.all([\n import(\"mdast-util-from-markdown\"),\n import(\"mdast-util-gfm\"),\n import(\"micromark-extension-gfm\"),\n ]);\n return fromMarkdown(body, {\n extensions: [gfm()],\n mdastExtensions: [gfmFromMarkdown()],\n });\n}\n\nexport async function mdastToHast(tree: MdastRoot): Promise<HastRoot> {\n const { toHast } = await import(\"mdast-util-to-hast\");\n const hast = toHast(tree, { allowDangerousHtml: false });\n if (!hast || hast.type !== \"root\") {\n return { type: \"root\", children: hast ? [hast] : [] } as HastRoot;\n }\n return hast as HastRoot;\n}\n\nexport async function sanitizeHast(\n tree: HastRoot,\n extensions?: MergedSanitizeExtensions,\n): Promise<{ tree: HastRoot; bannedTags: string[] }> {\n const schema = await getSlideSanitizeSchema(extensions);\n const { sanitize } = await import(\"hast-util-sanitize\");\n const preCount = collectTagCounts(tree);\n const safe = sanitize(tree, schema);\n const safeRoot: HastRoot =\n safe.type === \"root\" ? (safe as HastRoot) : ({ type: \"root\", children: [safe] } as HastRoot);\n const postCount = collectTagCounts(safeRoot);\n const bannedTags: string[] = [];\n for (const [tag, before] of preCount) {\n const after = postCount.get(tag) ?? 0;\n if (after < before) {\n // Push once per stripped tag name; agents can self-correct from this signal.\n bannedTags.push(tag);\n }\n }\n return { tree: safeRoot, bannedTags };\n}\n\nexport async function hastToReact(\n tree: HastRoot,\n components?: Record<string, unknown>,\n): Promise<ReactElement> {\n const { Fragment, jsx, jsxs } = await import(\"react/jsx-runtime\");\n const { toJsxRuntime } = await import(\"hast-util-to-jsx-runtime\");\n return toJsxRuntime(tree, {\n Fragment,\n jsx,\n jsxs,\n components,\n }) as ReactElement;\n}\n\n/**\n * Public entry point. Validates → parses → sanitizes → returns a React tree.\n *\n * Never throws on input. Errors (validation, BANNED_TAG) are collected into\n * `errors[]` so callers can show them via `onValidationError`.\n */\nexport async function parseSlide(\n markdown: string,\n opts: ParseSlideOptions = {},\n): Promise<ParsedSlide> {\n const errors: SlideValidationError[] = [];\n let frontmatter: SlideFrontmatter = {};\n let body = markdown;\n let truncated = false;\n\n const validation = await validateSlide(markdown);\n if (validation.ok) {\n frontmatter = validation.input.frontmatter;\n body = validation.input.body;\n errors.push(...validation.errors);\n truncated = validation.errors.some((e) => e.code === \"MULTIPLE_SLIDES\");\n } else {\n errors.push(...validation.errors);\n // Best-effort recovery: render the body as if frontmatter was absent.\n // This preserves \"never throw, always render something\" contract.\n body = markdown;\n }\n\n const compose = composePlugins(opts.plugins ?? []);\n\n const rawMdast = await parseBody(body);\n // Tier 1 — GFM alerts (D3): in-tree post-process, no plugin needed.\n // Runs BEFORE plugin mdastTransforms so plugins observing the tree see the\n // normalized aside shape (consistent with how `> [!NOTE]` is treated as\n // native GFM by consumers).\n detectAlerts(rawMdast);\n // Tier 1 — Marpit  (D18 / EC-5): extract before plugins run so the\n // tree handed to plugins is free of bg directives.\n const { tree: mdastNoBg, background: marpitBg } = extractMarpitBackgrounds(rawMdast);\n let extractedBackground: ExtractedBackground | undefined;\n if (marpitBg) {\n const safeUrl = sanitizeBgUrl(marpitBg.url);\n if (safeUrl) {\n extractedBackground = { url: safeUrl, modifier: marpitBg.modifier };\n } else {\n errors.push({\n code: \"MARPIT_BG_UNSAFE_URL\",\n path: [],\n message: \"Marpit  rejected: unsafe scheme or malformed URL.\",\n got: marpitBg.url.slice(0, 80),\n });\n }\n }\n const transformedMdast = await compose.runMdast(mdastNoBg, errors);\n const rawHast = await mdastToHast(transformedMdast);\n const transformedHast = await compose.runHast(rawHast, errors);\n\n // D17 / EC-3: merge plugin sanitize-schema extensions with defaultSchema so\n // plugins emitting non-default tags (Shiki spans, KaTeX MathML, Mermaid SVG)\n // survive the security barrier. Sanitize ALWAYS runs.\n const sanitizeExtensions = compose.mergedSanitizeExtensions();\n const { tree: safeTree, bannedTags } = await sanitizeHast(transformedHast, sanitizeExtensions);\n for (const tag of bannedTags) {\n errors.push({\n code: \"BANNED_TAG\",\n path: [\"body\"],\n message: `Tag <${tag}> was stripped by the slide sanitizer.`,\n got: tag,\n });\n }\n\n // D2 / D13: plugin component overrides merge on top of consumer's components;\n // plugin definitions win on conflict (last-write-wins in mergedComponents).\n const mergedComponents = {\n ...(opts.components ?? {}),\n ...compose.mergedComponents(),\n };\n const tree = await hastToReact(safeTree, mergedComponents);\n\n return { frontmatter, tree, errors, truncated, extractedBackground };\n}\n"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "components/primitives/slide/validate.ts",
|
|
40
|
+
"type": "registry:ui",
|
|
41
|
+
"target": "components/ui/slide/validate.ts",
|
|
42
|
+
"content": "import { extractFrontmatter } from \"@/components/ui/slide/frontmatter\";\n/**\n * Async validator for slide markdown input.\n *\n * Returns `Promise<ValidationResult>` (ADR D11 — sync impossible because\n * `yaml` and `mdast-util-from-markdown` are lazy-imported peer-deps).\n *\n * Pipeline:\n * 1. Strip BOM + extract frontmatter (`frontmatter.ts`).\n * 2. Reject `FRONTMATTER_TOO_LARGE` early if raw > 10 KB (ADR D14).\n * 3. Parse YAML in safe mode → validate against `slideFrontmatter` (Zod).\n * 4. Validate body length → emit `CONTENT_TOO_LARGE` if > 50 KB.\n * 5. Detect multi-slide via mdast `thematicBreak` walk (ADR D12) — not\n * a regex, so `---` inside fenced code blocks does not false-positive.\n * 6. Return `{ ok, input, errors[] }`.\n *\n * Errors are structured so an LLM can self-correct from the callback.\n */\nimport {\n type SlideFrontmatter,\n type SlideInput,\n type SlideValidationError,\n slideFrontmatter,\n} from \"@/components/ui/slide/schema\";\n\nexport type ValidationResult =\n | { ok: true; input: SlideInput; errors: SlideValidationError[] }\n | { ok: false; errors: SlideValidationError[] };\n\ninterface ZodLikeIssue {\n path: ReadonlyArray<string | number>;\n message: string;\n code: string;\n received?: unknown;\n}\n\nconst MAX_BODY = 50_000;\n\nfunction valueAtPath(input: unknown, path: ReadonlyArray<string | number>): unknown {\n let cursor: unknown = input;\n for (const segment of path) {\n if (cursor === null || typeof cursor !== \"object\") return undefined;\n cursor = (cursor as Record<string | number, unknown>)[segment];\n }\n return cursor;\n}\n\nfunction formatZodIssue(\n issue: ZodLikeIssue,\n input: unknown,\n pathPrefix: (string | number)[],\n): SlideValidationError {\n const error: SlideValidationError = {\n path: [...pathPrefix, ...issue.path],\n message: issue.message,\n code: \"INVALID_FRONTMATTER\",\n };\n if (\"received\" in issue && issue.received !== undefined) {\n error.got = issue.received;\n } else if (issue.path.length > 0) {\n const value = valueAtPath(input, issue.path);\n if (value !== undefined) error.got = value;\n }\n return error;\n}\n\nasync function detectMultiSlide(body: string): Promise<{\n multi: boolean;\n firstSlideBody: string;\n}> {\n // mdast-based — regex would false-positive on `---` inside fenced code blocks.\n // Cost: one extra parse. Cheap; would be parsed again in `parse.ts` anyway.\n const { fromMarkdown } = await import(\"mdast-util-from-markdown\");\n const tree = fromMarkdown(body);\n const hrIdx = tree.children.findIndex((n) => n.type === \"thematicBreak\");\n if (hrIdx === -1) {\n return { multi: false, firstSlideBody: body };\n }\n const firstHr = tree.children[hrIdx];\n const offset = firstHr?.position?.start.offset;\n if (typeof offset !== \"number\") {\n return { multi: true, firstSlideBody: body };\n }\n return { multi: true, firstSlideBody: body.slice(0, offset) };\n}\n\n/**\n * Validate the markdown input. Returns errors structured for LLM self-correction.\n *\n * Async because frontmatter YAML parsing and mdast multi-slide detection both\n * use lazy-imported peer-deps (yaml + mdast-util-from-markdown).\n */\nexport async function validateSlide(markdown: string): Promise<ValidationResult> {\n const errors: SlideValidationError[] = [];\n const extracted = extractFrontmatter(markdown);\n\n if (extracted.tooLarge) {\n errors.push({\n code: \"FRONTMATTER_TOO_LARGE\",\n path: [],\n message: `Raw frontmatter exceeds ${10_240} bytes.`,\n got: extracted.rawFrontmatter?.length,\n });\n return { ok: false, errors };\n }\n\n let frontmatter: SlideFrontmatter = {};\n if (extracted.rawFrontmatter !== null) {\n let parsed: unknown;\n try {\n const yaml = await import(\"yaml\");\n parsed = yaml.parse(extracted.rawFrontmatter);\n } catch (e) {\n errors.push({\n code: \"INVALID_FRONTMATTER\",\n path: [],\n message: e instanceof Error ? e.message : \"YAML parse failed.\",\n got: extracted.rawFrontmatter,\n });\n return { ok: false, errors };\n }\n if (parsed === null || parsed === undefined) {\n // empty frontmatter — treat as {}\n frontmatter = {};\n } else if (typeof parsed !== \"object\" || Array.isArray(parsed)) {\n errors.push({\n code: \"INVALID_FRONTMATTER\",\n path: [],\n message: \"Frontmatter must be a YAML mapping (object).\",\n got: parsed,\n });\n return { ok: false, errors };\n } else {\n const result = slideFrontmatter.safeParse(parsed);\n if (!result.success) {\n for (const issue of result.error.issues) {\n errors.push(formatZodIssue(issue as unknown as ZodLikeIssue, parsed, []));\n }\n return { ok: false, errors };\n }\n frontmatter = result.data;\n }\n }\n\n let body = extracted.body;\n\n if (body.length > MAX_BODY) {\n errors.push({\n code: \"CONTENT_TOO_LARGE\",\n path: [\"body\"],\n message: `Body exceeds ${MAX_BODY} characters.`,\n got: body.length,\n });\n return { ok: false, errors };\n }\n\n const multi = await detectMultiSlide(body);\n if (multi.multi) {\n body = multi.firstSlideBody;\n errors.push({\n code: \"MULTIPLE_SLIDES\",\n path: [\"body\"],\n message:\n \"Input contains a top-level thematic break (---). <Slide> renders single-slide markdown only. Only the first slide was rendered.\",\n });\n }\n\n return { ok: true, input: { frontmatter, body }, errors };\n}\n"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"path": "components/primitives/slide/sanitize.ts",
|
|
46
|
+
"type": "registry:ui",
|
|
47
|
+
"target": "components/ui/slide/sanitize.ts",
|
|
48
|
+
"content": "/**\n * Sanitize schema for slide body hast trees.\n *\n * ADR D8: `defaultSchema` from `hast-util-sanitize` is used WITHOUT extensions\n * in v0.1. The default schema:\n * - Strips `<script>`, `<iframe>`, `<object>`, `<embed>`, `<form>`, `<input>`,\n * `<style>`, `<link>` and other dangerous tags.\n * - Keeps `clobberPrefix: \"user-content-\"` so user-supplied IDs cannot\n * clobber DOM lookups.\n * - Allows the standard CommonMark + GFM safe subset (h1-h6, p, ul, ol, li,\n * blockquote, code, pre, a[href], img[src/alt], table family, em, strong,\n * del, br, hr, etc.).\n *\n * A `looseSlideSanitizeSchema` (with `figure`/`figcaption`) is an explicit\n * opt-in for v0.2 — gated by security review.\n */\nimport type { Element, Root } from \"hast\";\nimport type { MergedSanitizeExtensions } from \"@/components/ui/slide/plugin\";\n\nlet cachedBuiltSchema: import(\"hast-util-sanitize\").Schema | undefined;\n\n/**\n * Tier 1 built-in extensions to the default sanitize schema. These cover the\n * tags emitted by built-in Slide features (alerts in `aside`, layout / header\n * / footer / paginate metadata attributes on the outer section wrapper). They\n * are NOT plugin-declared because they ship with the Slide primitive.\n *\n * Note: layout / header / footer / paginate are applied at the React component\n * level (outside the hast tree), so their attributes don't need sanitize rules.\n * Only `aside` (alerts) ends up in the hast.\n */\nconst TIER_1_TAG_NAMES: string[] = [\"aside\"];\nconst TIER_1_ATTRIBUTES: Record<string, string[]> = {\n aside: [\"className\", \"data-theo-slide-alert-type\"],\n \"*\": [\"className\"],\n};\n\n/**\n * Lazy accessor for the sanitize schema.\n *\n * Always merges `defaultSchema` with the Tier 1 baseline (aside + className).\n * When `extensions` is provided, plugin-declared tag names + attributes are\n * unioned on top (D17 / EC-3). Plugins NEVER bypass sanitize — they declare\n * what they need via `sanitizeSchemaExtension`.\n *\n * Implementation notes:\n * - The baseline schema (default + Tier 1) is cached.\n * - Plugin-merged schemas are NOT cached (depends on combination).\n */\nexport async function getSlideSanitizeSchema(\n extensions?: MergedSanitizeExtensions,\n): Promise<import(\"hast-util-sanitize\").Schema> {\n const baseline = await getBaselineSchema();\n if (\n !extensions ||\n (extensions.tagNames.length === 0 && Object.keys(extensions.attributes).length === 0)\n ) {\n return baseline;\n }\n return mergeSchema(baseline, extensions);\n}\n\nasync function getBaselineSchema(): Promise<import(\"hast-util-sanitize\").Schema> {\n if (cachedBuiltSchema) return cachedBuiltSchema;\n const { defaultSchema } = await import(\"hast-util-sanitize\");\n cachedBuiltSchema = mergeSchema(defaultSchema, {\n tagNames: TIER_1_TAG_NAMES,\n attributes: TIER_1_ATTRIBUTES,\n });\n return cachedBuiltSchema;\n}\n\nfunction mergeSchema(\n base: import(\"hast-util-sanitize\").Schema,\n extensions: MergedSanitizeExtensions,\n): import(\"hast-util-sanitize\").Schema {\n // Merge tag names (deduplicated set).\n const baseTagNames = (base.tagNames ?? []) as string[];\n const tagSet = new Set<string>(baseTagNames);\n for (const t of extensions.tagNames) tagSet.add(t);\n // Merge attributes per tag.\n const baseAttrs = (base.attributes ?? {}) as Record<string, unknown[]>;\n const mergedAttrs: Record<string, unknown[]> = { ...baseAttrs };\n for (const [tag, attrs] of Object.entries(extensions.attributes)) {\n const baseline = (mergedAttrs[tag] ?? []) as unknown[];\n const seen = new Set<string>();\n const combined: unknown[] = [];\n for (const a of baseline) {\n const key = typeof a === \"string\" ? a : JSON.stringify(a);\n if (!seen.has(key)) {\n seen.add(key);\n combined.push(a);\n }\n }\n for (const a of attrs) {\n if (!seen.has(a)) {\n seen.add(a);\n combined.push(a);\n }\n }\n mergedAttrs[tag] = combined;\n }\n return {\n ...base,\n tagNames: Array.from(tagSet),\n attributes: mergedAttrs,\n } as import(\"hast-util-sanitize\").Schema;\n}\n\n/** Count elements by tagName in a hast tree. Used for BANNED_TAG detection (ADR D13). */\nexport function collectTagCounts(tree: Root | Element): Map<string, number> {\n const counts = new Map<string, number>();\n const walk = (node: Root | Element | { type: string; children?: unknown[] }): void => {\n if (node.type === \"element\") {\n const tag = (node as Element).tagName;\n counts.set(tag, (counts.get(tag) ?? 0) + 1);\n }\n const children = (node as { children?: unknown[] }).children;\n if (Array.isArray(children)) {\n for (const child of children) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n walk(child as Root | Element);\n }\n }\n }\n };\n walk(tree);\n return counts;\n}\n"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"path": "components/primitives/slide/schema.ts",
|
|
52
|
+
"type": "registry:ui",
|
|
53
|
+
"target": "components/ui/slide/schema.ts",
|
|
54
|
+
"content": "/**\n * Zod schema for the Slide YAML frontmatter (`SlideFrontmatter`) and the\n * composed `SlideInput` (frontmatter + body).\n *\n * Design (see RFC 0002 and the plan in\n * `.claude/knowledge-base/plans/slide-view-primitive-plan.md` §16.3 and ADRs D4 / D14):\n *\n * - **`.strict()`** on frontmatter — unknown keys produce `INVALID_FRONTMATTER`\n * with a precise path, so an LLM can self-correct.\n * - Numeric values are `.finite()` to reject `NaN` / `Infinity`. (Currently no\n * numeric directives ship; keep the helper for future fields.)\n * - String values capped to defensible sizes to reject DoS-shaped inputs.\n * - `body` capped to 50 KB (≈30 slides' worth of text) per the sanity rule.\n * - `theme` is the canonical enum of built-in themes. Custom themes are not\n * registered via frontmatter in v0.1 — caller wraps `<Slide>` with their\n * own CSS overrides.\n */\nimport { z } from \"zod\";\n\n/** Built-in slide themes. Adding a theme means landing a CSS file in `themes/`. */\nexport const slideTheme = z.enum([\"default\", \"violet-forge\"]);\nexport type SlideTheme = z.infer<typeof slideTheme>;\n\n/** Built-in slide layouts (rich-content plan T2.1). */\nexport const slideLayout = z.enum([\n \"default\",\n \"title\",\n \"two-column\",\n \"image-right\",\n \"image-left\",\n \"code-output\",\n \"section\",\n]);\nexport type SlideLayout = z.infer<typeof slideLayout>;\n\nconst cssColor = z.string().max(64);\n// BCP-47 language tag — loose validation, just sanity to reject obvious junk.\nconst langTag = z\n .string()\n .regex(/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/, \"Expected BCP-47 language tag\")\n .max(35);\n\n// EC-7 / rich-content T3.1 helpers.\nconst SAFE_URL_SCHEMES = [\"http://\", \"https://\"];\n\n/**\n * Sanitize a `backgroundImage` URL.\n *\n * Allowed: `http(s)://...` URLs (validated via `URL` constructor).\n * Rejected: `javascript:`, `vbscript:`, all `data:` URLs (EC-7 — perf + DoS).\n * Accepts a bare URL or a `url(...)` wrapper. Returns the unwrapped URL on\n * success, `null` on rejection.\n */\nexport function sanitizeBgUrl(input: string): string | null {\n try {\n const trimmed = input.trim();\n const url = trimmed.startsWith(\"url(\")\n ? trimmed.replace(/^url\\(\\s*['\"]?/, \"\").replace(/['\"]?\\s*\\)$/, \"\")\n : trimmed;\n const lower = url.toLowerCase();\n if (lower.startsWith(\"javascript:\") || lower.startsWith(\"vbscript:\")) return null;\n // EC-7: reject ALL data: URLs (including data:image/*) — slides inflate the\n // markdown payload and shifts loading cost. Consumer should host images.\n if (lower.startsWith(\"data:\")) return null;\n if (!SAFE_URL_SCHEMES.some((s) => lower.startsWith(s))) return null;\n new URL(url); // throws on malformed\n return url;\n } catch {\n return null;\n }\n}\n\n/** YAML frontmatter accepted by `<Slide>`. */\nexport const slideFrontmatter = z\n .object({\n theme: slideTheme.optional(),\n /** Rich-content T2: built-in layout (CSS grid). */\n layout: slideLayout.optional(),\n /** Rich-content T3: background URL (sanitized: http(s) only). */\n backgroundImage: z\n .string()\n .max(500_000)\n .transform((v) => {\n if (!v) return undefined;\n const sanitized = sanitizeBgUrl(v);\n return sanitized ?? undefined;\n })\n .optional(),\n /** Rich-content T3: CSS gradient string (validated by prefix). */\n backgroundGradient: z\n .string()\n .max(500)\n .regex(\n /^(linear|radial|conic)-gradient\\(/i,\n \"Must start with linear-/radial-/conic-gradient(\",\n )\n .optional(),\n /** Rich-content T5: header overlay text (plain, ≤ 200 chars). */\n header: z.string().max(200).optional(),\n /** Rich-content T5: footer overlay text (plain, ≤ 200 chars). */\n footer: z.string().max(200).optional(),\n /** Rich-content T5: pagination — `true` shows page number, `\"skip\"` hides. */\n paginate: z.union([z.boolean(), z.literal(\"skip\"), z.literal(\"hold\")]).optional(),\n lang: langTag.optional(),\n color: cssColor.optional(),\n backgroundColor: cssColor.optional(),\n })\n .strict();\nexport type SlideFrontmatter = z.infer<typeof slideFrontmatter>;\n\n/** Composed input: validated frontmatter object + raw markdown body string. */\nexport const slideInput = z.object({\n frontmatter: slideFrontmatter,\n body: z.string().max(50_000),\n});\nexport type SlideInput = z.infer<typeof slideInput>;\n\n/** Discrete validation-error codes — kept centralized so consumers can pattern-match safely. */\nexport type SlideValidationErrorCode =\n | \"INVALID_FRONTMATTER\"\n | \"FRONTMATTER_TOO_LARGE\"\n | \"MULTIPLE_SLIDES\"\n | \"CONTENT_TOO_LARGE\"\n | \"BANNED_TAG\"\n | \"BANNED_ATTRIBUTE\"\n | \"INVALID_ASPECT_RATIO\"\n // Rich-content plan (Tier 1 + Tier 2 plugins):\n | \"PLUGIN_ERROR\" // D16: a plugin's mdast/hast transform threw — pipeline continued\n | \"PLUGIN_PEER_DEP_MISSING\" // EC-2: dynamic import of a plugin's peer-dep failed\n | \"MARPIT_BG_UNSAFE_URL\"; // D18/EC-5: Marpit  rejected by URL sanitizer\n\nexport interface SlideValidationError {\n /** Path inside the input (frontmatter key, ['body'], or []). */\n path: (string | number)[];\n /** Human-readable explanation. */\n message: string;\n /** Discriminator for consumers. */\n code: SlideValidationErrorCode;\n /** Offending value when extractable from Zod's `received` or by walking the input. */\n got?: unknown;\n}\n"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"path": "components/primitives/slide/frontmatter.ts",
|
|
58
|
+
"type": "registry:ui",
|
|
59
|
+
"target": "components/ui/slide/frontmatter.ts",
|
|
60
|
+
"content": "/**\n * Lightweight, dependency-free extractor that separates YAML frontmatter from\n * the markdown body.\n *\n * Strips a leading BOM (``) before matching the regex — common when\n * markdown is pasted from Word/Notion. See plan ADR D14 and edge-case EC-4.\n *\n * Returns `{ rawFrontmatter, body, tooLarge? }`:\n * - `rawFrontmatter`: the YAML string between the two `---` delimiters, or\n * `null` if no frontmatter is present.\n * - `body`: the markdown after the closing `---\\n` (or the full input when\n * no frontmatter).\n * - `tooLarge`: `true` if `rawFrontmatter` exceeds `MAX_RAW_FRONTMATTER`\n * bytes — the caller turns this into `FRONTMATTER_TOO_LARGE`.\n */\n\n/** Maximum size of the raw frontmatter string before YAML parsing. 10 KB cap. */\nexport const MAX_RAW_FRONTMATTER = 10_240;\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n([\\s\\S]*)$/;\nconst BOM = \"\";\n\nexport interface ExtractFrontmatterResult {\n rawFrontmatter: string | null;\n body: string;\n tooLarge?: boolean;\n}\n\nexport function extractFrontmatter(md: string): ExtractFrontmatterResult {\n const normalized = md.startsWith(BOM) ? md.slice(1) : md;\n const match = FRONTMATTER_RE.exec(normalized);\n if (!match) {\n return { rawFrontmatter: null, body: normalized };\n }\n const raw = match[1] ?? \"\";\n const body = match[2] ?? \"\";\n if (raw.length > MAX_RAW_FRONTMATTER) {\n return { rawFrontmatter: raw, body, tooLarge: true };\n }\n return { rawFrontmatter: raw, body };\n}\n"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"path": "components/primitives/slide/alerts.ts",
|
|
64
|
+
"type": "registry:ui",
|
|
65
|
+
"target": "components/ui/slide/alerts.ts",
|
|
66
|
+
"content": "/**\n * GFM alerts post-processor — converts `> [!NOTE]` blockquotes into\n * `<aside class=\"theo-slide-alert\" data-theo-slide-alert-type=\"...\">` (ADR D3 of\n * the slide-rich-content plan).\n *\n * GitHub Flavored Markdown introduced this convention in 2023 for callouts.\n * `remark-gfm` (already in our dep tree) parses the blockquote but does NOT\n * distinguish alerts from regular blockquotes. We post-process the mdast tree\n * to detect the canonical marker and annotate with `hName` + `hProperties` so\n * `mdast-util-to-hast` emits the desired hast element.\n *\n * Five tag families are supported, mirroring GitHub:\n * - NOTE (info / blue)\n * - TIP (success / green)\n * - IMPORTANT (purple)\n * - WARNING (yellow)\n * - CAUTION (red)\n *\n * Case-insensitive. Marker is stripped from the rendered text. Regular\n * blockquotes (without `[!TYPE]` prefix) are left untouched.\n */\nimport type { Blockquote, Paragraph, Root, Text } from \"mdast\";\n\n/** Canonical alert types — used by CSS via `data-theo-slide-alert-type=\"...\"`. */\nexport const ALERT_TYPES = [\"note\", \"tip\", \"important\", \"warning\", \"caution\"] as const;\nexport type AlertType = (typeof ALERT_TYPES)[number];\n\nconst ALERT_RE = /^\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\][ \\t]*(?:\\r?\\n|$)/i;\n\n/**\n * Walk a mdast tree and mutate any blockquote that opens with `[!TYPE]` into\n * an `<aside>` annotation. Mutation is in-place to keep the pipeline allocation\n * profile flat; the same tree is returned for chainability.\n */\nexport function detectAlerts(tree: Root): Root {\n for (const node of tree.children) {\n if (node.type !== \"blockquote\") continue;\n transformIfAlert(node);\n }\n return tree;\n}\n\nfunction transformIfAlert(node: Blockquote): void {\n const firstChild = node.children[0];\n if (!firstChild || firstChild.type !== \"paragraph\") return;\n const paragraph = firstChild as Paragraph;\n const firstInline = paragraph.children[0];\n if (!firstInline || firstInline.type !== \"text\") return;\n const text = firstInline as Text;\n const match = ALERT_RE.exec(text.value);\n if (!match) return;\n\n const matched = match[1];\n if (!matched) return;\n const type = matched.toLowerCase() as AlertType;\n\n // Strip the marker from the rendered text.\n text.value = text.value.replace(ALERT_RE, \"\");\n // If the first inline became empty, drop it so the paragraph doesn't render\n // an empty leading line.\n if (text.value === \"\" && paragraph.children.length > 1) {\n paragraph.children.shift();\n }\n // Annotate so mdast-util-to-hast emits <aside> with the data attribute.\n node.data = {\n ...node.data,\n hName: \"aside\",\n hProperties: {\n ...((node.data as { hProperties?: Record<string, unknown> })?.hProperties ?? {}),\n className: [\"theo-slide-alert\"],\n \"data-theo-slide-alert-type\": type,\n },\n };\n}\n"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"path": "components/primitives/slide/marpit-bg.ts",
|
|
70
|
+
"type": "registry:ui",
|
|
71
|
+
"target": "components/ui/slide/marpit-bg.ts",
|
|
72
|
+
"content": "/**\n * Marpit-style `` background syntax detector.\n *\n * Marpit popularised the convention of declaring a slide background via a\n * standalone image whose alt-text starts with `bg`:\n *\n * \n *    \n *\n * LLMs trained on Marp output emit this naturally, so the slide primitive\n * adopts the same surface — but in a STRICTLY safe way:\n *\n * 1. Only triggers when the paragraph contains a SINGLE image (avoids\n * collisions with inline images mixed with text).\n * 2. First-bg-wins (multiple `![bg]` directives in one slide → only the first\n * is honoured; the others are also dropped from the tree to prevent\n * duplicate rendering).\n * 3. The extracted result goes into `ParsedSlide.extractedBackground` (D18 /\n * EC-5) and the Slide component prefers an explicit `frontmatter.backgroundImage`\n * over the Marpit extraction.\n * 4. URL sanitization happens at the caller (`parseSlide`) via `sanitizeBgUrl`.\n */\nimport type { Image, Paragraph, Root } from \"mdast\";\n\nconst BG_ALT_RE = /^bg(?:\\s+(\\w+))?/i;\n\nconst VALID_MODIFIERS = new Set([\"cover\", \"fit\", \"left\", \"right\"]);\n\nexport interface ExtractedMarpitBackground {\n url: string;\n modifier?: \"cover\" | \"fit\" | \"left\" | \"right\";\n}\n\n/**\n * Walk the mdast tree, extract the first `` directive, drop ALL\n * matching paragraphs from the tree. Returns a new tree (children array\n * is filtered; node identities are preserved otherwise).\n */\nexport function extractMarpitBackgrounds(tree: Root): {\n tree: Root;\n background?: ExtractedMarpitBackground;\n} {\n let background: ExtractedMarpitBackground | undefined;\n const filteredChildren = tree.children.filter((node) => {\n if (node.type !== \"paragraph\") return true;\n const p = node as Paragraph;\n // Single-child guard: paragraphs with mixed content (text + image) are\n // kept as-is — only \"image-only\" paragraphs are candidates.\n if (p.children.length !== 1) return true;\n const child = p.children[0];\n if (!child || child.type !== \"image\") return true;\n const img = child as Image;\n const match = BG_ALT_RE.exec(img.alt ?? \"\");\n if (!match) return true;\n // Capture once; drop subsequent bg paragraphs to avoid duplicate output.\n if (!background) {\n const modifierRaw = match[1]?.toLowerCase();\n const modifier =\n modifierRaw && VALID_MODIFIERS.has(modifierRaw)\n ? (modifierRaw as \"cover\" | \"fit\" | \"left\" | \"right\")\n : undefined;\n background = {\n url: img.url,\n modifier,\n };\n }\n return false; // drop this paragraph from the tree\n });\n return {\n tree: { ...tree, children: filteredChildren },\n background,\n };\n}\n"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"path": "components/primitives/slide/plugin.ts",
|
|
76
|
+
"type": "registry:ui",
|
|
77
|
+
"target": "components/ui/slide/plugin.ts",
|
|
78
|
+
"content": "/**\n * Slide plugin contract — extension points for rich-content engines.\n *\n * Plugins are composable transformers applied during `parseSlide`. Each plugin\n * may inspect/mutate the mdast tree (before HTML conversion), the hast tree\n * (after HTML conversion, before sanitize), and override React component\n * renderers (after sanitize). They may also declare sanitize-schema extensions\n * so custom tags (e.g. Shiki spans, KaTeX MathML, Mermaid SVG) survive the\n * security barrier.\n *\n * Design rules (ADRs in `.claude/knowledge-base/plans/slide-rich-content-plan.md`):\n * D1 — explicit `plugins` prop (no auto-detect).\n * D2 — plugin shape: { name, mdastTransform?, hastTransform?, components? }.\n * D13 — execution order: mdast → hast → sanitize (with merged extensions) → components.\n * D16 — plugin error isolation: each plugin call is try/catch wrapped; errors\n * are collected, not thrown.\n * D17 — sanitize-schema merge is OBLIGATORY for plugins emitting non-default\n * tags; without it, the sanitize step silently strips the content.\n *\n * Sanitize is the security barrier. Plugins NEVER bypass `hast-util-sanitize`.\n */\nimport type { Root as HastRoot } from \"hast\";\nimport type { Root as MdastRoot } from \"mdast\";\nimport type { FC } from \"react\";\nimport type { SlideValidationError } from \"@/components/ui/slide/schema\";\n\n/** Sanitize schema extension declared by a plugin. Merged with `defaultSchema` in `parseSlide`. */\nexport interface SlideSanitizeExtension {\n /** Additional element tag names allowed past the sanitizer. */\n tagNames?: string[];\n /** Additional attribute allow-list keyed by tag name (or `\"*\"` for all tags). */\n attributes?: Record<string, string[]>;\n}\n\n/** A composable transformer + component-override unit. */\nexport interface SlidePlugin {\n /** Stable identifier used in PLUGIN_ERROR messages and dev logs. */\n name: string;\n /** Mutate the mdast tree before HTML conversion. Return value is forwarded. */\n mdastTransform?: (tree: MdastRoot) => Promise<MdastRoot> | MdastRoot;\n /** Mutate the hast tree after HTML conversion, before sanitize. */\n hastTransform?: (tree: HastRoot) => Promise<HastRoot> | HastRoot;\n /** React component overrides merged into the consumer's `components` map. */\n // biome-ignore lint/suspicious/noExplicitAny: third-party component override map\n components?: Record<string, FC<any>>;\n /** Sanitize-schema extension. Required for plugins emitting non-default tags. */\n sanitizeSchemaExtension?: SlideSanitizeExtension;\n}\n\n/** Merged sanitize-schema extensions from all plugins. */\nexport interface MergedSanitizeExtensions {\n tagNames: string[];\n attributes: Record<string, string[]>;\n}\n\n/** Composer over a plugin array. Hooks orchestrate transformation + error isolation. */\nexport interface PluginComposer {\n /** Run all `mdastTransform` hooks in array order. Collects errors. */\n runMdast(tree: MdastRoot, errors: SlideValidationError[]): Promise<MdastRoot>;\n /** Run all `hastTransform` hooks in array order. Collects errors. */\n runHast(tree: HastRoot, errors: SlideValidationError[]): Promise<HastRoot>;\n /** Merge all plugin component overrides; later-plugin-wins on conflict. */\n // biome-ignore lint/suspicious/noExplicitAny: third-party component override map\n mergedComponents(): Record<string, FC<any>>;\n /** Merge sanitize-schema extensions across all plugins (dedupes tag names). */\n mergedSanitizeExtensions(): MergedSanitizeExtensions;\n}\n\n/**\n * Build a composer over an array of plugins.\n *\n * Order semantics:\n * - mdast transforms run sequentially in array order.\n * - hast transforms run sequentially in array order.\n * - components merge: later plugin wins on conflict (Object.assign semantics).\n * - sanitize-schema extensions: union of tag names; union of attributes by tag.\n *\n * Error semantics (D16):\n * - Each plugin call is wrapped in try/catch.\n * - On throw, an error of code `PLUGIN_ERROR` is pushed to `errors[]` and the\n * pipeline continues with the **non-transformed** input from that plugin\n * (i.e. subsequent plugins see the tree that failed plugin received).\n * - The composer NEVER throws; `parseSlide` keeps its \"never throws on input\"\n * contract (RFC 0002 D9).\n */\nexport function composePlugins(plugins: SlidePlugin[]): PluginComposer {\n return {\n async runMdast(tree, errors) {\n let current = tree;\n for (const p of plugins) {\n if (!p.mdastTransform) continue;\n const previous = current;\n try {\n const result = await p.mdastTransform(current);\n if (!result || result.type !== \"root\") {\n // Defensive: a buggy plugin may return a non-Root node. Reject and\n // keep the previous tree so subsequent plugins see a valid input.\n throw new Error(\"mdastTransform returned non-Root node\");\n }\n current = result;\n } catch (e) {\n current = previous;\n errors.push(makePluginError(p.name, \"mdastTransform\", e));\n }\n }\n return current;\n },\n async runHast(tree, errors) {\n let current = tree;\n for (const p of plugins) {\n if (!p.hastTransform) continue;\n const previous = current;\n try {\n const result = await p.hastTransform(current);\n if (!result || result.type !== \"root\") {\n throw new Error(\"hastTransform returned non-Root node\");\n }\n current = result;\n } catch (e) {\n current = previous;\n errors.push(makePluginError(p.name, \"hastTransform\", e));\n }\n }\n return current;\n },\n mergedComponents() {\n // biome-ignore lint/suspicious/noExplicitAny: third-party component override map\n const out: Record<string, FC<any>> = {};\n for (const p of plugins) {\n if (p.components) Object.assign(out, p.components);\n }\n return out;\n },\n mergedSanitizeExtensions() {\n const tagNames = new Set<string>();\n const attributes: Record<string, Set<string>> = {};\n for (const p of plugins) {\n const ext = p.sanitizeSchemaExtension;\n if (!ext) continue;\n if (ext.tagNames) {\n for (const tag of ext.tagNames) tagNames.add(tag);\n }\n if (ext.attributes) {\n for (const [tag, attrs] of Object.entries(ext.attributes)) {\n if (!attributes[tag]) attributes[tag] = new Set();\n for (const a of attrs) attributes[tag].add(a);\n }\n }\n }\n const mergedAttrs: Record<string, string[]> = {};\n for (const [tag, set] of Object.entries(attributes)) {\n mergedAttrs[tag] = Array.from(set);\n }\n return {\n tagNames: Array.from(tagNames),\n attributes: mergedAttrs,\n };\n },\n };\n}\n\nfunction makePluginError(\n name: string,\n hook: \"mdastTransform\" | \"hastTransform\",\n e: unknown,\n): SlideValidationError {\n const msg = e instanceof Error ? e.message : String(e);\n return {\n code: \"PLUGIN_ERROR\",\n path: [],\n message: `Plugin '${name}' failed in ${hook}: ${msg}`,\n got: name,\n };\n}\n"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"path": "components/primitives/slide/json-schema.ts",
|
|
82
|
+
"type": "registry:ui",
|
|
83
|
+
"target": "components/ui/slide/json-schema.ts",
|
|
84
|
+
"content": "/**\n * JSON Schema for the Slide YAML frontmatter — derived from the Zod\n * `slideFrontmatter` schema via Zod 4's native `z.toJSONSchema()`.\n *\n * Purpose: enable LLM-driven generation pipelines (OpenAI structured outputs,\n * Anthropic tool use, function calling, JSON-mode constrained generation) to\n * produce frontmatter that is GUARANTEED to pass `validateSlide` without\n * round-tripping through human-written prompt engineering.\n *\n * Usage (Anthropic tool use):\n *\n * import { slideFrontmatterJsonSchema } from \"@theokit/ui/slide\";\n *\n * const tool = {\n * name: \"render_slide\",\n * description: \"Render a presentation slide.\",\n * input_schema: {\n * type: \"object\",\n * properties: {\n * frontmatter: slideFrontmatterJsonSchema,\n * body: { type: \"string\", description: \"CommonMark + GFM markdown body\" },\n * },\n * required: [\"body\"],\n * },\n * };\n *\n * The output matches Zod's behaviour exactly — anything that passes the JSON\n * Schema validator passes Zod, and vice-versa.\n *\n * Companion guide: `docs/slide-llm-guide.md` — copy-paste system prompt that\n * documents every Tier 1 + Tier 2 feature with examples.\n */\nimport { z } from \"zod\";\nimport { slideFrontmatter } from \"@/components/ui/slide/schema\";\n\ninterface JsonSchemaProperties {\n [key: string]: Record<string, unknown>;\n}\n\ninterface JsonSchemaWithProperties {\n properties?: JsonSchemaProperties;\n [key: string]: unknown;\n}\n\n/**\n * JSON Schema (Draft 2020-12) describing all accepted frontmatter fields:\n * `theme`, `layout`, `lang`, `color`, `backgroundColor`, `backgroundImage`,\n * `backgroundGradient`, `header`, `footer`, `paginate`.\n *\n * Note: `backgroundImage` uses a Zod `.transform()` (URL sanitization) which\n * cannot be represented in JSON Schema natively. We pass `unrepresentable: \"any\"`\n * to fall back to `{}` for transformed fields, then enrich `backgroundImage`\n * with manual metadata so LLMs still get useful constraints.\n *\n * Generated once at module load. Treat as immutable.\n */\nconst rawSchema = z.toJSONSchema(slideFrontmatter, {\n unrepresentable: \"any\",\n}) as JsonSchemaWithProperties;\n\nif (rawSchema.properties) {\n // Backgrounds carry hard constraints (http(s) only, ≤500_000 chars) that\n // Zod erases through `.transform()`. Inline them here so the LLM tooling\n // sees the same contract `sanitizeBgUrl` enforces at runtime.\n rawSchema.properties.backgroundImage = {\n type: \"string\",\n format: \"uri\",\n maxLength: 500_000,\n pattern: \"^https?://\",\n description:\n \"Slide background image URL. Only http(s) schemes are accepted; data: URLs are rejected at runtime (use a hosted image).\",\n };\n}\n\nexport const slideFrontmatterJsonSchema = rawSchema;\n"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"path": "components/primitives/slide/use-slide-fit.ts",
|
|
88
|
+
"type": "registry:ui",
|
|
89
|
+
"target": "components/ui/slide/use-slide-fit.ts",
|
|
90
|
+
"content": "/**\n * Container-fit hook for the Slide primitive.\n *\n * Observes the host element via `ResizeObserver` and computes a CSS scale\n * factor that fits the fixed logical canvas (default 1280×720) inside it,\n * preserving aspect ratio. Clamped to `[minScale, maxScale]`.\n *\n * Algorithm: `scale = clamp(min(W / canvasW, H / canvasH), min, max)`.\n *\n * Adapted from Reveal.js `transformSlides` (see reference doc §4.5 / §14.2).\n *\n * Cleanup: disconnects the observer on unmount or when deps change.\n */\nimport { type RefObject, useEffect, useState } from \"react\";\n\nexport interface UseSlideFitOptions {\n /** Lower clamp for scale. Default 0.1. */\n minScale?: number;\n /** Upper clamp for scale. Default 4. */\n maxScale?: number;\n}\n\nexport function useSlideFit(\n ref: RefObject<HTMLElement | null>,\n canvasW: number,\n canvasH: number,\n opts: UseSlideFitOptions = {},\n): number {\n const { minScale = 0.1, maxScale = 4 } = opts;\n const [scale, setScale] = useState(1);\n\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n if (typeof ResizeObserver === \"undefined\") {\n // Test environments without ResizeObserver: do a one-shot measurement.\n const { width, height } = el.getBoundingClientRect();\n if (width > 0 && height > 0) {\n const raw = Math.min(width / canvasW, height / canvasH);\n setScale(Math.max(minScale, Math.min(raw, maxScale)));\n }\n return;\n }\n const update = (): void => {\n const { width, height } = el.getBoundingClientRect();\n if (width <= 0 || height <= 0) return;\n const raw = Math.min(width / canvasW, height / canvasH);\n const clamped = Math.max(minScale, Math.min(raw, maxScale));\n if (Number.isFinite(clamped)) setScale(clamped);\n };\n update();\n const ro = new ResizeObserver(update);\n ro.observe(el);\n return () => ro.disconnect();\n }, [ref, canvasW, canvasH, minScale, maxScale]);\n\n return scale;\n}\n"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"path": "components/primitives/slide/index.ts",
|
|
94
|
+
"type": "registry:ui",
|
|
95
|
+
"target": "components/ui/slide/index.ts",
|
|
96
|
+
"content": "/**\n * Public surface of `@theokit/ui/slide`. Subpath-isolated engine: this bundle\n * is emitted separately from the barrel (see `tsup.config.ts`) and depends on\n * the markdown stack via optional peer-deps.\n */\nexport { Slide } from \"@/components/ui/slide/slide\";\nexport type {\n SlideAspectRatio,\n SlideProps,\n SlideTheme,\n SlideValidationError,\n SlideValidationErrorCode,\n} from \"@/components/ui/slide/slide\";\nexport {\n parseSlide,\n parseBody,\n mdastToHast,\n sanitizeHast,\n hastToReact,\n type ParseSlideOptions,\n type ParsedSlide,\n} from \"@/components/ui/slide/parse\";\nexport { validateSlide, type ValidationResult } from \"@/components/ui/slide/validate\";\nexport {\n extractFrontmatter,\n MAX_RAW_FRONTMATTER,\n type ExtractFrontmatterResult,\n} from \"@/components/ui/slide/frontmatter\";\nexport {\n slideFrontmatter,\n slideInput,\n slideTheme,\n type SlideFrontmatter,\n type SlideInput,\n} from \"@/components/ui/slide/schema\";\nexport { isSlideTheme, slideThemes } from \"@/components/ui/slide/themes/index\";\nexport { useSlideFit, type UseSlideFitOptions } from \"@/components/ui/slide/use-slide-fit\";\nexport { collectTagCounts, getSlideSanitizeSchema } from \"@/components/ui/slide/sanitize\";\nexport {\n composePlugins,\n type SlidePlugin,\n type SlideSanitizeExtension,\n type MergedSanitizeExtensions,\n type PluginComposer,\n} from \"@/components/ui/slide/plugin\";\nexport { slideFrontmatterJsonSchema } from \"@/components/ui/slide/json-schema\";\n"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"path": "components/primitives/slide/themes/index.ts",
|
|
100
|
+
"type": "registry:ui",
|
|
101
|
+
"target": "components/ui/slide/themes/index.ts",
|
|
102
|
+
"content": "/**\n * Theme registry for the Slide primitive. Built-in themes that ship with the\n * `@theokit/ui/slide` subpath. Custom themes are NOT registered through this\n * module in v0.1 — consumers override CSS variables on `.theo-slide` directly.\n */\n\nexport const slideThemes = [\"default\", \"violet-forge\"] as const;\nexport type SlideTheme = (typeof slideThemes)[number];\n\n/** Returns true if `value` is a recognized built-in theme. */\nexport function isSlideTheme(value: unknown): value is SlideTheme {\n return typeof value === \"string\" && (slideThemes as readonly string[]).includes(value);\n}\n"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"path": "components/primitives/slide/themes/default.css",
|
|
106
|
+
"type": "registry:ui",
|
|
107
|
+
"target": "components/ui/slide/themes/default.css",
|
|
108
|
+
"content": "/*\n * Slide primitive — `default` theme.\n *\n * Inherit-first design (mirrors the Whiteboard SVG using `currentColor`):\n * the slide's foreground and background come from the parent. The consumer's\n * surface (Tailwind classes, `<ThemeProvider>`, dashboard chrome) decides\n * light/dark — the theme just provides typography + relative tints derived\n * from `currentColor` via `color-mix()`.\n *\n * Result: the slide automatically follows the host's color scheme without\n * needing a `colorScheme` prop or `prefers-color-scheme` media query.\n *\n * All selectors are scoped to `.theo-slide[data-theo-slide-theme=\"default\"]`\n * so themes coexist on the same page without bleed.\n */\n\n@import \"./layouts.css\";\n\n.theo-slide[data-theo-slide-theme=\"default\"] {\n --theo-slide-canvas-width: 1280px;\n --theo-slide-canvas-height: 720px;\n --theo-slide-padding: 64px;\n --theo-slide-font-base: 28px;\n --theo-slide-font-family: var(\n --vf-font-family-sans,\n system-ui,\n -apple-system,\n \"Segoe UI\",\n sans-serif\n );\n --theo-slide-font-mono: var(--vf-font-family-mono, ui-monospace, \"JetBrains Mono\", monospace);\n\n /* Derived tints — relative to the inherited `currentColor`. They lighten or\n darken automatically depending on whether the parent's color is dark\n (light surface) or light (dark surface). */\n --theo-slide-color-muted: color-mix(in srgb, currentColor 60%, transparent);\n --theo-slide-color-tint-strong: color-mix(in srgb, currentColor 12%, transparent);\n --theo-slide-color-tint-weak: color-mix(in srgb, currentColor 6%, transparent);\n --theo-slide-color-border: color-mix(in srgb, currentColor 20%, transparent);\n\n color: inherit;\n background: transparent;\n font-family: var(--theo-slide-font-family);\n font-size: var(--theo-slide-font-base);\n line-height: 1.5;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] h1,\n.theo-slide[data-theo-slide-theme=\"default\"] h2,\n.theo-slide[data-theo-slide-theme=\"default\"] h3,\n.theo-slide[data-theo-slide-theme=\"default\"] h4,\n.theo-slide[data-theo-slide-theme=\"default\"] h5,\n.theo-slide[data-theo-slide-theme=\"default\"] h6 {\n color: inherit;\n line-height: 1.2;\n margin: 0 0 0.5em 0;\n font-weight: 600;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] h1 {\n font-size: 1.8em;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] h2 {\n font-size: 1.4em;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] h3 {\n font-size: 1.2em;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] h4 {\n font-size: 1.05em;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] h5 {\n font-size: 1em;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] h6 {\n font-size: 0.9em;\n color: var(--theo-slide-color-muted);\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] p {\n margin: 0 0 0.75em 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] ul,\n.theo-slide[data-theo-slide-theme=\"default\"] ol {\n margin: 0 0 0.75em 0;\n padding-left: 1.5em;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] li {\n margin: 0.25em 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] blockquote {\n margin: 0 0 0.75em 0;\n padding: 0.25em 0.75em;\n border-left: 4px solid var(--theo-slide-color-border);\n color: var(--theo-slide-color-muted);\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] a {\n color: currentColor;\n text-decoration: underline;\n text-underline-offset: 2px;\n font-weight: 600;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] code {\n font-family: var(--theo-slide-font-mono);\n font-size: 0.9em;\n padding: 0.1em 0.4em;\n border-radius: 4px;\n background: var(--theo-slide-color-tint-strong);\n color: inherit;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] pre {\n font-family: var(--theo-slide-font-mono);\n background: var(--theo-slide-color-tint-strong);\n color: inherit;\n padding: 0.75em 1em;\n border-radius: 8px;\n overflow: auto;\n font-size: 0.85em;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] pre code {\n padding: 0;\n background: transparent;\n font-size: inherit;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] hr {\n border: none;\n border-top: 1px solid var(--theo-slide-color-border);\n margin: 1em 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] table {\n border-collapse: collapse;\n margin: 0 0 0.75em 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] th,\n.theo-slide[data-theo-slide-theme=\"default\"] td {\n border: 1px solid var(--theo-slide-color-border);\n padding: 0.4em 0.75em;\n text-align: left;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] thead th {\n background: var(--theo-slide-color-tint-weak);\n font-weight: 600;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] tbody tr:nth-child(even) td {\n background: var(--theo-slide-color-tint-weak);\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] img {\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] em {\n font-style: italic;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] strong {\n font-weight: 600;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] del {\n text-decoration: line-through;\n color: var(--theo-slide-color-muted);\n}\n.theo-slide[data-theo-slide-theme=\"default\"] kbd {\n font-family: var(--theo-slide-font-mono);\n font-size: 0.85em;\n padding: 0.1em 0.4em;\n border: 1px solid var(--theo-slide-color-border);\n border-radius: 4px;\n background: var(--theo-slide-color-tint-weak);\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * GFM alerts — `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`,\n * `> [!WARNING]`, `> [!CAUTION]`\n * Pre-processed in mdast (alerts.ts) → <aside class=\"theo-slide-alert\"\n * data-theo-slide-alert-type=\"…\"> rich-content plan T1.2.\n * ───────────────────────────────────────────────────────────────── */\n.theo-slide[data-theo-slide-theme=\"default\"] aside.theo-slide-alert {\n margin: 0 0 0.75em 0;\n padding: 0.75em 1em;\n border-left: 4px solid currentColor;\n border-radius: 0.4em;\n background: color-mix(in srgb, currentColor 8%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside.theo-slide-alert::before {\n display: block;\n margin-bottom: 0.35em;\n font-weight: 600;\n font-size: 0.9em;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside.theo-slide-alert > :first-child {\n margin-top: 0;\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside.theo-slide-alert > :last-child {\n margin-bottom: 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"note\"] {\n border-color: #3b82f6;\n background: color-mix(in srgb, #3b82f6 10%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"note\"]::before {\n content: \"ⓘ Note\";\n color: #3b82f6;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"tip\"] {\n border-color: #10b981;\n background: color-mix(in srgb, #10b981 10%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"tip\"]::before {\n content: \"✓ Tip\";\n color: #10b981;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"important\"] {\n border-color: #a855f7;\n background: color-mix(in srgb, #a855f7 10%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"important\"]::before {\n content: \"! Important\";\n color: #a855f7;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"warning\"] {\n border-color: #f59e0b;\n background: color-mix(in srgb, #f59e0b 10%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"warning\"]::before {\n content: \"⚠ Warning\";\n color: #f59e0b;\n}\n\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"caution\"] {\n border-color: #ef4444;\n background: color-mix(in srgb, #ef4444 10%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"default\"] aside[data-theo-slide-alert-type=\"caution\"]::before {\n content: \"✗ Caution\";\n color: #ef4444;\n}\n"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"path": "components/primitives/slide/themes/violet-forge.css",
|
|
112
|
+
"type": "registry:ui",
|
|
113
|
+
"target": "components/ui/slide/themes/violet-forge.css",
|
|
114
|
+
"content": "/*\n * Slide primitive — `violet-forge` theme.\n *\n * Branded variant: same inherit-first foundation as the `default` theme, but\n * with a Violet Forge accent (links, code backgrounds, blockquote bar)\n * blended with `currentColor` via `color-mix()`. The brand hue stays\n * recognizable across light and dark surfaces because the accent is mixed\n * with the inherited foreground.\n */\n\n@import \"./layouts.css\";\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] {\n --theo-slide-canvas-width: 1280px;\n --theo-slide-canvas-height: 720px;\n --theo-slide-padding: 64px;\n --theo-slide-font-base: 28px;\n --theo-slide-font-family: var(\n --vf-font-family-sans,\n system-ui,\n -apple-system,\n \"Segoe UI\",\n sans-serif\n );\n --theo-slide-font-mono: var(--vf-font-family-mono, ui-monospace, \"JetBrains Mono\", monospace);\n\n /* Violet accent — the brand hue. Used for relative mixing so it adapts\n to light/dark surfaces (mixed with `currentColor`). */\n --theo-slide-accent: #7c3aed;\n\n /* Derived tints — relative to inherited `currentColor` so they auto-flip\n when the parent surface flips. */\n --theo-slide-color-muted: color-mix(in srgb, currentColor 60%, transparent);\n --theo-slide-color-tint-strong: color-mix(in srgb, var(--theo-slide-accent) 15%, transparent);\n --theo-slide-color-tint-weak: color-mix(in srgb, var(--theo-slide-accent) 7%, transparent);\n --theo-slide-color-border: color-mix(in srgb, var(--theo-slide-accent) 30%, transparent);\n --theo-slide-color-link: color-mix(in srgb, var(--theo-slide-accent) 70%, currentColor);\n --theo-slide-color-blockquote-bar: color-mix(in srgb, var(--theo-slide-accent) 60%, currentColor);\n\n color: inherit;\n background: transparent;\n font-family: var(--theo-slide-font-family);\n font-size: var(--theo-slide-font-base);\n line-height: 1.5;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h1,\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h2,\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h3,\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h4,\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h5,\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h6 {\n color: inherit;\n line-height: 1.2;\n margin: 0 0 0.5em 0;\n font-weight: 600;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h1 {\n font-size: 1.8em;\n letter-spacing: -0.02em;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h2 {\n font-size: 1.4em;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h3 {\n font-size: 1.2em;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h4 {\n font-size: 1.05em;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h5 {\n font-size: 1em;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] h6 {\n font-size: 0.9em;\n color: var(--theo-slide-color-muted);\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] p {\n margin: 0 0 0.75em 0;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] ul,\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] ol {\n margin: 0 0 0.75em 0;\n padding-left: 1.5em;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] li {\n margin: 0.25em 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] blockquote {\n margin: 0 0 0.75em 0;\n padding: 0.25em 0.75em;\n border-left: 4px solid var(--theo-slide-color-blockquote-bar);\n color: var(--theo-slide-color-muted);\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] a {\n color: var(--theo-slide-color-link);\n text-decoration: underline;\n text-underline-offset: 2px;\n font-weight: 600;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] code {\n font-family: var(--theo-slide-font-mono);\n font-size: 0.9em;\n padding: 0.1em 0.4em;\n border-radius: 4px;\n background: var(--theo-slide-color-tint-strong);\n color: inherit;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] pre {\n font-family: var(--theo-slide-font-mono);\n background: var(--theo-slide-color-tint-strong);\n color: inherit;\n padding: 0.75em 1em;\n border-radius: 8px;\n overflow: auto;\n font-size: 0.85em;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] pre code {\n padding: 0;\n background: transparent;\n font-size: inherit;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] hr {\n border: none;\n border-top: 1px solid var(--theo-slide-color-border);\n margin: 1em 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] table {\n border-collapse: collapse;\n margin: 0 0 0.75em 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] th,\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] td {\n border: 1px solid var(--theo-slide-color-border);\n padding: 0.4em 0.75em;\n text-align: left;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] thead th {\n background: var(--theo-slide-color-tint-weak);\n font-weight: 600;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] tbody tr:nth-child(even) td {\n background: var(--theo-slide-color-tint-weak);\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] img {\n max-width: 100%;\n height: auto;\n border-radius: 6px;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] em {\n font-style: italic;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] strong {\n font-weight: 600;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] del {\n text-decoration: line-through;\n color: var(--theo-slide-color-muted);\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] kbd {\n font-family: var(--theo-slide-font-mono);\n font-size: 0.85em;\n padding: 0.1em 0.4em;\n border: 1px solid var(--theo-slide-color-border);\n border-radius: 4px;\n background: var(--theo-slide-color-tint-weak);\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * GFM alerts (rich-content T1.2) — Violet Forge palette.\n * Same structure as `default`; only the accent colors differ\n * so callouts feel native to the design system.\n * ───────────────────────────────────────────────────────────────── */\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside.theo-slide-alert {\n margin: 0 0 0.75em 0;\n padding: 0.75em 1em;\n border-left: 4px solid currentColor;\n border-radius: 0.4em;\n background: color-mix(in srgb, currentColor 8%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside.theo-slide-alert::before {\n display: block;\n margin-bottom: 0.35em;\n font-weight: 600;\n font-size: 0.9em;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside.theo-slide-alert > :first-child {\n margin-top: 0;\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside.theo-slide-alert > :last-child {\n margin-bottom: 0;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside[data-theo-slide-alert-type=\"note\"] {\n border-color: #6366f1;\n background: color-mix(in srgb, #6366f1 12%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside[data-theo-slide-alert-type=\"note\"]::before {\n content: \"ⓘ Note\";\n color: #6366f1;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside[data-theo-slide-alert-type=\"tip\"] {\n border-color: #22d3ee;\n background: color-mix(in srgb, #22d3ee 12%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside[data-theo-slide-alert-type=\"tip\"]::before {\n content: \"✓ Tip\";\n color: #22d3ee;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside[data-theo-slide-alert-type=\"important\"] {\n border-color: #c084fc;\n background: color-mix(in srgb, #c084fc 12%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"]\n aside[data-theo-slide-alert-type=\"important\"]::before {\n content: \"★ Important\";\n color: #c084fc;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside[data-theo-slide-alert-type=\"warning\"] {\n border-color: #facc15;\n background: color-mix(in srgb, #facc15 12%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"]\n aside[data-theo-slide-alert-type=\"warning\"]::before {\n content: \"⚠ Warning\";\n color: #facc15;\n}\n\n.theo-slide[data-theo-slide-theme=\"violet-forge\"] aside[data-theo-slide-alert-type=\"caution\"] {\n border-color: #f87171;\n background: color-mix(in srgb, #f87171 12%, transparent);\n}\n.theo-slide[data-theo-slide-theme=\"violet-forge\"]\n aside[data-theo-slide-alert-type=\"caution\"]::before {\n content: \"✗ Caution\";\n color: #f87171;\n}\n"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"path": "components/primitives/slide/themes/layouts.css",
|
|
118
|
+
"type": "registry:ui",
|
|
119
|
+
"target": "components/ui/slide/themes/layouts.css",
|
|
120
|
+
"content": "/*\n * Slide layouts (rich-content plan T2.2).\n *\n * Selectors are scoped by `[data-theo-slide-layout]` on the outer `<section>`.\n * Themes (`default`, `violet-forge`) @import this file so layouts work across\n * the design system without duplication.\n *\n * Each layout uses CSS grid for placement. `default` is a no-op so existing\n * slides remain pixel-identical. The 7 layouts cover the 80% case for tech\n * presentations: title hero, dual columns, image-prose, code-output, chapter\n * dividers. Custom layouts require consumer CSS overriding the data-attr\n * selector.\n */\n\n/* Default — vertical flow (no grid). Preserves existing slide rendering. */\n.theo-slide[data-theo-slide-layout=\"default\"] {\n /* no-op */\n}\n\n/* Title — centered hero. Used for cover slides and section dividers. */\n.theo-slide[data-theo-slide-layout=\"title\"] {\n display: grid;\n place-items: center;\n text-align: center;\n}\n.theo-slide[data-theo-slide-layout=\"title\"] h1 {\n font-size: 2.8em;\n letter-spacing: -0.02em;\n}\n.theo-slide[data-theo-slide-layout=\"title\"] h2 {\n font-size: 1.6em;\n font-weight: 500;\n color: color-mix(in srgb, currentColor 70%, transparent);\n}\n\n/* Two-column — equal split. First child block goes left, rest stack right. */\n.theo-slide[data-theo-slide-layout=\"two-column\"] {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 32px;\n align-items: start;\n}\n.theo-slide[data-theo-slide-layout=\"two-column\"] > * {\n min-width: 0;\n}\n\n/* Image-right — text left (1.5fr), image right (1fr). */\n.theo-slide[data-theo-slide-layout=\"image-right\"] {\n display: grid;\n grid-template-columns: 1.5fr 1fr;\n gap: 32px;\n align-items: center;\n}\n.theo-slide[data-theo-slide-layout=\"image-right\"] img {\n grid-column: 2;\n max-width: 100%;\n height: auto;\n border-radius: 8px;\n justify-self: end;\n}\n\n/* Image-left — mirror of image-right. */\n.theo-slide[data-theo-slide-layout=\"image-left\"] {\n display: grid;\n grid-template-columns: 1fr 1.5fr;\n gap: 32px;\n align-items: center;\n}\n.theo-slide[data-theo-slide-layout=\"image-left\"] img {\n grid-column: 1;\n grid-row: 1;\n max-width: 100%;\n height: auto;\n border-radius: 8px;\n}\n\n/* Code-output — code block left (slightly wider), prose right. */\n.theo-slide[data-theo-slide-layout=\"code-output\"] {\n display: grid;\n grid-template-columns: 1.2fr 1fr;\n gap: 24px;\n align-items: start;\n}\n.theo-slide[data-theo-slide-layout=\"code-output\"] pre {\n margin: 0;\n}\n\n/* Section — full-bleed chapter divider with tinted backdrop. */\n.theo-slide[data-theo-slide-layout=\"section\"] {\n display: grid;\n place-items: center;\n text-align: center;\n background: color-mix(in srgb, currentColor 10%, transparent);\n}\n.theo-slide[data-theo-slide-layout=\"section\"] h1 {\n font-size: 3.6em;\n letter-spacing: -0.03em;\n margin: 0;\n}\n.theo-slide[data-theo-slide-layout=\"section\"] p {\n font-size: 1.1em;\n color: color-mix(in srgb, currentColor 65%, transparent);\n}\n\n/* ─────────────────────────────────────────────────────────────────\n * Header / Footer / Pagination overlays (rich-content T5.1).\n * Plain text divs absolute-positioned in the canvas. They don't push\n * content; they sit ON TOP. Sized in slide units (28px base).\n * ───────────────────────────────────────────────────────────────── */\n.theo-slide .theo-slide-header {\n position: absolute;\n top: 16px;\n left: 32px;\n right: 32px;\n font-size: 0.6em;\n opacity: 0.7;\n pointer-events: none;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.theo-slide .theo-slide-footer {\n position: absolute;\n bottom: 16px;\n left: 32px;\n right: 96px;\n font-size: 0.6em;\n opacity: 0.7;\n text-align: center;\n pointer-events: none;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.theo-slide .theo-slide-paginate {\n position: absolute;\n bottom: 16px;\n right: 32px;\n font-size: 0.55em;\n opacity: 0.5;\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n}\n"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "social-auth-row",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "SocialAuthRow",
|
|
6
|
+
"description": "Row of OAuth provider buttons.",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
10
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json",
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/types.json"
|
|
12
|
+
],
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "components/primitives/social-auth-row/social-auth-row.tsx",
|
|
16
|
+
"type": "registry:ui",
|
|
17
|
+
"target": "components/ui/social-auth-row.tsx",
|
|
18
|
+
"content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport type { IconComponent } from \"@/lib/types\";\n\nexport interface SocialProvider {\n id: string;\n label: ReactNode;\n /** Icon component (e.g. brand-specific SVG). */\n icon: IconComponent;\n}\n\ninterface SocialAuthRowProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n providers: SocialProvider[];\n onSelect?: (id: string) => void;\n /**\n * Stack vertically instead of horizontally (single-column flow).\n */\n vertical?: boolean;\n}\n\n/**\n * SocialAuthRow — row of OAuth provider buttons.\n *\n * Stateless; caller wires the redirect on `onSelect`. Buttons share Theo button\n * styling but with provider icon prominently on the left.\n */\nconst SocialAuthRow = forwardRef<HTMLDivElement, SocialAuthRowProps>(\n ({ className, providers, onSelect, vertical, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"grid gap-2\",\n vertical ? \"grid-cols-1\" : `grid-cols-${Math.min(providers.length, 4)}`,\n className,\n )}\n style={\n !vertical\n ? { gridTemplateColumns: `repeat(${providers.length}, minmax(0, 1fr))` }\n : undefined\n }\n {...props}\n >\n {providers.map((p) => {\n const Icon = p.icon;\n return (\n <button\n key={p.id}\n type=\"button\"\n onClick={() => onSelect?.(p.id)}\n className={cn(\n \"inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-border bg-card\",\n \"px-4 font-medium font-sans text-body-sm text-foreground\",\n \"transition-colors duration-base ease-out-soft\",\n \"hover:bg-muted\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n )}\n >\n <Icon className=\"size-4\" aria-hidden=\"true\" />\n {p.label}\n </button>\n );\n })}\n </div>\n ),\n);\nSocialAuthRow.displayName = \"SocialAuthRow\";\n\nexport { SocialAuthRow };\n"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "stat-tile",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "StatTile",
|
|
6
|
+
"description": "Big-number stat tile primitive for dashboard summaries. Renders value + label + optional icon + optional delta (trend up/down/flat with TrendingUp/TrendingDown/Minus icons and success/destructive/muted color). Dual mode: with onClick renders as button with hover state + trailing ArrowUpRight chevron; without, renders as static div. Value uses font-display tabular-nums whitespace-nowrap.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/stat-tile/stat-tile.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/stat-tile.tsx",
|
|
19
|
+
"content": "import { ArrowUpRight, Minus, TrendingDown, TrendingUp } from \"lucide-react\";\nimport { forwardRef } from \"react\";\nimport type { ButtonHTMLAttributes, ElementType, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * StatTile — big number + label + optional delta + optional icon.\n *\n * Dual mode based on `onClick`:\n * - With onClick → renders as `<button>` with hover state + trailing\n * ArrowUpRight chevron (navigation affordance).\n * - Without onClick → renders as static `<div>`.\n *\n * Delta trend drives icon + color: up=success/TrendingUp, down=destructive/\n * TrendingDown, flat=muted/Minus. Big value uses font-display + tabular-nums.\n *\n * @example\n * <StatTile value=\"42\" label=\"Projects\" />\n * <StatTile value=\"$1,234\" label=\"MRR\" icon={DollarSign}\n * delta={{ value: \"+12%\", trend: \"up\" }} onClick={openBilling} />\n */\nexport interface StatTileProps\n extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"type\" | \"value\"> {\n value: ReactNode;\n label: ReactNode;\n icon?: ElementType;\n delta?: { value: ReactNode; trend: \"up\" | \"down\" | \"flat\" };\n}\n\nconst TREND: Record<\"up\" | \"down\" | \"flat\", { icon: ElementType; color: string }> = {\n up: { icon: TrendingUp, color: \"text-success\" },\n down: { icon: TrendingDown, color: \"text-destructive\" },\n flat: { icon: Minus, color: \"text-muted-foreground\" },\n};\n\nconst StatTile = forwardRef<HTMLElement, StatTileProps>(\n ({ className, value, label, icon: Icon, delta, onClick, children: _children, ...props }, ref) => {\n const isInteractive = onClick !== undefined;\n const TrendIcon = delta !== undefined ? TREND[delta.trend].icon : null;\n const trendColor = delta !== undefined ? TREND[delta.trend].color : \"\";\n\n const inner = (\n <>\n {(Icon !== undefined || isInteractive) && (\n <div className=\"mb-3 flex items-center justify-between\">\n {Icon !== undefined ? (\n <div className=\"flex size-8 items-center justify-center rounded-lg border border-border/40 bg-muted/40\">\n <Icon aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />\n </div>\n ) : (\n <div />\n )}\n {isInteractive ? (\n <ArrowUpRight\n aria-hidden=\"true\"\n className=\"size-3.5 text-muted-foreground transition-colors group-hover:text-foreground\"\n />\n ) : null}\n </div>\n )}\n <div className=\"whitespace-nowrap font-bold font-display text-display-md text-foreground tabular-nums leading-none tracking-tight\">\n {value}\n </div>\n <div className=\"mt-1 font-sans text-body-sm text-muted-foreground\">{label}</div>\n {delta !== undefined && TrendIcon !== null ? (\n <div\n className={cn(\"mt-2 inline-flex items-center gap-1 font-mono text-label\", trendColor)}\n >\n <TrendIcon aria-hidden=\"true\" className=\"size-3\" />\n <span>{delta.value}</span>\n </div>\n ) : null}\n </>\n );\n\n if (isInteractive) {\n return (\n <button\n ref={ref as React.Ref<HTMLButtonElement>}\n type=\"button\"\n onClick={onClick}\n className={cn(\n \"group block w-full rounded-xl border border-border/40 bg-card p-5 text-left\",\n \"cursor-pointer transition-colors hover:border-primary/30\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n className,\n )}\n {...props}\n >\n {inner}\n </button>\n );\n }\n\n return (\n <div\n ref={ref as React.Ref<HTMLDivElement>}\n className={cn(\"rounded-xl border border-border/40 bg-card p-5\", className)}\n >\n {inner}\n </div>\n );\n },\n);\nStatTile.displayName = \"StatTile\";\n\nexport { StatTile };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "status-dot",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "StatusDot",
|
|
6
|
+
"description": "Semantic status indicator (small colored circle + optional label). Five status kinds: live (success), building (warning, auto-pulses), failed (destructive), idle (muted), warning (warning, static). Three sizes (xs 6px / sm 8px default / md 10px). When neither label nor aria-label is provided, auto-applies aria-label=status and emits a dev warning.",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
10
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "components/primitives/status-dot/status-dot.tsx",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"target": "components/ui/status-dot.tsx",
|
|
17
|
+
"content": "import { forwardRef, useEffect } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * StatusDot — semantic status indicator (colored circle + optional label).\n *\n * Five status kinds:\n * - `live` — deployed / verified / healthy (success)\n * - `building` — in-progress / queued (warning, auto-pulses)\n * - `failed` — error / down / rejected (destructive)\n * - `idle` — pending / offline (muted)\n * - `warning` — degraded but functional (warning, static)\n *\n * Three sizes (xs 6px, sm 8px default, md 10px). `pulse` defaults to\n * `true` for `building` and `false` otherwise; passing `pulse` explicitly\n * overrides the auto behavior. When no visible `label` AND no `aria-label`\n * are provided, the component auto-applies `aria-label={status}` and\n * emits a dev-mode warning (a status communicated only by color is\n * invisible to screen readers).\n *\n * @example\n * <StatusDot status=\"live\" label=\"Production\" />\n * <StatusDot status=\"building\" /> // auto-pulses + auto-aria-label\n */\nexport type StatusKind = \"live\" | \"building\" | \"failed\" | \"idle\" | \"warning\";\n\nexport interface StatusDotProps extends Omit<HTMLAttributes<HTMLSpanElement>, \"children\"> {\n status: StatusKind;\n label?: ReactNode;\n size?: \"xs\" | \"sm\" | \"md\";\n pulse?: boolean;\n}\n\nconst DOT_COLOR: Record<StatusKind, string> = {\n live: \"bg-success\",\n building: \"bg-warning\",\n failed: \"bg-destructive\",\n idle: \"bg-muted-foreground/40\",\n warning: \"bg-warning\",\n};\n\nconst LABEL_COLOR: Record<StatusKind, string> = {\n live: \"text-success\",\n building: \"text-warning\",\n failed: \"text-destructive\",\n idle: \"text-muted-foreground\",\n warning: \"text-warning\",\n};\n\nconst SIZE: Record<NonNullable<StatusDotProps[\"size\"]>, string> = {\n xs: \"size-1.5\",\n sm: \"size-2\",\n md: \"size-2.5\",\n};\n\nconst StatusDot = forwardRef<HTMLSpanElement, StatusDotProps>(\n ({ className, status, label, size = \"sm\", pulse, \"aria-label\": ariaLabel, ...props }, ref) => {\n const shouldPulse = pulse ?? status === \"building\";\n\n const hasVisibleLabel = label !== undefined && label !== null;\n const effectiveAriaLabel = ariaLabel ?? (hasVisibleLabel ? undefined : status);\n\n // EC-6: dev warning when neither label nor aria-label is provided.\n useEffect(() => {\n if (process.env.NODE_ENV !== \"production\" && !hasVisibleLabel && ariaLabel === undefined) {\n // biome-ignore lint/suspicious/noConsole: dev-only diagnostic for a11y misconfiguration.\n console.warn(\n `<StatusDot status=\"${status}\" />: no \\`label\\` or \\`aria-label\\` provided. Color-only status is invisible to screen readers. Falling back to aria-label=\"${status}\".`,\n );\n }\n }, [hasVisibleLabel, ariaLabel, status]);\n\n const dot = (\n <span\n aria-hidden={hasVisibleLabel ? \"true\" : undefined}\n className={cn(\n \"inline-block shrink-0 rounded-full\",\n SIZE[size],\n DOT_COLOR[status],\n shouldPulse && \"animate-pulse\",\n )}\n />\n );\n\n if (!hasVisibleLabel) {\n return (\n <span\n ref={ref}\n // biome-ignore lint/a11y/useSemanticElements: StatusDot is a generic inline indicator; there is no HTML element with implicit role=\"status\" that is an inline span. The native <output> is block-level and form-bound, which doesn't fit this use case.\n role=\"status\"\n aria-label={effectiveAriaLabel}\n className={cn(\"inline-flex items-center\", className)}\n {...props}\n >\n {dot}\n </span>\n );\n }\n\n return (\n <span\n ref={ref}\n aria-label={effectiveAriaLabel}\n className={cn(\n \"inline-flex items-center gap-1.5 font-mono text-label\",\n LABEL_COLOR[status],\n className,\n )}\n {...props}\n >\n {dot}\n <span>{label}</span>\n </span>\n );\n },\n);\nStatusDot.displayName = \"StatusDot\";\n\nexport { StatusDot };\n"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "steps-rail",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "StepsRail",
|
|
6
|
+
"description": "Vertical numbered rail with connecting line.",
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"registryDependencies": [
|
|
9
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
10
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
11
|
+
],
|
|
12
|
+
"files": [
|
|
13
|
+
{
|
|
14
|
+
"path": "components/primitives/steps-rail/steps-rail.tsx",
|
|
15
|
+
"type": "registry:ui",
|
|
16
|
+
"target": "components/ui/steps-rail.tsx",
|
|
17
|
+
"content": "import { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface RailStep {\n id: string | number;\n label?: ReactNode;\n /**\n * Visual state: \"complete\", \"current\", \"pending\".\n */\n state?: \"complete\" | \"current\" | \"pending\";\n}\n\ninterface StepsRailProps extends Omit<HTMLAttributes<HTMLElement>, \"title\"> {\n steps: RailStep[];\n /**\n * Optional label rendered at the top of the rail (e.g. \"STEPS\").\n */\n title?: ReactNode;\n}\n\n/**\n * StepsRail — vertical numbered rail with connecting line.\n *\n * Mirrors the file-organisation wiremock right rail: 5 numbered dots, current\n * highlighted, line connecting them.\n */\nconst StepsRail = forwardRef<HTMLElement, StepsRailProps>(\n ({ className, steps, title, ...props }, ref) => (\n <aside\n ref={ref}\n className={cn(\n \"flex w-14 flex-col items-center gap-6 border-border/40 border-l py-6\",\n className,\n )}\n aria-label=\"Task steps\"\n {...props}\n >\n {title ? (\n <span className=\"font-mono text-label-caps text-muted-foreground uppercase tracking-wider\">\n {title}\n </span>\n ) : null}\n <ol className=\"before:-translate-x-1/2 relative grid place-items-center gap-6 before:absolute before:top-3 before:bottom-3 before:left-1/2 before:w-px before:bg-border/60\">\n {steps.map((step, idx) => {\n const state = step.state ?? (idx === 0 ? \"current\" : \"pending\");\n return (\n <li key={step.id} className=\"relative z-10\">\n <span\n className={cn(\n \"grid size-7 place-items-center rounded-full border-2 font-bold font-mono text-code-sm\",\n state === \"complete\" && \"border-primary bg-primary text-primary-foreground\",\n state === \"current\" && \"border-foreground bg-foreground text-background\",\n state === \"pending\" && \"border-border bg-card text-muted-foreground\",\n )}\n aria-current={state === \"current\" ? \"step\" : undefined}\n >\n {step.label ?? idx + 1}\n </span>\n </li>\n );\n })}\n </ol>\n </aside>\n ),\n);\nStepsRail.displayName = \"StepsRail\";\n\nexport { StepsRail };\n"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "sub-agent-dispatch",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "SubAgentDispatch",
|
|
6
|
+
"description": "Visualization for a Task() / sub-agent invocation.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/sub-agent-dispatch/sub-agent-dispatch.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/sub-agent-dispatch.tsx",
|
|
19
|
+
"content": "import { Bot, CornerDownRight, Loader2 } from \"lucide-react\";\nimport { forwardRef } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SubAgentState = \"spawning\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n\nexport interface SubAgentRun {\n id: string;\n /** Profile name (matches AgentProfile.name). */\n agent: string;\n /** Short task description given to the sub-agent. */\n task: string;\n state: SubAgentState;\n /** Optional duration label. */\n duration?: string;\n /** Optional last status line (preview of the latest event). */\n lastEvent?: string;\n /** Optional result summary (one-liner). */\n result?: ReactNode;\n}\n\ninterface SubAgentDispatchProps extends HTMLAttributes<HTMLElement> {\n run: SubAgentRun;\n /** When provided, renders a cancel button while the run is in flight. */\n onCancel?: (id: string) => void;\n}\n\nconst STATE_CONFIG: Record<SubAgentState, { label: string; class: string }> = {\n spawning: {\n label: \"Spawning\",\n class: \"border-primary/40 bg-primary/10 text-primary animate-pulse\",\n },\n running: { label: \"Running\", class: \"border-primary/40 bg-primary/15 text-primary\" },\n completed: { label: \"Done\", class: \"border-success/40 bg-success/15 text-success\" },\n failed: { label: \"Failed\", class: \"border-destructive/40 bg-destructive/15 text-destructive\" },\n cancelled: { label: \"Cancelled\", class: \"border-border/40 bg-muted text-muted-foreground\" },\n};\n\n/**\n * SubAgentDispatch — visualization for a Task() / sub-agent invocation.\n *\n * Shows the agent name, the task summary, current state, an inline event\n * preview, and an optional result. Use inside the agent timeline to make\n * delegation visible.\n */\nconst SubAgentDispatch = forwardRef<HTMLElement, SubAgentDispatchProps>(\n ({ className, run, onCancel, ...props }, ref) => {\n const cfg = STATE_CONFIG[run.state];\n const isLive = run.state === \"spawning\" || run.state === \"running\";\n return (\n <article\n ref={ref}\n className={cn(\n \"grid gap-2 rounded-lg border border-primary/30 border-l-2 border-l-primary bg-card px-4 py-3\",\n className,\n )}\n {...props}\n >\n <header className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-2\">\n <CornerDownRight className=\"size-3.5 shrink-0 text-primary\" aria-hidden=\"true\" />\n <Bot className=\"size-4 shrink-0 text-primary\" aria-hidden=\"true\" />\n <span className=\"font-medium font-mono text-code-sm text-foreground\">dispatch</span>\n <span className=\"font-mono text-code-sm text-primary\">{run.agent}</span>\n {run.duration ? (\n <span className=\"font-mono text-label text-muted-foreground tabular-nums\">\n · {run.duration}\n </span>\n ) : null}\n </div>\n <span\n className={cn(\n \"inline-flex shrink-0 items-center gap-1 rounded-full border px-2.5 py-0.5\",\n \"font-mono text-label uppercase tracking-wider\",\n cfg.class,\n )}\n >\n {isLive ? <Loader2 className=\"size-3 animate-spin\" aria-hidden=\"true\" /> : null}\n {cfg.label}\n </span>\n </header>\n\n <p className=\"text-body-sm text-foreground\">\n <span className=\"mr-2 font-mono text-label-caps text-muted-foreground uppercase tracking-wider\">\n task\n </span>\n {run.task}\n </p>\n\n {run.lastEvent ? (\n <p className=\"truncate font-mono text-code-sm text-muted-foreground\">\n <span className=\"text-muted-foreground/60\">›</span> {run.lastEvent}\n </p>\n ) : null}\n\n {run.result ? (\n <p className=\"rounded-md bg-muted/40 px-3 py-2 text-body-sm text-foreground\">\n <span className=\"mr-2 font-mono text-label-caps text-muted-foreground uppercase tracking-wider\">\n result\n </span>\n {run.result}\n </p>\n ) : null}\n\n {isLive && onCancel ? (\n <footer className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={() => onCancel(run.id)}\n className=\"rounded-md border border-border/60 bg-card px-2.5 py-1 font-mono text-label text-muted-foreground hover:bg-muted hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n Cancel\n </button>\n </footer>\n ) : null}\n </article>\n );\n },\n);\nSubAgentDispatch.displayName = \"SubAgentDispatch\";\n\nexport { SubAgentDispatch };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "switch",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Switch",
|
|
6
|
+
"description": "Built on Radix Switch — accessible binary toggle with on / off states and disabled support.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-switch",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
13
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "components/primitives/switch/switch.tsx",
|
|
18
|
+
"type": "registry:ui",
|
|
19
|
+
"target": "components/ui/switch.tsx",
|
|
20
|
+
"content": "import * as SwitchPrimitive from \"@radix-ui/react-switch\";\nimport { type VariantProps, cva } from \"class-variance-authority\";\nimport { forwardRef } from \"react\";\nimport type { ComponentPropsWithoutRef, ElementRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Switch — built on Radix Switch. Used for binary toggles (autoaccept,\n * dark mode preview, feature flags).\n *\n * Off-state uses --muted, on-state uses --primary with a subtle glow shadow\n * to mark \"this is active\" in the violet brand language.\n *\n * The `size` prop accepts `\"sm\" | \"md\" | \"lg\"`. Default `md` preserves the\n * 20×36 track from before this prop existed.\n */\nconst switchVariants = cva(\n [\n \"peer inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent\",\n \"transition-[background-color,box-shadow] duration-base ease-out-soft\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"data-[state=checked]:bg-primary data-[state=checked]:shadow-[0_0_8px_hsl(var(--primary)/0.35)]\",\n \"data-[state=unchecked]:bg-muted\",\n \"disabled:cursor-not-allowed disabled:opacity-50\",\n ],\n {\n variants: {\n size: {\n sm: \"h-4 w-7\",\n md: \"h-5 w-9\",\n lg: \"h-6 w-11\",\n },\n },\n defaultVariants: { size: \"md\" },\n },\n);\n\nconst thumbClassBySize: Record<NonNullable<VariantProps<typeof switchVariants>[\"size\"]>, string> = {\n sm: \"size-3 data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0.5\",\n md: \"size-4 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0.5\",\n lg: \"size-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0.5\",\n};\n\ninterface SwitchProps\n extends ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>,\n VariantProps<typeof switchVariants> {}\n\nconst Switch = forwardRef<ElementRef<typeof SwitchPrimitive.Root>, SwitchProps>(\n ({ className, size, ...props }, ref) => (\n <SwitchPrimitive.Root ref={ref} className={cn(switchVariants({ size }), className)} {...props}>\n <SwitchPrimitive.Thumb\n className={cn(\n \"pointer-events-none block rounded-full bg-card shadow-sm\",\n \"transition-transform duration-base ease-out-soft\",\n thumbClassBySize[size ?? \"md\"],\n )}\n />\n </SwitchPrimitive.Root>\n ),\n);\nSwitch.displayName = \"Switch\";\n\nexport { Switch, switchVariants };\n"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "system-prompt-editor",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "SystemPromptEditor",
|
|
6
|
+
"description": "Surface the agent's system prompt with a clear",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/system-prompt-editor/system-prompt-editor.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/system-prompt-editor.tsx",
|
|
19
|
+
"content": "import { RotateCcw, Sparkles } from \"lucide-react\";\nimport { forwardRef, useState } from \"react\";\nimport type { HTMLAttributes, ReactNode } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\ninterface SystemPromptEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, \"title\"> {\n /** Vendor / default system prompt (read-only). */\n defaultPrompt: string;\n /** Current override; pass empty string for \"use default\". */\n override: string;\n onOverrideChange: (next: string) => void;\n /** Approximate token count for the active prompt. */\n tokenEstimate?: number;\n title?: ReactNode;\n}\n\n/**\n * SystemPromptEditor — surface the agent's system prompt with a clear\n * \"vendor default\" vs \"user override\" toggle.\n *\n * Behavior:\n * - When override is empty, the textarea shows the default (greyed out,\n * editable starts blank).\n * - When user types, override takes effect.\n * - \"Reset to default\" wipes the override.\n *\n * Critical for transparency: a user must be able to see and edit the prompt.\n */\nconst SystemPromptEditor = forwardRef<HTMLDivElement, SystemPromptEditorProps>(\n (\n {\n className,\n defaultPrompt,\n override,\n onOverrideChange,\n tokenEstimate,\n title = \"System prompt\",\n ...props\n },\n ref,\n ) => {\n const usingOverride = override.length > 0;\n const [showDefault, setShowDefault] = useState(false);\n\n return (\n <section ref={ref} className={cn(\"rounded-xl border bg-card\", className)} {...props}>\n <header className=\"flex items-center justify-between gap-3 border-border/40 border-b px-4 py-3\">\n <div className=\"flex items-center gap-2\">\n <Sparkles className=\"size-4 text-primary\" aria-hidden=\"true\" />\n <h3 className=\"font-display text-title-md tracking-tight\">{title}</h3>\n <span\n className={cn(\n \"inline-flex items-center rounded-full border px-2 py-0.5 font-mono text-label uppercase tracking-wider\",\n usingOverride\n ? \"border-primary/40 bg-primary/10 text-primary\"\n : \"border-border/40 bg-muted text-muted-foreground\",\n )}\n >\n {usingOverride ? \"Override active\" : \"Vendor default\"}\n </span>\n </div>\n <div className=\"flex items-center gap-2\">\n {tokenEstimate !== undefined ? (\n <span className=\"font-mono text-label text-muted-foreground tabular-nums\">\n ~{tokenEstimate.toLocaleString()} tokens\n </span>\n ) : null}\n <button\n type=\"button\"\n onClick={() => setShowDefault((v) => !v)}\n className=\"rounded-md px-2 py-1 font-mono text-label text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n {showDefault ? \"Hide\" : \"Show\"} default\n </button>\n {usingOverride ? (\n <button\n type=\"button\"\n onClick={() => onOverrideChange(\"\")}\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 font-mono text-label text-muted-foreground hover:bg-muted hover:text-destructive focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <RotateCcw className=\"size-3\" /> Reset\n </button>\n ) : null}\n </div>\n </header>\n\n {showDefault ? (\n <pre className=\"max-h-48 overflow-auto border-border/40 border-b bg-muted/40 px-4 py-3 font-mono text-code-sm text-muted-foreground\">\n {defaultPrompt}\n </pre>\n ) : null}\n\n <textarea\n value={override}\n onChange={(e) => onOverrideChange(e.target.value)}\n placeholder={\n usingOverride ? \"\" : \"Leave empty to use the vendor default. Type here to override.\"\n }\n rows={8}\n className={cn(\n \"w-full resize-y bg-transparent px-4 py-3 font-mono text-code-md text-foreground\",\n \"placeholder:text-muted-foreground\",\n \"focus:outline-none\",\n )}\n />\n </section>\n );\n },\n);\nSystemPromptEditor.displayName = \"SystemPromptEditor\";\n\nexport { SystemPromptEditor };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "table",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Table",
|
|
6
|
+
"description": "Semantic data-table primitive with sub-components (Table.Header, Table.Body, Table.Row, Table.Cell, Table.HeaderCell). Supports density (default | compact via Context), per-cell align (left | center | right), numeric cells (font-mono tabular-nums), and sortable header cells (onSort + sortDirection with ChevronUp/ChevronDown affordance + aria-sort).",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"lucide-react"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/table/table.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/table.tsx",
|
|
19
|
+
"content": "import { ChevronDown, ChevronUp } from \"lucide-react\";\nimport { createContext, forwardRef, useContext } from \"react\";\nimport type { HTMLAttributes, TdHTMLAttributes, ThHTMLAttributes } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Table — semantic data table primitive with sub-components.\n *\n * Composition:\n * <Table density=\"default\">\n * <Table.Header>\n * <Table.Row>\n * <Table.HeaderCell>Date</Table.HeaderCell>\n * <Table.HeaderCell align=\"right\">Amount</Table.HeaderCell>\n * </Table.Row>\n * </Table.Header>\n * <Table.Body>\n * <Table.Row>\n * <Table.Cell>2026-05-23</Table.Cell>\n * <Table.Cell align=\"right\" numeric>$ 42.00</Table.Cell>\n * </Table.Row>\n * </Table.Body>\n * </Table>\n *\n * density propagates via Context so Cells pick up vertical padding without\n * prop drilling. Sub-components are attached as static properties on the\n * root (`Table.Header`, etc.) — single import surface.\n *\n * Sortable header: pass `onSort` + `sortDirection`. The HeaderCell renders\n * the sort affordance (ChevronUp/ChevronDown) and triggers the consumer\n * callback. `sortDirection` without `onSort` is a no-op (header stays\n * static); `sortDirection=\"none\"` with `onSort` shows a dimmed affordance.\n */\n\ntype TableDensity = \"default\" | \"compact\";\ntype AlignKind = \"left\" | \"center\" | \"right\";\ntype SortDirection = \"asc\" | \"desc\" | \"none\";\n\nconst TableDensityContext = createContext<TableDensity>(\"default\");\n\nconst alignClass: Record<AlignKind, string> = {\n left: \"text-left\",\n center: \"text-center\",\n right: \"text-right\",\n};\n\nexport interface TableProps extends HTMLAttributes<HTMLTableElement> {\n density?: TableDensity;\n}\n\nconst Root = forwardRef<HTMLTableElement, TableProps>(\n ({ className, density = \"default\", children, ...props }, ref) => (\n <TableDensityContext.Provider value={density}>\n <table\n ref={ref}\n className={cn(\"w-full border-collapse font-sans text-body-sm\", className)}\n {...props}\n >\n {children}\n </table>\n </TableDensityContext.Provider>\n ),\n);\nRoot.displayName = \"Table\";\n\nconst Header = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(\n ({ className, ...props }, ref) => (\n <thead\n ref={ref}\n className={cn(\n \"border-border/40 border-b text-label-caps text-muted-foreground uppercase tracking-wider\",\n className,\n )}\n {...props}\n />\n ),\n);\nHeader.displayName = \"Table.Header\";\n\nconst Body = forwardRef<HTMLTableSectionElement, HTMLAttributes<HTMLTableSectionElement>>(\n ({ className, ...props }, ref) => (\n <tbody ref={ref} className={cn(\"text-foreground\", className)} {...props} />\n ),\n);\nBody.displayName = \"Table.Body\";\n\nconst Row = forwardRef<HTMLTableRowElement, HTMLAttributes<HTMLTableRowElement>>(\n ({ className, ...props }, ref) => (\n <tr\n ref={ref}\n className={cn(\n \"border-border/20 border-b transition-colors last:border-0 hover:bg-muted/40\",\n className,\n )}\n {...props}\n />\n ),\n);\nRow.displayName = \"Table.Row\";\n\nexport interface TableCellProps extends TdHTMLAttributes<HTMLTableCellElement> {\n align?: AlignKind;\n numeric?: boolean;\n}\n\nconst Cell = forwardRef<HTMLTableCellElement, TableCellProps>(\n ({ className, align = \"left\", numeric, children, ...props }, ref) => {\n const density = useContext(TableDensityContext);\n return (\n <td\n ref={ref}\n className={cn(\n \"px-3\",\n density === \"compact\" ? \"py-1.5\" : \"py-3\",\n alignClass[align],\n numeric && \"font-mono tabular-nums\",\n className,\n )}\n {...props}\n >\n {children}\n </td>\n );\n },\n);\nCell.displayName = \"Table.Cell\";\n\nexport interface TableHeaderCellProps extends ThHTMLAttributes<HTMLTableCellElement> {\n align?: AlignKind;\n /** When provided, header becomes a sort trigger. */\n onSort?: () => void;\n /** Current sort state for this column. */\n sortDirection?: SortDirection;\n}\n\nconst HeaderCell = forwardRef<HTMLTableCellElement, TableHeaderCellProps>(\n ({ className, align = \"left\", onSort, sortDirection = \"none\", children, ...props }, ref) => {\n const sortAffordance =\n onSort !== undefined ? (\n <span className=\"ml-1 inline-flex flex-col\">\n <ChevronUp\n aria-hidden=\"true\"\n className={cn(\"-mb-1 size-3\", sortDirection === \"asc\" ? \"opacity-100\" : \"opacity-30\")}\n />\n <ChevronDown\n aria-hidden=\"true\"\n className={cn(\"size-3\", sortDirection === \"desc\" ? \"opacity-100\" : \"opacity-30\")}\n />\n </span>\n ) : null;\n\n const ariaSort: ThHTMLAttributes<HTMLTableCellElement>[\"aria-sort\"] =\n onSort === undefined\n ? undefined\n : sortDirection === \"asc\"\n ? \"ascending\"\n : sortDirection === \"desc\"\n ? \"descending\"\n : \"none\";\n\n return (\n <th\n ref={ref}\n scope=\"col\"\n aria-sort={ariaSort}\n className={cn(\n \"px-3 py-2.5 font-medium\",\n alignClass[align],\n align === \"right\" && \"[&_button]:justify-end\",\n className,\n )}\n {...props}\n >\n {onSort !== undefined ? (\n <button\n type=\"button\"\n onClick={onSort}\n className={cn(\n \"inline-flex items-center gap-1\",\n \"text-label-caps uppercase tracking-wider\",\n \"transition-colors hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n )}\n >\n {children}\n {sortAffordance}\n </button>\n ) : (\n children\n )}\n </th>\n );\n },\n);\nHeaderCell.displayName = \"Table.HeaderCell\";\n\ntype TableRoot = typeof Root & {\n Header: typeof Header;\n Body: typeof Body;\n Row: typeof Row;\n Cell: typeof Cell;\n HeaderCell: typeof HeaderCell;\n};\n\nconst Table: TableRoot = Object.assign(Root, { Header, Body, Row, Cell, HeaderCell });\n\nexport { Table };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "tabs",
|
|
4
|
+
"type": "registry:ui",
|
|
5
|
+
"title": "Tabs",
|
|
6
|
+
"description": "Built on Radix Tabs with active-underline styling and focus-visible ring.",
|
|
7
|
+
"dependencies": [
|
|
8
|
+
"@radix-ui/react-tabs"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [
|
|
11
|
+
"https://usetheodev.github.io/theo-ui/r/cn.json",
|
|
12
|
+
"https://usetheodev.github.io/theo-ui/r/tailwind-preset.json"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
{
|
|
16
|
+
"path": "components/primitives/tabs/tabs.tsx",
|
|
17
|
+
"type": "registry:ui",
|
|
18
|
+
"target": "components/ui/tabs.tsx",
|
|
19
|
+
"content": "import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { forwardRef } from \"react\";\nimport type { ComponentPropsWithoutRef, ElementRef } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Tabs — built on Radix Tabs.\n *\n * Visual: underlined active tab in primary (violet), inactive in muted-foreground.\n * Used in project views (Overview / Deployments / Logs / Settings).\n */\n\nconst List = forwardRef<\n ElementRef<typeof TabsPrimitive.List>,\n ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.List\n ref={ref}\n className={cn(\n \"inline-flex h-10 items-center border-border/40 border-b text-muted-foreground\",\n className,\n )}\n {...props}\n />\n));\nList.displayName = \"Tabs.List\";\n\nconst Trigger = forwardRef<\n ElementRef<typeof TabsPrimitive.Trigger>,\n ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Trigger\n ref={ref}\n className={cn(\n \"relative inline-flex h-10 items-center justify-center whitespace-nowrap px-4\",\n \"font-medium font-sans text-body-sm text-muted-foreground\",\n \"transition-colors duration-base ease-out-soft\",\n \"hover:text-foreground\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"data-[state=active]:text-foreground\",\n // Active underline using a pseudo-element via shadow\n \"data-[state=active]:shadow-[inset_0_-2px_0_0_hsl(var(--primary))]\",\n className,\n )}\n {...props}\n />\n));\nTrigger.displayName = \"Tabs.Trigger\";\n\nconst Content = forwardRef<\n ElementRef<typeof TabsPrimitive.Content>,\n ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n <TabsPrimitive.Content\n ref={ref}\n className={cn(\n \"mt-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n className,\n )}\n {...props}\n />\n));\nContent.displayName = \"Tabs.Content\";\n\nconst Tabs = /*#__PURE__*/ Object.assign(TabsPrimitive.Root, {\n List,\n Trigger,\n Content,\n});\n\nexport { Tabs };\n"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|