@signalflare-ai/ui 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -1
- package/ai/component-registry.json +208 -550
- package/ai/component-registry.md +3042 -3115
- package/ai/schemas.ts +504 -96
- package/dist/.build-complete +1 -1
- package/dist/ai/schemas.d.ts.map +1 -1
- package/dist/{ai-actions-BdUZI3Gk.js → ai-actions-CBfz5XEf.js} +4 -4
- package/dist/{ai-actions-BdUZI3Gk.js.map → ai-actions-CBfz5XEf.js.map} +1 -1
- package/dist/{ai-agent-card-BR2NIYhi.js → ai-agent-card-CByAUe0q.js} +3 -3
- package/dist/ai-agent-card-CByAUe0q.js.map +1 -0
- package/dist/{ai-approval-Ba7mrKba.js → ai-approval-Ci8N70a7.js} +4 -3
- package/dist/{ai-approval-Ba7mrKba.js.map → ai-approval-Ci8N70a7.js.map} +1 -1
- package/dist/{ai-code-block-CZtoL73R.js → ai-code-block-P9TJHvaC.js} +37 -39
- package/dist/ai-code-block-P9TJHvaC.js.map +1 -0
- package/dist/{ai-conversation-Cc7WlaBg.js → ai-conversation-Qslfdi1t.js} +28 -42
- package/dist/ai-conversation-Qslfdi1t.js.map +1 -0
- package/dist/{ai-info-banner-C7EWPBj7.js → ai-info-banner-B_9vtGK3.js} +3 -3
- package/dist/{ai-info-banner-C7EWPBj7.js.map → ai-info-banner-B_9vtGK3.js.map} +1 -1
- package/dist/{ai-message-Bp7L68U_.js → ai-message-Ci3gwM7G.js} +6 -6
- package/dist/{ai-message-Bp7L68U_.js.map → ai-message-Ci3gwM7G.js.map} +1 -1
- package/dist/{ai-mission-header-TiCJfTNt.js → ai-mission-header-CaBc19-t.js} +2 -2
- package/dist/{ai-mission-header-TiCJfTNt.js.map → ai-mission-header-CaBc19-t.js.map} +1 -1
- package/dist/{ai-part-group-DNb9I446.js → ai-part-group-Dx1Mr92B.js} +5 -4
- package/dist/ai-part-group-Dx1Mr92B.js.map +1 -0
- package/dist/{ai-prompt-input-BVvov_KF.js → ai-prompt-input-Bm4XoSj2.js} +19 -17
- package/dist/ai-prompt-input-Bm4XoSj2.js.map +1 -0
- package/dist/{ai-question-GPPMk7YM.js → ai-question-OyJovxGe.js} +4 -3
- package/dist/{ai-question-GPPMk7YM.js.map → ai-question-OyJovxGe.js.map} +1 -1
- package/dist/{ai-reasoning-_feFjk56.js → ai-reasoning-BLfBXx3F.js} +9 -5
- package/dist/ai-reasoning-BLfBXx3F.js.map +1 -0
- package/dist/{ai-response-CvjV3WhV.js → ai-response-hbVCZJmo.js} +2 -2
- package/dist/{ai-response-CvjV3WhV.js.map → ai-response-hbVCZJmo.js.map} +1 -1
- package/dist/{ai-shimmer-j6lKIrjj.js → ai-shimmer-BamNMNK3.js} +2 -2
- package/dist/{ai-shimmer-j6lKIrjj.js.map → ai-shimmer-BamNMNK3.js.map} +1 -1
- package/dist/{ai-status-badge-CSU_QOdz.js → ai-status-badge-BZLczdkI.js} +2 -2
- package/dist/{ai-status-badge-CSU_QOdz.js.map → ai-status-badge-BZLczdkI.js.map} +1 -1
- package/dist/{ai-streaming-text-IWW1BhvZ.js → ai-streaming-text-DgYu64UH.js} +1 -1
- package/dist/{ai-streaming-text-IWW1BhvZ.js.map → ai-streaming-text-DgYu64UH.js.map} +1 -1
- package/dist/{ai-subagent-JA4iIMW3.js → ai-subagent-p97AI1h9.js} +3 -3
- package/dist/{ai-subagent-JA4iIMW3.js.map → ai-subagent-p97AI1h9.js.map} +1 -1
- package/dist/{ai-suggestion-BdO6MBuH.js → ai-suggestion-Bj6vF7CT.js} +3 -3
- package/dist/{ai-suggestion-BdO6MBuH.js.map → ai-suggestion-Bj6vF7CT.js.map} +1 -1
- package/dist/{ai-task-list-DYw4R1FA.js → ai-task-list-C_UQYpk9.js} +6 -4
- package/dist/{ai-task-list-DYw4R1FA.js.map → ai-task-list-C_UQYpk9.js.map} +1 -1
- package/dist/{ai-timeline-C42tOUT8.js → ai-timeline-CePL1LOU.js} +3 -3
- package/dist/ai-timeline-CePL1LOU.js.map +1 -0
- package/dist/{ai-tool-03jOTwUI.js → ai-tool-CfRcwmHT.js} +17 -11
- package/dist/ai-tool-CfRcwmHT.js.map +1 -0
- package/dist/{ai-usage-bar-BRf5LC_b.js → ai-usage-bar-45pVRCGA.js} +2 -2
- package/dist/{ai-usage-bar-BRf5LC_b.js.map → ai-usage-bar-45pVRCGA.js.map} +1 -1
- package/dist/{badge-BheXjMc8.js → badge-Beb-6uut.js} +5 -5
- package/dist/{badge-BheXjMc8.js.map → badge-Beb-6uut.js.map} +1 -1
- package/dist/{banner-CcsjunJg.js → banner-CCEksxPg.js} +3 -3
- package/dist/{banner-CcsjunJg.js.map → banner-CCEksxPg.js.map} +1 -1
- package/dist/{breadcrumbs-CouSyy3H.js → breadcrumbs-HiTmgaZ4.js} +5 -5
- package/dist/{breadcrumbs-CouSyy3H.js.map → breadcrumbs-HiTmgaZ4.js.map} +1 -1
- package/dist/{button-CO6-qPax.js → button-BHOgXJRU.js} +4 -4
- package/dist/{button-CO6-qPax.js.map → button-BHOgXJRU.js.map} +1 -1
- package/dist/catalog.js +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/{chart-Dg0qUeSc.js → chart-B9FfZdKs.js} +7 -7
- package/dist/chart-B9FfZdKs.js.map +1 -0
- package/dist/{checkbox-D7p4QKsC.js → checkbox-Cy_OCyay.js} +3 -3
- package/dist/{checkbox-D7p4QKsC.js.map → checkbox-Cy_OCyay.js.map} +1 -1
- package/dist/{clipboard-text-kLaMogs3.js → clipboard-text-CKSvNp9L.js} +6 -5
- package/dist/clipboard-text-CKSvNp9L.js.map +1 -0
- package/dist/{cn-YROP2_ox.js → cn-CmAOpn49.js} +2 -2
- package/dist/{cn-YROP2_ox.js.map → cn-CmAOpn49.js.map} +1 -1
- package/dist/{code-BN8InC0G.js → code-JsQz-0G_.js} +4 -4
- package/dist/{code-BN8InC0G.js.map → code-JsQz-0G_.js.map} +1 -1
- package/dist/{collapsible-D_ueZ0jz.js → collapsible-1kOZ-89L.js} +2 -2
- package/dist/{collapsible-D_ueZ0jz.js.map → collapsible-1kOZ-89L.js.map} +1 -1
- package/dist/{combobox-B7TOK0U2.js → combobox-CQwDmqgA.js} +4 -4
- package/dist/{combobox-B7TOK0U2.js.map → combobox-CQwDmqgA.js.map} +1 -1
- package/dist/command-line/cli.js +3 -3
- package/dist/{command-palette-CuNUyJca.js → command-palette-Bkuv3e6o.js} +20 -5
- package/dist/command-palette-Bkuv3e6o.js.map +1 -0
- package/dist/components/ai-actions.js +1 -1
- package/dist/components/ai-agent-card.js +1 -1
- package/dist/components/ai-approval.js +1 -1
- package/dist/components/ai-code-block.js +1 -1
- package/dist/components/ai-conversation.js +1 -1
- package/dist/components/ai-info-banner.js +1 -1
- package/dist/components/ai-message.js +1 -1
- package/dist/components/ai-mission-header.js +1 -1
- package/dist/components/ai-part-group.js +1 -1
- package/dist/components/ai-prompt-input.js +1 -1
- package/dist/components/ai-question.js +1 -1
- package/dist/components/ai-reasoning.js +1 -1
- package/dist/components/ai-response.js +1 -1
- package/dist/components/ai-shimmer.js +1 -1
- package/dist/components/ai-status-badge.js +1 -1
- package/dist/components/ai-streaming-text.js +1 -1
- package/dist/components/ai-subagent.js +1 -1
- package/dist/components/ai-suggestion.js +1 -1
- package/dist/components/ai-task-list.js +1 -1
- package/dist/components/ai-timeline.js +1 -1
- package/dist/components/ai-tool.js +1 -1
- package/dist/components/ai-usage-bar.js +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/banner.js +1 -1
- package/dist/components/breadcrumbs.js +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/chart.js +2 -2
- package/dist/components/checkbox.js +1 -1
- package/dist/components/clipboard-text.js +1 -1
- package/dist/components/code.js +1 -1
- package/dist/components/collapsible.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command-palette.js +1 -1
- package/dist/components/data-grid.js +1 -1
- package/dist/components/date-picker.js +1 -1
- package/dist/components/date-range-picker.js +1 -1
- package/dist/components/dialog.js +1 -1
- package/dist/components/dropdown.js +1 -1
- package/dist/components/empty.js +1 -1
- package/dist/components/field.js +1 -1
- package/dist/components/filters.js +1 -1
- package/dist/components/flow.js +1 -1
- package/dist/components/grid.js +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/label.js +1 -1
- package/dist/components/layer-card.js +1 -1
- package/dist/components/link.js +3 -3
- package/dist/components/link.js.map +1 -1
- package/dist/components/loader.js +2 -2
- package/dist/components/menubar.js +1 -1
- package/dist/components/meter.js +1 -1
- package/dist/components/pagination.js +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/radio.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/sensitive-input.js +1 -1
- package/dist/components/sidebar.js +1 -1
- package/dist/components/signalflare-ai-logo.js +1 -1
- package/dist/components/sparkline.js +1 -1
- package/dist/components/stat-card.js +1 -1
- package/dist/components/surface.js +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table.js +1 -1
- package/dist/components/tabs.js +1 -1
- package/dist/components/text-roll.js +1 -1
- package/dist/components/text.js +1 -1
- package/dist/components/theme-toggle.js +1 -1
- package/dist/components/toast.js +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/use-agent-harness.js +1 -1
- package/dist/{data-grid-DGHmU0w3.js → data-grid-DDSFMHud.js} +136 -53
- package/dist/data-grid-DDSFMHud.js.map +1 -0
- package/dist/{date-picker--ox89RBy.js → date-picker-O34AqG3f.js} +2 -2
- package/dist/{date-picker--ox89RBy.js.map → date-picker-O34AqG3f.js.map} +1 -1
- package/dist/{date-range-picker-DVa7QBqE.js → date-range-picker-YKYvum_r.js} +29 -39
- package/dist/{date-range-picker-DVa7QBqE.js.map → date-range-picker-YKYvum_r.js.map} +1 -1
- package/dist/{dialog-Bv1oSFOd.js → dialog-DYqu4aDO.js} +3 -3
- package/dist/{dialog-Bv1oSFOd.js.map → dialog-DYqu4aDO.js.map} +1 -1
- package/dist/{dist-B6iWiWwp.js → dist-6AtBsaJE.js} +153 -47
- package/dist/dist-6AtBsaJE.js.map +1 -0
- package/dist/{dropdown-B_nrGXjV.js → dropdown-XzbnRLYR.js} +15 -5
- package/dist/dropdown-XzbnRLYR.js.map +1 -0
- package/dist/{echart-CdOUaT-r.js → echart-DGBIVAv1.js} +23 -57
- package/dist/{echart-CdOUaT-r.js.map → echart-DGBIVAv1.js.map} +1 -1
- package/dist/{empty-DZnN0zKX.js → empty-C1tAkawe.js} +6 -6
- package/dist/{empty-DZnN0zKX.js.map → empty-C1tAkawe.js.map} +1 -1
- package/dist/{field-B_yVof52.js → field-DBpFzzBS.js} +3 -3
- package/dist/{field-B_yVof52.js.map → field-DBpFzzBS.js.map} +1 -1
- package/dist/{filters-cpJCY21R.js → filters-SmEl93za.js} +10 -10
- package/dist/filters-SmEl93za.js.map +1 -0
- package/dist/{flow-B4v198ot.js → flow-BLzgbq1T.js} +6 -6
- package/dist/flow-BLzgbq1T.js.map +1 -0
- package/dist/genui.js +2 -2
- package/dist/genui.js.map +1 -1
- package/dist/{grid-CEd64Lnh.js → grid-CifjQL-5.js} +2 -2
- package/dist/{grid-CEd64Lnh.js.map → grid-CifjQL-5.js.map} +1 -1
- package/dist/{highlight-to-react-D0Yav4jk.js → highlight-to-react-DN9dUCS2.js} +9 -15
- package/dist/highlight-to-react-DN9dUCS2.js.map +1 -0
- package/dist/index.js +71 -71
- package/dist/index.js.map +1 -1
- package/dist/{input-ClB_E4Lb.js → input-COmx2M_R.js} +5 -5
- package/dist/{input-ClB_E4Lb.js.map → input-COmx2M_R.js.map} +1 -1
- package/dist/{input-B2bbijRh.js → input-GkfMQZC_.js} +3 -3
- package/dist/{input-B2bbijRh.js.map → input-GkfMQZC_.js.map} +1 -1
- package/dist/{label-DUv_urO1.js → label-CiGZ464N.js} +3 -3
- package/dist/{label-DUv_urO1.js.map → label-CiGZ464N.js.map} +1 -1
- package/dist/{layer-card-BK7eYfwn.js → layer-card-8l8GuLQr.js} +2 -2
- package/dist/{layer-card-BK7eYfwn.js.map → layer-card-8l8GuLQr.js.map} +1 -1
- package/dist/{layout-DJHMMap2.js → layout-CWBE0qwx.js} +258 -154
- package/dist/layout-CWBE0qwx.js.map +1 -0
- package/dist/{link-provider-BUZKXaNE.js → link-provider-BSn8YJon.js} +2 -2
- package/dist/link-provider-BSn8YJon.js.map +1 -0
- package/dist/{loader-DAcc-Uag.js → loader-BEMz8pJO.js} +1 -1
- package/dist/{loader-DAcc-Uag.js.map → loader-BEMz8pJO.js.map} +1 -1
- package/dist/{measured-text-BI3dTJmH.js → measured-text-CXkdw9Yr.js} +45 -30
- package/dist/measured-text-CXkdw9Yr.js.map +1 -0
- package/dist/{menubar-Cxf3xeAt.js → menubar-CoOr4ocj.js} +3 -3
- package/dist/{menubar-Cxf3xeAt.js.map → menubar-CoOr4ocj.js.map} +1 -1
- package/dist/{meter-BFFe9l5b.js → meter-Pf_VOl59.js} +2 -2
- package/dist/{meter-BFFe9l5b.js.map → meter-Pf_VOl59.js.map} +1 -1
- package/dist/{pagination-yS372Tr4.js → pagination-DSY279Ta.js} +2 -2
- package/dist/{pagination-yS372Tr4.js.map → pagination-DSY279Ta.js.map} +1 -1
- package/dist/{popover-SRoJaCZr.js → popover-BY-e9co1.js} +2 -2
- package/dist/{popover-SRoJaCZr.js.map → popover-BY-e9co1.js.map} +1 -1
- package/dist/{radio-BcwhwYNB.js → radio-DZwL13j0.js} +2 -2
- package/dist/{radio-BcwhwYNB.js.map → radio-DZwL13j0.js.map} +1 -1
- package/dist/{select-DMhdoHMa.js → select-BFifYqHA.js} +6 -6
- package/dist/{select-DMhdoHMa.js.map → select-BFifYqHA.js.map} +1 -1
- package/dist/{sensitive-input-CJUpIRal.js → sensitive-input-DHLZcM73.js} +4 -4
- package/dist/{sensitive-input-CJUpIRal.js.map → sensitive-input-DHLZcM73.js.map} +1 -1
- package/dist/{sidebar-D4zrlYpn.js → sidebar-odGsdvG4.js} +6 -7
- package/dist/sidebar-odGsdvG4.js.map +1 -0
- package/dist/{signalflare-ai-logo-Bipogceq.js → signalflare-ai-logo-CNaDT_w8.js} +2 -2
- package/dist/{signalflare-ai-logo-Bipogceq.js.map → signalflare-ai-logo-CNaDT_w8.js.map} +1 -1
- package/dist/{skeleton-line-CH1-h6e2.js → skeleton-line-CxxYVTO2.js} +2 -2
- package/dist/{skeleton-line-CH1-h6e2.js.map → skeleton-line-CxxYVTO2.js.map} +1 -1
- package/dist/{sparkline-DHmgj1d0.js → sparkline-BQ-4j2W2.js} +2 -2
- package/dist/{sparkline-DHmgj1d0.js.map → sparkline-BQ-4j2W2.js.map} +1 -1
- package/dist/src/blocks/agent-harness/agent-harness.tsx +11 -11
- package/dist/src/blocks/commander/commander.tsx +15 -15
- package/dist/src/blocks/map-block/map-block.d.ts.map +1 -1
- package/dist/src/blocks/map-block/map-block.tsx +11 -7
- package/dist/src/components/ai-approval/ai-approval.d.ts.map +1 -1
- package/dist/src/components/ai-code-block/ai-code-block.d.ts +14 -13
- package/dist/src/components/ai-code-block/ai-code-block.d.ts.map +1 -1
- package/dist/src/components/ai-conversation/ai-conversation.d.ts.map +1 -1
- package/dist/src/components/ai-part-group/ai-part-group.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/ai-prompt-input.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/controller.d.ts.map +1 -1
- package/dist/src/components/ai-prompt-input/types.d.ts.map +1 -1
- package/dist/src/components/ai-question/ai-question.d.ts.map +1 -1
- package/dist/src/components/ai-reasoning/ai-reasoning.d.ts.map +1 -1
- package/dist/src/components/ai-response/ai-response.d.ts.map +1 -1
- package/dist/src/components/ai-subagent/ai-subagent.d.ts.map +1 -1
- package/dist/src/components/ai-tool/ai-tool.d.ts.map +1 -1
- package/dist/src/components/chart/echart.d.ts.map +1 -1
- package/dist/src/components/clipboard-text/clipboard-text.d.ts.map +1 -1
- package/dist/src/components/data-grid/data-grid.d.ts +2 -1
- package/dist/src/components/data-grid/data-grid.d.ts.map +1 -1
- package/dist/src/components/data-grid/features.d.ts +20 -0
- package/dist/src/components/data-grid/features.d.ts.map +1 -0
- package/dist/src/components/data-grid/types.d.ts +38 -7
- package/dist/src/components/data-grid/types.d.ts.map +1 -1
- package/dist/src/components/filters/filters.d.ts.map +1 -1
- package/dist/src/components/flow/use-children.d.ts +1 -1
- package/dist/src/components/link/link.d.ts.map +1 -1
- package/dist/src/components/sidebar/sidebar.d.ts +1 -1
- package/dist/src/components/signalflare-ai-logo/signalflare-ai-logo.d.ts.map +1 -1
- package/dist/src/components/text/text.d.ts +2 -1
- package/dist/src/components/text/text.d.ts.map +1 -1
- package/dist/src/components/text-roll/text-roll.d.ts.map +1 -1
- package/dist/src/components/theme-toggle/theme-toggle.d.ts.map +1 -1
- package/dist/src/components/toast/toast.d.ts.map +1 -1
- package/dist/src/utils/highlight-to-react.d.ts.map +1 -1
- package/dist/src/utils/measured-text.d.ts.map +1 -1
- package/dist/src/utils/use-measured-text.d.ts.map +1 -1
- package/dist/{stat-card-Ew-ofzEm.js → stat-card-Bspk4XFr.js} +4 -4
- package/dist/stat-card-Bspk4XFr.js.map +1 -0
- package/dist/styles/sf-standalone.css +1 -1
- package/dist/styles/theme-fedramp.css +3 -12
- package/dist/styles/theme-minimal.css +26 -104
- package/dist/styles/theme-sf.css +37 -142
- package/dist/{surface-DGwRlC0o.js → surface-CWdSFVUx.js} +3 -3
- package/dist/{surface-DGwRlC0o.js.map → surface-CWdSFVUx.js.map} +1 -1
- package/dist/{switch-BxAMfHdt.js → switch-TA4cByCJ.js} +5 -5
- package/dist/switch-TA4cByCJ.js.map +1 -0
- package/dist/{table-BBeAtYVZ.js → table-BM8JBGBs.js} +3 -3
- package/dist/{table-BBeAtYVZ.js.map → table-BM8JBGBs.js.map} +1 -1
- package/dist/{tabs-CeHu7Scn.js → tabs-bnH2vGLv.js} +2 -2
- package/dist/{tabs-CeHu7Scn.js.map → tabs-bnH2vGLv.js.map} +1 -1
- package/dist/{text-Cqryz7rk.js → text-iQ0YUFNg.js} +4 -5
- package/dist/{text-Cqryz7rk.js.map → text-iQ0YUFNg.js.map} +1 -1
- package/dist/{text-roll-Ch52hcQj.js → text-roll-C3U2jd2u.js} +5 -2
- package/dist/text-roll-C3U2jd2u.js.map +1 -0
- package/dist/{theme-toggle-LDfIKEqx.js → theme-toggle-BTVxD-fD.js} +10 -9
- package/dist/theme-toggle-BTVxD-fD.js.map +1 -0
- package/dist/{toast-CaFQNYng.js → toast-CgZVaAkw.js} +3 -3
- package/dist/{toast-CaFQNYng.js.map → toast-CgZVaAkw.js.map} +1 -1
- package/dist/{tooltip-g9lFsvcT.js → tooltip-uobk6Oh-.js} +3 -3
- package/dist/{tooltip-g9lFsvcT.js.map → tooltip-uobk6Oh-.js.map} +1 -1
- package/dist/{use-agent-harness-BTcNJdw4.js → use-agent-harness-Dl8w6X5O.js} +3 -3
- package/dist/{use-agent-harness-BTcNJdw4.js.map → use-agent-harness-Dl8w6X5O.js.map} +1 -1
- package/dist/utils.js +3 -3
- package/package.json +27 -25
- package/scripts/component-registry/discovery.ts +11 -10
- package/scripts/component-registry/example-cleanup.ts +8 -8
- package/scripts/component-registry/index.ts +6 -6
- package/scripts/component-registry/schema-generator.ts +1 -1
- package/scripts/component-registry/sub-components.ts +35 -23
- package/scripts/component-registry/utils.ts +11 -11
- package/scripts/component-registry/variant-parser.ts +17 -15
- package/scripts/convert-demos-to-stories.ts +5 -5
- package/scripts/theme-generator/config.ts +1 -5
- package/scripts/theme-generator/generate-css.ts +1 -1
- package/scripts/theme-generator/migrate.ts +3 -3
- package/dist/ai-agent-card-BR2NIYhi.js.map +0 -1
- package/dist/ai-code-block-CZtoL73R.js.map +0 -1
- package/dist/ai-conversation-Cc7WlaBg.js.map +0 -1
- package/dist/ai-part-group-DNb9I446.js.map +0 -1
- package/dist/ai-prompt-input-BVvov_KF.js.map +0 -1
- package/dist/ai-reasoning-_feFjk56.js.map +0 -1
- package/dist/ai-timeline-C42tOUT8.js.map +0 -1
- package/dist/ai-tool-03jOTwUI.js.map +0 -1
- package/dist/chart-Dg0qUeSc.js.map +0 -1
- package/dist/clipboard-text-kLaMogs3.js.map +0 -1
- package/dist/command-palette-CuNUyJca.js.map +0 -1
- package/dist/data-grid-DGHmU0w3.js.map +0 -1
- package/dist/dist-B6iWiWwp.js.map +0 -1
- package/dist/dropdown-B_nrGXjV.js.map +0 -1
- package/dist/filters-cpJCY21R.js.map +0 -1
- package/dist/flow-B4v198ot.js.map +0 -1
- package/dist/highlight-to-react-D0Yav4jk.js.map +0 -1
- package/dist/layout-DJHMMap2.js.map +0 -1
- package/dist/link-provider-BUZKXaNE.js.map +0 -1
- package/dist/measured-text-BI3dTJmH.js.map +0 -1
- package/dist/sidebar-D4zrlYpn.js.map +0 -1
- package/dist/stat-card-Ew-ofzEm.js.map +0 -1
- package/dist/switch-BxAMfHdt.js.map +0 -1
- package/dist/text-roll-Ch52hcQj.js.map +0 -1
- package/dist/theme-toggle-LDfIKEqx.js.map +0 -1
|
@@ -30,7 +30,7 @@ export function detectSubComponents(filePath: string): SubComponentConfig[] {
|
|
|
30
30
|
// Pattern 1: Object.assign with sub-components
|
|
31
31
|
// Find the start of Object.assign and then extract the balanced braces
|
|
32
32
|
// Supports both simple names (Component) and dotted names (SomeBase.Root)
|
|
33
|
-
const objectAssignStart = /Object\.assign\s*\(\s*[\w.]+\s*,\s*\{/
|
|
33
|
+
const objectAssignStart = /Object\.assign\s*\(\s*[\w.]+\s*,\s*\{/gu;
|
|
34
34
|
let startMatch: RegExpExecArray | null;
|
|
35
35
|
|
|
36
36
|
while ((startMatch = objectAssignStart.exec(content)) !== null) {
|
|
@@ -42,7 +42,7 @@ export function detectSubComponents(filePath: string): SubComponentConfig[] {
|
|
|
42
42
|
|
|
43
43
|
// Extract sub-component assignments: SubName: Value or SubName: SomeBase.SubName
|
|
44
44
|
// Handle multi-line with comments
|
|
45
|
-
const subPattern = /^\s*(\w+)\s*[,:]/
|
|
45
|
+
const subPattern = /^\s*(\w+)\s*[,:]/gmu;
|
|
46
46
|
let subMatch: RegExpExecArray | null;
|
|
47
47
|
|
|
48
48
|
while ((subMatch = subPattern.exec(assignBlock)) !== null) {
|
|
@@ -59,14 +59,15 @@ export function detectSubComponents(filePath: string): SubComponentConfig[] {
|
|
|
59
59
|
"function",
|
|
60
60
|
"description",
|
|
61
61
|
].includes(subName) ||
|
|
62
|
-
!/^[A-Z]
|
|
62
|
+
!/^[A-Z]/u.test(subName)
|
|
63
63
|
) {
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Find the value after the colon for this sub-component
|
|
68
68
|
const valuePattern = new RegExp(
|
|
69
|
-
`\\b${subName}\\s*:\\s*(\\w+(?:\\.\\w+)?)
|
|
69
|
+
`\\b${subName}\\s*:\\s*(\\w+(?:\\.\\w+)?)`,
|
|
70
|
+
"u"
|
|
70
71
|
);
|
|
71
72
|
const valueMatch = assignBlock.match(valuePattern);
|
|
72
73
|
const value = valueMatch ? valueMatch[1] : subName;
|
|
@@ -81,7 +82,8 @@ export function detectSubComponents(filePath: string): SubComponentConfig[] {
|
|
|
81
82
|
// Look for function signature or interface for this sub-component
|
|
82
83
|
// Pattern: function SubName({ ... }: SubNameProps) or function SubName(props: SubNameProps)
|
|
83
84
|
const funcPropsPattern = new RegExp(
|
|
84
|
-
`function\\s+${value}\\s*(?:<[^>]*>)?\\s*\\([^)]*:\\s*(\\w+Props)
|
|
85
|
+
`function\\s+${value}\\s*(?:<[^>]*>)?\\s*\\([^)]*:\\s*(\\w+Props)`,
|
|
86
|
+
"u"
|
|
85
87
|
);
|
|
86
88
|
const funcMatch = content.match(funcPropsPattern);
|
|
87
89
|
if (funcMatch) {
|
|
@@ -90,7 +92,8 @@ export function detectSubComponents(filePath: string): SubComponentConfig[] {
|
|
|
90
92
|
|
|
91
93
|
// Also check for inline type in PropsWithChildren pattern
|
|
92
94
|
const propsWithChildrenPattern = new RegExp(
|
|
93
|
-
`function\\s+${value}\\s*\\([^)]*:\\s*PropsWithChildren<\\{([^}]+)\\}
|
|
95
|
+
`function\\s+${value}\\s*\\([^)]*:\\s*PropsWithChildren<\\{([^}]+)\\}>`,
|
|
96
|
+
"u"
|
|
94
97
|
);
|
|
95
98
|
const pwcMatch = content.match(propsWithChildrenPattern);
|
|
96
99
|
if (pwcMatch && !propsType) {
|
|
@@ -122,7 +125,7 @@ export function detectSubComponents(filePath: string): SubComponentConfig[] {
|
|
|
122
125
|
|
|
123
126
|
// Pattern 2: Direct property assignment at module level (e.g., Breadcrumb.Link = Link)
|
|
124
127
|
// Must be at start of line (module level) and sub-component name must be PascalCase
|
|
125
|
-
const directAssignPattern = /^([A-Z]\w+)\.([A-Z]\w+)\s*=\s*(\w+)\s*;/
|
|
128
|
+
const directAssignPattern = /^([A-Z]\w+)\.([A-Z]\w+)\s*=\s*(\w+)\s*;/gmu;
|
|
126
129
|
let directMatch: RegExpExecArray | null;
|
|
127
130
|
|
|
128
131
|
while ((directMatch = directAssignPattern.exec(content)) !== null) {
|
|
@@ -142,7 +145,8 @@ export function detectSubComponents(filePath: string): SubComponentConfig[] {
|
|
|
142
145
|
// Try to find props type for this sub-component
|
|
143
146
|
let propsType: string | null = null;
|
|
144
147
|
const funcPropsPattern = new RegExp(
|
|
145
|
-
`function\\s+${value}\\s*(?:<[^>]*>)?\\s*\\([^)]*:\\s*(\\w+Props)
|
|
148
|
+
`function\\s+${value}\\s*(?:<[^>]*>)?\\s*\\([^)]*:\\s*(\\w+Props)`,
|
|
149
|
+
"u"
|
|
146
150
|
);
|
|
147
151
|
const funcMatch = content.match(funcPropsPattern);
|
|
148
152
|
if (funcMatch) {
|
|
@@ -195,11 +199,13 @@ export function extractSubComponentProps(
|
|
|
195
199
|
const inlinePropsPatterns = [
|
|
196
200
|
// function Name({ destructured }: { inline props })
|
|
197
201
|
new RegExp(
|
|
198
|
-
`(?:function|const)\\s+${funcName}\\s*=?\\s*\\([^)]*:\\s*(?:PropsWithChildren<)?\\{([^}]+)\\}
|
|
202
|
+
`(?:function|const)\\s+${funcName}\\s*=?\\s*\\([^)]*:\\s*(?:PropsWithChildren<)?\\{([^}]+)\\}`,
|
|
203
|
+
"u"
|
|
199
204
|
),
|
|
200
205
|
// function Name({ destructured }: PropsWithChildren<InterfaceName>)
|
|
201
206
|
new RegExp(
|
|
202
|
-
`(?:function|const)\\s+${funcName}\\s*=?\\s*\\([^)]*:\\s*PropsWithChildren<(\\w+)
|
|
207
|
+
`(?:function|const)\\s+${funcName}\\s*=?\\s*\\([^)]*:\\s*PropsWithChildren<(\\w+)>`,
|
|
208
|
+
"u"
|
|
203
209
|
),
|
|
204
210
|
];
|
|
205
211
|
|
|
@@ -209,7 +215,7 @@ export function extractSubComponentProps(
|
|
|
209
215
|
const propsBlock = match[1];
|
|
210
216
|
|
|
211
217
|
// Check if it's an interface name (single word) or inline props
|
|
212
|
-
if (/^\w
|
|
218
|
+
if (/^\w+$/u.test(propsBlock)) {
|
|
213
219
|
// It's an interface name, try to find and parse it
|
|
214
220
|
const interfaceProps = extractPropsFromInterface(
|
|
215
221
|
content,
|
|
@@ -219,7 +225,7 @@ export function extractSubComponentProps(
|
|
|
219
225
|
Object.assign(props, interfaceProps);
|
|
220
226
|
} else {
|
|
221
227
|
// Parse inline props: propName?: Type or propName: Type
|
|
222
|
-
const propPattern = /(\w+)(\?)?:\s*([^;,\n}]+)/
|
|
228
|
+
const propPattern = /(\w+)(\?)?:\s*([^;,\n}]+)/gu;
|
|
223
229
|
let propMatch: RegExpExecArray | null;
|
|
224
230
|
|
|
225
231
|
while ((propMatch = propPattern.exec(propsBlock)) !== null) {
|
|
@@ -228,7 +234,7 @@ export function extractSubComponentProps(
|
|
|
228
234
|
let propType = propMatch[3].trim();
|
|
229
235
|
|
|
230
236
|
// Clean up type
|
|
231
|
-
propType = propType.replace(/[,;]
|
|
237
|
+
propType = propType.replace(/[,;]$/u, "").trim();
|
|
232
238
|
|
|
233
239
|
if (shouldSkipProp(propName, cliFlags)) continue;
|
|
234
240
|
|
|
@@ -263,9 +269,9 @@ export function extractSubComponentProps(
|
|
|
263
269
|
// This handles multi-line destructuring patterns
|
|
264
270
|
const funcDefPatterns = [
|
|
265
271
|
// Arrow function: const Name = (...)
|
|
266
|
-
new RegExp(`const\\s+${funcName}\\s*=\\s*\\([\\s\\S]*?\\)\\s
|
|
272
|
+
new RegExp(`const\\s+${funcName}\\s*=\\s*\\([\\s\\S]*?\\)\\s*=>`, "u"),
|
|
267
273
|
// Regular function: function Name(...)
|
|
268
|
-
new RegExp(`function\\s+${funcName}\\s*\\([\\s\\S]*?\\)\\s*\\{
|
|
274
|
+
new RegExp(`function\\s+${funcName}\\s*\\([\\s\\S]*?\\)\\s*\\{`, "u"),
|
|
269
275
|
];
|
|
270
276
|
|
|
271
277
|
for (const defPattern of funcDefPatterns) {
|
|
@@ -275,9 +281,9 @@ export function extractSubComponentProps(
|
|
|
275
281
|
|
|
276
282
|
// Extract PropsWithChildren<InterfaceName> or direct interface reference
|
|
277
283
|
const typePatterns = [
|
|
278
|
-
/PropsWithChildren<(\w+)
|
|
279
|
-
/:\s*(\w+Props)\s*\)
|
|
280
|
-
/:\s*(\w+ItemProps)\s*\)
|
|
284
|
+
/PropsWithChildren<(\w+)>/u,
|
|
285
|
+
/:\s*(\w+Props)\s*\)/u,
|
|
286
|
+
/:\s*(\w+ItemProps)\s*\)/u,
|
|
281
287
|
];
|
|
282
288
|
|
|
283
289
|
for (const typePattern of typePatterns) {
|
|
@@ -319,15 +325,21 @@ export function extractPropsFromInterface(
|
|
|
319
325
|
): Record<string, PropSchema> {
|
|
320
326
|
const props: Record<string, PropSchema> = {};
|
|
321
327
|
|
|
322
|
-
// Match interface definition
|
|
328
|
+
// Match interface definition, or a type alias whose definition contains an
|
|
329
|
+
// inline object literal (e.g. `type FooProps = ComponentProps<X> & { ... }`).
|
|
323
330
|
const interfacePattern = new RegExp(
|
|
324
|
-
`interface\\s+${interfaceName}\\s*(?:extends[^{]*)?\\{([^}]+)\\}
|
|
331
|
+
`interface\\s+${interfaceName}\\s*(?:extends[^{]*)?\\{([^}]+)\\}`,
|
|
332
|
+
"u"
|
|
325
333
|
);
|
|
326
|
-
const
|
|
334
|
+
const typePattern = new RegExp(
|
|
335
|
+
`type\\s+${interfaceName}\\s*=[^{]*\\{([^}]+)\\}`,
|
|
336
|
+
"u"
|
|
337
|
+
);
|
|
338
|
+
const match = content.match(interfacePattern) ?? content.match(typePattern);
|
|
327
339
|
|
|
328
340
|
if (match) {
|
|
329
341
|
const propsBlock = match[1];
|
|
330
|
-
const propPattern = /(\w+)(\?)?:\s*([^;,\n}]+)/
|
|
342
|
+
const propPattern = /(\w+)(\?)?:\s*([^;,\n}]+)/gu;
|
|
331
343
|
let propMatch: RegExpExecArray | null;
|
|
332
344
|
|
|
333
345
|
while ((propMatch = propPattern.exec(propsBlock)) !== null) {
|
|
@@ -335,7 +347,7 @@ export function extractPropsFromInterface(
|
|
|
335
347
|
const isOptional = propMatch[2] === "?";
|
|
336
348
|
let propType = propMatch[3].trim();
|
|
337
349
|
|
|
338
|
-
propType = propType.replace(/[,;]
|
|
350
|
+
propType = propType.replace(/[,;]$/u, "").trim();
|
|
339
351
|
|
|
340
352
|
if (shouldSkipProp(propName, cliFlags)) continue;
|
|
341
353
|
|
|
@@ -31,8 +31,8 @@ export function toPascalCase(str: string): string {
|
|
|
31
31
|
*/
|
|
32
32
|
export function toScreamingSnakeCase(str: string): string {
|
|
33
33
|
return str
|
|
34
|
-
.replace(/([a-z])([A-Z])/
|
|
35
|
-
.replace(/([A-Z]+)([A-Z][a-z])/
|
|
34
|
+
.replace(/([a-z])([A-Z])/gu, "$1_$2")
|
|
35
|
+
.replace(/([A-Z]+)([A-Z][a-z])/gu, "$1_$2")
|
|
36
36
|
.toUpperCase();
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -90,7 +90,7 @@ export function parseSemanticColorNames(): string[] {
|
|
|
90
90
|
// Pattern: "--color-<name>: light-dark(" or "--text-color-<name>: light-dark("
|
|
91
91
|
// This excludes raw palette colors which are defined with direct values like "oklch(...)"
|
|
92
92
|
const semanticColorPattern =
|
|
93
|
-
/--(?:text-)?color-([a-zA-Z][a-zA-Z0-9-]*)\s*:\s*light-dark\(/
|
|
93
|
+
/--(?:text-)?color-([a-zA-Z][a-zA-Z0-9-]*)\s*:\s*light-dark\(/gu;
|
|
94
94
|
let match: RegExpExecArray | null;
|
|
95
95
|
|
|
96
96
|
while ((match = semanticColorPattern.exec(content)) !== null) {
|
|
@@ -129,7 +129,7 @@ export function extractSemanticColors(sourceFile: string): string[] {
|
|
|
129
129
|
// Escape hyphens for regex and create pattern
|
|
130
130
|
const pattern = new RegExp(
|
|
131
131
|
`(?:[\\w\\[\\]=_-]+:)*(${prefix}-${colorName})(?![a-zA-Z0-9-])`,
|
|
132
|
-
"
|
|
132
|
+
"gu"
|
|
133
133
|
);
|
|
134
134
|
let match: RegExpExecArray | null;
|
|
135
135
|
while ((match = pattern.exec(content)) !== null) {
|
|
@@ -159,19 +159,19 @@ export function extractStateClasses(
|
|
|
159
159
|
const states: Record<string, string> = {};
|
|
160
160
|
|
|
161
161
|
// Split by whitespace to process each class individually
|
|
162
|
-
const classes = classString.split(/\s+/);
|
|
162
|
+
const classes = classString.split(/\s+/u);
|
|
163
163
|
|
|
164
164
|
for (const cls of classes) {
|
|
165
165
|
if (!cls) continue;
|
|
166
166
|
|
|
167
167
|
// Check for hover states
|
|
168
|
-
if (cls.startsWith("hover:") || /^\[&:hover[^\]]*\]
|
|
168
|
+
if (cls.startsWith("hover:") || /^\[&:hover[^\]]*\]:/u.exec(cls)) {
|
|
169
169
|
states.hover = states.hover ? `${states.hover} ${cls}` : cls;
|
|
170
170
|
}
|
|
171
171
|
// Check for focus states (focus, focus-visible, focus-within)
|
|
172
172
|
else if (
|
|
173
|
-
/^(focus|focus-visible|focus-within)
|
|
174
|
-
/^\[&:focus(-visible|-within)?[^\]]*\]
|
|
173
|
+
/^(focus|focus-visible|focus-within):/u.exec(cls) ||
|
|
174
|
+
/^\[&:focus(-visible|-within)?[^\]]*\]:/u.exec(cls)
|
|
175
175
|
) {
|
|
176
176
|
states.focus = states.focus ? `${states.focus} ${cls}` : cls;
|
|
177
177
|
}
|
|
@@ -190,7 +190,7 @@ export function extractStateClasses(
|
|
|
190
190
|
: cls;
|
|
191
191
|
}
|
|
192
192
|
// Check for data-state
|
|
193
|
-
else if (/^data-\[state=[^\]]+\]
|
|
193
|
+
else if (/^data-\[state=[^\]]+\]:/u.test(cls)) {
|
|
194
194
|
states["data-state"] = states["data-state"]
|
|
195
195
|
? `${states["data-state"]} ${cls}`
|
|
196
196
|
: cls;
|
|
@@ -218,7 +218,7 @@ export function extractBlockDependencies(sourceFile: string): string[] {
|
|
|
218
218
|
// Pattern: import { Foo, Bar } from "../../components/something"
|
|
219
219
|
// Pattern: import { Foo } from "../../blocks/something"
|
|
220
220
|
const importPattern =
|
|
221
|
-
/import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+["']\.\.\/\.\.\/(?:components|blocks)\/[^"']+["']/
|
|
221
|
+
/import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+["']\.\.\/\.\.\/(?:components|blocks)\/[^"']+["']/gu;
|
|
222
222
|
let match: RegExpExecArray | null;
|
|
223
223
|
|
|
224
224
|
while ((match = importPattern.exec(content)) !== null) {
|
|
@@ -235,7 +235,7 @@ export function extractBlockDependencies(sourceFile: string): string[] {
|
|
|
235
235
|
continue;
|
|
236
236
|
}
|
|
237
237
|
// Extract name (handle "Foo as Bar" -> "Foo")
|
|
238
|
-
const nameMatch = /^(\w+)(?:\s+as\s+\w+)
|
|
238
|
+
const nameMatch = /^(\w+)(?:\s+as\s+\w+)?/u.exec(item);
|
|
239
239
|
if (nameMatch) {
|
|
240
240
|
dependencies.add(nameMatch[1]);
|
|
241
241
|
}
|
|
@@ -36,7 +36,7 @@ export function extractBaseStylesFromFile(filePath: string): string | null {
|
|
|
36
36
|
// Match: export const SF_*_BASE_STYLES = "..." or '...' or `...`
|
|
37
37
|
// Handles multi-line strings with template literals
|
|
38
38
|
const baseStylesMatch =
|
|
39
|
-
/export\s+const\s+SF_\w+_BASE_STYLES\s*=\s*["'`]([^"'`]+)["'`]
|
|
39
|
+
/export\s+const\s+SF_\w+_BASE_STYLES\s*=\s*["'`]([^"'`]+)["'`]/u.exec(
|
|
40
40
|
content
|
|
41
41
|
);
|
|
42
42
|
|
|
@@ -67,7 +67,7 @@ export function parseVariantsObject(
|
|
|
67
67
|
|
|
68
68
|
// Find top-level property names (e.g., shape, size, variant)
|
|
69
69
|
// These are identifiers followed by `: {` at the first nesting level
|
|
70
|
-
const topLevelPropPattern = /^\s*(\w+)\s*:\s*\{/
|
|
70
|
+
const topLevelPropPattern = /^\s*(\w+)\s*:\s*\{/gmu;
|
|
71
71
|
let propMatch: RegExpExecArray | null;
|
|
72
72
|
|
|
73
73
|
while ((propMatch = topLevelPropPattern.exec(objStr)) !== null) {
|
|
@@ -86,7 +86,7 @@ export function parseVariantsObject(
|
|
|
86
86
|
// biome-ignore lint/suspicious/noExplicitAny: Variants have varying shapes
|
|
87
87
|
const variants: Record<string, any> = {};
|
|
88
88
|
// Match variant names including quoted keys like "secondary-destructive"
|
|
89
|
-
const variantPropPattern = /^\s*(?:"([^"]+)"|'([^']+)'|(\w+))\s*:\s*\{/
|
|
89
|
+
const variantPropPattern = /^\s*(?:"([^"]+)"|'([^']+)'|(\w+))\s*:\s*\{/gmu;
|
|
90
90
|
let variantMatch: RegExpExecArray | null;
|
|
91
91
|
|
|
92
92
|
while ((variantMatch = variantPropPattern.exec(propBlock)) !== null) {
|
|
@@ -103,9 +103,11 @@ export function parseVariantsObject(
|
|
|
103
103
|
if (!variantBlock) continue;
|
|
104
104
|
|
|
105
105
|
// Extract description if present
|
|
106
|
-
const descMatch = /description\s*:\s*["']([^"']*)["']
|
|
106
|
+
const descMatch = /description\s*:\s*["']([^"']*)["']/u.exec(
|
|
107
|
+
variantBlock
|
|
108
|
+
);
|
|
107
109
|
// Extract classes if present (for Figma plugin consumption)
|
|
108
|
-
const classesMatch = /classes\s*:\s*["']([^"']*)["']
|
|
110
|
+
const classesMatch = /classes\s*:\s*["']([^"']*)["']/u.exec(variantBlock);
|
|
109
111
|
|
|
110
112
|
// Extract state classes from the classes string
|
|
111
113
|
const stateClasses = classesMatch
|
|
@@ -134,7 +136,7 @@ export function parseDefaultsObject(objStr: string): Record<string, string> {
|
|
|
134
136
|
const result: Record<string, string> = {};
|
|
135
137
|
|
|
136
138
|
// Match properties like: variant: "primary", size: "base"
|
|
137
|
-
const propPattern = /(\w+)\s*:\s*["']([^"']*)["']/
|
|
139
|
+
const propPattern = /(\w+)\s*:\s*["']([^"']*)["']/gu;
|
|
138
140
|
let match: RegExpExecArray | null;
|
|
139
141
|
|
|
140
142
|
while ((match = propPattern.exec(objStr)) !== null) {
|
|
@@ -159,12 +161,12 @@ export function extractVariantsFromFile(
|
|
|
159
161
|
const content = readFileSync(filePath, "utf-8");
|
|
160
162
|
|
|
161
163
|
// Find SF_*_VARIANTS export start position
|
|
162
|
-
const variantsStartMatch = /export\s+const\s+SF_\w+_VARIANTS\s*=\s
|
|
164
|
+
const variantsStartMatch = /export\s+const\s+SF_\w+_VARIANTS\s*=\s*/u.exec(
|
|
163
165
|
content
|
|
164
166
|
);
|
|
165
167
|
// Find SF_*_DEFAULT_VARIANTS export start position
|
|
166
168
|
const defaultsStartMatch =
|
|
167
|
-
/export\s+const\s+SF_\w+_DEFAULT_VARIANTS\s*=\s
|
|
169
|
+
/export\s+const\s+SF_\w+_DEFAULT_VARIANTS\s*=\s*/u.exec(content);
|
|
168
170
|
|
|
169
171
|
if (!variantsStartMatch || !defaultsStartMatch) {
|
|
170
172
|
return null;
|
|
@@ -212,7 +214,7 @@ export function extractVariantsFromFile(
|
|
|
212
214
|
function parseStylingObject(objStr: string): ComponentStyling | null {
|
|
213
215
|
try {
|
|
214
216
|
// Remove 'as const' suffix if present
|
|
215
|
-
const cleanedStr = objStr.replace(/\s*as\s+const\s
|
|
217
|
+
const cleanedStr = objStr.replace(/\s*as\s+const\s*$/u, "");
|
|
216
218
|
|
|
217
219
|
// Security: Parse as JSON-like structure instead of executing code
|
|
218
220
|
// This prevents RCE if a source file is compromised
|
|
@@ -234,14 +236,14 @@ function parseObjectLiteralSafely(
|
|
|
234
236
|
const result: Record<string, unknown> = {};
|
|
235
237
|
|
|
236
238
|
// Remove outer braces
|
|
237
|
-
const content = objStr.trim().replace(/^\{
|
|
239
|
+
const content = objStr.trim().replace(/^\{/u, "").replace(/\}$/u, "").trim();
|
|
238
240
|
if (!content) return {};
|
|
239
241
|
|
|
240
242
|
// Split by top-level commas (not inside nested braces)
|
|
241
243
|
const properties = splitTopLevelProperties(content);
|
|
242
244
|
|
|
243
245
|
for (const prop of properties) {
|
|
244
|
-
const match = /^\s*(\w+)\s*:\s*(.+)$/
|
|
246
|
+
const match = /^\s*(\w+)\s*:\s*(.+)$/su.exec(prop);
|
|
245
247
|
if (!match) continue;
|
|
246
248
|
|
|
247
249
|
const key = match[1];
|
|
@@ -301,20 +303,20 @@ function parseValue(valueStr: string): unknown {
|
|
|
301
303
|
if (valueStr.startsWith("[")) {
|
|
302
304
|
try {
|
|
303
305
|
// Arrays are typically simple in styling configs
|
|
304
|
-
return JSON.parse(valueStr.replace(/'/
|
|
306
|
+
return JSON.parse(valueStr.replace(/'/gu, '"'));
|
|
305
307
|
} catch {
|
|
306
308
|
return null;
|
|
307
309
|
}
|
|
308
310
|
}
|
|
309
311
|
|
|
310
312
|
// Handle strings (single or double quotes)
|
|
311
|
-
const stringMatch = /^["'](.+)["']$/
|
|
313
|
+
const stringMatch = /^["'](.+)["']$/su.exec(valueStr);
|
|
312
314
|
if (stringMatch) {
|
|
313
315
|
return stringMatch[1];
|
|
314
316
|
}
|
|
315
317
|
|
|
316
318
|
// Handle numbers
|
|
317
|
-
if (/^-?\d+(\.\d+)
|
|
319
|
+
if (/^-?\d+(\.\d+)?$/u.test(valueStr)) {
|
|
318
320
|
return Number(valueStr);
|
|
319
321
|
}
|
|
320
322
|
|
|
@@ -343,7 +345,7 @@ export function extractStylingFromFile(
|
|
|
343
345
|
const content = readFileSync(filePath, "utf-8");
|
|
344
346
|
|
|
345
347
|
// Find SF_*_STYLING export start position
|
|
346
|
-
const stylingStartMatch = /export\s+const\s+SF_\w+_STYLING\s*=\s
|
|
348
|
+
const stylingStartMatch = /export\s+const\s+SF_\w+_STYLING\s*=\s*/u.exec(
|
|
347
349
|
content
|
|
348
350
|
);
|
|
349
351
|
|
|
@@ -113,13 +113,13 @@ function collectEntries(): Entry[] {
|
|
|
113
113
|
function extractExportedDemoFunctions(source: string): string[] {
|
|
114
114
|
// Strip template literal contents so we don't match `export function`
|
|
115
115
|
// inside code strings.
|
|
116
|
-
const cleaned = source.replace(/`(?:\\.|[^`\\])*`/
|
|
116
|
+
const cleaned = source.replace(/`(?:\\.|[^`\\])*`/gu, "``");
|
|
117
117
|
|
|
118
118
|
const names = new Set<string>();
|
|
119
119
|
|
|
120
120
|
// Only match at start of a line (after optional whitespace) to avoid
|
|
121
121
|
// matching inside expressions/strings.
|
|
122
|
-
const fnRegex = /^\s*export\s+function\s+([A-Z][A-Za-z0-9_]*)\s*\(/
|
|
122
|
+
const fnRegex = /^\s*export\s+function\s+([A-Z][A-Za-z0-9_]*)\s*\(/gmu;
|
|
123
123
|
let m: RegExpExecArray | null = fnRegex.exec(cleaned);
|
|
124
124
|
while (m) {
|
|
125
125
|
names.add(m[1]);
|
|
@@ -127,7 +127,7 @@ function extractExportedDemoFunctions(source: string): string[] {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const constRegex =
|
|
130
|
-
/^\s*export\s+const\s+([A-Z][A-Za-z0-9_]*)\s*(?::\s*[^=]+)?=\s*(?:\([^)]*\)\s*=>|function)/
|
|
130
|
+
/^\s*export\s+const\s+([A-Z][A-Za-z0-9_]*)\s*(?::\s*[^=]+)?=\s*(?:\([^)]*\)\s*=>|function)/gmu;
|
|
131
131
|
m = constRegex.exec(cleaned);
|
|
132
132
|
while (m) {
|
|
133
133
|
names.add(m[1]);
|
|
@@ -161,7 +161,7 @@ function demoImportPath(entry: Entry): string {
|
|
|
161
161
|
const up = "../".repeat(depth);
|
|
162
162
|
const relFromRegistry = path.relative(
|
|
163
163
|
REGISTRY_ROOT,
|
|
164
|
-
entry.demoFile.replace(/\.tsx
|
|
164
|
+
entry.demoFile.replace(/\.tsx$/u, "")
|
|
165
165
|
);
|
|
166
166
|
return `${up}docs/${relFromRegistry.replace("../", "")}`;
|
|
167
167
|
}
|
|
@@ -170,7 +170,7 @@ function buildDemoImportPath(entry: Entry): string {
|
|
|
170
170
|
// Story lives at packages/registry/src/{components|blocks}/{name}/{name}.stories.tsx
|
|
171
171
|
// Demo lives at packages/docs/src/components/demos/{Name}Demo.tsx
|
|
172
172
|
// Relative path: ../../../../docs/src/components/demos/{Name}Demo
|
|
173
|
-
const demoBase = path.basename(entry.demoFile).replace(/\.tsx
|
|
173
|
+
const demoBase = path.basename(entry.demoFile).replace(/\.tsx$/u, "");
|
|
174
174
|
return `../../../../docs/src/components/demos/${demoBase}`;
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -656,11 +656,7 @@ export const THEME_CONFIG: ThemeConfig = {
|
|
|
656
656
|
};
|
|
657
657
|
|
|
658
658
|
/** List of all available themes */
|
|
659
|
-
export const AVAILABLE_THEMES = [
|
|
660
|
-
"sf",
|
|
661
|
-
"fedramp",
|
|
662
|
-
"minimal",
|
|
663
|
-
] as const;
|
|
659
|
+
export const AVAILABLE_THEMES = ["sf", "fedramp", "minimal"] as const;
|
|
664
660
|
export type AvailableTheme = (typeof AVAILABLE_THEMES)[number];
|
|
665
661
|
|
|
666
662
|
/**
|
|
@@ -32,7 +32,7 @@ function lightDark(light: string, dark: string): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function toThemeFileName(themeName: string): string {
|
|
35
|
-
return themeName.replace(/([a-z])([A-Z])/
|
|
35
|
+
return themeName.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -116,7 +116,7 @@ function buildPatterns(renameMap: TokenRenameMap): Array<{
|
|
|
116
116
|
patterns.push({
|
|
117
117
|
pattern: new RegExp(
|
|
118
118
|
`(${variants})(${prefix}-${escapeRegex(oldName)})(${opacity})(?=\\s|"|'|\`|$|\\))`,
|
|
119
|
-
"
|
|
119
|
+
"gu"
|
|
120
120
|
),
|
|
121
121
|
replacement: `$1${prefix}-${newName}$3`,
|
|
122
122
|
});
|
|
@@ -129,7 +129,7 @@ function buildPatterns(renameMap: TokenRenameMap): Array<{
|
|
|
129
129
|
patterns.push({
|
|
130
130
|
pattern: new RegExp(
|
|
131
131
|
`(${variants})(${prefix}-${escapeRegex(oldName)})(${opacity})(?=\\s|"|'|\`|$|\\))`,
|
|
132
|
-
"
|
|
132
|
+
"gu"
|
|
133
133
|
),
|
|
134
134
|
replacement: `$1${prefix}-${newName}$3`,
|
|
135
135
|
});
|
|
@@ -140,7 +140,7 @@ function buildPatterns(renameMap: TokenRenameMap): Array<{
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
function escapeRegex(str: string): string {
|
|
143
|
-
return str.replace(/[.*+?^${}()|[\]\\]/
|
|
143
|
+
return str.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ai-agent-card-BR2NIYhi.js","names":[],"sources":["../src/components/ai-agent-card/ai-agent-card.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n BrainIcon,\n CheckCircleIcon,\n CircleIcon,\n CodeIcon,\n MagnifyingGlassIcon,\n RobotIcon,\n SpinnerGapIcon,\n WrenchIcon,\n XCircleIcon,\n} from \"@phosphor-icons/react\";\nimport type { ComponentProps, ElementType } from \"react\";\nimport { forwardRef } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_AGENT_CARD_VARIANTS = {\n status: {\n idle: { classes: \"\", description: \"Agent is idle, not yet started\" },\n running: { classes: \"\", description: \"Agent is actively working\" },\n completed: { classes: \"\", description: \"Agent finished successfully\" },\n error: { classes: \"\", description: \"Agent encountered an error\" },\n },\n size: {\n sm: { classes: \"\", description: \"Compact card for dense grids\" },\n md: { classes: \"\", description: \"Default card size\" },\n },\n} as const;\n\nexport const SF_AI_AGENT_CARD_DEFAULT_VARIANTS = {\n status: \"idle\",\n size: \"md\",\n} as const;\n\nexport type SFAiAgentCardStatus = keyof typeof SF_AI_AGENT_CARD_VARIANTS.status;\nexport type SFAiAgentCardSize = keyof typeof SF_AI_AGENT_CARD_VARIANTS.size;\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type AiAgentCardProps = Omit<ComponentProps<\"button\">, \"children\"> & {\n /** Human-readable agent name (e.g. \"Explore\", \"Execute\"). */\n name: string;\n /** Agent type ID — used for icon mapping. */\n agentType?: string;\n /** Current status. @default \"idle\" */\n status?: SFAiAgentCardStatus;\n /** Model ID being used (e.g. \"claude-haiku-3.5\"). */\n modelId?: string;\n /** What the agent is currently doing. */\n currentTask?: string;\n /** Total elapsed duration in ms. */\n duration?: number;\n /** Number of tool calls made. */\n toolCallCount?: number;\n /** Whether this card is currently selected/active. */\n selected?: boolean;\n /** Custom icon override. */\n icon?: ElementType;\n /** Card size. @default \"md\" */\n size?: SFAiAgentCardSize;\n};\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\nconst AGENT_TYPE_ICONS: Record<string, ElementType> = {\n explore: MagnifyingGlassIcon,\n search: MagnifyingGlassIcon,\n execute: CodeIcon,\n code: CodeIcon,\n plan: BrainIcon,\n think: BrainIcon,\n tool: WrenchIcon,\n build: WrenchIcon,\n};\n\nfunction getAgentIcon(\n agentType?: string,\n customIcon?: ElementType\n): ElementType {\n if (customIcon) return customIcon;\n if (agentType) {\n const lower = agentType.toLowerCase();\n for (const [key, icon] of Object.entries(AGENT_TYPE_ICONS)) {\n if (lower.includes(key)) return icon;\n }\n }\n return RobotIcon;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const s = Math.round(ms / 100) / 10;\n if (s < 60) return `${s}s`;\n const m = Math.floor(s / 60);\n const rem = Math.round(s % 60);\n return `${m}m ${rem}s`;\n}\n\nfunction getModelShortName(modelId?: string): string {\n if (!modelId) return \"\";\n return (\n modelId\n .split(\"/\")\n .pop()\n ?.replace(/^claude-/, \"\")\n .replace(/-\\d+$/, \"\") ?? modelId\n );\n}\n\n// ─── Status decorations ───────────────────────────────────────────────────────\n\nconst STATUS_DOT: Record<SFAiAgentCardStatus, string> = {\n idle: \"bg-sf-fill\",\n running: \"bg-sf-brand animate-pulse\",\n completed: \"bg-sf-success\",\n error: \"bg-sf-danger\",\n};\n\nfunction StatusIcon({\n status,\n size = 14,\n}: {\n status: SFAiAgentCardStatus;\n size?: number;\n}) {\n switch (status) {\n case \"running\":\n return (\n <SpinnerGapIcon size={size} className=\"animate-spin text-sf-brand\" />\n );\n case \"completed\":\n return <CheckCircleIcon size={size} className=\"text-sf-success\" />;\n case \"error\":\n return <XCircleIcon size={size} className=\"text-sf-danger\" />;\n default:\n return <CircleIcon size={size} className=\"text-sf-inactive\" />;\n }\n}\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * `AiAgentCard` — compact card showing one agent's status in a commander dashboard.\n *\n * Displays: agent icon, name, status indicator, current task, model, duration,\n * and tool call count. Clickable for selection.\n *\n * @example\n * ```tsx\n * <AiAgentCard\n * name=\"Explore\"\n * agentType=\"explore\"\n * status=\"running\"\n * modelId=\"claude-haiku-3.5\"\n * currentTask=\"Scanning auth files...\"\n * duration={4200}\n * toolCallCount={3}\n * selected\n * onClick={handleSelect}\n * />\n * ```\n */\nexport const AiAgentCard = forwardRef<HTMLButtonElement, AiAgentCardProps>(\n (\n {\n name,\n agentType,\n status = SF_AI_AGENT_CARD_DEFAULT_VARIANTS.status,\n modelId,\n currentTask,\n duration,\n toolCallCount,\n selected,\n icon,\n size = SF_AI_AGENT_CARD_DEFAULT_VARIANTS.size,\n className,\n onClick,\n ...props\n },\n ref\n ) => {\n const Icon = getAgentIcon(agentType, icon);\n const modelShort = getModelShortName(modelId);\n const isSm = size === \"sm\";\n\n return (\n <button\n ref={ref}\n type=\"button\"\n aria-pressed={selected}\n onClick={onClick}\n className={cn(\n \"group flex flex-col gap-2 rounded-xl border text-left transition-all\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sf-ring focus-visible:ring-offset-1\",\n isSm ? \"p-2.5\" : \"p-3.5\",\n selected\n ? \"border-sf-line bg-sf-recessed shadow-sm\"\n : \"border-sf-line bg-sf-elevated hover:border-sf-line hover:bg-sf-tint\",\n !onClick && \"cursor-default\",\n className\n )}\n {...props}\n >\n {/* Header row */}\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-center gap-2 min-w-0\">\n {/* Agent icon */}\n <div\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-lg\",\n isSm ? \"size-6\" : \"size-8\",\n status === \"running\"\n ? \"bg-sf-brand/10 text-sf-brand\"\n : status === \"completed\"\n ? \"bg-sf-tint text-sf-success\"\n : status === \"error\"\n ? \"bg-sf-tint text-sf-danger\"\n : \"bg-sf-fill text-sf-subtle\"\n )}\n >\n <Icon size={isSm ? 12 : 16} />\n </div>\n\n {/* Name */}\n <span\n className={cn(\n \"truncate font-medium text-sf-default\",\n isSm ? \"text-xs\" : \"text-sm\"\n )}\n >\n {name}\n </span>\n </div>\n\n {/* Status icon */}\n <StatusIcon status={status} size={isSm ? 12 : 14} />\n </div>\n\n {/* Current task */}\n {currentTask && (\n <p\n className={cn(\n \"truncate text-sf-subtle leading-snug\",\n isSm ? \"text-[10px]\" : \"text-xs\"\n )}\n >\n {currentTask}\n </p>\n )}\n\n {/* Footer row: model + stats */}\n {!isSm && (\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-1.5\">\n {/* Status dot + model */}\n <span\n className={cn(\n \"size-1.5 shrink-0 rounded-full\",\n STATUS_DOT[status]\n )}\n />\n {modelShort && (\n <span className=\"font-mono text-[10px] text-sf-inactive\">\n {modelShort}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center gap-2 text-[10px] text-sf-subtle\">\n {typeof toolCallCount === \"number\" && toolCallCount > 0 && (\n <span className=\"flex items-center gap-0.5\">\n <WrenchIcon size={9} />\n {toolCallCount}\n </span>\n )}\n {typeof duration === \"number\" && (\n <span>{formatDuration(duration)}</span>\n )}\n </div>\n </div>\n )}\n </button>\n );\n }\n);\n\nAiAgentCard.displayName = \"AiAgentCard\";\n"],"mappings":";;;;;;AAoBA,IAAa,4BAA4B;CACvC,QAAQ;EACN,MAAM;GAAE,SAAS;GAAI,aAAa;GAAkC;EACpE,SAAS;GAAE,SAAS;GAAI,aAAa;GAA6B;EAClE,WAAW;GAAE,SAAS;GAAI,aAAa;GAA+B;EACtE,OAAO;GAAE,SAAS;GAAI,aAAa;GAA8B;EAClE;CACD,MAAM;EACJ,IAAI;GAAE,SAAS;GAAI,aAAa;GAAgC;EAChE,IAAI;GAAE,SAAS;GAAI,aAAa;GAAqB;EACtD;CACF;AAED,IAAa,oCAAoC;CAC/C,QAAQ;CACR,MAAM;CACP;AAgCD,IAAM,mBAAgD;CACpD,SAAS;CACT,QAAQ;CACR,SAAS;CACT,MAAM;CACN,MAAM;CACN,OAAO;CACP,MAAM;CACN,OAAO;CACR;AAED,SAAS,aACP,WACA,YACa;AACb,KAAI,WAAY,QAAO;AACvB,KAAI,WAAW;EACb,MAAM,QAAQ,UAAU,aAAa;AACrC,OAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,iBAAiB,CACxD,KAAI,MAAM,SAAS,IAAI,CAAE,QAAO;;AAGpC,QAAO;;AAGT,SAAS,eAAe,IAAoB;AAC1C,KAAI,KAAK,IAAM,QAAO,GAAG,GAAG;CAC5B,MAAM,IAAI,KAAK,MAAM,KAAK,IAAI,GAAG;AACjC,KAAI,IAAI,GAAI,QAAO,GAAG,EAAE;AAGxB,QAAO,GAFG,KAAK,MAAM,IAAI,GAAG,CAEhB,IADA,KAAK,MAAM,IAAI,GAAG,CACV;;AAGtB,SAAS,kBAAkB,SAA0B;AACnD,KAAI,CAAC,QAAS,QAAO;AACrB,QACE,QACG,MAAM,IAAI,CACV,KAAK,EACJ,QAAQ,YAAY,GAAG,CACxB,QAAQ,SAAS,GAAG,IAAI;;AAM/B,IAAM,aAAkD;CACtD,MAAM;CACN,SAAS;CACT,WAAW;CACX,OAAO;CACR;AAED,SAAS,WAAW,EAClB,QACA,OAAO,MAIN;AACD,SAAQ,QAAR;EACE,KAAK,UACH,QACE,oBAAC,gBAAD;GAAsB;GAAM,WAAU;GAA+B,CAAA;EAEzE,KAAK,YACH,QAAO,oBAAC,iBAAD;GAAuB;GAAM,WAAU;GAAoB,CAAA;EACpE,KAAK,QACH,QAAO,oBAAC,aAAD;GAAmB;GAAM,WAAU;GAAmB,CAAA;EAC/D,QACE,QAAO,oBAAC,YAAD;GAAkB;GAAM,WAAU;GAAqB,CAAA;;;;;;;;;;;;;;;;;;;;;;;;AA2BpE,IAAa,cAAc,YAEvB,EACE,MACA,WACA,SAAS,kCAAkC,QAC3C,SACA,aACA,UACA,eACA,UACA,MACA,OAAO,kCAAkC,MACzC,WACA,SACA,GAAG,SAEL,QACG;CACH,MAAM,OAAO,aAAa,WAAW,KAAK;CAC1C,MAAM,aAAa,kBAAkB,QAAQ;CAC7C,MAAM,OAAO,SAAS;AAEtB,QACE,qBAAC,UAAD;EACO;EACL,MAAK;EACL,gBAAc;EACL;EACT,WAAW,GACT,wEACA,0GACA,OAAO,UAAU,SACjB,WACI,4CACA,uEACJ,CAAC,WAAW,kBACZ,UACD;EACD,GAAI;YAfN;GAkBE,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,qBAAC,OAAD;KAAK,WAAU;eAAf,CAEE,oBAAC,OAAD;MACE,WAAW,GACT,wDACA,OAAO,WAAW,UAClB,WAAW,YACP,iCACA,WAAW,cACT,+BACA,WAAW,UACT,8BACA,4BACT;gBAED,oBAAC,MAAD,EAAM,MAAM,OAAO,KAAK,IAAM,CAAA;MAC1B,CAAA,EAGN,oBAAC,QAAD;MACE,WAAW,GACT,wCACA,OAAO,YAAY,UACpB;gBAEA;MACI,CAAA,CACH;QAGN,oBAAC,YAAD;KAAoB;KAAQ,MAAM,OAAO,KAAK;KAAM,CAAA,CAChD;;GAGL,eACC,oBAAC,KAAD;IACE,WAAW,GACT,wCACA,OAAO,gBAAgB,UACxB;cAEA;IACC,CAAA;GAIL,CAAC,QACA,qBAAC,OAAD;IAAK,WAAU;cAAf,CACE,qBAAC,OAAD;KAAK,WAAU;eAAf,CAEE,oBAAC,QAAD,EACE,WAAW,GACT,kCACA,WAAW,QACZ,EACD,CAAA,EACD,cACC,oBAAC,QAAD;MAAM,WAAU;gBACb;MACI,CAAA,CAEL;QAEN,qBAAC,OAAD;KAAK,WAAU;eAAf,CACG,OAAO,kBAAkB,YAAY,gBAAgB,KACpD,qBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,oBAAC,YAAD,EAAY,MAAM,GAAK,CAAA,EACtB,cACI;SAER,OAAO,aAAa,YACnB,oBAAC,QAAD,EAAA,UAAO,eAAe,SAAS,EAAQ,CAAA,CAErC;OACF;;GAED;;EAGd;AAED,YAAY,cAAc"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ai-code-block-CZtoL73R.js","names":[],"sources":["../src/components/ai-code-block/ai-code-block.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ComponentProps, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, memo, useContext, useState } from \"react\";\nimport { highlight } from \"sugar-high\";\n\nimport { cn } from \"../../utils/cn\";\nimport { highlightToLines } from \"../../utils/highlight-to-react\";\nimport { Button } from \"../button\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_CODE_BLOCK_VARIANTS = {} as const;\nexport const SF_AI_CODE_BLOCK_DEFAULT_VARIANTS = {} as const;\n\n// ─── Context ─────────────────────────────────────────────────────────────────\n\ninterface AiCodeBlockContextValue {\n code: string;\n}\n\nconst AiCodeBlockContext = createContext<AiCodeBlockContextValue>({ code: \"\" });\n\n// ─── AiCodeBlock ─────────────────────────────────────────────────────────────\n\nexport type AiCodeBlockProps = HTMLAttributes<HTMLDivElement> & {\n /** The raw code string to display and make copyable. */\n code: string;\n /** Language identifier for syntax highlighting (display only, no highlighting applied). */\n language?: string;\n /** Show line numbers. @default false */\n showLineNumbers?: boolean;\n /** Extra content rendered in the top-right overlay (e.g. copy button). */\n children?: ReactNode;\n};\n\n/**\n * Displays a code block with monospace formatting and an optional copy button.\n * Does not include a syntax highlighter — avoids the heavy `react-syntax-highlighter`\n * dependency. Wrap with `AiCodeBlockCopyButton` for clipboard support.\n *\n * @example\n * ```tsx\n * <AiCodeBlock code={`const x = 1;`} language=\"ts\">\n * <AiCodeBlockCopyButton />\n * </AiCodeBlock>\n * ```\n */\nexport const AiCodeBlock = memo(\n ({\n code,\n language,\n showLineNumbers = false,\n className,\n children,\n ...props\n }: AiCodeBlockProps) => {\n const highlightedLines = highlightToLines(highlight(code));\n\n return (\n <AiCodeBlockContext.Provider value={{ code }}>\n <div\n className={cn(\n \"relative w-full overflow-hidden rounded-lg border border-sf-line bg-sf-recessed font-mono text-sm text-sf-default\",\n className\n )}\n {...props}\n >\n {language && (\n <div className=\"flex items-center justify-between border-b border-sf-line px-4 py-2\">\n <span className=\"text-sf-subtle text-xs\">{language}</span>\n {children && (\n <div className=\"flex items-center gap-2\">{children}</div>\n )}\n </div>\n )}\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse\">\n <tbody>\n {highlightedLines.map((line, i) => (\n // biome-ignore lint/suspicious/noArrayIndexKey: line index is stable\n <tr key={i} className=\"leading-6\">\n {showLineNumbers && (\n <td className=\"select-none pr-4 pl-4 text-right text-sf-inactive\">\n {i + 1}\n </td>\n )}\n <td className={cn(\"pr-4\", !showLineNumbers && \"px-4\")}>\n <pre className=\"whitespace-pre\">{line}</pre>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {!language && children && (\n <div className=\"absolute top-2 right-2 flex items-center gap-2\">\n {children}\n </div>\n )}\n </div>\n </AiCodeBlockContext.Provider>\n );\n },\n (prev, next) =>\n prev.code === next.code &&\n prev.language === next.language &&\n prev.showLineNumbers === next.showLineNumbers\n);\n\nAiCodeBlock.displayName = \"AiCodeBlock\";\n\n// ─── AiCodeBlockCopyButton ───────────────────────────────────────────────────\n\nexport type AiCodeBlockCopyButtonProps = ComponentProps<typeof Button> & {\n /** Called after a successful copy. */\n onCopy?: () => void;\n /** Called if the clipboard API is unavailable or errors. */\n onError?: (error: Error) => void;\n /** How long (ms) to show the \"copied\" state. @default 2000 */\n timeout?: number;\n};\n\n/**\n * Copy-to-clipboard button for use inside `AiCodeBlock`.\n * Must be a descendant of `AiCodeBlock` to access the code via context.\n *\n * @example\n * ```tsx\n * <AiCodeBlock code=\"const x = 1;\" language=\"ts\">\n * <AiCodeBlockCopyButton />\n * </AiCodeBlock>\n * ```\n */\nexport function AiCodeBlockCopyButton({\n onCopy,\n onError,\n timeout = 2000,\n children,\n className,\n size = \"sm\",\n variant = \"ghost\",\n ...props\n}: AiCodeBlockCopyButtonProps) {\n const [copied, setCopied] = useState(false);\n const { code } = useContext(AiCodeBlockContext);\n\n const handleCopy = async () => {\n if (typeof navigator === \"undefined\" || !navigator.clipboard?.writeText) {\n onError?.(new Error(\"Clipboard API not available\"));\n return;\n }\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n onCopy?.();\n setTimeout(() => setCopied(false), timeout);\n } catch (err) {\n onError?.(err as Error);\n }\n };\n\n return (\n <Button\n className={cn(\"shrink-0\", className)}\n onClick={handleCopy}\n size={size}\n variant={variant}\n {...props}\n >\n {children ?? (copied ? \"Copied\" : \"Copy\")}\n </Button>\n );\n}\n"],"mappings":";;;;;;;;AAYA,IAAa,4BAA4B,EAAE;AAC3C,IAAa,oCAAoC,EAAE;AAQnD,IAAM,qBAAqB,cAAuC,EAAE,MAAM,IAAI,CAAC;;;;;;;;;;;;;AA2B/E,IAAa,cAAc,MACxB,EACC,MACA,UACA,kBAAkB,OAClB,WACA,UACA,GAAG,YACmB;CACtB,MAAM,mBAAmB,iBAAiB,UAAU,KAAK,CAAC;AAE1D,QACE,oBAAC,mBAAmB,UAApB;EAA6B,OAAO,EAAE,MAAM;YAC1C,qBAAC,OAAD;GACE,WAAW,GACT,qHACA,UACD;GACD,GAAI;aALN;IAOG,YACC,qBAAC,OAAD;KAAK,WAAU;eAAf,CACE,oBAAC,QAAD;MAAM,WAAU;gBAA0B;MAAgB,CAAA,EACzD,YACC,oBAAC,OAAD;MAAK,WAAU;MAA2B;MAAe,CAAA,CAEvD;;IAER,oBAAC,OAAD;KAAK,WAAU;eACb,oBAAC,SAAD;MAAO,WAAU;gBACf,oBAAC,SAAD,EAAA,UACG,iBAAiB,KAAK,MAAM,MAE3B,qBAAC,MAAD;OAAY,WAAU;iBAAtB,CACG,mBACC,oBAAC,MAAD;QAAI,WAAU;kBACX,IAAI;QACF,CAAA,EAEP,oBAAC,MAAD;QAAI,WAAW,GAAG,QAAQ,CAAC,mBAAmB,OAAO;kBACnD,oBAAC,OAAD;SAAK,WAAU;mBAAkB;SAAW,CAAA;QACzC,CAAA,CACF;SATI,EASJ,CACL,EACI,CAAA;MACF,CAAA;KACJ,CAAA;IACL,CAAC,YAAY,YACZ,oBAAC,OAAD;KAAK,WAAU;KACZ;KACG,CAAA;IAEJ;;EACsB,CAAA;IAGjC,MAAM,SACL,KAAK,SAAS,KAAK,QACnB,KAAK,aAAa,KAAK,YACvB,KAAK,oBAAoB,KAAK,gBACjC;AAED,YAAY,cAAc;;;;;;;;;;;;AAwB1B,SAAgB,sBAAsB,EACpC,QACA,SACA,UAAU,KACV,UACA,WACA,OAAO,MACP,UAAU,SACV,GAAG,SAC0B;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;CAC3C,MAAM,EAAE,SAAS,WAAW,mBAAmB;CAE/C,MAAM,aAAa,YAAY;AAC7B,MAAI,OAAO,cAAc,eAAe,CAAC,UAAU,WAAW,WAAW;AACvE,6BAAU,IAAI,MAAM,8BAA8B,CAAC;AACnD;;AAEF,MAAI;AACF,SAAM,UAAU,UAAU,UAAU,KAAK;AACzC,aAAU,KAAK;AACf,aAAU;AACV,oBAAiB,UAAU,MAAM,EAAE,QAAQ;WACpC,KAAK;AACZ,aAAU,IAAa;;;AAI3B,QACE,oBAAC,QAAD;EACE,WAAW,GAAG,YAAY,UAAU;EACpC,SAAS;EACH;EACG;EACT,GAAI;YAEH,aAAa,SAAS,WAAW;EAC3B,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ai-conversation-Cc7WlaBg.js","names":[],"sources":["../src/components/ai-conversation/ai-conversation.tsx","../src/components/ai-conversation/measurement-constants.ts"],"sourcesContent":["\"use client\";\n\nimport { ArrowDownIcon } from \"@phosphor-icons/react\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport type { Virtualizer, VirtualItem } from \"@tanstack/react-virtual\";\nimport { layout, prepare } from \"@chenglou/pretext\";\nimport type { ComponentProps, ReactNode, RefObject } from \"react\";\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\n\nimport { cn } from \"../../utils/cn\";\n\nimport { Button } from \"../button\";\n\n// ─── Variants ────────────────────────────────────────────────────────────────\n\nexport const SF_AI_CONVERSATION_VARIANTS = {} as const;\nexport const SF_AI_CONVERSATION_DEFAULT_VARIANTS = {} as const;\n\n// ─── AiConversation ──────────────────────────────────────────────────────────\n\nexport type AiConversationProps = ComponentProps<\"div\">;\n\n/**\n * Outer scroll container for a conversation. Sticks to the bottom as new\n * messages are added. Pair with `AiConversationContent` and optionally\n * `AiConversationScrollButton`.\n *\n * @example\n * ```tsx\n * <AiConversation ref={scrollRef}>\n * <AiConversationContent>\n * {messages.map(m => <AiMessage key={m.id} from={m.role}>...</AiMessage>)}\n * </AiConversationContent>\n * <AiConversationScrollButton scrollRef={scrollRef} />\n * </AiConversation>\n * ```\n */\nexport const AiConversation = forwardRef<HTMLDivElement, AiConversationProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\"relative flex-1 overflow-y-auto\", className)}\n role=\"log\"\n aria-live=\"polite\"\n aria-label=\"Conversation\"\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nAiConversation.displayName = \"AiConversation\";\n\n// ─── AiConversationContent ───────────────────────────────────────────────────\n\nexport type AiConversationContentProps = ComponentProps<\"div\">;\n\n/**\n * Inner content wrapper with padding. Place message components inside here.\n */\nexport function AiConversationContent({\n className,\n children,\n ...props\n}: AiConversationContentProps) {\n return (\n <div\n className={cn(\"flex flex-col gap-0 overscroll-y-none p-4\", className)}\n {...props}\n >\n {children}\n </div>\n );\n}\n\n// ─── AiConversationScrollButton ──────────────────────────────────────────────\n\nexport type AiConversationScrollButtonProps = ComponentProps<typeof Button> & {\n /**\n * Ref to the scroll container (the `AiConversation` element).\n * When omitted, the button is always visible.\n */\n scrollRef?: RefObject<HTMLElement | null>;\n /** Scroll threshold in pixels from bottom to show the button. @default 100 */\n threshold?: number;\n};\n\n/**\n * Floating \"scroll to bottom\" button. Appears when the user has scrolled up\n * more than `threshold` pixels from the bottom.\n */\nexport function AiConversationScrollButton({\n scrollRef,\n threshold = 100,\n className,\n onClick,\n ...props\n}: AiConversationScrollButtonProps) {\n const [show, setShow] = useState(false);\n\n useEffect(() => {\n const el = scrollRef?.current;\n if (!el) return;\n\n const check = () => {\n const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;\n setShow(distFromBottom > threshold);\n };\n\n check();\n el.addEventListener(\"scroll\", check, { passive: true });\n return () => el.removeEventListener(\"scroll\", check);\n }, [scrollRef, threshold]);\n\n const handleClick = useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n scrollRef?.current?.scrollTo({\n top: scrollRef.current.scrollHeight,\n behavior: \"smooth\",\n });\n onClick?.(e);\n },\n [scrollRef, onClick]\n );\n\n if (scrollRef && !show) return null;\n\n return (\n <Button\n aria-label=\"Scroll to bottom\"\n className={cn(\n \"absolute bottom-4 left-1/2 -translate-x-1/2 rounded-full shadow-md\",\n className\n )}\n onClick={handleClick}\n size=\"sm\"\n variant=\"secondary\"\n {...props}\n >\n <ArrowDownIcon className=\"size-4\" />\n </Button>\n );\n}\n\n// ─── Virtualized Conversation ────────────────────────────────────────────────\n\n/** Measurement spec for predictive height calculation using pretext. */\nexport interface ConversationItemMeasureSpec {\n /** Text content to measure */\n text: string;\n /** CSS font shorthand (e.g. \"14px Inter, sans-serif\"). Auto-detected if omitted. */\n font?: string;\n /** Line height in pixels. Auto-detected if omitted. */\n lineHeight?: number;\n /** Additional vertical padding (top + bottom) in pixels @default 0 */\n padding?: number;\n /**\n * Fraction of column width this item occupies (0-1). Used to compute the\n * actual content width for items that are constrained (e.g. user message\n * bubbles at max-w-[70%]). @default 1\n */\n widthFraction?: number;\n /**\n * Horizontal padding (left + right) inside the content box, subtracted from\n * the available width before line wrapping. @default 0\n */\n horizontalPadding?: number;\n /**\n * Margin below this item (gap to next item). @default 0\n */\n gapAfter?: number;\n /**\n * White-space handling — must match the rendered CSS. Use `\"pre-wrap\"` when\n * the rendered content preserves explicit newlines (e.g. plain text inside\n * a <pre> or `whitespace-pre-wrap` div). @default \"normal\"\n */\n whiteSpace?: \"normal\" | \"pre-wrap\";\n /**\n * Word-break handling — match CSS `word-break`. Use `\"keep-all\"` for CJK\n * text that should not break mid-word. @default \"normal\"\n */\n wordBreak?: \"normal\" | \"keep-all\";\n}\n\n/** A single item in the virtualized conversation list. */\nexport interface ConversationItem {\n /** Unique key for this item. */\n key: string;\n /**\n * Render the item content. The virtualizer handles measurement and\n * positioning on the wrapper — just return the content.\n */\n render: () => ReactNode;\n /**\n * Optional predictive height measurement. When provided, the virtualizer\n * uses pretext to compute exact height without DOM measurement, eliminating\n * layout shift during streaming. Falls back to estimateSize + DOM measurement\n * when omitted.\n *\n * **Attach this only when rendering plain text** (no markdown). If your\n * `render` output uses `<AiResponse>` or any rich-content component whose\n * blocks have varying typography (headings, code, lists, blockquotes), omit\n * `measure` to let the virtualizer use DOM measurement instead. Pretext\n * predicts heights from a single (font, line-height) pair and cannot\n * accurately model multi-block markdown.\n *\n * Plain-text + `measure` gives jitter-free virtualization (no estimate-then-\n * adjust feedback loop). Rich content + DOM measurement is rendering-correct\n * but slightly less smooth on first paint.\n *\n * For streaming items, update `measure.text` as text grows so the virtualizer\n * can recalculate height per tick (rebuild the `items` array with the latest\n * text on each token arrival).\n */\n measure?: ConversationItemMeasureSpec;\n}\n\nexport interface UseConversationVirtualizerOptions {\n /** Ref to the scroll container element. */\n scrollRef: RefObject<HTMLElement | null>;\n /** The conversation items to virtualize. */\n items: ConversationItem[];\n /**\n * Estimated height of a single item in pixels.\n * Used before measurement. Doesn't need to be exact.\n * @default 80\n */\n estimateSize?: number;\n /**\n * Number of items to render beyond the visible area.\n * @default 8\n */\n overscan?: number;\n /**\n * Pixel distance from bottom to consider \"at bottom\" for stick-to-bottom.\n * @default 40\n */\n stickThreshold?: number;\n}\n\nexport interface UseConversationVirtualizerReturn {\n /** The TanStack virtualizer instance. */\n virtualizer: Virtualizer<HTMLElement, Element>;\n /** The virtual items to render. */\n virtualItems: VirtualItem[];\n /** Total measured/estimated height of all items. */\n totalSize: number;\n /** Whether the user is currently at the bottom of the conversation. */\n isAtBottom: boolean;\n /** Smoothly scroll to the bottom. */\n scrollToBottom: (behavior?: ScrollBehavior) => void;\n /**\n * Get the measurement ref for a virtual item by index.\n * @deprecated Use `getMeasureRef(index)` instead to respect predictive heights.\n */\n measureRef: (node: HTMLElement | null) => void;\n /**\n * Get the measurement ref for a specific item index.\n * Returns `null` for items with predictive height (no DOM measurement needed),\n * or the measurement callback for legacy items.\n */\n getMeasureRef: (index: number) => ((node: HTMLElement | null) => void) | null;\n}\n\n/**\n * `useConversationVirtualizer` — virtualizes a conversation list with\n * dynamic item heights and automatic stick-to-bottom behavior.\n *\n * Built on `@tanstack/react-virtual`. Items are measured dynamically via\n * `ResizeObserver`, so tool calls expanding, subagents collapsing, and\n * streaming text growing all work correctly.\n *\n * **Stick-to-bottom**: When the user is scrolled to (or near) the bottom,\n * new items and item resizes automatically keep the viewport pinned to the\n * bottom. When the user scrolls up, stick-to-bottom disengages until they\n * scroll back down.\n *\n * @example\n * ```tsx\n * const { virtualItems, totalSize, measureRef, isAtBottom, scrollToBottom } =\n * useConversationVirtualizer({ scrollRef, items });\n *\n * return (\n * <AiConversation ref={scrollRef}>\n * <div style={{ height: totalSize, position: \"relative\" }}>\n * {virtualItems.map((vi) => {\n * const item = items[vi.index];\n * return (\n * <div key={item.key} ref={measureRef} data-index={vi.index}\n * style={{ position: \"absolute\", top: vi.start, width: \"100%\" }}>\n * {item.render()}\n * </div>\n * );\n * })}\n * </div>\n * {!isAtBottom && <button onClick={() => scrollToBottom()}>↓</button>}\n * </AiConversation>\n * );\n * ```\n */\nconst isSSR = typeof window === \"undefined\";\nconst useIsomorphicLayoutEffect = isSSR ? useEffect : useLayoutEffect;\n\nexport function useConversationVirtualizer({\n scrollRef,\n items,\n estimateSize = 80,\n overscan = 8,\n stickThreshold = 40,\n}: UseConversationVirtualizerOptions): UseConversationVirtualizerReturn {\n const [isAtBottom, setIsAtBottom] = useState(true);\n const isAtBottomRef = useRef(true);\n const prevCountRef = useRef(items.length);\n\n // Track container width for predictive height calculation\n const [containerWidth, setContainerWidth] = useState(0);\n\n // Track whether webfonts are ready. Pretext measures via canvas which uses\n // the browser font engine — if Inter (or any custom font) hasn't loaded\n // yet, the canvas falls back to system-ui and predictions diverge from the\n // rendered DOM. Once fonts.ready resolves we re-trigger measurement.\n const [fontsReady, setFontsReady] = useState(() => {\n if (typeof document === \"undefined\") return true;\n // `document.fonts.status === \"loaded\"` means all current FontFaces are\n // loaded. If still \"loading\" we wait for fonts.ready below.\n return document.fonts?.status === \"loaded\";\n });\n\n useEffect(() => {\n if (fontsReady) return;\n if (typeof document === \"undefined\" || !document.fonts) {\n setFontsReady(true);\n return;\n }\n let cancelled = false;\n document.fonts.ready.then(() => {\n if (!cancelled) setFontsReady(true);\n });\n return () => {\n cancelled = true;\n };\n }, [fontsReady]);\n\n useIsomorphicLayoutEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const updateWidth = () => {\n // clientWidth excludes scrollbars but includes padding.\n // Subtract horizontal padding to get the actual content width.\n const style = window.getComputedStyle(el);\n const padLeft = parseFloat(style.paddingLeft) || 0;\n const padRight = parseFloat(style.paddingRight) || 0;\n setContainerWidth(el.clientWidth - padLeft - padRight);\n };\n\n updateWidth();\n\n const observer = new ResizeObserver(updateWidth);\n observer.observe(el);\n\n return () => observer.disconnect();\n }, [scrollRef]);\n\n // Track whether user is at the bottom\n useEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n\n const check = () => {\n const dist = el.scrollHeight - el.scrollTop - el.clientHeight;\n const atBottom = dist <= stickThreshold;\n isAtBottomRef.current = atBottom;\n setIsAtBottom(atBottom);\n };\n\n check();\n el.addEventListener(\"scroll\", check, { passive: true });\n return () => el.removeEventListener(\"scroll\", check);\n }, [scrollRef, stickThreshold]);\n\n // Track the previous totalSize so we can detect growth from item resizes\n const prevTotalSizeRef = useRef(0);\n\n const virtualizer = useVirtualizer({\n count: items.length,\n getScrollElement: () => scrollRef.current as HTMLElement | null,\n estimateSize: (index) => {\n const item = items[index];\n if (!item?.measure || containerWidth <= 0) {\n return estimateSize;\n }\n const {\n text,\n font = \"14px sans-serif\",\n lineHeight = 20,\n padding = 0,\n widthFraction = 1,\n horizontalPadding = 0,\n gapAfter = 0,\n whiteSpace = \"normal\",\n wordBreak = \"normal\",\n } = item.measure;\n\n // Apply the same width constraints as the rendered message bubble.\n const contentWidth = Math.max(\n 40,\n Math.floor(containerWidth * widthFraction) - horizontalPadding\n );\n\n try {\n const prepared = prepare(text, font, { whiteSpace, wordBreak });\n const { height } = layout(prepared, contentWidth, lineHeight);\n return height + padding + gapAfter;\n } catch {\n return estimateSize;\n }\n },\n overscan,\n getItemKey: (index) => items[index]?.key ?? index,\n });\n\n // When container width, item measurement specs, or font-readiness change,\n // force TanStack to recompute sizes through `estimateSize`. For predictive\n // rows this calls pretext again and still skips DOM measurement.\n useEffect(() => {\n if (containerWidth > 0) {\n virtualizer.measure();\n }\n }, [containerWidth, items, virtualizer, fontsReady]);\n\n // When items resize (streaming text, collapsible expand) and we're at\n // bottom, adjust scroll so the bottom stays pinned.\n virtualizer.shouldAdjustScrollPositionOnItemSizeChange = () =>\n isAtBottomRef.current;\n\n const virtualItems = virtualizer.getVirtualItems();\n const totalSize = virtualizer.getTotalSize();\n\n // When new items are added and we're at bottom, scroll to the end\n useEffect(() => {\n const count = items.length;\n if (count > prevCountRef.current && isAtBottomRef.current) {\n requestAnimationFrame(() => {\n virtualizer.scrollToIndex(count - 1, {\n align: \"end\",\n behavior: \"smooth\",\n });\n });\n }\n prevCountRef.current = count;\n }, [items.length, virtualizer]);\n\n // When totalSize grows (item content expanded, streaming text grew)\n // and we're stuck to bottom, smoothly follow by scrolling down.\n // This complements shouldAdjustScrollPositionOnItemSizeChange which\n // handles the instantaneous adjustment — this handles the smooth follow.\n useEffect(() => {\n const prev = prevTotalSizeRef.current;\n prevTotalSizeRef.current = totalSize;\n\n if (!isAtBottomRef.current || totalSize <= prev) return;\n\n const el = scrollRef.current;\n if (!el) return;\n\n // Only nudge if we're slightly off — the shouldAdjust callback handles\n // most cases, but measurement batching can leave a small gap.\n const dist = el.scrollHeight - el.scrollTop - el.clientHeight;\n if (dist > 1 && dist < 300) {\n el.scrollTop = el.scrollHeight - el.clientHeight;\n }\n }, [totalSize, scrollRef]);\n\n const scrollToBottom = useCallback(\n (behavior: ScrollBehavior = \"smooth\") => {\n if (items.length === 0) return;\n virtualizer.scrollToIndex(items.length - 1, { align: \"end\", behavior });\n },\n [virtualizer, items.length]\n );\n\n const measureRef = useCallback(\n (node: HTMLElement | null) => {\n if (node) virtualizer.measureElement(node);\n },\n [virtualizer]\n );\n\n // For items with predictive height, return null so TanStack doesn't remeasure.\n // For legacy items without measure spec, return the measurement callback.\n const getMeasureRef = useCallback(\n (index: number): ((node: HTMLElement | null) => void) | null => {\n const item = items[index];\n if (item?.measure) {\n return null; // Predicted height — no DOM measurement needed\n }\n return measureRef;\n },\n [items, measureRef]\n );\n\n return {\n virtualizer,\n virtualItems,\n totalSize,\n isAtBottom,\n scrollToBottom,\n measureRef,\n getMeasureRef,\n };\n}\n","/**\n * Shared pretext measurement constants for `useConversationVirtualizer`.\n *\n * These match the rendered styling of `<AiMessage>` + `<AiMessageContent>`\n * (text-sm = 14px / line-height 20px, user bubble `max-w-[70%]` with `px-4 py-3`).\n *\n * Anywhere that builds `ConversationItem.measure` for plain-text AI chat messages\n * should import from here so calibration stays in one place. If production CSS\n * for `<AiMessage>` ever changes (font size, padding, bubble width), update these\n * constants — pretext predictions will then automatically follow.\n */\n\n/** CSS `font` shorthand matching `text-sm` rendering. */\nexport const SF_AI_MEASURE_FONT =\n '14px \"Inter\", ui-sans-serif, system-ui, sans-serif';\n\n/** Line height in pixels, matching `text-sm`. */\nexport const SF_AI_MEASURE_LINE_HEIGHT = 20;\n\n/** User bubbles: `max-w-[70%]`. */\nexport const SF_AI_USER_WIDTH_FRACTION = 0.7;\n\n/** User bubbles: `px-4` → 16px × 2 horizontal padding. */\nexport const SF_AI_USER_HORIZONTAL_PADDING = 32;\n\n/** User bubbles: `py-3` → 12px × 2 vertical padding. */\nexport const SF_AI_USER_VERTICAL_PADDING = 24;\n\n/** Assistant text: full width column, no bubble. */\nexport const SF_AI_ASSISTANT_WIDTH_FRACTION = 1;\n\n/** Assistant text: no horizontal padding inside the bubble. */\nexport const SF_AI_ASSISTANT_HORIZONTAL_PADDING = 0;\n\n/** Assistant text: no vertical padding inside the bubble. */\nexport const SF_AI_ASSISTANT_VERTICAL_PADDING = 0;\n\n/** Default gap between conversation items. */\nexport const SF_AI_DEFAULT_MESSAGE_GAP = 12;\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ai-part-group-DNb9I446.js","names":[],"sources":["../src/components/ai-part-group/ai-part-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { Collapsible as BaseCollapsible } from \"@base-ui/react/collapsible\";\nimport { CaretDownIcon, SpinnerGapIcon } from \"@phosphor-icons/react\";\nimport type { ComponentProps, ReactElement, ReactNode } from \"react\";\nimport { Children, isValidElement, useState } from \"react\";\n\nimport { cn } from \"../../utils/cn\";\nimport { type AiToolPart, hasToolError, hasToolResult } from \"../ai-tool\";\nimport { TextRoll } from \"../text-roll\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\ntype FrozenStep = {\n kind: \"tool\" | \"reasoning\";\n name: string;\n ok: boolean;\n duration?: number;\n};\n\ntype FrozenSnapshot = {\n title: string;\n steps: FrozenStep[];\n errorCount: number;\n totalDuration: number;\n};\n\n// Module-level snapshot cache. Survives re-renders within the page session.\n// Keyed by `groupKey` — usually the parent message id.\nconst snapshotCache = new Map<string, FrozenSnapshot>();\n\n/** Test-only escape hatch for clearing the snapshot cache. */\nexport function _clearAiPartGroupCache() {\n snapshotCache.clear();\n}\n\n/**\n * Reads the frozen snapshot for a group, or `undefined` if it hasn't completed\n * yet. Useful for consumers driving custom layouts.\n */\nexport function getAiPartGroupSnapshot(\n groupKey: string\n): FrozenSnapshot | undefined {\n return snapshotCache.get(groupKey);\n}\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport type AiPartGroupProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n /**\n * Stable identifier — usually the parent message id. Required so the group\n * can persist its frozen snapshot across re-mounts (scroll-back into a\n * virtualized list re-renders without restarting animations).\n */\n groupKey: string;\n /**\n * Set to `true` once the parent stream has finished. Triggers the group to\n * snapshot its current children and from then on render only the static\n * collapsed summary — ephemeral children never re-mount.\n */\n complete?: boolean;\n /**\n * Max number of live ephemeral rows visible at once during streaming.\n * Older rows fall off the top. Pinned rows (errors / user-expanded) render\n * above the live window and do not count toward this cap.\n * @default 3\n */\n max?: number;\n /**\n * Title shown in the collapsed summary line. Falls back to the first\n * reasoning text snippet, then to a step count.\n */\n title?: string;\n /** Child ephemeral rows (`AiToolCall variant=\"ephemeral\"`, `AiReasoning variant=\"ephemeral\"`). */\n children?: ReactNode;\n};\n\n/**\n * Groups streaming ephemeral activity (tool calls + reasoning) into a single\n * rolling window during streaming, then collapses to a frozen summary line\n * once the parent message completes.\n *\n * Behavior:\n * - **Streaming:** shows the most recent `max` (default 3) ephemeral children.\n * Pinned rows (errors / user-expanded) sit above the live window.\n * - **Completed:** writes a snapshot keyed by `groupKey`, unmounts ephemeral\n * children entirely, and renders a static `▸ {title} · N steps` line.\n * - **Re-mount after completion:** always renders the cached snapshot — no\n * animations, no `useEffect` re-runs, no tool re-execution risk.\n *\n * @example\n * ```tsx\n * <AiPartGroup\n * groupKey={message.id}\n * complete={message.status === \"complete\"}\n * title={message.reasoningSummary}\n * max={3}\n * >\n * {message.parts.map((p) =>\n * p.type === \"reasoning\"\n * ? <AiReasoning key={p.id} variant=\"ephemeral\" part={p} />\n * : <AiToolCall key={p.toolCallId} variant=\"ephemeral\" part={p} />\n * )}\n * </AiPartGroup>\n * ```\n */\nexport function AiPartGroup({\n groupKey,\n complete = false,\n max = 3,\n title,\n children,\n className,\n ...props\n}: AiPartGroupProps) {\n const cached = snapshotCache.get(groupKey);\n\n // Cache hit: always render the frozen snapshot, never re-mount ephemerals.\n // This is the scroll-back-into-an-old-message path.\n if (cached) {\n return (\n <TitledSummary\n snapshot={cached}\n className={className}\n defaultOpen={false}\n {...props}\n />\n );\n }\n\n // First-time completion: derive a snapshot, persist it, render frozen.\n if (complete) {\n const snapshot = deriveSnapshot(children, title);\n snapshotCache.set(groupKey, snapshot);\n return (\n <TitledSummary\n snapshot={snapshot}\n className={className}\n defaultOpen={false}\n {...props}\n />\n );\n }\n\n // Streaming: persistent title row + rolling window of the latest `max` children.\n // oxlint-disable-next-line no-react-children\n const allChildren = Children.toArray(children).filter(isValidElement);\n const live = allChildren.slice(Math.max(0, allChildren.length - max));\n const liveTitle = title?.trim() || deriveLiveTitle(allChildren);\n const stepCount = allChildren.length;\n\n return (\n <LiveGroup\n className={className}\n liveRows={live}\n stepCount={stepCount}\n title={liveTitle}\n {...props}\n />\n );\n}\n\nAiPartGroup.displayName = \"AiPartGroup\";\n\n// ─── Live group (streaming) ──────────────────────────────────────────────────\n\nfunction LiveGroup({\n title,\n liveRows,\n stepCount,\n className,\n ...props\n}: {\n title: string;\n liveRows: ReactElement[];\n stepCount: number;\n} & Omit<ComponentProps<\"div\">, \"title\">) {\n // During streaming the group is always open — the caret is purely decorative\n // so the frozen/live states share a visual footprint (no layout jump on settle).\n const stepLabel = stepCount === 1 ? \"step\" : \"steps\";\n\n return (\n <div\n className={cn(\"flex flex-col\", className)}\n data-streaming=\"true\"\n {...props}\n >\n <div\n className={cn(\"flex w-fit items-center gap-1.5 rounded px-1 py-0.5\")}\n >\n <SpinnerGapIcon className=\"size-3.5 shrink-0 animate-spin text-sf-subtle\" />\n <span className=\"min-w-0 truncate text-sm text-sf-default\">\n <TextRoll\n duration={0.3}\n getEnterDelay={(i) => i * 0.03}\n getExitDelay={(i) => i * 0.03 + 0.12}\n key={title}\n >\n {title}\n </TextRoll>\n </span>\n {stepCount > 0 ? (\n <span className=\"text-xs tabular-nums text-sf-subtle/60\">\n {stepCount} {stepLabel}\n </span>\n ) : null}\n <CaretDownIcon className=\"size-3 shrink-0 text-sf-subtle/60\" />\n </div>\n {liveRows.length > 0 ? (\n <div className=\"ml-[0.3125rem] flex flex-col gap-0.5 border-l border-sf-line/40 pl-3\">\n {liveRows}\n </div>\n ) : null}\n </div>\n );\n}\n\n// ─── Frozen summary (titled) ─────────────────────────────────────────────────\n\nfunction TitledSummary({\n snapshot,\n defaultOpen,\n className,\n ...props\n}: {\n snapshot: FrozenSnapshot;\n defaultOpen: boolean;\n} & Omit<ComponentProps<\"div\">, \"title\">) {\n const [isOpen, setIsOpen] = useState(defaultOpen);\n const stepLabel = snapshot.steps.length === 1 ? \"step\" : \"steps\";\n\n return (\n <BaseCollapsible.Root open={isOpen} onOpenChange={setIsOpen}>\n <div\n className={cn(\"flex flex-col\", className)}\n data-streaming=\"false\"\n {...props}\n >\n <BaseCollapsible.Trigger\n render={\n <button\n type=\"button\"\n className={cn(\n \"flex w-fit items-center gap-1.5 rounded px-1 py-0.5 text-left\",\n \"transition-colors hover:bg-sf-tint\"\n )}\n />\n }\n >\n <span\n aria-hidden\n className={cn(\n \"size-1.5 shrink-0 rounded-full bg-current\",\n snapshot.errorCount > 0 ? \"text-sf-danger\" : \"text-sf-success\"\n )}\n />\n <span className=\"min-w-0 truncate text-sm text-sf-subtle\">\n {snapshot.title}\n </span>\n <span className=\"text-xs tabular-nums text-sf-subtle/60\">\n {snapshot.steps.length} {stepLabel}\n {snapshot.errorCount > 0 ? ` · ${snapshot.errorCount} error` : \"\"}\n </span>\n <CaretDownIcon\n className={cn(\n \"size-3 shrink-0 text-sf-subtle/60 transition-transform duration-200\",\n !isOpen && \"-rotate-90\"\n )}\n />\n </BaseCollapsible.Trigger>\n\n <BaseCollapsible.Panel className=\"overflow-hidden\">\n <ul className=\"mt-1 ml-[0.3125rem] flex flex-col gap-0.5 border-l border-sf-line/40 pl-3\">\n {snapshot.steps.map((step, i) => (\n <li\n className=\"flex items-center gap-1.5 text-xs text-sf-subtle\"\n key={`${step.kind}-${i}-${step.name}`}\n >\n <span\n aria-hidden\n className={cn(\n \"size-1.5 shrink-0 rounded-full bg-current\",\n step.ok ? \"text-sf-success\" : \"text-sf-danger\"\n )}\n />\n <span className=\"min-w-0 truncate\">{step.name}</span>\n {step.duration ? (\n <span className=\"tabular-nums text-sf-subtle/60\">\n {step.duration}ms\n </span>\n ) : null}\n </li>\n ))}\n </ul>\n </BaseCollapsible.Panel>\n </div>\n </BaseCollapsible.Root>\n );\n}\n\nfunction deriveLiveTitle(elements: ReactElement[]): string {\n for (const el of elements as ChildLike[]) {\n const part = el.props.part as\n | { text?: string; summary?: string }\n | undefined;\n const text = part?.summary ?? part?.text;\n if (text) return truncate(text, 60);\n }\n return \"Working…\";\n}\n\n// ─── Snapshot derivation ─────────────────────────────────────────────────────\n\ntype ChildLike = ReactElement<{\n part?: AiToolPart | { text?: string; summary?: string; name?: string };\n duration?: number;\n}>;\n\nfunction deriveSnapshot(\n children: ReactNode,\n explicitTitle: string | undefined\n): FrozenSnapshot {\n // oxlint-disable-next-line no-react-children\n const elements = Children.toArray(children).filter(\n isValidElement\n ) as ChildLike[];\n\n const steps: FrozenStep[] = [];\n let errorCount = 0;\n let totalDuration = 0;\n let firstReasoningSnippet: string | undefined;\n\n for (const el of elements) {\n const kind = inferKind(el);\n const part = el.props.part as\n | (AiToolPart & { text?: string; summary?: string; name?: string })\n | undefined;\n const duration = el.props.duration;\n if (typeof duration === \"number\") totalDuration += duration;\n\n if (kind === \"tool\") {\n const toolPart = part as AiToolPart | undefined;\n const ok = toolPart ? !hasToolError(toolPart) : true;\n const settled = toolPart ? hasToolResult(toolPart) : true;\n if (!ok) errorCount += 1;\n steps.push({\n kind: \"tool\",\n name: toolPart?.toolName ?? \"tool\",\n ok: ok && settled,\n duration,\n });\n } else {\n const text =\n (part?.summary as string | undefined) ??\n (part?.text as string | undefined);\n if (!firstReasoningSnippet && text) {\n firstReasoningSnippet = text;\n }\n steps.push({\n kind: \"reasoning\",\n name: text ? truncate(text, 60) : \"Thinking\",\n ok: true,\n duration,\n });\n }\n }\n\n const title =\n explicitTitle?.trim() ||\n (firstReasoningSnippet && truncate(firstReasoningSnippet, 60)) ||\n `${steps.length} ${steps.length === 1 ? \"step\" : \"steps\"}`;\n\n return { title, steps, errorCount, totalDuration };\n}\n\nfunction inferKind(el: ChildLike): \"tool\" | \"reasoning\" {\n // Best-effort: check common indicators. Tool parts have `toolName`/`toolCallId`.\n // Reasoning parts have `text`.\n const part = el.props.part as Record<string, unknown> | undefined;\n if (part && typeof part === \"object\") {\n if (\"toolName\" in part || \"toolCallId\" in part) return \"tool\";\n if (\"text\" in part || \"summary\" in part) return \"reasoning\";\n }\n // Fallback: inspect displayName.\n const displayName =\n (el.type as { displayName?: string; name?: string } | undefined)\n ?.displayName ??\n (el.type as { displayName?: string; name?: string } | undefined)?.name ??\n \"\";\n if (displayName.includes(\"Tool\")) return \"tool\";\n return \"reasoning\";\n}\n\nfunction truncate(s: string, n: number): string {\n if (s.length <= n) return s;\n return `${s.slice(0, n - 1).trimEnd()}…`;\n}\n\n// ─── groupParts ──────────────────────────────────────────────────────\n\n/**\n * A message part that is either narrative text or an \"activity\" (reasoning or\n * tool call). Consumers pass parts shaped like this — the helper does not read\n * any field other than `type`.\n */\nexport type GroupablePart =\n | { type: \"text\"; [key: string]: unknown }\n | { type: \"reasoning\"; [key: string]: unknown }\n | { type: \"tool-call\" | \"tool-result\" | string; [key: string]: unknown };\n\n/** Emitted by {@link groupParts}. */\nexport type PartChunk<TPart = GroupablePart> =\n | { kind: \"text\"; parts: TPart[] }\n | { kind: \"activity\"; parts: TPart[] };\n\n/**\n * Split a message's parts into alternating `text` and `activity` chunks so each\n * contiguous run of reasoning/tool parts can be rendered inside its own\n * {@link AiPartGroup}, with narrative text rendered between them.\n *\n * @example\n * ```tsx\n * {groupParts(message.parts).map((chunk, i) =>\n * chunk.kind === \"text\" ? (\n * <AiMessageContent key={i}>{joinText(chunk.parts)}</AiMessageContent>\n * ) : (\n * <AiPartGroup\n * key={i}\n * groupKey={`${message.id}:activity:${i}`}\n * complete={!message.isStreaming}\n * >\n * {chunk.parts.map(renderActivityPart)}\n * </AiPartGroup>\n * )\n * )}\n * ```\n */\nexport function groupParts<TPart extends { type: string }>(\n parts: readonly TPart[],\n options: {\n /** Which `type` values count as \"activity\". Defaults to reasoning + tool parts. */\n isActivity?: (part: TPart) => boolean;\n } = {}\n): PartChunk<TPart>[] {\n const isActivity =\n options.isActivity ??\n ((p: TPart) => {\n const t = p.type;\n return (\n t === \"reasoning\" ||\n t === \"tool-call\" ||\n t === \"tool-result\" ||\n t.startsWith(\"tool-\")\n );\n });\n\n const chunks: PartChunk<TPart>[] = [];\n for (const part of parts) {\n const kind: PartChunk<TPart>[\"kind\"] = isActivity(part)\n ? \"activity\"\n : \"text\";\n const last = chunks.at(-1);\n if (last && last.kind === kind) {\n last.parts.push(part);\n } else {\n chunks.push({ kind, parts: [part] });\n }\n }\n return chunks;\n}\n"],"mappings":"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"}
|