@flamingo-stack/openframe-frontend-core 0.0.490 → 0.0.492-1506.3956.1
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/dist/chat-protocol/decode.d.ts +58 -0
- package/dist/chat-protocol/decode.d.ts.map +1 -0
- package/dist/chat-protocol/encode.d.ts +44 -0
- package/dist/chat-protocol/encode.d.ts.map +1 -0
- package/dist/chat-protocol/env-flag.d.ts +35 -0
- package/dist/chat-protocol/env-flag.d.ts.map +1 -0
- package/dist/chat-protocol/events.d.ts +207 -0
- package/dist/chat-protocol/events.d.ts.map +1 -0
- package/dist/chat-protocol/frames.d.ts +213 -0
- package/dist/chat-protocol/frames.d.ts.map +1 -0
- package/dist/chat-protocol/index.cjs +500 -0
- package/dist/chat-protocol/index.cjs.map +1 -0
- package/dist/chat-protocol/index.d.ts +18 -0
- package/dist/chat-protocol/index.d.ts.map +1 -0
- package/dist/chat-protocol/index.js +484 -0
- package/dist/chat-protocol/index.js.map +1 -0
- package/dist/chat-protocol/ip-normalize.d.ts +44 -0
- package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +26 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
- package/dist/{chunk-WLM3F3WU.js → chunk-34TDA22K.js} +3 -3
- package/dist/{chunk-EGXCWZYP.js → chunk-3LKB722U.js} +3 -3
- package/dist/{chunk-RW2LPL72.js → chunk-6WTCJI5R.js} +2 -2
- package/dist/{chunk-VUXNXWOQ.cjs → chunk-7N7YZCHR.cjs} +7503 -5167
- package/dist/chunk-7N7YZCHR.cjs.map +1 -0
- package/dist/{chunk-AW63ET2A.cjs → chunk-A6FPYAUD.cjs} +19 -19
- package/dist/{chunk-AW63ET2A.cjs.map → chunk-A6FPYAUD.cjs.map} +1 -1
- package/dist/{chunk-BZMOEWWS.js → chunk-BEEPU44B.js} +12 -12
- package/dist/chunk-BEEPU44B.js.map +1 -0
- package/dist/{chunk-EJQUYZXF.cjs → chunk-C65NCIMV.cjs} +12 -12
- package/dist/{chunk-EJQUYZXF.cjs.map → chunk-C65NCIMV.cjs.map} +1 -1
- package/dist/{chunk-VL2YMDPH.js → chunk-J3VBVR4W.js} +6 -6
- package/dist/{chunk-AIRNG27G.js → chunk-JHEIOXXZ.js} +8 -7
- package/dist/chunk-JHEIOXXZ.js.map +1 -0
- package/dist/{chunk-D5LQMRPU.cjs → chunk-JSEJ5MH5.cjs} +5 -5
- package/dist/{chunk-D5LQMRPU.cjs.map → chunk-JSEJ5MH5.cjs.map} +1 -1
- package/dist/{chunk-FY5XAW3H.cjs → chunk-KX3K2EH4.cjs} +18 -18
- package/dist/{chunk-FY5XAW3H.cjs.map → chunk-KX3K2EH4.cjs.map} +1 -1
- package/dist/{chunk-3IEVA5AO.cjs → chunk-M6FEGPQJ.cjs} +93 -93
- package/dist/{chunk-3IEVA5AO.cjs.map → chunk-M6FEGPQJ.cjs.map} +1 -1
- package/dist/{chunk-7I42V2OD.cjs → chunk-MEIGDK7M.cjs} +74 -74
- package/dist/chunk-MEIGDK7M.cjs.map +1 -0
- package/dist/{chunk-OQIMH2WQ.js → chunk-MEMRQTOI.js} +12263 -9927
- package/dist/chunk-MEMRQTOI.js.map +1 -0
- package/dist/{chunk-GUTYUMGV.cjs → chunk-MPUQFRJG.cjs} +12 -12
- package/dist/{chunk-GUTYUMGV.cjs.map → chunk-MPUQFRJG.cjs.map} +1 -1
- package/dist/{chunk-UZZT65PQ.cjs → chunk-MSL5NE3N.cjs} +40 -40
- package/dist/{chunk-UZZT65PQ.cjs.map → chunk-MSL5NE3N.cjs.map} +1 -1
- package/dist/{chunk-6CJ2K7BD.js → chunk-MZRIRQYV.js} +3 -3
- package/dist/{chunk-NGEFYPY7.js → chunk-NCAN6VUF.js} +4 -4
- package/dist/{chunk-6QK3ONDD.js → chunk-OL3MJKUI.js} +2 -2
- package/dist/{chunk-MPYDX5WX.js → chunk-PLB6CQSO.js} +4 -4
- package/dist/{chunk-6CFFISUC.js → chunk-PTXZB2KW.js} +3 -3
- package/dist/{chunk-GMPYR3MX.cjs → chunk-QDGB2DKY.cjs} +33 -33
- package/dist/{chunk-GMPYR3MX.cjs.map → chunk-QDGB2DKY.cjs.map} +1 -1
- package/dist/{chunk-2MKUHJ3O.cjs → chunk-QG2EBIH5.cjs} +40 -40
- package/dist/{chunk-2MKUHJ3O.cjs.map → chunk-QG2EBIH5.cjs.map} +1 -1
- package/dist/{chunk-GZK7FWJJ.js → chunk-QKWOBV55.js} +12 -12
- package/dist/chunk-QKWOBV55.js.map +1 -0
- package/dist/{chunk-STYA77PH.cjs → chunk-SGL6URIJ.cjs} +12 -12
- package/dist/chunk-SGL6URIJ.cjs.map +1 -0
- package/dist/{chunk-KP5KRRD6.cjs → chunk-THC3K262.cjs} +3 -3
- package/dist/{chunk-KP5KRRD6.cjs.map → chunk-THC3K262.cjs.map} +1 -1
- package/dist/{chunk-BIKOJ4FH.cjs → chunk-TTYJAZ7P.cjs} +58 -32
- package/dist/chunk-TTYJAZ7P.cjs.map +1 -0
- package/dist/{chunk-IEZ3YBAP.cjs → chunk-W56JYDSC.cjs} +34 -33
- package/dist/chunk-W56JYDSC.cjs.map +1 -0
- package/dist/{chunk-PB3BTZDU.js → chunk-WEUNPE3S.js} +139 -113
- package/dist/chunk-WEUNPE3S.js.map +1 -0
- package/dist/{chunk-VDZO6IDR.js → chunk-XOFE2RT4.js} +5 -5
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +0 -1
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -57
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +12 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +23 -15
- package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
- package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
- package/dist/components/chat/stream/index.d.ts +17 -0
- package/dist/components/chat/stream/index.d.ts.map +1 -0
- package/dist/components/chat/stream/message-mutations.d.ts +77 -0
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/types/api.types.d.ts +1 -81
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +2 -93
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +2 -3
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/features/select-button.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +203 -161
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +66 -24
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +7 -1
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/title-block.d.ts +23 -1
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/checkbox-block.d.ts.map +1 -1
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/field-wrapper.d.ts +10 -1
- package/dist/components/ui/field-wrapper.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/floating-tooltip.d.ts +8 -1
- package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +46 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +57 -15
- package/dist/components/ui/input-trigger.d.ts.map +1 -1
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/markdown/base-components.d.ts +51 -0
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
- package/dist/components/ui/markdown/engine.d.ts +99 -0
- package/dist/components/ui/markdown/engine.d.ts.map +1 -0
- package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
- package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
- package/dist/components/ui/markdown/index.d.ts +18 -0
- package/dist/components/ui/markdown/index.d.ts.map +1 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
- package/dist/components/ui/markdown/sanitize.d.ts +153 -0
- package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/streaming.d.ts +78 -0
- package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
- package/dist/components/ui/markdown/text-size.d.ts +34 -0
- package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
- package/dist/components/ui/page-actions.d.ts +7 -1
- package/dist/components/ui/page-actions.d.ts.map +1 -1
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/search-input.d.ts.map +1 -1
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.d.ts +10 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tag-search-input.d.ts.map +1 -1
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/truncate-text.d.ts +2 -2
- package/dist/components/ui/truncate-text.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-is-truncated.d.ts +33 -0
- package/dist/hooks/ui/use-is-truncated.d.ts.map +1 -0
- package/dist/index.cjs +64 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +75 -15
- package/dist/utils/index.cjs +169 -40
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +162 -41
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-fences.d.ts +42 -0
- package/dist/utils/markdown-fences.d.ts.map +1 -0
- package/dist/utils/markdown-heading-id.d.ts +85 -0
- package/dist/utils/markdown-heading-id.d.ts.map +1 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +317 -0
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
- package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +232 -0
- package/src/chat-protocol/decode.ts +278 -0
- package/src/chat-protocol/encode.ts +71 -0
- package/src/chat-protocol/env-flag.ts +39 -0
- package/src/chat-protocol/events.ts +245 -0
- package/src/chat-protocol/frames.ts +245 -0
- package/src/chat-protocol/index.ts +21 -0
- package/src/chat-protocol/ip-normalize.ts +146 -0
- package/src/chat-protocol/nats-decoder.ts +258 -0
- package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +230 -0
- package/src/components/chat/chat-message-enhanced.tsx +122 -20
- package/src/components/chat/chat-message-list.tsx +358 -109
- package/src/components/chat/embeddable-chat.tsx +6 -0
- package/src/components/chat/hooks/.index.md +30 -33
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
- package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
- package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
- package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
- package/src/components/chat/hooks/index.ts +0 -1
- package/src/components/chat/hooks/use-chat.ts +8 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -825
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
- package/src/components/chat/index.ts +5 -0
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +973 -0
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
- package/src/components/chat/stream/chat-dialog-store.ts +536 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +1808 -0
- package/src/components/chat/stream/delta-batcher.ts +168 -0
- package/src/components/chat/stream/index.ts +55 -0
- package/src/components/chat/stream/message-mutations.ts +340 -0
- package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +39 -49
- package/src/components/chat/types/.processing.types.md +29 -58
- package/src/components/chat/types/api.types.ts +6 -73
- package/src/components/chat/types/processing.types.ts +11 -53
- package/src/components/chat/utils/.index.md +31 -40
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
- package/src/components/chat/utils/index.ts +5 -8
- package/src/components/chat/utils/process-historical-messages.ts +358 -386
- package/src/components/features/select-button.tsx +4 -1
- package/src/components/layout/page-layout.tsx +16 -0
- package/src/components/layout/title-block.tsx +106 -7
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +19 -4
- package/src/components/ui/.field-wrapper.md +5 -4
- package/src/components/ui/.tab-navigation.md +14 -1
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
- package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
- package/src/components/ui/autocomplete.tsx +13 -1
- package/src/components/ui/button/button.tsx +1 -1
- package/src/components/ui/checkbox-block.tsx +27 -3
- package/src/components/ui/date-picker.tsx +15 -1
- package/src/components/ui/field-wrapper.tsx +12 -6
- package/src/components/ui/floating-tooltip.tsx +9 -1
- package/src/components/ui/index.ts +1 -2
- package/src/components/ui/input-trigger.tsx +8 -1
- package/src/components/ui/input.tsx +8 -1
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
- package/src/components/ui/markdown/base-components.tsx +360 -0
- package/src/components/ui/markdown/engine.tsx +315 -0
- package/src/components/ui/markdown/heading-ids.ts +239 -0
- package/src/components/ui/markdown/index.ts +49 -0
- package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
- package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
- package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
- package/src/components/ui/markdown/sanitize.ts +3499 -0
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
- package/src/components/ui/markdown/streaming.ts +362 -0
- package/src/components/ui/markdown/text-size.ts +106 -0
- package/src/components/ui/page-actions.tsx +142 -30
- package/src/components/ui/radio-group.tsx +28 -3
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/search-input.tsx +2 -1
- package/src/components/ui/select.tsx +7 -0
- package/src/components/ui/tab-navigation.tsx +26 -4
- package/src/components/ui/tag-search-input.tsx +9 -2
- package/src/components/ui/textarea.tsx +8 -1
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/components/ui/truncate-text.tsx +3 -18
- package/src/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/use-is-truncated.ts +66 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown-fences.ts +127 -0
- package/src/utils/markdown-heading-id.ts +348 -0
- package/src/utils/markdown-section-extractor.ts +41 -56
- package/dist/chunk-7I42V2OD.cjs.map +0 -1
- package/dist/chunk-AIRNG27G.js.map +0 -1
- package/dist/chunk-BIKOJ4FH.cjs.map +0 -1
- package/dist/chunk-BZMOEWWS.js.map +0 -1
- package/dist/chunk-GZK7FWJJ.js.map +0 -1
- package/dist/chunk-IEZ3YBAP.cjs.map +0 -1
- package/dist/chunk-OQIMH2WQ.js.map +0 -1
- package/dist/chunk-PB3BTZDU.js.map +0 -1
- package/dist/chunk-STYA77PH.cjs.map +0 -1
- package/dist/chunk-VUXNXWOQ.cjs.map +0 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
- package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -489
- package/src/components/chat/utils/.chunk-parser.md +0 -62
- package/src/components/chat/utils/chunk-parser.ts +0 -262
- package/src/components/ui/.simple-markdown-renderer.md +0 -52
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
- package/src/components/ui/simple-markdown-renderer.tsx +0 -964
- /package/dist/{chunk-WLM3F3WU.js.map → chunk-34TDA22K.js.map} +0 -0
- /package/dist/{chunk-EGXCWZYP.js.map → chunk-3LKB722U.js.map} +0 -0
- /package/dist/{chunk-RW2LPL72.js.map → chunk-6WTCJI5R.js.map} +0 -0
- /package/dist/{chunk-VL2YMDPH.js.map → chunk-J3VBVR4W.js.map} +0 -0
- /package/dist/{chunk-6CJ2K7BD.js.map → chunk-MZRIRQYV.js.map} +0 -0
- /package/dist/{chunk-NGEFYPY7.js.map → chunk-NCAN6VUF.js.map} +0 -0
- /package/dist/{chunk-6QK3ONDD.js.map → chunk-OL3MJKUI.js.map} +0 -0
- /package/dist/{chunk-MPYDX5WX.js.map → chunk-PLB6CQSO.js.map} +0 -0
- /package/dist/{chunk-6CFFISUC.js.map → chunk-PTXZB2KW.js.map} +0 -0
- /package/dist/{chunk-VDZO6IDR.js.map → chunk-XOFE2RT4.js.map} +0 -0
|
@@ -124,7 +124,8 @@ const innerInputStyles = cn(
|
|
|
124
124
|
"flex-1 min-w-[60px] bg-transparent border-none outline-none",
|
|
125
125
|
"text-h4",
|
|
126
126
|
"text-ods-text-primary placeholder:text-ods-text-secondary",
|
|
127
|
-
|
|
127
|
+
// Disabled - match Input exactly (value greys out, placeholder dims further)
|
|
128
|
+
"disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border"
|
|
128
129
|
)
|
|
129
130
|
|
|
130
131
|
function AutocompleteInner<T = string>(
|
|
@@ -421,6 +422,9 @@ function AutocompleteInner<T = string>(
|
|
|
421
422
|
<PopoverPrimitive.Root open={isOpen} onOpenChange={handleOpenChange} modal={false}>
|
|
422
423
|
<PopoverPrimitive.Anchor asChild>
|
|
423
424
|
<div
|
|
425
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
426
|
+
// (and scrolls to) the first field that failed validation.
|
|
427
|
+
data-invalid={isInvalid || undefined}
|
|
424
428
|
className={cn(
|
|
425
429
|
// Layout — single line, no wrapping
|
|
426
430
|
"flex items-center rounded-[6px] border min-h-11 md:min-h-12 cursor-text",
|
|
@@ -430,6 +434,14 @@ function AutocompleteInner<T = string>(
|
|
|
430
434
|
"group",
|
|
431
435
|
!disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active",
|
|
432
436
|
disabled && "!cursor-not-allowed bg-ods-bg",
|
|
437
|
+
// Adornments / chevron carry their own colour — grey them with the
|
|
438
|
+
// value so a disabled field reads as one flat colour. Scoped to the
|
|
439
|
+
// DIRECT span child (the start adornment) on purpose: as a descendant
|
|
440
|
+
// rule it also hit the selected `Tag` chips in the middle zone, whose
|
|
441
|
+
// label is a span, and they went invisible against their own fill in
|
|
442
|
+
// the light theme. Anything nested inside the adornment that sets its
|
|
443
|
+
// own colour keeps it; anything that doesn't inherits the grey.
|
|
444
|
+
"has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled",
|
|
433
445
|
isOpen && !isInvalid && "border-ods-accent hover:border-ods-accent",
|
|
434
446
|
isInvalid && "border-ods-error hover:border-ods-error"
|
|
435
447
|
)}
|
|
@@ -30,7 +30,7 @@ const buttonVariants = cva(
|
|
|
30
30
|
overlay: buttonSurfaceClasses.overlay,
|
|
31
31
|
},
|
|
32
32
|
size: {
|
|
33
|
-
default: "py-[var(--spacing-system-sf)] px-[var(--spacing-system-m)] text-h3 md:h-12 h-
|
|
33
|
+
default: "py-[var(--spacing-system-sf)] px-[var(--spacing-system-m)] text-h3 md:h-12 h-11",
|
|
34
34
|
small: "p-[var(--spacing-system-xs)] text-h5 h-6 md:h-8",
|
|
35
35
|
"small-legacy": "py-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] h-10 text-[14px] font-bold", // Temporary alias for "small" — deprecated; grep size="small-legacy" (lib + hub) and migrate the remaining consumers before removal
|
|
36
36
|
// 24px pill for slim strips (announcement/promo bars, inline banner
|
|
@@ -36,6 +36,9 @@ const CheckboxBlock = React.forwardRef<
|
|
|
36
36
|
<div className={cn("relative flex w-full flex-col", className)}>
|
|
37
37
|
<label
|
|
38
38
|
htmlFor={id}
|
|
39
|
+
// Same marker every field exposes — it is how a form finds (and scrolls
|
|
40
|
+
// to) the first control that failed validation.
|
|
41
|
+
data-invalid={error ? true : undefined}
|
|
39
42
|
className={cn(
|
|
40
43
|
"flex items-center gap-[var(--spacing-system-s)] rounded-md ring-1 ring-inset w-full",
|
|
41
44
|
// Trailing content stacks full-width below the text on mobile.
|
|
@@ -45,8 +48,17 @@ const CheckboxBlock = React.forwardRef<
|
|
|
45
48
|
description && "min-h-[60px] md:min-h-[64px]",
|
|
46
49
|
"bg-ods-card ring-ods-border",
|
|
47
50
|
"cursor-pointer transition-colors duration-200",
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
// States mirror Input: hover / active move the BACKGROUND, the ring
|
|
52
|
+
// stays put. Disabled swaps to the flat `ods-bg` fill and greys the
|
|
53
|
+
// text + the box instead of fading the whole block, so a disabled block
|
|
54
|
+
// reads the same as a disabled field. The greying itself lives on the
|
|
55
|
+
// TEXT COLUMN below, not here: as a descendant rule on the label it also
|
|
56
|
+
// repainted the checked INDICATOR (Radix renders it as a `span`, and the
|
|
57
|
+
// checkmark svg inherits `currentColor`) and whatever `trailing` renders
|
|
58
|
+
// — a `Tag`'s label is a span too, and it went invisible against its own
|
|
59
|
+
// fill in the light theme.
|
|
60
|
+
!disabled && "hover:bg-ods-bg-hover active:bg-ods-bg-active",
|
|
61
|
+
disabled && "cursor-not-allowed bg-ods-bg",
|
|
50
62
|
error && "ring-ods-error",
|
|
51
63
|
)}
|
|
52
64
|
>
|
|
@@ -63,7 +75,11 @@ const CheckboxBlock = React.forwardRef<
|
|
|
63
75
|
error ? "border-ods-error" : "border-[var(--color-border-strong)]",
|
|
64
76
|
"bg-ods-card",
|
|
65
77
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-card",
|
|
78
|
+
// Disabled: the EMPTY box dims with the text instead of the whole
|
|
79
|
+
// block fading. A checked box keeps its accent fill — greying it too
|
|
80
|
+
// would bury the checkmark, and "on but locked" has to stay readable.
|
|
66
81
|
"disabled:cursor-not-allowed",
|
|
82
|
+
"disabled:data-[state=unchecked]:bg-ods-bg disabled:data-[state=unchecked]:border-ods-border",
|
|
67
83
|
"data-[state=checked]:bg-[var(--color-accent-primary)] data-[state=checked]:border-[var(--color-accent-primary)]"
|
|
68
84
|
)}
|
|
69
85
|
>
|
|
@@ -73,7 +89,15 @@ const CheckboxBlock = React.forwardRef<
|
|
|
73
89
|
<CheckboxCheckmarkIcon className="w-2 h-2 md:w-2.5 md:h-2.5" />
|
|
74
90
|
</CheckboxPrimitive.Indicator>
|
|
75
91
|
</CheckboxPrimitive.Root>
|
|
76
|
-
<div
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
"flex flex-1 flex-col justify-center min-w-0",
|
|
95
|
+
// Direct children only — these are the two component-owned wrappers.
|
|
96
|
+
// `label` and `description` are ReactNode (rich text, inline links),
|
|
97
|
+
// so a descendant rule would repaint content the caller styled itself.
|
|
98
|
+
disabled && "[&>span]:text-ods-text-disabled",
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
77
101
|
<span className={cn(
|
|
78
102
|
"text-h4 !leading-5 md:!leading-6",
|
|
79
103
|
"text-ods-text-primary select-none",
|
|
@@ -451,8 +451,12 @@ const triggerButtonStyles = cn(
|
|
|
451
451
|
// Hover & active (not disabled)
|
|
452
452
|
"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active",
|
|
453
453
|
"focus:outline-none",
|
|
454
|
-
// Disabled
|
|
454
|
+
// Disabled - match Input exactly. The value/placeholder span inside sets its
|
|
455
|
+
// own colour, so the child rule (higher specificity) greys it too. Scoped to
|
|
456
|
+
// DIRECT children like every other field, so nested content that owns its
|
|
457
|
+
// colour keeps it.
|
|
455
458
|
"disabled:!cursor-not-allowed disabled:bg-ods-bg",
|
|
459
|
+
"disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled",
|
|
456
460
|
// Animation
|
|
457
461
|
"transition-colors duration-200"
|
|
458
462
|
);
|
|
@@ -465,6 +469,7 @@ const timeSelectTriggerStyles = cn(
|
|
|
465
469
|
"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active",
|
|
466
470
|
"focus:outline-none",
|
|
467
471
|
"disabled:!cursor-not-allowed disabled:bg-ods-bg",
|
|
472
|
+
"disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled",
|
|
468
473
|
"transition-colors duration-200 cursor-pointer",
|
|
469
474
|
"text-ods-text-primary"
|
|
470
475
|
);
|
|
@@ -519,6 +524,9 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
519
524
|
<button
|
|
520
525
|
type="button"
|
|
521
526
|
disabled={disabled}
|
|
527
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
528
|
+
// (and scrolls to) the first field that failed validation.
|
|
529
|
+
data-invalid={isInvalid || undefined}
|
|
522
530
|
className={cn(triggerButtonStyles, "group", open && !isInvalid && "border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card", isInvalid && "border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card", className)}
|
|
523
531
|
>
|
|
524
532
|
<Calendar className="size-6 text-ods-text-secondary shrink-0" />
|
|
@@ -692,6 +700,9 @@ export function DatePickerInput({
|
|
|
692
700
|
<button
|
|
693
701
|
type="button"
|
|
694
702
|
disabled={disabled}
|
|
703
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
704
|
+
// (and scrolls to) the first field that failed validation.
|
|
705
|
+
data-invalid={isInvalid || undefined}
|
|
695
706
|
className={cn(triggerButtonStyles, "group", open && !isInvalid && "border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card", isInvalid && "border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card", "flex-1")}
|
|
696
707
|
>
|
|
697
708
|
<Calendar className="size-6 text-ods-text-secondary shrink-0" />
|
|
@@ -923,6 +934,9 @@ export function DatePickerInputSimple({
|
|
|
923
934
|
<button
|
|
924
935
|
type="button"
|
|
925
936
|
disabled={disabled}
|
|
937
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
938
|
+
// (and scrolls to) the first field that failed validation.
|
|
939
|
+
data-invalid={isInvalid || undefined}
|
|
926
940
|
className={cn(triggerButtonStyles, "group", open && !isInvalid && "border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card", isInvalid && "border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card", "flex-1")}
|
|
927
941
|
>
|
|
928
942
|
<Calendar className="size-6 text-ods-text-secondary shrink-0" />
|
|
@@ -6,7 +6,16 @@ import { cn } from "../../utils/cn"
|
|
|
6
6
|
export interface FieldWrapperProps {
|
|
7
7
|
/** Label text displayed above the field */
|
|
8
8
|
label?: string
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Status message displayed below the field, on ONE line, ellipsized on
|
|
11
|
+
* overflow (full text via the native `title`).
|
|
12
|
+
*
|
|
13
|
+
* It is positioned OUT OF FLOW (absolute, hanging below the wrapper), so it
|
|
14
|
+
* never changes the field's height when it appears — the same treatment
|
|
15
|
+
* `CheckboxBlock` uses. The trade is that it overlaps whatever sits directly
|
|
16
|
+
* below: a form stacking fields needs at least ~20px of vertical gap (e.g.
|
|
17
|
+
* `gap-[var(--spacing-system-lf)]`) for the message to land in clear space.
|
|
18
|
+
*/
|
|
10
19
|
error?: string
|
|
11
20
|
/** Color variant for the message: "error" (red), "warning" (yellow), "success" (green) or "muted" (grey) */
|
|
12
21
|
errorVariant?: "error" | "warning" | "success" | "muted"
|
|
@@ -27,7 +36,7 @@ const FieldWrapper = React.forwardRef<HTMLDivElement, FieldWrapperProps>(
|
|
|
27
36
|
const hasChrome = label != null || error != null
|
|
28
37
|
|
|
29
38
|
return (
|
|
30
|
-
<div ref={ref} className={cn(hasChrome ? "flex w-full flex-col" : "contents", className)}>
|
|
39
|
+
<div ref={ref} className={cn(hasChrome ? "relative flex w-full flex-col" : "contents", className)}>
|
|
31
40
|
{label && (
|
|
32
41
|
<label className="text-h4 text-ods-text-primary mb-1">
|
|
33
42
|
{label}
|
|
@@ -35,10 +44,7 @@ const FieldWrapper = React.forwardRef<HTMLDivElement, FieldWrapperProps>(
|
|
|
35
44
|
)}
|
|
36
45
|
{children}
|
|
37
46
|
{error && (
|
|
38
|
-
<p
|
|
39
|
-
className={cn("mt-1 text-h6 break-words line-clamp-2", errorVariantClasses[errorVariant])}
|
|
40
|
-
title={error}
|
|
41
|
-
>
|
|
47
|
+
<p className={cn("absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate", errorVariantClasses[errorVariant])} title={error}>
|
|
42
48
|
{error}
|
|
43
49
|
</p>
|
|
44
50
|
)}
|
|
@@ -26,6 +26,13 @@ interface FloatingTooltipProps {
|
|
|
26
26
|
delayDuration?: number
|
|
27
27
|
/** Disable the tooltip without unmounting the trigger wrapper. */
|
|
28
28
|
disabled?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Classes for the trigger wrapper — the `div` this component puts around
|
|
31
|
+
* `children` to anchor the tooltip. That div becomes the flex/grid item in the
|
|
32
|
+
* caller's layout, so anything the child needed there (`min-w-0`, `flex-1`)
|
|
33
|
+
* must move onto it. Omit it and the wrapper carries no class, exactly as before.
|
|
34
|
+
*/
|
|
35
|
+
triggerClassName?: string
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
// Parse colored text markup like [YELLOW]text[/YELLOW] into JSX
|
|
@@ -85,6 +92,7 @@ export function FloatingTooltip({
|
|
|
85
92
|
className,
|
|
86
93
|
delayDuration = 0,
|
|
87
94
|
disabled = false,
|
|
95
|
+
triggerClassName,
|
|
88
96
|
}: FloatingTooltipProps) {
|
|
89
97
|
const [isOpen, setIsOpen] = React.useState(false)
|
|
90
98
|
const arrowRef = React.useRef<HTMLDivElement>(null)
|
|
@@ -151,7 +159,7 @@ export function FloatingTooltip({
|
|
|
151
159
|
|
|
152
160
|
return (
|
|
153
161
|
<>
|
|
154
|
-
<div ref={refs.setReference} {...getReferenceProps()}>
|
|
162
|
+
<div ref={refs.setReference} className={triggerClassName} {...getReferenceProps()}>
|
|
155
163
|
{children}
|
|
156
164
|
</div>
|
|
157
165
|
<FloatingPortal>
|
|
@@ -176,7 +176,6 @@ export * from './ticket-attachments-list'
|
|
|
176
176
|
export * from './ticket-note-card'
|
|
177
177
|
export * from './ticket-notes-section'
|
|
178
178
|
export * from './marquee-wall'
|
|
179
|
-
export * from './
|
|
180
|
-
export { RichMarkdownRenderer, type RichMarkdownRendererProps } from './rich-markdown-renderer'
|
|
179
|
+
export * from './markdown'
|
|
181
180
|
export * from './filter-pill-row'
|
|
182
181
|
|
|
@@ -52,7 +52,14 @@ export const InputTrigger = React.forwardRef<HTMLButtonElement, InputTriggerProp
|
|
|
52
52
|
"data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent",
|
|
53
53
|
invalid &&
|
|
54
54
|
"border-ods-error enabled:hover:border-ods-error data-[state=open]:border-ods-error",
|
|
55
|
-
|
|
55
|
+
// Disabled - match Input / SelectTrigger: grey the TEXT, don't fade the
|
|
56
|
+
// whole control (a blanket opacity also washes out the border and the
|
|
57
|
+
// adornment icons, which no other field does). The child rule catches
|
|
58
|
+
// the label span, which sets its own placeholder colour. Scoped to
|
|
59
|
+
// DIRECT children: `selectedLabel` is a ReactNode the caller owns, so
|
|
60
|
+
// anything inside it that sets its own colour keeps it.
|
|
61
|
+
"disabled:cursor-not-allowed disabled:bg-ods-bg",
|
|
62
|
+
"disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled",
|
|
56
63
|
"transition-colors duration-200",
|
|
57
64
|
className,
|
|
58
65
|
)}
|
|
@@ -83,8 +83,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
83
83
|
"bg-ods-card border-ods-border has-[:focus]:border-ods-accent",
|
|
84
84
|
// Hover & active (not disabled)
|
|
85
85
|
!props.disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active",
|
|
86
|
-
// Disabled
|
|
86
|
+
// Disabled. The adornments (and any icon inside them) carry their own
|
|
87
|
+
// `text-ods-text-secondary`, so grey them from here — a disabled field
|
|
88
|
+
// must read as one flat colour, not grey text next to a live icon.
|
|
89
|
+
// Scoped to the DIRECT span children (the two adornment wrappers): a
|
|
90
|
+
// descendant rule would also repaint whatever the caller renders INSIDE
|
|
91
|
+
// an adornment, which is passed verbatim precisely so it can own its
|
|
92
|
+
// colour (a `Tag` there went invisible against its own fill).
|
|
87
93
|
props.disabled && "!cursor-not-allowed bg-ods-bg",
|
|
94
|
+
"has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled",
|
|
88
95
|
// Invalid
|
|
89
96
|
isInvalid && invalidBorderClasses[variantIsInvalid ? (errorVariant as "error" | "warning") : "error"],
|
|
90
97
|
className
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid renders MODEL-authored diagram source on the chat path, and the
|
|
3
|
+
* result is injected with `dangerouslySetInnerHTML`. This fixture pins the
|
|
4
|
+
* hardening in ../mermaid-diagram.tsx: a hostile node label must NOT survive
|
|
5
|
+
* as live HTML.
|
|
6
|
+
*
|
|
7
|
+
* The test drives `mermaid` with the SAME security knobs the component sets,
|
|
8
|
+
* by IMPORTING them: `MERMAID_SECURITY_OPTIONS` is the single source of truth
|
|
9
|
+
* and is spread into both `mermaid.initialize` calls. There are no literals
|
|
10
|
+
* here to drift from the component — flipping `securityLevel` to `'loose'` in
|
|
11
|
+
* the constant fails this suite (verified).
|
|
12
|
+
*
|
|
13
|
+
* `secure` is part of that constant: it is the allowlist of config keys a
|
|
14
|
+
* `%%{init}%%` directive inside the diagram SOURCE may not override. mermaid's
|
|
15
|
+
* default list omits `htmlLabels`, so untrusted source could otherwise
|
|
16
|
+
* re-enable HTML labels while `securityLevel` stayed locked.
|
|
17
|
+
*
|
|
18
|
+
* jsdom NOTE (recorded, not proven): before `htmlLabels` was added to `secure`,
|
|
19
|
+
* a `%%{init: {"htmlLabels": true}}%%` directive was observed to make
|
|
20
|
+
* `mermaid.render` never settle under jsdom (>60s, against a passing
|
|
21
|
+
* two-render control). That was NOT reproduced in a real browser and may be an
|
|
22
|
+
* artifact of jsdom having no layout — it is NOT a confirmed browser DoS. The
|
|
23
|
+
* component wraps its render in a timeout regardless.
|
|
24
|
+
*/
|
|
25
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
26
|
+
import { MERMAID_SECURITY_OPTIONS } from '../mermaid-diagram'
|
|
27
|
+
|
|
28
|
+
// jsdom implements no SVG layout, and mermaid's dagre pass measures text.
|
|
29
|
+
// These stubs are only about making the renderer RUN; they do not touch the
|
|
30
|
+
// sanitization path under test.
|
|
31
|
+
function installSvgLayoutStubs(): void {
|
|
32
|
+
const proto = (globalThis as unknown as { SVGElement?: { prototype: Record<string, unknown> } }).SVGElement?.prototype
|
|
33
|
+
if (!proto) return
|
|
34
|
+
proto.getBBox = function getBBox() {
|
|
35
|
+
return { x: 0, y: 0, width: 100, height: 20 }
|
|
36
|
+
}
|
|
37
|
+
proto.getComputedTextLength = function getComputedTextLength() {
|
|
38
|
+
return 100
|
|
39
|
+
}
|
|
40
|
+
proto.getScreenCTM = function getScreenCTM() {
|
|
41
|
+
return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0, inverse: () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }) }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Second tuple slot: text that MUST be present in the rendered output — a
|
|
46
|
+
// positive control proving the diagram was actually drawn, so the negative
|
|
47
|
+
// assertions below can't pass on empty output. Note the two payloads are
|
|
48
|
+
// neutralized differently: the `<img>` survives as ESCAPED label text, while
|
|
49
|
+
// the `<script>` is dropped outright, so its control is the sibling node.
|
|
50
|
+
const HOSTILE_LABELS: ReadonlyArray<readonly [chart: string, expectedText: string]> = [
|
|
51
|
+
['graph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]', '<img'],
|
|
52
|
+
['graph TD\n A["<script>alert(1)</script>"] --> B["ok"]', 'ok'],
|
|
53
|
+
// A `%%{init}%%` directive trying to unlock the two knobs. Both keys are in
|
|
54
|
+
// `secure`, so the override is rejected and the payload stays escaped.
|
|
55
|
+
[
|
|
56
|
+
'%%{init:{"securityLevel":"loose"}}%%\ngraph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]',
|
|
57
|
+
'<img',
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
'%%{init:{"htmlLabels":true}}%%\ngraph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]',
|
|
61
|
+
'<img',
|
|
62
|
+
],
|
|
63
|
+
// YAML front-matter config — the other in-source config channel.
|
|
64
|
+
[
|
|
65
|
+
'---\nconfig:\n securityLevel: loose\n---\ngraph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]',
|
|
66
|
+
'<img',
|
|
67
|
+
],
|
|
68
|
+
// `click` interaction directive: disabled outright by 'strict'.
|
|
69
|
+
['graph TD\n A["ok"] --> B["b"]\n click A "javascript:alert(1)"', 'ok'],
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
/** Initialize with the COMPONENT's security options — imported, never
|
|
73
|
+
* re-typed — so the fixture cannot drift from the renderer. */
|
|
74
|
+
function initializeLikeComponent(mermaid: { initialize: (c: Record<string, unknown>) => void }): void {
|
|
75
|
+
mermaid.initialize({
|
|
76
|
+
startOnLoad: false,
|
|
77
|
+
theme: 'dark',
|
|
78
|
+
flowchart: { useMaxWidth: true },
|
|
79
|
+
...MERMAID_SECURITY_OPTIONS,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('mermaid hostile-label hardening', () => {
|
|
84
|
+
beforeAll(() => {
|
|
85
|
+
installSvgLayoutStubs()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it.each(HOSTILE_LABELS)('does not emit live HTML for %j', async (chart, expectedText) => {
|
|
89
|
+
const { default: mermaid } = await import('mermaid')
|
|
90
|
+
|
|
91
|
+
initializeLikeComponent(mermaid)
|
|
92
|
+
|
|
93
|
+
const { svg } = await mermaid.render(`mermaid-security-${Math.random().toString(36).slice(2)}`, chart)
|
|
94
|
+
|
|
95
|
+
// No live element, and no event-handler attribute, anywhere in the output.
|
|
96
|
+
expect(svg).not.toMatch(/<img[\s>]/i)
|
|
97
|
+
expect(svg).not.toMatch(/<script[\s>]/i)
|
|
98
|
+
expect(svg).not.toMatch(/\son[a-z]+\s*=/i)
|
|
99
|
+
// `htmlLabels: false` means no HTML subtree is minted for labels at all.
|
|
100
|
+
expect(svg).not.toMatch(/<foreignObject/i)
|
|
101
|
+
// 'strict' drops `click` bindings, so no javascript: URL is ever emitted.
|
|
102
|
+
expect(svg).not.toMatch(/javascript:/i)
|
|
103
|
+
|
|
104
|
+
// And it stays inert once parsed as HTML: the hostile markup must appear
|
|
105
|
+
// as TEXT (escaped), never as an element node.
|
|
106
|
+
const host = document.createElement('div')
|
|
107
|
+
host.innerHTML = svg
|
|
108
|
+
expect(host.querySelector('img')).toBeNull()
|
|
109
|
+
expect(host.querySelector('script')).toBeNull()
|
|
110
|
+
const text = host.textContent ?? ''
|
|
111
|
+
expect(text).toContain(expectedText)
|
|
112
|
+
// The script payload never survives in any form.
|
|
113
|
+
expect(text).not.toContain('alert(1)')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MermaidDiagram's render effect re-runs on every `chart` change, and during
|
|
3
|
+
* STREAMING the chart text grows chunk by chunk — so several renders are in
|
|
4
|
+
* flight at once. Without a per-effect cancellation guard a slower EARLIER
|
|
5
|
+
* render can resolve last and overwrite the newer output (and, on the error
|
|
6
|
+
* path, paint an error for a chart that is no longer displayed).
|
|
7
|
+
*
|
|
8
|
+
* The 15s render timeout does not close this: it rejects the race, but does
|
|
9
|
+
* not abort the underlying `mermaid.render`, so the stale write is merely
|
|
10
|
+
* bounded, not prevented. These fixtures pin the guard.
|
|
11
|
+
*
|
|
12
|
+
* `mermaid` is mocked with a hand-settled deferred so resolution ORDER is
|
|
13
|
+
* under the test's control. (The real-mermaid security fixture lives in
|
|
14
|
+
* ./mermaid-security.test.ts and must stay unmocked, hence a separate file.)
|
|
15
|
+
*/
|
|
16
|
+
import { render, act } from '@testing-library/react'
|
|
17
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
18
|
+
|
|
19
|
+
interface Deferred {
|
|
20
|
+
chart: string
|
|
21
|
+
resolve: (value: { svg: string }) => void
|
|
22
|
+
reject: (err: Error) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const renders: Deferred[] = []
|
|
26
|
+
|
|
27
|
+
vi.mock('mermaid', () => ({
|
|
28
|
+
default: {
|
|
29
|
+
initialize: vi.fn(),
|
|
30
|
+
render: vi.fn(
|
|
31
|
+
(_id: string, chart: string) =>
|
|
32
|
+
new Promise<{ svg: string }>((resolve, reject) => {
|
|
33
|
+
renders.push({ chart, resolve, reject })
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
import { MermaidDiagram } from '../mermaid-diagram'
|
|
40
|
+
|
|
41
|
+
/** Let the mocked dynamic import + the resolved promise chain flush. */
|
|
42
|
+
const flush = async () => {
|
|
43
|
+
await act(async () => {
|
|
44
|
+
await Promise.resolve()
|
|
45
|
+
await Promise.resolve()
|
|
46
|
+
await Promise.resolve()
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const svgOf = (container: HTMLElement) =>
|
|
51
|
+
container.querySelector('.mermaid-svg-container')?.innerHTML ?? ''
|
|
52
|
+
|
|
53
|
+
describe('MermaidDiagram stale-render guard', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
renders.length = 0
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('ignores an earlier render that resolves after a newer chart', async () => {
|
|
59
|
+
const view = render(<MermaidDiagram chart="graph TD\n A-->B" />)
|
|
60
|
+
await flush()
|
|
61
|
+
view.rerender(<MermaidDiagram chart="graph TD\n A-->B\n B-->C" />)
|
|
62
|
+
await flush()
|
|
63
|
+
expect(renders).toHaveLength(2)
|
|
64
|
+
|
|
65
|
+
// Newer render lands first, then the abandoned earlier one.
|
|
66
|
+
await act(async () => {
|
|
67
|
+
renders[1]!.resolve({ svg: '<svg id="new"></svg>' })
|
|
68
|
+
})
|
|
69
|
+
await act(async () => {
|
|
70
|
+
renders[0]!.resolve({ svg: '<svg id="stale"></svg>' })
|
|
71
|
+
})
|
|
72
|
+
await flush()
|
|
73
|
+
|
|
74
|
+
expect(svgOf(view.container)).toContain('id="new"')
|
|
75
|
+
expect(svgOf(view.container)).not.toContain('id="stale"')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('ignores a late failure from an abandoned render', async () => {
|
|
79
|
+
const view = render(<MermaidDiagram chart="graph TD\n A-->B" />)
|
|
80
|
+
await flush()
|
|
81
|
+
view.rerender(<MermaidDiagram chart="graph TD\n A-->B\n B-->C" />)
|
|
82
|
+
await flush()
|
|
83
|
+
|
|
84
|
+
await act(async () => {
|
|
85
|
+
renders[1]!.resolve({ svg: '<svg id="new"></svg>' })
|
|
86
|
+
})
|
|
87
|
+
await act(async () => {
|
|
88
|
+
renders[0]!.reject(new Error('Diagram rendering timed out after 15000ms'))
|
|
89
|
+
})
|
|
90
|
+
await flush()
|
|
91
|
+
|
|
92
|
+
expect(view.container.textContent).not.toContain('Diagram Error')
|
|
93
|
+
expect(svgOf(view.container)).toContain('id="new"')
|
|
94
|
+
})
|
|
95
|
+
})
|