@womp/kookie-ui 0.4.0 → 0.6.2
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/components.css +783 -783
- package/dist/cjs/components/_internal/base-menu.utils.d.ts +48 -0
- package/dist/cjs/components/_internal/base-menu.utils.d.ts.map +1 -0
- package/dist/cjs/components/_internal/base-menu.utils.js +2 -0
- package/dist/cjs/components/_internal/base-menu.utils.js.map +7 -0
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.d.ts +77 -59
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.d.ts.map +1 -1
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.js +1 -1
- package/dist/cjs/components/_internal/dropdown-menu-drill-down.js.map +4 -4
- package/dist/cjs/components/_internal/menu-context.d.ts +7 -3
- package/dist/cjs/components/_internal/menu-context.d.ts.map +1 -1
- package/dist/cjs/components/_internal/menu-context.js +1 -1
- package/dist/cjs/components/_internal/menu-context.js.map +4 -4
- package/dist/cjs/components/_internal/shell-bottom.js +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js.map +2 -2
- package/dist/cjs/components/_internal/shell-inspector.js +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js.map +2 -2
- package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js.map +2 -2
- package/dist/cjs/components/accordion.js +1 -1
- package/dist/cjs/components/alert-dialog.js +1 -1
- package/dist/cjs/components/alert-dialog.js.map +2 -2
- package/dist/cjs/components/badge.js +1 -1
- package/dist/cjs/components/badge.js.map +2 -2
- package/dist/cjs/components/blockquote.js +1 -1
- package/dist/cjs/components/button.js +1 -1
- package/dist/cjs/components/button.js.map +2 -2
- package/dist/cjs/components/callout.js +1 -1
- package/dist/cjs/components/callout.js.map +2 -2
- package/dist/cjs/components/card.js +1 -1
- package/dist/cjs/components/card.js.map +2 -2
- package/dist/cjs/components/chatbar.d.ts +46 -44
- package/dist/cjs/components/chatbar.d.ts.map +1 -1
- package/dist/cjs/components/chatbar.js +2 -2
- package/dist/cjs/components/chatbar.js.map +3 -3
- package/dist/cjs/components/checkbox-cards.js +1 -1
- package/dist/cjs/components/checkbox-cards.js.map +2 -2
- package/dist/cjs/components/checkbox-group.js +1 -1
- package/dist/cjs/components/checkbox.js +1 -1
- package/dist/cjs/components/combobox.d.ts +28 -15
- package/dist/cjs/components/combobox.d.ts.map +1 -1
- package/dist/cjs/components/combobox.js +1 -1
- package/dist/cjs/components/combobox.js.map +4 -4
- package/dist/cjs/components/combobox.props.d.ts +48 -0
- package/dist/cjs/components/combobox.props.d.ts.map +1 -1
- package/dist/cjs/components/combobox.props.js +1 -1
- package/dist/cjs/components/combobox.props.js.map +4 -4
- package/dist/cjs/components/context-menu.d.ts +18 -5
- package/dist/cjs/components/context-menu.d.ts.map +1 -1
- package/dist/cjs/components/context-menu.js +1 -1
- package/dist/cjs/components/context-menu.js.map +4 -4
- package/dist/cjs/components/context-menu.props.d.ts +1 -1
- package/dist/cjs/components/context-menu.props.d.ts.map +1 -1
- package/dist/cjs/components/context-menu.props.js +1 -1
- package/dist/cjs/components/context-menu.props.js.map +3 -3
- package/dist/cjs/components/dialog.js +1 -1
- package/dist/cjs/components/dialog.js.map +2 -2
- package/dist/cjs/components/dropdown-menu.d.ts +11 -18
- package/dist/cjs/components/dropdown-menu.d.ts.map +1 -1
- package/dist/cjs/components/dropdown-menu.js +1 -1
- package/dist/cjs/components/dropdown-menu.js.map +4 -4
- package/dist/cjs/components/heading.js +1 -1
- package/dist/cjs/components/hover-card.js +1 -1
- package/dist/cjs/components/hover-card.js.map +2 -2
- package/dist/cjs/components/icon-button.js +1 -1
- package/dist/cjs/components/icon-button.js.map +2 -2
- package/dist/cjs/components/index.js +5 -5
- package/dist/cjs/components/index.js.map +4 -4
- package/dist/cjs/components/link.js +1 -1
- package/dist/cjs/components/popover.js +1 -1
- package/dist/cjs/components/popover.js.map +2 -2
- package/dist/cjs/components/radio-cards.js +1 -1
- package/dist/cjs/components/radio-cards.js.map +2 -2
- package/dist/cjs/components/radio.js +1 -1
- package/dist/cjs/components/radio.js.map +2 -2
- package/dist/cjs/components/schemas/base-button.schema.d.ts +111 -256
- package/dist/cjs/components/schemas/base-button.schema.d.ts.map +1 -1
- package/dist/cjs/components/schemas/button.schema.d.ts +136 -557
- package/dist/cjs/components/schemas/button.schema.d.ts.map +1 -1
- package/dist/cjs/components/schemas/icon-button.schema.d.ts +123 -240
- package/dist/cjs/components/schemas/icon-button.schema.d.ts.map +1 -1
- package/dist/cjs/components/schemas/index.js +1 -1
- package/dist/cjs/components/schemas/index.js.map +2 -2
- package/dist/cjs/components/schemas/shell.schema.d.ts +592 -851
- package/dist/cjs/components/schemas/shell.schema.d.ts.map +1 -1
- package/dist/cjs/components/schemas/shell.schema.js +1 -1
- package/dist/cjs/components/schemas/shell.schema.js.map +2 -2
- package/dist/cjs/components/schemas/toggle-button.schema.d.ts +138 -1026
- package/dist/cjs/components/schemas/toggle-button.schema.d.ts.map +1 -1
- package/dist/cjs/components/schemas/toggle-icon-button.schema.d.ts +124 -465
- package/dist/cjs/components/schemas/toggle-icon-button.schema.d.ts.map +1 -1
- package/dist/cjs/components/scroll-area.js +1 -1
- package/dist/cjs/components/segmented-control.js +1 -1
- package/dist/cjs/components/select.js +1 -1
- package/dist/cjs/components/select.js.map +2 -2
- package/dist/cjs/components/sheet.js +1 -1
- package/dist/cjs/components/sheet.js.map +2 -2
- package/dist/cjs/components/shell.js +1 -1
- package/dist/cjs/components/shell.js.map +3 -3
- package/dist/cjs/components/sidebar.js +1 -1
- package/dist/cjs/components/sidebar.js.map +4 -4
- package/dist/cjs/components/slider.js +1 -1
- package/dist/cjs/components/slot.d.ts +6 -2
- package/dist/cjs/components/slot.d.ts.map +1 -1
- package/dist/cjs/components/switch.js +1 -1
- package/dist/cjs/components/tab-nav.js +1 -1
- package/dist/cjs/components/text-field.js +2 -2
- package/dist/cjs/components/text.js +1 -1
- package/dist/cjs/components/theme-panel.js +3 -3
- package/dist/cjs/components/theme-panel.js.map +2 -2
- package/dist/cjs/components/theme.js +1 -1
- package/dist/cjs/components/theme.js.map +2 -2
- package/dist/cjs/components/theme.props.d.ts +1 -1
- package/dist/cjs/components/theme.props.js +1 -1
- package/dist/cjs/components/theme.props.js.map +2 -2
- package/dist/cjs/components/toggle-button.js +1 -1
- package/dist/cjs/components/toggle-button.js.map +2 -2
- package/dist/cjs/components/toggle-icon-button.js +1 -1
- package/dist/cjs/components/toggle-icon-button.js.map +2 -2
- package/dist/cjs/components/tooltip.js +1 -1
- package/dist/cjs/components/tooltip.js.map +2 -2
- package/dist/cjs/components/user-card.js +1 -1
- package/dist/cjs/components/user-card.js.map +2 -2
- package/dist/cjs/components/virtual-menu.d.ts.map +1 -1
- package/dist/cjs/components/virtual-menu.js +1 -1
- package/dist/cjs/components/virtual-menu.js.map +4 -4
- package/dist/cjs/helpers/extract-props.js +1 -1
- package/dist/cjs/helpers/extract-props.js.map +2 -2
- package/dist/cjs/helpers/get-margin-styles.js +1 -1
- package/dist/cjs/helpers/get-margin-styles.js.map +2 -2
- package/dist/cjs/hooks/use-breakpoint.d.ts +4 -1
- package/dist/cjs/hooks/use-breakpoint.d.ts.map +1 -1
- package/dist/cjs/hooks/use-breakpoint.js +1 -1
- package/dist/cjs/hooks/use-breakpoint.js.map +3 -3
- package/dist/cjs/index.js +5 -5
- package/dist/cjs/index.js.map +4 -4
- package/dist/cjs/props/index.js +1 -1
- package/dist/cjs/props/index.js.map +3 -3
- package/dist/esm/{chunk-P7HYVJSU.js → chunk-2TIADHPD.js} +2 -2
- package/dist/esm/chunk-37OGSEQR.js +2 -0
- package/dist/esm/chunk-37OGSEQR.js.map +7 -0
- package/dist/esm/{chunk-RMKXWYOV.js → chunk-3OBXZUFJ.js} +2 -2
- package/dist/esm/{chunk-S2HA3W3V.js → chunk-3R7XMRV5.js} +2 -2
- package/dist/esm/chunk-4SMJ7D4S.js +2 -0
- package/dist/esm/chunk-4SMJ7D4S.js.map +7 -0
- package/dist/esm/{chunk-S56YSRHP.js → chunk-56TS6OKQ.js} +2 -2
- package/dist/esm/{chunk-2FYJTSOP.js → chunk-5BYHEYO6.js} +2 -2
- package/dist/esm/{chunk-B24BDVG2.js → chunk-5MNBQROW.js} +2 -2
- package/dist/esm/{chunk-W7N7V5YD.js → chunk-7ESHXHHO.js} +2 -2
- package/dist/esm/{chunk-CRWYCLTE.js → chunk-ABSKRHVM.js} +2 -2
- package/dist/esm/chunk-ADBLURLJ.js +2 -0
- package/dist/esm/chunk-ADBLURLJ.js.map +7 -0
- package/dist/esm/{chunk-3MWWP4MW.js → chunk-BBJUB52V.js} +2 -2
- package/dist/esm/{chunk-PTLFMWCN.js → chunk-CQNPBTM5.js} +2 -2
- package/dist/esm/{chunk-GJSGKE7B.js → chunk-CTA5ODAO.js} +2 -2
- package/dist/esm/{chunk-GWXVETMF.js → chunk-DCAZPMOB.js} +2 -2
- package/dist/esm/{chunk-PLWXTEVE.js → chunk-DMJJM5IS.js} +2 -2
- package/dist/esm/{chunk-A2RZGGMY.js → chunk-DQAUX5ZL.js} +2 -2
- package/dist/esm/{chunk-6XGZPIUT.js → chunk-DX5JJYMZ.js} +2 -2
- package/dist/esm/{chunk-6XGZPIUT.js.map → chunk-DX5JJYMZ.js.map} +2 -2
- package/dist/esm/{chunk-YUP6EICB.js → chunk-DXL6V2A5.js} +2 -2
- package/dist/esm/{chunk-XMRIRFM7.js → chunk-FG7XM7KJ.js} +2 -2
- package/dist/esm/{chunk-IKSN4RNB.js → chunk-FIB3XATL.js} +3 -3
- package/dist/esm/chunk-FMBECDL4.js +2 -0
- package/dist/esm/{chunk-4XJY6FIK.js.map → chunk-FMBECDL4.js.map} +3 -3
- package/dist/esm/{chunk-374ZYJUP.js → chunk-HBR42MIX.js} +2 -2
- package/dist/esm/{chunk-2UPAYDKR.js → chunk-HIKVMKYQ.js} +2 -2
- package/dist/esm/{chunk-VX6ARMWP.js → chunk-HSAG7C53.js} +2 -2
- package/dist/esm/{chunk-OSRJRBKP.js → chunk-HW324FK3.js} +2 -2
- package/dist/esm/{chunk-DNHUQLXO.js → chunk-JBO5YOUM.js} +2 -2
- package/dist/esm/{chunk-HF5WY5YE.js → chunk-JGLAE5G7.js} +2 -2
- package/dist/esm/{chunk-4YFJWXL2.js → chunk-JJKQ7CGI.js} +2 -2
- package/dist/esm/chunk-JSW6J6RS.js +2 -0
- package/dist/esm/chunk-JSW6J6RS.js.map +7 -0
- package/dist/esm/chunk-KRDP5SQ7.js +2 -0
- package/dist/esm/chunk-KRDP5SQ7.js.map +7 -0
- package/dist/esm/{chunk-CWRNW44F.js → chunk-L6PVH3RM.js} +2 -2
- package/dist/esm/{chunk-MK2R6DAU.js → chunk-LAOMR7SE.js} +2 -2
- package/dist/esm/{chunk-FHOCUZDR.js → chunk-NIQT45PJ.js} +2 -2
- package/dist/esm/chunk-NKEJTTIL.js +2 -0
- package/dist/esm/chunk-NKEJTTIL.js.map +7 -0
- package/dist/esm/chunk-NVDXXGVW.js +2 -0
- package/dist/esm/chunk-NVDXXGVW.js.map +7 -0
- package/dist/esm/chunk-OS3FCYZY.js +2 -0
- package/dist/esm/{chunk-7QDTSBJF.js → chunk-OYF6YXH7.js} +2 -2
- package/dist/esm/{chunk-WFMYNZXZ.js → chunk-P5OSU4NX.js} +2 -2
- package/dist/esm/{chunk-QEOAXJLN.js → chunk-PTS6QYQW.js} +2 -2
- package/dist/esm/{chunk-CVWCKOHO.js → chunk-RLPKKCBL.js} +2 -2
- package/dist/esm/{chunk-LKAK5GXF.js → chunk-SPIU5JYC.js} +2 -2
- package/dist/esm/chunk-SZTZ2XLI.js +2 -0
- package/dist/esm/chunk-SZTZ2XLI.js.map +7 -0
- package/dist/esm/chunk-T2FZN7G3.js +2 -0
- package/dist/esm/chunk-T2FZN7G3.js.map +7 -0
- package/dist/esm/{chunk-4AQTUBVP.js → chunk-T4UHUQFJ.js} +2 -2
- package/dist/esm/{chunk-K6QZMNCD.js → chunk-TS623UPD.js} +2 -2
- package/dist/esm/{chunk-YV2KYHGR.js → chunk-UBOOUDS4.js} +2 -2
- package/dist/esm/{chunk-D6B7IMZU.js → chunk-UFI53QUC.js} +2 -2
- package/dist/esm/{chunk-AU3D6F42.js → chunk-UIRSGVJG.js} +2 -2
- package/dist/esm/{chunk-FXUUI6Z2.js → chunk-UUUZXUIP.js} +2 -2
- package/dist/esm/{chunk-25WZA7ZU.js → chunk-VA4CTVQL.js} +2 -2
- package/dist/esm/{chunk-TOLLPRMM.js → chunk-VNPNEJI6.js} +2 -2
- package/dist/esm/{chunk-6IGTK4JE.js → chunk-YMUGKYGW.js} +2 -2
- package/dist/esm/chunk-ZWYYPFZW.js +2 -0
- package/dist/esm/chunk-ZWYYPFZW.js.map +7 -0
- package/dist/esm/{chunk-M24UXMOQ.js → chunk-ZXZOH63L.js} +2 -2
- package/dist/esm/components/_internal/base-button.js +1 -1
- package/dist/esm/components/_internal/base-button.props.js +1 -1
- package/dist/esm/components/_internal/base-checkbox.props.js +1 -1
- package/dist/esm/components/_internal/base-menu.props.js +1 -1
- package/dist/esm/components/_internal/base-menu.utils.d.ts +48 -0
- package/dist/esm/components/_internal/base-menu.utils.d.ts.map +1 -0
- package/dist/esm/components/_internal/base-menu.utils.js +2 -0
- package/dist/esm/components/_internal/base-menu.utils.js.map +7 -0
- package/dist/esm/components/_internal/base-radio.props.js +1 -1
- package/dist/esm/components/_internal/base-tab-list.props.js +1 -1
- package/dist/esm/components/_internal/dropdown-menu-drill-down.d.ts +77 -59
- package/dist/esm/components/_internal/dropdown-menu-drill-down.d.ts.map +1 -1
- package/dist/esm/components/_internal/dropdown-menu-drill-down.js +1 -1
- package/dist/esm/components/_internal/menu-context.d.ts +7 -3
- package/dist/esm/components/_internal/menu-context.d.ts.map +1 -1
- package/dist/esm/components/_internal/menu-context.js +1 -1
- package/dist/esm/components/_internal/shell-bottom.js +1 -1
- package/dist/esm/components/_internal/shell-handles.js +1 -1
- package/dist/esm/components/_internal/shell-inspector.js +1 -1
- package/dist/esm/components/_internal/shell-prop-helpers.js +1 -1
- package/dist/esm/components/_internal/shell-resize.js +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js +1 -1
- package/dist/esm/components/accessible-icon.js +1 -1
- package/dist/esm/components/accordion.js +1 -1
- package/dist/esm/components/accordion.props.js +1 -1
- package/dist/esm/components/alert-dialog.js +1 -1
- package/dist/esm/components/alert-dialog.props.js +1 -1
- package/dist/esm/components/aspect-ratio.js +1 -1
- package/dist/esm/components/avatar.js +1 -1
- package/dist/esm/components/avatar.props.js +1 -1
- package/dist/esm/components/badge.js +1 -1
- package/dist/esm/components/badge.props.js +1 -1
- package/dist/esm/components/blockquote.js +1 -1
- package/dist/esm/components/blockquote.props.js +1 -1
- package/dist/esm/components/box.js +1 -1
- package/dist/esm/components/box.props.js +1 -1
- package/dist/esm/components/button.js +1 -1
- package/dist/esm/components/button.props.js +1 -1
- package/dist/esm/components/callout.js +1 -1
- package/dist/esm/components/callout.props.js +1 -1
- package/dist/esm/components/card.js +1 -1
- package/dist/esm/components/card.props.js +1 -1
- package/dist/esm/components/chatbar.d.ts +46 -44
- package/dist/esm/components/chatbar.d.ts.map +1 -1
- package/dist/esm/components/chatbar.js +1 -1
- package/dist/esm/components/checkbox-cards.js +1 -1
- package/dist/esm/components/checkbox-cards.props.js +1 -1
- package/dist/esm/components/checkbox-group.js +1 -1
- package/dist/esm/components/checkbox-group.primitive.js +1 -1
- package/dist/esm/components/checkbox-group.props.js +1 -1
- package/dist/esm/components/checkbox.js +1 -1
- package/dist/esm/components/checkbox.props.js +1 -1
- package/dist/esm/components/code.js +1 -1
- package/dist/esm/components/code.props.js +1 -1
- package/dist/esm/components/combobox.d.ts +28 -15
- package/dist/esm/components/combobox.d.ts.map +1 -1
- package/dist/esm/components/combobox.js +1 -1
- package/dist/esm/components/combobox.props.d.ts +48 -0
- package/dist/esm/components/combobox.props.d.ts.map +1 -1
- package/dist/esm/components/combobox.props.js +1 -1
- package/dist/esm/components/container.js +1 -1
- package/dist/esm/components/container.props.js +1 -1
- package/dist/esm/components/context-menu.d.ts +18 -5
- package/dist/esm/components/context-menu.d.ts.map +1 -1
- package/dist/esm/components/context-menu.js +1 -1
- package/dist/esm/components/context-menu.props.d.ts +1 -1
- package/dist/esm/components/context-menu.props.d.ts.map +1 -1
- package/dist/esm/components/context-menu.props.js +1 -1
- package/dist/esm/components/data-list.js +1 -1
- package/dist/esm/components/data-list.props.js +1 -1
- package/dist/esm/components/dialog.js +1 -1
- package/dist/esm/components/dialog.props.js +1 -1
- package/dist/esm/components/dropdown-menu.d.ts +11 -18
- package/dist/esm/components/dropdown-menu.d.ts.map +1 -1
- package/dist/esm/components/dropdown-menu.js +1 -1
- package/dist/esm/components/dropdown-menu.props.js +1 -1
- package/dist/esm/components/em.js +1 -1
- package/dist/esm/components/em.props.js +1 -1
- package/dist/esm/components/flex.js +1 -1
- package/dist/esm/components/flex.props.js +1 -1
- package/dist/esm/components/grid.js +1 -1
- package/dist/esm/components/grid.props.js +1 -1
- package/dist/esm/components/heading.js +1 -1
- package/dist/esm/components/heading.props.js +1 -1
- package/dist/esm/components/hover-card.js +1 -1
- package/dist/esm/components/hover-card.props.js +1 -1
- package/dist/esm/components/icon-button.js +1 -1
- package/dist/esm/components/icon-button.props.js +1 -1
- package/dist/esm/components/icons.js +1 -1
- package/dist/esm/components/image.js +1 -1
- package/dist/esm/components/image.props.js +1 -1
- package/dist/esm/components/index.js +1 -1
- package/dist/esm/components/inset.js +1 -1
- package/dist/esm/components/inset.props.js +1 -1
- package/dist/esm/components/kbd.js +1 -1
- package/dist/esm/components/kbd.props.js +1 -1
- package/dist/esm/components/link.js +1 -1
- package/dist/esm/components/link.props.js +1 -1
- package/dist/esm/components/navbar.js +1 -1
- package/dist/esm/components/navbar.props.js +1 -1
- package/dist/esm/components/popover.js +1 -1
- package/dist/esm/components/popover.props.js +1 -1
- package/dist/esm/components/portal.js +1 -1
- package/dist/esm/components/progress.js +1 -1
- package/dist/esm/components/progress.props.js +1 -1
- package/dist/esm/components/quote.js +1 -1
- package/dist/esm/components/quote.props.js +1 -1
- package/dist/esm/components/radio-cards.js +1 -1
- package/dist/esm/components/radio-cards.props.js +1 -1
- package/dist/esm/components/radio-group.js +1 -1
- package/dist/esm/components/radio-group.props.js +1 -1
- package/dist/esm/components/radio.js +1 -1
- package/dist/esm/components/radio.props.js +1 -1
- package/dist/esm/components/reset.js +1 -1
- package/dist/esm/components/schemas/base-button.schema.d.ts +111 -256
- package/dist/esm/components/schemas/base-button.schema.d.ts.map +1 -1
- package/dist/esm/components/schemas/base-button.schema.js +1 -1
- package/dist/esm/components/schemas/button.schema.d.ts +136 -557
- package/dist/esm/components/schemas/button.schema.d.ts.map +1 -1
- package/dist/esm/components/schemas/button.schema.js +1 -1
- package/dist/esm/components/schemas/icon-button.schema.d.ts +123 -240
- package/dist/esm/components/schemas/icon-button.schema.d.ts.map +1 -1
- package/dist/esm/components/schemas/icon-button.schema.js +1 -1
- package/dist/esm/components/schemas/index.js +1 -1
- package/dist/esm/components/schemas/shell.schema.d.ts +592 -851
- package/dist/esm/components/schemas/shell.schema.d.ts.map +1 -1
- package/dist/esm/components/schemas/shell.schema.js +1 -1
- package/dist/esm/components/schemas/toggle-button.schema.d.ts +138 -1026
- package/dist/esm/components/schemas/toggle-button.schema.d.ts.map +1 -1
- package/dist/esm/components/schemas/toggle-button.schema.js +1 -1
- package/dist/esm/components/schemas/toggle-icon-button.schema.d.ts +124 -465
- package/dist/esm/components/schemas/toggle-icon-button.schema.d.ts.map +1 -1
- package/dist/esm/components/schemas/toggle-icon-button.schema.js +1 -1
- package/dist/esm/components/scroll-area.js +1 -1
- package/dist/esm/components/scroll-area.props.js +1 -1
- package/dist/esm/components/section.js +1 -1
- package/dist/esm/components/section.props.js +1 -1
- package/dist/esm/components/segmented-control.js +1 -1
- package/dist/esm/components/segmented-control.props.js +1 -1
- package/dist/esm/components/select.js +1 -1
- package/dist/esm/components/select.props.js +1 -1
- package/dist/esm/components/separator.js +1 -1
- package/dist/esm/components/separator.props.js +1 -1
- package/dist/esm/components/sheet.js +1 -1
- package/dist/esm/components/shell.context.js +1 -1
- package/dist/esm/components/shell.hooks.js +1 -1
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/components/shell.types.js +1 -1
- package/dist/esm/components/sidebar.js +1 -1
- package/dist/esm/components/sidebar.props.js +1 -1
- package/dist/esm/components/skeleton.js +1 -1
- package/dist/esm/components/skeleton.props.js +1 -1
- package/dist/esm/components/slider.js +1 -1
- package/dist/esm/components/slider.props.js +1 -1
- package/dist/esm/components/slot.d.ts +6 -2
- package/dist/esm/components/slot.d.ts.map +1 -1
- package/dist/esm/components/slot.js +1 -1
- package/dist/esm/components/spinner.js +1 -1
- package/dist/esm/components/spinner.props.js +1 -1
- package/dist/esm/components/strong.js +1 -1
- package/dist/esm/components/strong.props.js +1 -1
- package/dist/esm/components/switch.js +1 -1
- package/dist/esm/components/switch.props.js +1 -1
- package/dist/esm/components/tab-nav.js +1 -1
- package/dist/esm/components/tab-nav.props.js +1 -1
- package/dist/esm/components/table.js +1 -1
- package/dist/esm/components/table.props.js +1 -1
- package/dist/esm/components/tabs.js +1 -1
- package/dist/esm/components/tabs.props.js +1 -1
- package/dist/esm/components/text-area.js +1 -1
- package/dist/esm/components/text-area.props.js +1 -1
- package/dist/esm/components/text-field.js +1 -1
- package/dist/esm/components/text-field.props.js +1 -1
- package/dist/esm/components/text.js +1 -1
- package/dist/esm/components/text.props.js +1 -1
- package/dist/esm/components/theme-panel.js +1 -1
- package/dist/esm/components/theme.js +1 -1
- package/dist/esm/components/theme.props.d.ts +1 -1
- package/dist/esm/components/theme.props.js +1 -1
- package/dist/esm/components/toggle-button.js +1 -1
- package/dist/esm/components/toggle-icon-button.js +1 -1
- package/dist/esm/components/toolbar.js +1 -1
- package/dist/esm/components/tooltip.js +1 -1
- package/dist/esm/components/tooltip.props.js +1 -1
- package/dist/esm/components/user-card.js +1 -1
- package/dist/esm/components/user-card.props.js +1 -1
- package/dist/esm/components/virtual-menu.d.ts.map +1 -1
- package/dist/esm/components/virtual-menu.js +1 -1
- package/dist/esm/components/visually-hidden.js +1 -1
- package/dist/esm/helpers/compile-prop-defs.js +1 -1
- package/dist/esm/helpers/extract-margin-props.js +1 -1
- package/dist/esm/helpers/extract-props.js +1 -1
- package/dist/esm/helpers/font-config.js +1 -1
- package/dist/esm/helpers/get-margin-styles.js +1 -1
- package/dist/esm/helpers/get-matching-gray-color.js +1 -1
- package/dist/esm/helpers/get-responsive-styles.js +1 -1
- package/dist/esm/helpers/get-subtree.js +1 -1
- package/dist/esm/helpers/has-own-property.js +1 -1
- package/dist/esm/helpers/index.js +1 -1
- package/dist/esm/helpers/inert.js +1 -1
- package/dist/esm/helpers/is-responsive-object.js +1 -1
- package/dist/esm/helpers/map-prop-values.js +1 -1
- package/dist/esm/helpers/merge-styles.js +1 -1
- package/dist/esm/helpers/normalize-to-px.js +1 -1
- package/dist/esm/helpers/require-react-element.js +1 -1
- package/dist/esm/helpers/use-deprecation-warning.js +1 -1
- package/dist/esm/hooks/index.js +1 -1
- package/dist/esm/hooks/use-body-pointer-events-cleanup.js +1 -1
- package/dist/esm/hooks/use-breakpoint.d.ts +4 -1
- package/dist/esm/hooks/use-breakpoint.d.ts.map +1 -1
- package/dist/esm/hooks/use-breakpoint.js +1 -1
- package/dist/esm/hooks/use-live-announcer.js +1 -1
- package/dist/esm/hooks/use-prop-synced-state.js +1 -1
- package/dist/esm/hooks/use-toggle-state.js +1 -1
- package/dist/esm/hooks/use-tooltip-wrapper.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/props/as-child.prop.js +1 -1
- package/dist/esm/props/color.prop.js +1 -1
- package/dist/esm/props/font-family.prop.js +1 -1
- package/dist/esm/props/gap.props.js +1 -1
- package/dist/esm/props/height.props.js +1 -1
- package/dist/esm/props/high-contrast.prop.js +1 -1
- package/dist/esm/props/index.js +1 -1
- package/dist/esm/props/layout.props.js +1 -1
- package/dist/esm/props/leading-trim.prop.js +1 -1
- package/dist/esm/props/margin.props.js +1 -1
- package/dist/esm/props/padding.props.js +1 -1
- package/dist/esm/props/prop-def.js +1 -1
- package/dist/esm/props/radius.prop.js +1 -1
- package/dist/esm/props/shadow.props.js +1 -1
- package/dist/esm/props/text-align.prop.js +1 -1
- package/dist/esm/props/text-wrap.prop.js +1 -1
- package/dist/esm/props/truncate.prop.js +1 -1
- package/dist/esm/props/weight.prop.js +1 -1
- package/dist/esm/props/width.props.js +1 -1
- package/fonts.css +2 -0
- package/package.json +36 -36
- package/schemas/base-button.json +66 -60
- package/schemas/button.json +80 -74
- package/schemas/icon-button.json +78 -72
- package/schemas/index.json +390 -360
- package/schemas/toggle-button.json +84 -78
- package/schemas/toggle-icon-button.json +82 -76
- package/src/components/_internal/base-card.css +5 -5
- package/src/components/_internal/base-checkbox.css +17 -3
- package/src/components/_internal/base-dialog.css +4 -4
- package/src/components/_internal/base-menu.css +68 -143
- package/src/components/_internal/base-menu.utils.tsx +144 -0
- package/src/components/_internal/dropdown-menu-drill-down.tsx +281 -183
- package/src/components/_internal/menu-context.tsx +24 -15
- package/src/components/accordion.css +5 -5
- package/src/components/callout.css +5 -5
- package/src/components/chatbar.css +100 -32
- package/src/components/chatbar.tsx +739 -735
- package/src/components/checkbox-cards.css +30 -0
- package/src/components/combobox.css +62 -115
- package/src/components/combobox.props.tsx +14 -0
- package/src/components/combobox.tsx +440 -212
- package/src/components/context-menu.props.tsx +4 -1
- package/src/components/context-menu.tsx +108 -62
- package/src/components/dropdown-menu.css +41 -121
- package/src/components/dropdown-menu.tsx +430 -275
- package/src/components/hover-card.css +3 -3
- package/src/components/popover.css +4 -4
- package/src/components/radio-cards.css +30 -0
- package/src/components/schemas/shell.schema.ts +11 -18
- package/src/components/scroll-area.css +2 -3
- package/src/components/select.css +2 -2
- package/src/components/shell.css +1 -1
- package/src/components/table.css +5 -5
- package/src/components/theme.props.tsx +1 -1
- package/src/components/tooltip.css +1 -1
- package/src/components/virtual-menu.tsx +12 -8
- package/src/hooks/use-breakpoint.ts +19 -3
- package/src/styles/fonts.css +6 -1
- package/src/styles/tokens/font-family.css +13 -5
- package/src/styles/tokens/radius.css +47 -4
- package/styles.css +801 -784
- package/tokens/base.css +19 -2
- package/tokens.css +19 -2
- package/dist/esm/chunk-3XT5P2HO.js +0 -2
- package/dist/esm/chunk-4XJY6FIK.js +0 -2
- package/dist/esm/chunk-4Z33XHOK.js +0 -2
- package/dist/esm/chunk-4Z33XHOK.js.map +0 -7
- package/dist/esm/chunk-E2Z5IA2P.js +0 -2
- package/dist/esm/chunk-E2Z5IA2P.js.map +0 -7
- package/dist/esm/chunk-HVWEPVBH.js +0 -2
- package/dist/esm/chunk-HVWEPVBH.js.map +0 -7
- package/dist/esm/chunk-KKBP2CRH.js +0 -2
- package/dist/esm/chunk-KKBP2CRH.js.map +0 -7
- package/dist/esm/chunk-MOR3NSXQ.js +0 -2
- package/dist/esm/chunk-MOR3NSXQ.js.map +0 -7
- package/dist/esm/chunk-QCNUTT4R.js +0 -2
- package/dist/esm/chunk-QCNUTT4R.js.map +0 -7
- package/dist/esm/chunk-UGD7Z3L4.js +0 -2
- package/dist/esm/chunk-UGD7Z3L4.js.map +0 -7
- package/dist/esm/chunk-YEB3H7QN.js +0 -2
- package/dist/esm/chunk-YEB3H7QN.js.map +0 -7
- package/dist/esm/chunk-ZY6SQWHT.js +0 -2
- package/dist/esm/chunk-ZY6SQWHT.js.map +0 -7
- /package/dist/esm/{chunk-P7HYVJSU.js.map → chunk-2TIADHPD.js.map} +0 -0
- /package/dist/esm/{chunk-RMKXWYOV.js.map → chunk-3OBXZUFJ.js.map} +0 -0
- /package/dist/esm/{chunk-S2HA3W3V.js.map → chunk-3R7XMRV5.js.map} +0 -0
- /package/dist/esm/{chunk-S56YSRHP.js.map → chunk-56TS6OKQ.js.map} +0 -0
- /package/dist/esm/{chunk-2FYJTSOP.js.map → chunk-5BYHEYO6.js.map} +0 -0
- /package/dist/esm/{chunk-B24BDVG2.js.map → chunk-5MNBQROW.js.map} +0 -0
- /package/dist/esm/{chunk-W7N7V5YD.js.map → chunk-7ESHXHHO.js.map} +0 -0
- /package/dist/esm/{chunk-CRWYCLTE.js.map → chunk-ABSKRHVM.js.map} +0 -0
- /package/dist/esm/{chunk-3MWWP4MW.js.map → chunk-BBJUB52V.js.map} +0 -0
- /package/dist/esm/{chunk-PTLFMWCN.js.map → chunk-CQNPBTM5.js.map} +0 -0
- /package/dist/esm/{chunk-GJSGKE7B.js.map → chunk-CTA5ODAO.js.map} +0 -0
- /package/dist/esm/{chunk-GWXVETMF.js.map → chunk-DCAZPMOB.js.map} +0 -0
- /package/dist/esm/{chunk-PLWXTEVE.js.map → chunk-DMJJM5IS.js.map} +0 -0
- /package/dist/esm/{chunk-A2RZGGMY.js.map → chunk-DQAUX5ZL.js.map} +0 -0
- /package/dist/esm/{chunk-YUP6EICB.js.map → chunk-DXL6V2A5.js.map} +0 -0
- /package/dist/esm/{chunk-XMRIRFM7.js.map → chunk-FG7XM7KJ.js.map} +0 -0
- /package/dist/esm/{chunk-IKSN4RNB.js.map → chunk-FIB3XATL.js.map} +0 -0
- /package/dist/esm/{chunk-374ZYJUP.js.map → chunk-HBR42MIX.js.map} +0 -0
- /package/dist/esm/{chunk-2UPAYDKR.js.map → chunk-HIKVMKYQ.js.map} +0 -0
- /package/dist/esm/{chunk-VX6ARMWP.js.map → chunk-HSAG7C53.js.map} +0 -0
- /package/dist/esm/{chunk-OSRJRBKP.js.map → chunk-HW324FK3.js.map} +0 -0
- /package/dist/esm/{chunk-DNHUQLXO.js.map → chunk-JBO5YOUM.js.map} +0 -0
- /package/dist/esm/{chunk-HF5WY5YE.js.map → chunk-JGLAE5G7.js.map} +0 -0
- /package/dist/esm/{chunk-4YFJWXL2.js.map → chunk-JJKQ7CGI.js.map} +0 -0
- /package/dist/esm/{chunk-CWRNW44F.js.map → chunk-L6PVH3RM.js.map} +0 -0
- /package/dist/esm/{chunk-MK2R6DAU.js.map → chunk-LAOMR7SE.js.map} +0 -0
- /package/dist/esm/{chunk-FHOCUZDR.js.map → chunk-NIQT45PJ.js.map} +0 -0
- /package/dist/esm/{chunk-3XT5P2HO.js.map → chunk-OS3FCYZY.js.map} +0 -0
- /package/dist/esm/{chunk-7QDTSBJF.js.map → chunk-OYF6YXH7.js.map} +0 -0
- /package/dist/esm/{chunk-WFMYNZXZ.js.map → chunk-P5OSU4NX.js.map} +0 -0
- /package/dist/esm/{chunk-QEOAXJLN.js.map → chunk-PTS6QYQW.js.map} +0 -0
- /package/dist/esm/{chunk-CVWCKOHO.js.map → chunk-RLPKKCBL.js.map} +0 -0
- /package/dist/esm/{chunk-LKAK5GXF.js.map → chunk-SPIU5JYC.js.map} +0 -0
- /package/dist/esm/{chunk-4AQTUBVP.js.map → chunk-T4UHUQFJ.js.map} +0 -0
- /package/dist/esm/{chunk-K6QZMNCD.js.map → chunk-TS623UPD.js.map} +0 -0
- /package/dist/esm/{chunk-YV2KYHGR.js.map → chunk-UBOOUDS4.js.map} +0 -0
- /package/dist/esm/{chunk-D6B7IMZU.js.map → chunk-UFI53QUC.js.map} +0 -0
- /package/dist/esm/{chunk-AU3D6F42.js.map → chunk-UIRSGVJG.js.map} +0 -0
- /package/dist/esm/{chunk-FXUUI6Z2.js.map → chunk-UUUZXUIP.js.map} +0 -0
- /package/dist/esm/{chunk-25WZA7ZU.js.map → chunk-VA4CTVQL.js.map} +0 -0
- /package/dist/esm/{chunk-TOLLPRMM.js.map → chunk-VNPNEJI6.js.map} +0 -0
- /package/dist/esm/{chunk-6IGTK4JE.js.map → chunk-YMUGKYGW.js.map} +0 -0
- /package/dist/esm/{chunk-M24UXMOQ.js.map → chunk-ZXZOH63L.js.map} +0 -0
|
@@ -1,20 +1,58 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
|
-
import { LazyMotion, domAnimation, m } from 'motion/react';
|
|
3
|
+
import { LazyMotion, MotionConfig, domAnimation, m } from 'motion/react';
|
|
4
|
+
import { useComposedRefs } from 'radix-ui/internal';
|
|
5
|
+
import { useDropzone } from 'react-dropzone';
|
|
4
6
|
|
|
5
7
|
import { IconButton, type IconButtonProps } from './icon-button.js';
|
|
6
8
|
import { CloseIcon, FileTextIcon } from './icons.js';
|
|
7
9
|
import { Flex } from './flex.js';
|
|
8
|
-
|
|
9
10
|
import { ScrollArea } from './scroll-area.js';
|
|
10
11
|
import { Slot } from './slot.js';
|
|
11
12
|
import { Text } from './text.js';
|
|
12
|
-
import {
|
|
13
|
+
import { VisuallyHidden } from './visually-hidden.js';
|
|
13
14
|
import type { ComponentPropsWithout, RemovedProps } from '../helpers/component-props.js';
|
|
15
|
+
import type { accentColors } from '../props/color.prop.js';
|
|
16
|
+
import type { radii } from '../props/radius.prop.js';
|
|
14
17
|
|
|
15
18
|
// Avoid SSR warnings by using an isomorphic layout effect
|
|
16
19
|
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Runs `measure` with the placeholder taken out of layout, so a height read
|
|
23
|
+
* reflects the value alone.
|
|
24
|
+
*
|
|
25
|
+
* A textarea with an empty value lays its placeholder out as real text, so
|
|
26
|
+
* `scrollHeight` reports the placeholder wrapped at whatever width the field
|
|
27
|
+
* has at that instant. That sizes an empty bar to the suggestion showing in it
|
|
28
|
+
* rather than to `minLines`, and when the read lands before the field has its
|
|
29
|
+
* final width -- a panel still animating open, a flex parent whose basis is 0
|
|
30
|
+
* -- it wraps that text at a fraction of the real width and opens the bar at
|
|
31
|
+
* `maxLines`. The first keystroke takes the placeholder out of layout and the
|
|
32
|
+
* height snaps back, which is what made the bug look intermittent rather than
|
|
33
|
+
* simply wrong.
|
|
34
|
+
*
|
|
35
|
+
* The reset to 'auto' and the `scrollHeight` read sit in different functions
|
|
36
|
+
* here, so the whole measurement window is wrapped rather than a single read.
|
|
37
|
+
* Costs one extra style recalc on a path that is already forcing a reflow.
|
|
38
|
+
*/
|
|
39
|
+
function withoutPlaceholder(el: HTMLTextAreaElement, measure: () => void): void {
|
|
40
|
+
const { placeholder } = el;
|
|
41
|
+
if (!placeholder) {
|
|
42
|
+
measure();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
el.placeholder = '';
|
|
46
|
+
try {
|
|
47
|
+
measure();
|
|
48
|
+
} finally {
|
|
49
|
+
el.placeholder = placeholder;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Shared layout spring; hoisted so every motion node receives the same object
|
|
54
|
+
const LAYOUT_TRANSITION = { layout: { type: 'spring', visualDuration: 0.15, bounce: 0.1 } } as const;
|
|
55
|
+
|
|
18
56
|
type ExpandOn = 'none' | 'focus' | 'overflow' | 'both';
|
|
19
57
|
type SendMode = 'always' | 'whenDirty' | 'never';
|
|
20
58
|
|
|
@@ -24,7 +62,9 @@ type AttachmentStatus = 'idle' | 'uploading' | 'error' | 'done';
|
|
|
24
62
|
/**
|
|
25
63
|
* Attachment data model used by Chatbar.
|
|
26
64
|
* - `file` exposes the original File object for uploads and processing.
|
|
27
|
-
* - `url` is an object URL used for image previews
|
|
65
|
+
* - `url` is an object URL used for image previews. Chatbar revokes it when the
|
|
66
|
+
* attachment is removed; once an attachment is handed to `onSubmit`, the
|
|
67
|
+
* consumer owns the URL and is responsible for revoking it.
|
|
28
68
|
*/
|
|
29
69
|
interface ChatbarAttachment {
|
|
30
70
|
id: string;
|
|
@@ -39,14 +79,27 @@ interface ChatbarAttachment {
|
|
|
39
79
|
meta?: Record<string, unknown>;
|
|
40
80
|
}
|
|
41
81
|
|
|
82
|
+
type ChatbarRejectReason = 'type' | 'size' | 'count';
|
|
83
|
+
interface ChatbarAttachmentRejection {
|
|
84
|
+
file: File;
|
|
85
|
+
reason: ChatbarRejectReason;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ChatbarSubmitPayload {
|
|
89
|
+
value: string;
|
|
90
|
+
attachments: ChatbarAttachment[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface FilePickerOptions {
|
|
94
|
+
accept?: string | string[];
|
|
95
|
+
multiple?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
42
98
|
interface ChatbarContextValue {
|
|
43
99
|
open: boolean;
|
|
44
100
|
setOpen(next: boolean): void;
|
|
45
|
-
isOpenControlled: boolean;
|
|
46
101
|
|
|
47
|
-
value: string;
|
|
48
102
|
setValue(next: string): void;
|
|
49
|
-
isValueControlled: boolean;
|
|
50
103
|
|
|
51
104
|
size: '1' | '2' | '3';
|
|
52
105
|
expandOn: ExpandOn;
|
|
@@ -55,48 +108,64 @@ interface ChatbarContextValue {
|
|
|
55
108
|
sendMode: SendMode;
|
|
56
109
|
disabled?: boolean;
|
|
57
110
|
readOnly?: boolean;
|
|
58
|
-
|
|
59
|
-
// Submit returns both message and attachments
|
|
60
|
-
onSubmit?: (payload: { value: string; attachments: ChatbarAttachment[] }) => void;
|
|
61
|
-
|
|
62
|
-
rootRef: React.RefObject<HTMLDivElement | null>;
|
|
63
|
-
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
64
|
-
|
|
65
|
-
// Attachments state
|
|
66
|
-
attachments: ChatbarAttachment[];
|
|
67
|
-
setAttachments(next: ChatbarAttachment[]): void;
|
|
68
|
-
isAttachmentsControlled: boolean;
|
|
69
|
-
|
|
70
|
-
// Config
|
|
71
|
-
accept?: string | string[];
|
|
72
|
-
multiple: boolean;
|
|
73
|
-
maxAttachments?: number;
|
|
74
|
-
maxFileSize?: number;
|
|
75
111
|
paste: boolean;
|
|
76
|
-
pasteAccept?: string | string[];
|
|
77
112
|
clearOnSubmit: boolean;
|
|
113
|
+
multiple: boolean;
|
|
78
114
|
|
|
79
|
-
|
|
80
|
-
dropzone: boolean;
|
|
81
|
-
|
|
82
|
-
// Events
|
|
83
|
-
onAttachmentReject?: (rejections: { file: File; reason: 'type' | 'size' | 'count' }[]) => void;
|
|
84
|
-
|
|
85
|
-
// Helpers
|
|
86
|
-
appendFiles(files: File[]): void;
|
|
87
|
-
appendFilesFromPaste(files: File[]): void;
|
|
115
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
attachments: ChatbarAttachment[];
|
|
118
|
+
removeAttachment(id: string): void;
|
|
119
|
+
|
|
120
|
+
/** Validates and submits; returns false when there was nothing to submit */
|
|
121
|
+
submit(clear: boolean): boolean;
|
|
122
|
+
appendFiles(files: File[]): number;
|
|
123
|
+
appendFilesFromPaste(files: File[]): number;
|
|
124
|
+
openFilePicker(options?: FilePickerOptions): void;
|
|
125
|
+
/** Guards blur-collapse between pointerdown on a picker trigger and its click */
|
|
126
|
+
beginFilePickerPress(): void;
|
|
91
127
|
}
|
|
92
128
|
|
|
93
129
|
const ChatbarContext = React.createContext<ChatbarContextValue | null>(null);
|
|
130
|
+
// Value lives in its own context so keystrokes only re-render parts that read it
|
|
131
|
+
const ChatbarValueContext = React.createContext<string>('');
|
|
132
|
+
|
|
94
133
|
const useChatbarContext = () => {
|
|
95
134
|
const ctx = React.useContext(ChatbarContext);
|
|
96
135
|
if (!ctx) throw new Error('Chatbar context not found. Wrap parts in <Chatbar.Root>.');
|
|
97
136
|
return ctx;
|
|
98
137
|
};
|
|
99
138
|
|
|
139
|
+
const toArray = (val: string | string[] | undefined) =>
|
|
140
|
+
Array.isArray(val) ? val : typeof val === 'string' ? val.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
141
|
+
|
|
142
|
+
const matchesAccept = (file: File, patterns: string[]) => {
|
|
143
|
+
if (patterns.length === 0) return true;
|
|
144
|
+
const mime = file.type.toLowerCase();
|
|
145
|
+
const name = file.name.toLowerCase();
|
|
146
|
+
for (const patRaw of patterns) {
|
|
147
|
+
const pat = patRaw.toLowerCase();
|
|
148
|
+
if (pat.includes('/')) {
|
|
149
|
+
const [type, subtype] = pat.split('/');
|
|
150
|
+
const [fmType, fmSubtype] = mime.split('/');
|
|
151
|
+
if (type === '*' || (type === fmType && (subtype === '*' || subtype === fmSubtype))) return true;
|
|
152
|
+
} else if (pat.startsWith('.')) {
|
|
153
|
+
if (name.endsWith(pat)) return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|bmp|svg|avif)$/i;
|
|
160
|
+
const isImageLike = (name: string, type: string) => (type || '').toLowerCase().startsWith('image/') || IMAGE_EXT_RE.test(name);
|
|
161
|
+
|
|
162
|
+
const hasText = (value: string) => value.trim().length > 0;
|
|
163
|
+
|
|
164
|
+
const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
165
|
+
|
|
166
|
+
let attachmentCounter = 0;
|
|
167
|
+
const createAttachmentId = () => `chatbar-attachment-${Date.now()}-${++attachmentCounter}`;
|
|
168
|
+
|
|
100
169
|
/**
|
|
101
170
|
* Chatbar container and state provider.
|
|
102
171
|
*
|
|
@@ -105,11 +174,12 @@ const useChatbarContext = () => {
|
|
|
105
174
|
*
|
|
106
175
|
* Attachments
|
|
107
176
|
* - Controlled/uncontrolled attachments with client-side filtering.
|
|
108
|
-
* - Filters by `accept`/`pasteAccept`, `maxAttachments`, and `maxFileSize`.
|
|
177
|
+
* - Filters by `accept`/`pasteAccept`, `multiple`, `maxAttachments`, and `maxFileSize`.
|
|
109
178
|
* - Rejections are reported via `onAttachmentReject`.
|
|
110
179
|
*
|
|
111
180
|
* Submit
|
|
112
181
|
* - `onSubmit({ value, attachments })` emits both message and attachments.
|
|
182
|
+
* - Submitting requires text or at least one attachment.
|
|
113
183
|
* - `clearOnSubmit` clears message and attachments by default.
|
|
114
184
|
*/
|
|
115
185
|
interface ChatbarRootBaseProps {
|
|
@@ -121,54 +191,60 @@ interface ChatbarRootBaseProps {
|
|
|
121
191
|
defaultOpen?: boolean;
|
|
122
192
|
onOpenChange?: (open: boolean) => void;
|
|
123
193
|
|
|
194
|
+
/** When the chatbar expands on its own. `none` leaves open state entirely to the consumer. */
|
|
124
195
|
expandOn?: ExpandOn;
|
|
125
196
|
|
|
126
|
-
/** Minimum
|
|
197
|
+
/** Minimum visible lines when expanded (default: 3). The compact state is always one line. */
|
|
127
198
|
minLines?: number;
|
|
128
199
|
/** Maximum number of lines before scrolling (default: 6) */
|
|
129
200
|
maxLines?: number;
|
|
130
201
|
|
|
202
|
+
/** Controls Send button visibility only. Enter-to-submit is controlled by `Textarea submitOnEnter`. */
|
|
131
203
|
sendMode?: SendMode;
|
|
132
204
|
|
|
205
|
+
/** Disables text input, submission and every way of adding attachments */
|
|
133
206
|
disabled?: boolean;
|
|
207
|
+
/** Prevents editing, submission and adding attachments */
|
|
134
208
|
readOnly?: boolean;
|
|
135
209
|
|
|
136
210
|
/** Combined submit payload */
|
|
137
|
-
onSubmit?: (payload:
|
|
211
|
+
onSubmit?: (payload: ChatbarSubmitPayload) => void;
|
|
138
212
|
|
|
139
213
|
size?: '1' | '2' | '3';
|
|
140
214
|
variant?: 'surface' | 'outline' | 'classic' | 'ghost' | 'soft';
|
|
141
215
|
/** Accent color for the control (matches TextArea) */
|
|
142
|
-
color?:
|
|
216
|
+
color?: (typeof accentColors)[number];
|
|
143
217
|
/** Optional radius override (matches TextArea) */
|
|
144
|
-
radius?:
|
|
218
|
+
radius?: (typeof radii)[number];
|
|
145
219
|
/** Panel/material translucency flags (matches TextArea) */
|
|
146
220
|
panelBackground?: 'solid' | 'translucent';
|
|
147
221
|
material?: 'solid' | 'translucent';
|
|
148
222
|
|
|
149
223
|
width?: React.CSSProperties['width'];
|
|
150
224
|
maxWidth?: React.CSSProperties['maxWidth'];
|
|
151
|
-
asChild?: boolean;
|
|
152
225
|
|
|
153
226
|
// Attachments API
|
|
154
227
|
attachments?: ChatbarAttachment[];
|
|
155
228
|
defaultAttachments?: ChatbarAttachment[];
|
|
156
229
|
onAttachmentsChange?: (attachments: ChatbarAttachment[]) => void;
|
|
157
230
|
accept?: string | string[];
|
|
231
|
+
/** Allow more than one file per pick, paste or drop */
|
|
158
232
|
multiple?: boolean;
|
|
159
233
|
maxAttachments?: number;
|
|
160
234
|
maxFileSize?: number;
|
|
161
235
|
paste?: boolean;
|
|
236
|
+
/** Accepted types for pasted files. Replaces `accept` for paste when set. */
|
|
162
237
|
pasteAccept?: string | string[];
|
|
238
|
+
/** Clear message and attachments after submit */
|
|
163
239
|
clearOnSubmit?: boolean;
|
|
164
|
-
onAttachmentReject?: (rejections:
|
|
240
|
+
onAttachmentReject?: (rejections: ChatbarAttachmentRejection[]) => void;
|
|
165
241
|
|
|
166
242
|
/**
|
|
167
243
|
* Enables drag-and-drop file uploads when true.
|
|
168
244
|
*
|
|
169
245
|
* When enabled:
|
|
170
246
|
* - Files can be dropped anywhere on the chatbar
|
|
171
|
-
* - Same validation rules apply (accept, maxFileSize, maxAttachments)
|
|
247
|
+
* - Same validation rules apply (accept, multiple, maxFileSize, maxAttachments)
|
|
172
248
|
* - Visual feedback shows during drag operations
|
|
173
249
|
* - Rejected files trigger onAttachmentReject
|
|
174
250
|
*
|
|
@@ -188,7 +264,7 @@ type RootElement = React.ElementRef<'div'>;
|
|
|
188
264
|
export interface ChatbarApi {
|
|
189
265
|
/** Focus the textarea input */
|
|
190
266
|
focusTextarea: () => void;
|
|
191
|
-
/** Open the file picker dialog (respects accept/multiple) */
|
|
267
|
+
/** Open the file picker dialog (respects accept/multiple). No-op when disabled or read-only. */
|
|
192
268
|
openFilePicker: () => void;
|
|
193
269
|
}
|
|
194
270
|
/**
|
|
@@ -198,30 +274,19 @@ export interface ChatbarApi {
|
|
|
198
274
|
* - Supports controlled and uncontrolled `value` and `open` states via props.
|
|
199
275
|
* - Provides context to subcomponents like `Textarea`, `Row`, and `Send`.
|
|
200
276
|
* - Exposes `data-state`, `data-disabled`, and `data-readonly` attributes for styling.
|
|
201
|
-
* - Sets `aria-expanded` to reflect open/closed state for assistive technologies.
|
|
202
277
|
*
|
|
203
278
|
* Attachments
|
|
204
|
-
* - Controlled/uncontrolled attachments with client-side filtering.
|
|
205
|
-
* - Filters by `accept`/`pasteAccept`, `maxAttachments`, and `maxFileSize`.
|
|
206
|
-
* - Rejections are reported via `onAttachmentReject`.
|
|
207
279
|
* - Paste-to-attach: when `paste` is enabled, pasting files adds attachments.
|
|
280
|
+
* - Additions and rejections are announced through a polite live region.
|
|
208
281
|
*
|
|
209
282
|
* Dropzone
|
|
210
283
|
* - When `dropzone` is true, enables drag-and-drop file uploads.
|
|
211
|
-
* - Files are validated using the same rules as paste and file picker.
|
|
212
284
|
* - Visual feedback via `data-drop-active` attribute during drag operations.
|
|
213
|
-
* - Rejected files trigger `onAttachmentReject` with appropriate reasons.
|
|
214
|
-
*
|
|
215
|
-
* Submit
|
|
216
|
-
* - `onSubmit` receives both message text and attachments array.
|
|
217
|
-
* - `clearOnSubmit` controls whether attachments are cleared after submission.
|
|
218
285
|
*
|
|
219
286
|
* Accessibility
|
|
220
287
|
* - Consumers should label the `Textarea` via `aria-label`/`aria-labelledby`.
|
|
221
|
-
* - `aria-expanded` on the root reflects the disclosure state of the input area.
|
|
222
|
-
* - Dropzone provides proper ARIA attributes for drag and drop operations.
|
|
223
288
|
*/
|
|
224
|
-
interface RootProps extends ComponentPropsWithout<'div', RemovedProps | 'onSubmit'>, ChatbarRootBaseProps {}
|
|
289
|
+
interface RootProps extends ComponentPropsWithout<'div', RemovedProps | 'onSubmit' | 'color'>, ChatbarRootBaseProps {}
|
|
225
290
|
|
|
226
291
|
const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
|
|
227
292
|
const {
|
|
@@ -230,10 +295,10 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
|
|
|
230
295
|
children,
|
|
231
296
|
value: valueProp,
|
|
232
297
|
defaultValue = '',
|
|
233
|
-
onValueChange
|
|
298
|
+
onValueChange,
|
|
234
299
|
open: openProp,
|
|
235
300
|
defaultOpen = false,
|
|
236
|
-
onOpenChange
|
|
301
|
+
onOpenChange,
|
|
237
302
|
expandOn = 'both',
|
|
238
303
|
minLines = 3,
|
|
239
304
|
maxLines = 6,
|
|
@@ -249,9 +314,8 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
|
|
|
249
314
|
material,
|
|
250
315
|
width,
|
|
251
316
|
maxWidth,
|
|
252
|
-
asChild,
|
|
253
317
|
attachments: attachmentsProp,
|
|
254
|
-
defaultAttachments
|
|
318
|
+
defaultAttachments,
|
|
255
319
|
onAttachmentsChange,
|
|
256
320
|
accept,
|
|
257
321
|
multiple = true,
|
|
@@ -263,404 +327,415 @@ const Root = React.forwardRef<RootElement, RootProps>((props, forwardedRef) => {
|
|
|
263
327
|
onAttachmentReject,
|
|
264
328
|
dropzone = true,
|
|
265
329
|
apiRef,
|
|
330
|
+
onBlurCapture,
|
|
266
331
|
...divProps
|
|
267
332
|
} = props;
|
|
268
333
|
const effectiveMaterial = material || panelBackground;
|
|
269
334
|
|
|
270
335
|
const isValueControlled = valueProp != null;
|
|
271
336
|
const [valueUncontrolled, setValueUncontrolled] = React.useState<string>(defaultValue);
|
|
272
|
-
const value = isValueControlled ?
|
|
337
|
+
const value = isValueControlled ? valueProp : valueUncontrolled;
|
|
273
338
|
|
|
274
339
|
const isOpenControlled = openProp != null;
|
|
275
340
|
const [openUncontrolled, setOpenUncontrolled] = React.useState<boolean>(defaultOpen);
|
|
276
|
-
const open = isOpenControlled ?
|
|
341
|
+
const open = isOpenControlled ? openProp : openUncontrolled;
|
|
277
342
|
|
|
278
|
-
const rootRef = React.useRef<HTMLDivElement>(null);
|
|
279
|
-
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
280
|
-
const fileDialogOpenRef = React.useRef<boolean>(false);
|
|
281
|
-
|
|
282
|
-
// Attachments state
|
|
283
343
|
// Treat `attachments` as controlled if the prop is provided, even if its value is `undefined`.
|
|
284
344
|
// This avoids switching between controlled and uncontrolled when a consumer sets
|
|
285
345
|
// `attachments={undefined}` to clear attachments. In that case we normalize to an empty array.
|
|
286
346
|
const isAttachmentsControlled = 'attachments' in props;
|
|
287
|
-
const [attachmentsUncontrolled, setAttachmentsUncontrolled] = React.useState<ChatbarAttachment[]>(defaultAttachments);
|
|
347
|
+
const [attachmentsUncontrolled, setAttachmentsUncontrolled] = React.useState<ChatbarAttachment[]>(() => defaultAttachments ?? []);
|
|
288
348
|
const attachments = React.useMemo(
|
|
289
|
-
() => (isAttachmentsControlled ? (
|
|
349
|
+
() => (isAttachmentsControlled ? (attachmentsProp ?? []) : attachmentsUncontrolled),
|
|
290
350
|
[isAttachmentsControlled, attachmentsProp, attachmentsUncontrolled],
|
|
291
351
|
);
|
|
292
352
|
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
353
|
+
// `id` changes on every announcement so identical messages still update the live region
|
|
354
|
+
const [announcement, setAnnouncementState] = React.useState({ text: '', id: 0 });
|
|
355
|
+
const announce = React.useCallback((text: string) => setAnnouncementState((prev) => ({ text, id: prev.id + 1 })), []);
|
|
356
|
+
|
|
357
|
+
const rootRef = React.useRef<HTMLDivElement>(null);
|
|
358
|
+
const composedRootRef = useComposedRefs(forwardedRef, rootRef);
|
|
359
|
+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
360
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
|
361
|
+
|
|
362
|
+
// Blur-collapse guards: `pickerOpenRef` is true while the native dialog is open,
|
|
363
|
+
// `pressGuardRef` covers the gap between pointerdown on a trigger and its click
|
|
364
|
+
// (Safari blurs the textarea before the click fires).
|
|
365
|
+
const pickerOpenRef = React.useRef(false);
|
|
366
|
+
const pressGuardRef = React.useRef(false);
|
|
367
|
+
const pressGuardTimerRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
368
|
+
const pickerRef = React.useRef<{ patterns: string[]; multiple: boolean } | null>(null);
|
|
369
|
+
|
|
370
|
+
const acceptKey = toArray(accept).join(',');
|
|
371
|
+
const pasteAcceptKey = toArray(pasteAccept).join(',');
|
|
372
|
+
const accepts = React.useMemo(() => toArray(acceptKey), [acceptKey]);
|
|
373
|
+
const pasteAccepts = React.useMemo(() => (pasteAcceptKey ? toArray(pasteAcceptKey) : accepts), [pasteAcceptKey, accepts]);
|
|
374
|
+
|
|
375
|
+
// Latest props/state for stable callbacks. Synced during render (not in an effect) so
|
|
376
|
+
// child layout effects that call setOpen/setValue see this render's handlers.
|
|
377
|
+
const snapshot = {
|
|
378
|
+
value,
|
|
379
|
+
attachments,
|
|
380
|
+
accepts,
|
|
381
|
+
pasteAccepts,
|
|
382
|
+
multiple,
|
|
383
|
+
maxAttachments,
|
|
384
|
+
maxFileSize,
|
|
385
|
+
disabled,
|
|
386
|
+
readOnly,
|
|
387
|
+
expandOn,
|
|
388
|
+
isValueControlled,
|
|
389
|
+
isOpenControlled,
|
|
390
|
+
isAttachmentsControlled,
|
|
391
|
+
onValueChange,
|
|
392
|
+
onOpenChange,
|
|
393
|
+
onAttachmentsChange,
|
|
394
|
+
onAttachmentReject,
|
|
395
|
+
onSubmit,
|
|
316
396
|
};
|
|
397
|
+
const latest = React.useRef(snapshot);
|
|
398
|
+
// eslint-disable-next-line react-hooks/refs -- read only in callbacks/effects; see comment above
|
|
399
|
+
latest.current = snapshot;
|
|
400
|
+
|
|
401
|
+
// Attachments set during the current event, before Root re-renders. Cleared at the end
|
|
402
|
+
// of the task so a controlled parent that rejects the change is not overridden.
|
|
403
|
+
const pendingAttachmentsRef = React.useRef<ChatbarAttachment[] | null>(null);
|
|
404
|
+
const getAttachments = React.useCallback(() => pendingAttachmentsRef.current ?? latest.current.attachments, []);
|
|
405
|
+
|
|
406
|
+
const setOpen = React.useCallback((next: boolean) => {
|
|
407
|
+
if (!latest.current.isOpenControlled) setOpenUncontrolled(next);
|
|
408
|
+
latest.current.onOpenChange?.(next);
|
|
409
|
+
}, []);
|
|
410
|
+
|
|
411
|
+
const setValue = React.useCallback((next: string) => {
|
|
412
|
+
if (!latest.current.isValueControlled) setValueUncontrolled(next);
|
|
413
|
+
latest.current.onValueChange?.(next);
|
|
414
|
+
}, []);
|
|
317
415
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
416
|
+
const setAttachments = React.useCallback((next: ChatbarAttachment[]) => {
|
|
417
|
+
// Back-to-back updates in one event must see each other
|
|
418
|
+
pendingAttachmentsRef.current = next;
|
|
419
|
+
queueMicrotask(() => {
|
|
420
|
+
pendingAttachmentsRef.current = null;
|
|
421
|
+
});
|
|
422
|
+
if (!latest.current.isAttachmentsControlled) setAttachmentsUncontrolled(next);
|
|
423
|
+
latest.current.onAttachmentsChange?.(next);
|
|
424
|
+
}, []);
|
|
425
|
+
|
|
426
|
+
// Object URLs Chatbar created and still owns
|
|
427
|
+
const ownedUrlsRef = React.useRef<Set<string>>(new Set());
|
|
428
|
+
|
|
429
|
+
const addFiles = React.useCallback(
|
|
430
|
+
(files: File[], patterns: string[], multiple: boolean = latest.current.multiple): number => {
|
|
431
|
+
const cfg = latest.current;
|
|
432
|
+
if (cfg.disabled || cfg.readOnly || files.length === 0) return 0;
|
|
433
|
+
|
|
434
|
+
const current = getAttachments();
|
|
435
|
+
const next: ChatbarAttachment[] = [];
|
|
436
|
+
const rejected: ChatbarAttachmentRejection[] = [];
|
|
437
|
+
const remainingSlots = typeof cfg.maxAttachments === 'number' ? Math.max(cfg.maxAttachments - current.length, 0) : Infinity;
|
|
438
|
+
const perActionLimit = multiple ? Infinity : 1;
|
|
439
|
+
|
|
440
|
+
for (const file of files) {
|
|
441
|
+
if (next.length >= remainingSlots || next.length >= perActionLimit) {
|
|
442
|
+
rejected.push({ file, reason: 'count' });
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (typeof cfg.maxFileSize === 'number' && file.size > cfg.maxFileSize) {
|
|
446
|
+
rejected.push({ file, reason: 'size' });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (!matchesAccept(file, patterns)) {
|
|
450
|
+
rejected.push({ file, reason: 'type' });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const url = isImageLike(file.name, file.type) ? URL.createObjectURL(file) : undefined;
|
|
454
|
+
if (url) ownedUrlsRef.current.add(url);
|
|
455
|
+
next.push({ id: createAttachmentId(), name: file.name, size: file.size, type: file.type, file, url, status: 'idle' });
|
|
350
456
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
457
|
+
|
|
458
|
+
const messages: string[] = [];
|
|
459
|
+
if (next.length > 0) {
|
|
460
|
+
setAttachments(current.concat(next));
|
|
461
|
+
if (cfg.expandOn !== 'none') setOpen(true);
|
|
462
|
+
messages.push(`${plural(next.length, 'file')} attached.`);
|
|
354
463
|
}
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
|
|
464
|
+
if (rejected.length > 0) {
|
|
465
|
+
cfg.onAttachmentReject?.(rejected);
|
|
466
|
+
messages.push(`${plural(rejected.length, 'file')} could not be attached.`);
|
|
358
467
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
next.push({ id, name: file.name, size: file.size, type: file.type, file, url, status: 'idle' });
|
|
365
|
-
}
|
|
468
|
+
if (messages.length > 0) announce(messages.join(' '));
|
|
469
|
+
return next.length;
|
|
470
|
+
},
|
|
471
|
+
[getAttachments, setAttachments, setOpen, announce],
|
|
472
|
+
);
|
|
366
473
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
if (typeof currentMaxFileSize === 'number' && file.size > currentMaxFileSize) {
|
|
395
|
-
rejected.push({ file, reason: 'size' });
|
|
396
|
-
continue;
|
|
474
|
+
const appendFiles = React.useCallback((files: File[]) => addFiles(files, latest.current.accepts), [addFiles]);
|
|
475
|
+
const appendFilesFromPaste = React.useCallback((files: File[]) => addFiles(files, latest.current.pasteAccepts), [addFiles]);
|
|
476
|
+
|
|
477
|
+
const removeAttachment = React.useCallback(
|
|
478
|
+
(id: string) => {
|
|
479
|
+
const current = getAttachments();
|
|
480
|
+
const removed = current.find((a) => a.id === id);
|
|
481
|
+
if (!removed) return;
|
|
482
|
+
setAttachments(current.filter((a) => a.id !== id));
|
|
483
|
+
announce(`Removed ${removed.name}.`);
|
|
484
|
+
},
|
|
485
|
+
[getAttachments, setAttachments, announce],
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
const submit = React.useCallback(
|
|
489
|
+
(clear: boolean) => {
|
|
490
|
+
const cfg = latest.current;
|
|
491
|
+
if (cfg.disabled || cfg.readOnly) return false;
|
|
492
|
+
const current = getAttachments();
|
|
493
|
+
if (!hasText(cfg.value) && current.length === 0) return false;
|
|
494
|
+
const payload = { value: cfg.value, attachments: current };
|
|
495
|
+
// Hand preview URLs to the consumer only when they leave the chatbar; URLs still
|
|
496
|
+
// rendered here stay owned so removing them later revokes them
|
|
497
|
+
if (clear && cfg.onSubmit) {
|
|
498
|
+
for (const att of current) if (att.url) ownedUrlsRef.current.delete(att.url);
|
|
397
499
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
500
|
+
cfg.onSubmit?.(payload);
|
|
501
|
+
if (clear) {
|
|
502
|
+
setValue('');
|
|
503
|
+
setAttachments([]);
|
|
401
504
|
}
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}, [appendFiles]);
|
|
505
|
+
return true;
|
|
506
|
+
},
|
|
507
|
+
[getAttachments, setValue, setAttachments],
|
|
508
|
+
);
|
|
407
509
|
|
|
408
|
-
// Revoke object URLs that are no longer referenced by current attachments
|
|
510
|
+
// Revoke owned object URLs that are no longer referenced by current attachments
|
|
409
511
|
React.useEffect(() => {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
512
|
+
const owned = ownedUrlsRef.current;
|
|
513
|
+
if (owned.size === 0) return;
|
|
514
|
+
const current = new Set(attachments.map((a) => a.url));
|
|
515
|
+
for (const url of Array.from(owned)) {
|
|
516
|
+
if (!current.has(url)) {
|
|
413
517
|
URL.revokeObjectURL(url);
|
|
414
|
-
|
|
518
|
+
owned.delete(url);
|
|
415
519
|
}
|
|
416
520
|
}
|
|
417
521
|
}, [attachments]);
|
|
418
522
|
|
|
419
|
-
//
|
|
523
|
+
// On unmount, revoke URLs only when Chatbar owns the attachment list;
|
|
524
|
+
// controlled attachments may still be rendered by the parent.
|
|
420
525
|
React.useEffect(() => {
|
|
421
|
-
const
|
|
526
|
+
const owned = ownedUrlsRef.current;
|
|
422
527
|
return () => {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
urlSet.clear();
|
|
528
|
+
if (latest.current.isAttachmentsControlled) return;
|
|
529
|
+
for (const url of Array.from(owned)) URL.revokeObjectURL(url);
|
|
530
|
+
owned.clear();
|
|
427
531
|
};
|
|
428
532
|
}, []);
|
|
429
533
|
|
|
430
|
-
const
|
|
534
|
+
const openFilePicker = React.useCallback((options?: FilePickerOptions) => {
|
|
535
|
+
const cfg = latest.current;
|
|
536
|
+
const input = fileInputRef.current;
|
|
537
|
+
// The picker guard takes over from the press guard
|
|
538
|
+
clearTimeout(pressGuardTimerRef.current);
|
|
539
|
+
pressGuardRef.current = false;
|
|
540
|
+
if (!input || cfg.disabled || cfg.readOnly) return;
|
|
541
|
+
const patterns = options?.accept != null ? toArray(options.accept) : cfg.accepts;
|
|
542
|
+
const multiple = options?.multiple ?? cfg.multiple;
|
|
543
|
+
pickerRef.current = { patterns, multiple };
|
|
544
|
+
input.accept = patterns.join(',');
|
|
545
|
+
input.multiple = multiple;
|
|
546
|
+
pickerOpenRef.current = true;
|
|
547
|
+
input.click();
|
|
548
|
+
// Browsers block the dialog outside a user gesture; if the window kept focus,
|
|
549
|
+
// no dialog opened and no focus/cancel/change event will clear the guard
|
|
550
|
+
setTimeout(() => {
|
|
551
|
+
if (document.hasFocus()) pickerOpenRef.current = false;
|
|
552
|
+
}, 1000);
|
|
553
|
+
}, []);
|
|
431
554
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
555
|
+
const beginFilePickerPress = React.useCallback(() => {
|
|
556
|
+
// On touch (notably iOS), the compatibility mousedown, blur and click arrive in later
|
|
557
|
+
// tasks than pointerup, so hold the guard until the click or a timeout
|
|
558
|
+
pressGuardRef.current = true;
|
|
559
|
+
clearTimeout(pressGuardTimerRef.current);
|
|
560
|
+
pressGuardTimerRef.current = setTimeout(() => {
|
|
561
|
+
pressGuardRef.current = false;
|
|
562
|
+
}, 800);
|
|
563
|
+
}, []);
|
|
564
|
+
|
|
565
|
+
React.useEffect(() => () => clearTimeout(pressGuardTimerRef.current), []);
|
|
566
|
+
|
|
567
|
+
// Clear the picker guard when the dialog closes, whichever way it closes
|
|
568
|
+
React.useEffect(() => {
|
|
569
|
+
const input = fileInputRef.current;
|
|
570
|
+
const reset = () => {
|
|
571
|
+
pickerOpenRef.current = false;
|
|
572
|
+
};
|
|
573
|
+
window.addEventListener('focus', reset);
|
|
574
|
+
input?.addEventListener('cancel', reset);
|
|
575
|
+
return () => {
|
|
576
|
+
window.removeEventListener('focus', reset);
|
|
577
|
+
input?.removeEventListener('cancel', reset);
|
|
578
|
+
};
|
|
579
|
+
}, []);
|
|
580
|
+
|
|
581
|
+
const handleFileInputChange = React.useCallback(
|
|
582
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
583
|
+
const input = event.currentTarget;
|
|
584
|
+
const files = Array.from(input.files ?? []);
|
|
585
|
+
const picker = pickerRef.current;
|
|
586
|
+
addFiles(files, picker?.patterns ?? latest.current.accepts, picker?.multiple);
|
|
587
|
+
pickerOpenRef.current = false;
|
|
588
|
+
pickerRef.current = null;
|
|
589
|
+
// Reset so selecting the same file again still fires change
|
|
590
|
+
input.value = '';
|
|
451
591
|
},
|
|
452
|
-
[
|
|
592
|
+
[addFiles],
|
|
453
593
|
);
|
|
454
594
|
|
|
455
595
|
const handleBlurCapture = React.useCallback(
|
|
456
|
-
(event: React.FocusEvent) => {
|
|
457
|
-
|
|
596
|
+
(event: React.FocusEvent<HTMLDivElement>) => {
|
|
597
|
+
onBlurCapture?.(event);
|
|
598
|
+
const cfg = latest.current;
|
|
458
599
|
const rootEl = rootRef.current;
|
|
459
|
-
if (!rootEl) return;
|
|
460
|
-
|
|
600
|
+
if (!rootEl || cfg.expandOn === 'none') return;
|
|
601
|
+
const nextTarget = event.relatedTarget as Node | null;
|
|
461
602
|
if (nextTarget && rootEl.contains(nextTarget)) return;
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
// Collapse when leaving the root if the value is empty
|
|
465
|
-
// Only collapse when both message and attachments are empty
|
|
466
|
-
if ((value?.trim?.() ?? '').length === 0 && attachments.length === 0) {
|
|
467
|
-
if (!isOpenControlled) setOpenUncontrolled(false);
|
|
468
|
-
onOpenChangeProp?.(false);
|
|
469
|
-
}
|
|
603
|
+
if (pickerOpenRef.current || pressGuardRef.current) return;
|
|
604
|
+
if (!hasText(cfg.value) && getAttachments().length === 0) setOpen(false);
|
|
470
605
|
},
|
|
471
|
-
[
|
|
606
|
+
[onBlurCapture, setOpen, getAttachments],
|
|
472
607
|
);
|
|
473
608
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
appendFiles(acceptedFiles);
|
|
484
|
-
}
|
|
485
|
-
if (rejectedFiles.length > 0 && onAttachmentReject) {
|
|
486
|
-
const rejections = rejectedFiles.map(({ file, errors }) => {
|
|
487
|
-
const reason = errors[0]?.code === 'file-too-large' ? 'size' : errors[0]?.code === 'file-invalid-type' ? 'type' : 'count';
|
|
488
|
-
return { file, reason: reason as 'type' | 'size' | 'count' };
|
|
489
|
-
});
|
|
490
|
-
onAttachmentReject(rejections);
|
|
491
|
-
}
|
|
492
|
-
},
|
|
493
|
-
accept:
|
|
494
|
-
accepts.length > 0
|
|
495
|
-
? accepts.reduce(
|
|
496
|
-
(acc, pattern) => {
|
|
497
|
-
if (pattern.includes('/')) {
|
|
498
|
-
// MIME type pattern
|
|
499
|
-
acc[pattern] = [];
|
|
500
|
-
} else if (pattern.startsWith('.')) {
|
|
501
|
-
// File extension pattern
|
|
502
|
-
acc[pattern] = [];
|
|
503
|
-
}
|
|
504
|
-
return acc;
|
|
505
|
-
},
|
|
506
|
-
{} as Record<string, string[]>,
|
|
507
|
-
)
|
|
508
|
-
: undefined,
|
|
509
|
-
multiple,
|
|
510
|
-
maxSize: maxFileSize,
|
|
609
|
+
const handleDrop = React.useCallback((files: File[]) => {
|
|
610
|
+
appendFiles(files);
|
|
611
|
+
}, [appendFiles]);
|
|
612
|
+
|
|
613
|
+
// Validation happens in addFiles so drop, paste and picker share one rule set
|
|
614
|
+
const { getRootProps, isDragActive } = useDropzone({
|
|
615
|
+
onDrop: handleDrop,
|
|
616
|
+
// Drop any earlier message so it is not re-announced when the drag hint goes away
|
|
617
|
+
onDragEnter: () => announce(''),
|
|
511
618
|
noClick: true,
|
|
512
619
|
noKeyboard: true,
|
|
513
|
-
|
|
620
|
+
// Textarea handles paste itself; letting the dropzone see it attaches twice
|
|
621
|
+
noPaste: true,
|
|
622
|
+
disabled: !dropzone || disabled || readOnly,
|
|
514
623
|
});
|
|
624
|
+
const dragActive = dropzone && isDragActive;
|
|
515
625
|
|
|
516
|
-
// Expose imperative API via apiRef (non-breaking; forwardedRef still receives the DOM node)
|
|
517
626
|
React.useImperativeHandle(
|
|
518
627
|
apiRef,
|
|
519
628
|
() => ({
|
|
520
629
|
focusTextarea: () => textareaRef.current?.focus({ preventScroll: true }),
|
|
521
|
-
openFilePicker: () =>
|
|
522
|
-
// Guard against blur-collapse while native dialog is open
|
|
523
|
-
fileDialogOpenRef.current = true;
|
|
524
|
-
openFileDialog();
|
|
525
|
-
},
|
|
630
|
+
openFilePicker: () => openFilePicker(),
|
|
526
631
|
}),
|
|
527
|
-
[
|
|
632
|
+
[openFilePicker],
|
|
528
633
|
);
|
|
529
634
|
|
|
530
635
|
// Click-to-focus: focus textarea when clicking non-interactive areas inside the container
|
|
531
|
-
const isInteractiveTarget = React.useCallback((target: EventTarget | null) => {
|
|
532
|
-
if (!(target instanceof Element)) return false;
|
|
533
|
-
const el = target as Element;
|
|
534
|
-
if (el.closest('.rt-ChatbarDropOverlay')) return true;
|
|
535
|
-
return !!el.closest('button, [role="button"], a[href], input, textarea, select, [contenteditable], [tabindex]:not([tabindex="-1"])');
|
|
536
|
-
}, []);
|
|
537
|
-
|
|
538
636
|
const handleContainerPointerDown = React.useCallback(
|
|
539
637
|
(event: React.PointerEvent) => {
|
|
540
638
|
if (disabled) return;
|
|
541
|
-
if (isInteractiveTarget(event.target)) return;
|
|
542
639
|
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
|
640
|
+
const target = event.target;
|
|
641
|
+
if (target instanceof Element && target.closest('button, [role="button"], a[href], input, textarea, select, [contenteditable], [tabindex]:not([tabindex="-1"])')) return;
|
|
543
642
|
event.preventDefault();
|
|
544
643
|
textareaRef.current?.focus({ preventScroll: true });
|
|
545
644
|
},
|
|
546
|
-
[disabled
|
|
645
|
+
[disabled],
|
|
547
646
|
);
|
|
548
647
|
|
|
549
|
-
|
|
648
|
+
const contextValue = React.useMemo<ChatbarContextValue>(
|
|
649
|
+
() => ({
|
|
650
|
+
open,
|
|
651
|
+
setOpen,
|
|
652
|
+
setValue,
|
|
653
|
+
size,
|
|
654
|
+
expandOn,
|
|
655
|
+
minLines,
|
|
656
|
+
maxLines,
|
|
657
|
+
sendMode,
|
|
658
|
+
disabled,
|
|
659
|
+
readOnly,
|
|
660
|
+
paste,
|
|
661
|
+
clearOnSubmit,
|
|
662
|
+
multiple,
|
|
663
|
+
textareaRef,
|
|
664
|
+
attachments,
|
|
665
|
+
removeAttachment,
|
|
666
|
+
submit,
|
|
667
|
+
appendFiles,
|
|
668
|
+
appendFilesFromPaste,
|
|
669
|
+
openFilePicker,
|
|
670
|
+
beginFilePickerPress,
|
|
671
|
+
}),
|
|
672
|
+
[
|
|
673
|
+
open, setOpen, setValue, size, expandOn, minLines, maxLines, sendMode, disabled, readOnly, paste,
|
|
674
|
+
clearOnSubmit, multiple, attachments, removeAttachment, submit, appendFiles, appendFilesFromPaste,
|
|
675
|
+
openFilePicker, beginFilePickerPress,
|
|
676
|
+
],
|
|
677
|
+
);
|
|
550
678
|
|
|
551
679
|
return (
|
|
552
|
-
<ChatbarContext.Provider
|
|
553
|
-
value={
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
sendMode,
|
|
566
|
-
disabled,
|
|
567
|
-
readOnly,
|
|
568
|
-
onSubmit,
|
|
569
|
-
rootRef,
|
|
570
|
-
textareaRef,
|
|
571
|
-
attachments,
|
|
572
|
-
setAttachments: handleSetAttachments,
|
|
573
|
-
isAttachmentsControlled,
|
|
574
|
-
accept,
|
|
575
|
-
multiple,
|
|
576
|
-
maxAttachments,
|
|
577
|
-
maxFileSize,
|
|
578
|
-
paste,
|
|
579
|
-
pasteAccept,
|
|
580
|
-
clearOnSubmit,
|
|
581
|
-
onAttachmentReject,
|
|
582
|
-
dropzone,
|
|
583
|
-
appendFiles,
|
|
584
|
-
appendFilesFromPaste,
|
|
585
|
-
fileDialogOpenRef,
|
|
586
|
-
}),
|
|
587
|
-
[
|
|
588
|
-
open, handleSetOpen, isOpenControlled,
|
|
589
|
-
value, handleSetValue, isValueControlled,
|
|
590
|
-
size, expandOn, minLines, maxLines, sendMode,
|
|
591
|
-
disabled, readOnly, onSubmit,
|
|
592
|
-
attachments, handleSetAttachments, isAttachmentsControlled,
|
|
593
|
-
accept, multiple, maxAttachments, maxFileSize,
|
|
594
|
-
paste, pasteAccept, clearOnSubmit, onAttachmentReject,
|
|
595
|
-
dropzone, appendFiles, appendFilesFromPaste,
|
|
596
|
-
],
|
|
597
|
-
)}
|
|
598
|
-
>
|
|
599
|
-
<LazyMotion features={domAnimation}>
|
|
600
|
-
<Comp
|
|
601
|
-
{...divProps}
|
|
602
|
-
ref={(node: HTMLDivElement) => {
|
|
603
|
-
if (typeof forwardedRef === 'function') forwardedRef(node);
|
|
604
|
-
else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
605
|
-
(rootRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
606
|
-
}}
|
|
607
|
-
className={classNames('rt-ChatbarRoot', `rt-r-size-${size}`, className)}
|
|
608
|
-
style={{ position: 'relative', width, maxWidth, ...style }}
|
|
609
|
-
data-state={open ? 'open' : 'closed'}
|
|
610
|
-
data-disabled={disabled ? '' : undefined}
|
|
611
|
-
data-readonly={readOnly ? '' : undefined}
|
|
612
|
-
data-drop-active={dropzone && isDragActive ? '' : undefined}
|
|
613
|
-
data-accent-color={color}
|
|
614
|
-
data-radius={radius as any}
|
|
615
|
-
data-panel-background={effectiveMaterial}
|
|
616
|
-
data-material={effectiveMaterial}
|
|
617
|
-
aria-expanded={open}
|
|
618
|
-
onBlurCapture={handleBlurCapture}
|
|
619
|
-
>
|
|
620
|
-
{dropzone && <input {...getInputProps()} />}
|
|
621
|
-
<div {...(dropzone ? getRootProps() : {})} style={{ width: '100%', height: '100%' }} onPointerDown={handleContainerPointerDown}>
|
|
622
|
-
<m.div
|
|
623
|
-
className={classNames('rt-ChatbarBox', `rt-variant-${variant ?? 'surface'}`)}
|
|
624
|
-
style={{ position: 'relative' }}
|
|
680
|
+
<ChatbarContext.Provider value={contextValue}>
|
|
681
|
+
<ChatbarValueContext.Provider value={value}>
|
|
682
|
+
<LazyMotion features={domAnimation}>
|
|
683
|
+
<MotionConfig reducedMotion="user">
|
|
684
|
+
<div
|
|
685
|
+
{...divProps}
|
|
686
|
+
ref={composedRootRef}
|
|
687
|
+
className={classNames('rt-ChatbarRoot', `rt-r-size-${size}`, className)}
|
|
688
|
+
style={{ position: 'relative', width, maxWidth, ...style }}
|
|
689
|
+
data-state={open ? 'open' : 'closed'}
|
|
690
|
+
data-disabled={disabled ? '' : undefined}
|
|
691
|
+
data-readonly={readOnly ? '' : undefined}
|
|
692
|
+
data-drop-active={dragActive ? '' : undefined}
|
|
625
693
|
data-accent-color={color}
|
|
626
|
-
data-radius={radius
|
|
694
|
+
data-radius={radius}
|
|
627
695
|
data-panel-background={effectiveMaterial}
|
|
628
696
|
data-material={effectiveMaterial}
|
|
629
|
-
|
|
630
|
-
transition={{
|
|
631
|
-
layout: {
|
|
632
|
-
type: 'spring',
|
|
633
|
-
visualDuration: 0.15,
|
|
634
|
-
bounce: 0.1,
|
|
635
|
-
},
|
|
636
|
-
}}
|
|
697
|
+
onBlurCapture={handleBlurCapture}
|
|
637
698
|
>
|
|
638
|
-
<
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
699
|
+
<input
|
|
700
|
+
ref={fileInputRef}
|
|
701
|
+
type="file"
|
|
702
|
+
tabIndex={-1}
|
|
703
|
+
aria-hidden
|
|
704
|
+
style={{ display: 'none' }}
|
|
705
|
+
onChange={handleFileInputChange}
|
|
706
|
+
/>
|
|
707
|
+
<div {...(dropzone ? getRootProps() : {})} style={{ width: '100%', height: '100%' }} onPointerDown={handleContainerPointerDown}>
|
|
708
|
+
<m.div
|
|
709
|
+
className={classNames('rt-ChatbarBox', `rt-variant-${variant ?? 'surface'}`)}
|
|
710
|
+
style={{ position: 'relative' }}
|
|
711
|
+
data-accent-color={color}
|
|
712
|
+
data-radius={radius}
|
|
713
|
+
data-panel-background={effectiveMaterial}
|
|
714
|
+
data-material={effectiveMaterial}
|
|
715
|
+
layout
|
|
716
|
+
transition={LAYOUT_TRANSITION}
|
|
717
|
+
>
|
|
718
|
+
<m.div className="rt-ChatbarGrid" layout="position" transition={LAYOUT_TRANSITION}>
|
|
719
|
+
{children}
|
|
720
|
+
</m.div>
|
|
721
|
+
{dragActive && (
|
|
722
|
+
<div className="rt-ChatbarDropOverlay" aria-hidden>
|
|
723
|
+
<div className="rt-ChatbarDropContent">
|
|
724
|
+
<Text color="gray" size={size} weight="medium">
|
|
725
|
+
Drop files here to attach
|
|
726
|
+
</Text>
|
|
727
|
+
</div>
|
|
728
|
+
</div>
|
|
729
|
+
)}
|
|
730
|
+
</m.div>
|
|
731
|
+
</div>
|
|
732
|
+
<VisuallyHidden role="status" aria-live="polite">
|
|
733
|
+
{dragActive ? 'Drop files here to attach.' : <span key={announcement.id}>{announcement.text}</span>}
|
|
734
|
+
</VisuallyHidden>
|
|
735
|
+
</div>
|
|
736
|
+
</MotionConfig>
|
|
737
|
+
</LazyMotion>
|
|
738
|
+
</ChatbarValueContext.Provider>
|
|
664
739
|
</ChatbarContext.Provider>
|
|
665
740
|
);
|
|
666
741
|
});
|
|
@@ -674,15 +749,11 @@ Root.displayName = 'Chatbar.Root';
|
|
|
674
749
|
* - Paste-to-attach: when `paste` is enabled on Root, pasting files adds attachments.
|
|
675
750
|
* - Provide `aria-label` or `aria-labelledby` for an accessible name.
|
|
676
751
|
*/
|
|
677
|
-
interface TextareaProps extends Omit<React.ComponentPropsWithoutRef<'textarea'>, 'size'> {
|
|
752
|
+
interface TextareaProps extends Omit<React.ComponentPropsWithoutRef<'textarea'>, 'size' | 'rows' | 'value' | 'defaultValue'> {
|
|
678
753
|
asChild?: boolean;
|
|
679
754
|
/**
|
|
680
|
-
*
|
|
681
|
-
|
|
682
|
-
onPaste?: React.ClipboardEventHandler<HTMLTextAreaElement>;
|
|
683
|
-
/**
|
|
684
|
-
* When true, pressing Enter submits via onSend (Shift+Enter inserts newline).
|
|
685
|
-
* Defaults to false.
|
|
755
|
+
* When true, pressing Enter submits (Shift+Enter inserts a newline).
|
|
756
|
+
* Works with every `sendMode`. Defaults to false.
|
|
686
757
|
*/
|
|
687
758
|
submitOnEnter?: boolean;
|
|
688
759
|
}
|
|
@@ -691,125 +762,113 @@ interface TextareaProps extends Omit<React.ComponentPropsWithoutRef<'textarea'>,
|
|
|
691
762
|
* Chatbar multi-line text input.
|
|
692
763
|
*
|
|
693
764
|
* Behavior
|
|
694
|
-
* - Controls the Chatbar value via React onChange.
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
* changes via an isomorphic layout effect to avoid SSR warnings.
|
|
765
|
+
* - Controls the Chatbar value via React onChange.
|
|
766
|
+
* - Auto-resizes between minLines and maxLines in a single layout effect, which
|
|
767
|
+
* also opens the Chatbar when content overflows the compact height and
|
|
768
|
+
* `expandOn` is `overflow` or `both`.
|
|
769
|
+
* - Consumer handlers run first; calling `preventDefault` in `onKeyDown` or
|
|
770
|
+
* `onPaste` skips the built-in submit or attach behavior.
|
|
701
771
|
*
|
|
702
772
|
* Accessibility
|
|
703
773
|
* - Consumers should provide labeling via aria-label or aria-labelledby
|
|
704
774
|
* on this component, as no implicit label is rendered.
|
|
705
775
|
*/
|
|
706
776
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, forwardedRef) => {
|
|
707
|
-
const {
|
|
777
|
+
const {
|
|
778
|
+
className,
|
|
779
|
+
style,
|
|
780
|
+
asChild,
|
|
781
|
+
onFocus,
|
|
782
|
+
onChange,
|
|
783
|
+
onPaste,
|
|
784
|
+
onKeyDown,
|
|
785
|
+
onCompositionStart,
|
|
786
|
+
onCompositionEnd,
|
|
787
|
+
submitOnEnter = false,
|
|
788
|
+
...textareaProps
|
|
789
|
+
} = props;
|
|
708
790
|
const ctx = useChatbarContext();
|
|
709
|
-
const
|
|
791
|
+
const value = React.useContext(ChatbarValueContext);
|
|
792
|
+
const { open, minLines, maxLines, expandOn, disabled, readOnly, setOpen, setValue, textareaRef, paste, appendFilesFromPaste, size, submit, clearOnSubmit } = ctx;
|
|
793
|
+
const composedRef = useComposedRefs(forwardedRef, textareaRef);
|
|
710
794
|
|
|
711
795
|
// Cached metrics to avoid repeated getComputedStyle calls
|
|
712
|
-
const lineHeightRef = React.useRef
|
|
713
|
-
const paddingRef = React.useRef
|
|
714
|
-
const
|
|
796
|
+
const lineHeightRef = React.useRef(0);
|
|
797
|
+
const paddingRef = React.useRef(0);
|
|
798
|
+
const composingRef = React.useRef(false);
|
|
799
|
+
|
|
800
|
+
const sizingRef = React.useRef({ open, minLines, maxLines });
|
|
801
|
+
// Declared before the sizing effect so it runs first in the same commit
|
|
802
|
+
useIsomorphicLayoutEffect(() => {
|
|
803
|
+
sizingRef.current = { open, minLines, maxLines };
|
|
804
|
+
}, [open, minLines, maxLines]);
|
|
715
805
|
|
|
716
806
|
const recomputeMetrics = React.useCallback(() => {
|
|
717
807
|
const el = textareaRef.current;
|
|
718
808
|
if (!el) return;
|
|
719
809
|
const computedStyle = window.getComputedStyle(el);
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
const paddingBottom = parseFloat(computedStyle.paddingBottom) || 0;
|
|
723
|
-
lineHeightRef.current = lineHeight;
|
|
724
|
-
paddingRef.current = paddingTop + paddingBottom;
|
|
725
|
-
compactHeightRef.current = Math.ceil(1 * lineHeight) + paddingTop + paddingBottom;
|
|
810
|
+
lineHeightRef.current = parseFloat(computedStyle.lineHeight) || 20;
|
|
811
|
+
paddingRef.current = (parseFloat(computedStyle.paddingTop) || 0) + (parseFloat(computedStyle.paddingBottom) || 0);
|
|
726
812
|
}, [textareaRef]);
|
|
727
813
|
|
|
728
|
-
//
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const isOpen = forceOpen ?? open;
|
|
743
|
-
const effectiveMinLines = isOpen ? minLines : 1;
|
|
744
|
-
const effectiveMaxLines = isOpen ? maxLines : 1;
|
|
745
|
-
|
|
746
|
-
const minHeight = Math.ceil(effectiveMinLines * lineHeight) + padding;
|
|
747
|
-
const maxHeight = Math.ceil(effectiveMaxLines * lineHeight) + padding;
|
|
748
|
-
|
|
749
|
-
const contentHeight = Math.max(textarea.scrollHeight, minHeight);
|
|
750
|
-
const finalHeight = Math.min(contentHeight, maxHeight);
|
|
751
|
-
|
|
752
|
-
textarea.style.height = `${finalHeight}px`;
|
|
753
|
-
|
|
754
|
-
if (contentHeight > maxHeight) {
|
|
755
|
-
textarea.style.overflowY = 'auto';
|
|
756
|
-
textarea.style.maxHeight = `${maxHeight}px`;
|
|
757
|
-
} else {
|
|
758
|
-
textarea.style.overflowY = 'hidden';
|
|
759
|
-
textarea.style.maxHeight = 'none';
|
|
760
|
-
}
|
|
761
|
-
},
|
|
762
|
-
[open, minLines, maxLines, textareaRef, recomputeMetrics],
|
|
763
|
-
);
|
|
814
|
+
// Sizes `el` for the given open state; caller must have set height to 'auto' first
|
|
815
|
+
const applyHeight = React.useCallback((el: HTMLTextAreaElement, isOpen: boolean) => {
|
|
816
|
+
const { minLines: min, maxLines: max } = sizingRef.current;
|
|
817
|
+
const lineHeight = lineHeightRef.current;
|
|
818
|
+
const padding = paddingRef.current;
|
|
819
|
+
const minHeight = Math.ceil((isOpen ? min : 1) * lineHeight) + padding;
|
|
820
|
+
const maxHeight = Math.ceil((isOpen ? max : 1) * lineHeight) + padding;
|
|
821
|
+
const contentHeight = Math.max(el.scrollHeight, minHeight);
|
|
822
|
+
el.style.height = `${Math.min(contentHeight, maxHeight)}px`;
|
|
823
|
+
const overflowing = contentHeight > maxHeight;
|
|
824
|
+
el.style.overflowY = overflowing ? 'auto' : 'hidden';
|
|
825
|
+
el.style.maxHeight = overflowing ? `${maxHeight}px` : 'none';
|
|
826
|
+
}, []);
|
|
764
827
|
|
|
765
|
-
//
|
|
828
|
+
// Typography depends on size
|
|
766
829
|
useIsomorphicLayoutEffect(() => {
|
|
767
|
-
|
|
768
|
-
}, [
|
|
830
|
+
recomputeMetrics();
|
|
831
|
+
}, [recomputeMetrics, size]);
|
|
769
832
|
|
|
770
|
-
//
|
|
833
|
+
// Single place for sizing and overflow auto-open: one height reset, one scrollHeight read
|
|
771
834
|
useIsomorphicLayoutEffect(() => {
|
|
772
|
-
if (!(expandOn === 'overflow' || expandOn === 'both')) return;
|
|
773
|
-
if (open) return;
|
|
774
835
|
const el = textareaRef.current;
|
|
775
836
|
if (!el) return;
|
|
776
|
-
|
|
777
|
-
el
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
// Recompute metrics on mount and when size changes may affect typography
|
|
791
|
-
useIsomorphicLayoutEffect(() => {
|
|
792
|
-
recomputeMetrics();
|
|
793
|
-
updateHeight();
|
|
794
|
-
}, [recomputeMetrics, updateHeight, size]);
|
|
837
|
+
if (lineHeightRef.current === 0) recomputeMetrics();
|
|
838
|
+
withoutPlaceholder(el, () => {
|
|
839
|
+
el.style.height = 'auto';
|
|
840
|
+
let isOpen = open;
|
|
841
|
+
if (!open && (expandOn === 'overflow' || expandOn === 'both')) {
|
|
842
|
+
const compactHeight = Math.ceil(lineHeightRef.current) + paddingRef.current;
|
|
843
|
+
if (el.scrollHeight > compactHeight + 1) {
|
|
844
|
+
isOpen = true;
|
|
845
|
+
setOpen(true);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
applyHeight(el, isOpen);
|
|
849
|
+
});
|
|
850
|
+
}, [value, open, expandOn, minLines, maxLines, size, setOpen, textareaRef, recomputeMetrics, applyHeight]);
|
|
795
851
|
|
|
796
|
-
//
|
|
852
|
+
// Width changes re-wrap text; re-measure only when the width actually changes
|
|
797
853
|
React.useEffect(() => {
|
|
798
854
|
const el = textareaRef.current;
|
|
799
855
|
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
800
|
-
let
|
|
801
|
-
const ro = new ResizeObserver(() => {
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
856
|
+
let prevWidth = -1;
|
|
857
|
+
const ro = new ResizeObserver((entries) => {
|
|
858
|
+
const width = entries[0]?.contentRect.width ?? 0;
|
|
859
|
+
if (width === prevWidth) return;
|
|
860
|
+
const first = prevWidth === -1;
|
|
861
|
+
prevWidth = width;
|
|
862
|
+
if (first) return;
|
|
863
|
+
recomputeMetrics();
|
|
864
|
+
withoutPlaceholder(el, () => {
|
|
865
|
+
el.style.height = 'auto';
|
|
866
|
+
applyHeight(el, sizingRef.current.open);
|
|
867
|
+
});
|
|
809
868
|
});
|
|
810
869
|
ro.observe(el);
|
|
811
870
|
return () => ro.disconnect();
|
|
812
|
-
}, [textareaRef, recomputeMetrics,
|
|
871
|
+
}, [textareaRef, recomputeMetrics, applyHeight]);
|
|
813
872
|
|
|
814
873
|
// Dev-only warning if no accessible name is provided
|
|
815
874
|
React.useEffect(() => {
|
|
@@ -822,134 +881,82 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, fo
|
|
|
822
881
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
823
882
|
}, []);
|
|
824
883
|
|
|
825
|
-
// Note: No MutationObserver is used because <textarea> value changes are not
|
|
826
|
-
// reflected in DOM text nodes. Height updates are handled by effects and events.
|
|
827
|
-
|
|
828
884
|
const handleFocus = React.useCallback<React.FocusEventHandler<HTMLTextAreaElement>>(
|
|
829
885
|
(event) => {
|
|
886
|
+
onFocus?.(event);
|
|
830
887
|
if (disabled || readOnly) return;
|
|
831
888
|
if ((expandOn === 'focus' || expandOn === 'both') && !open) setOpen(true);
|
|
832
|
-
onFocus?.(event);
|
|
833
889
|
},
|
|
834
890
|
[disabled, readOnly, expandOn, open, setOpen, onFocus],
|
|
835
891
|
);
|
|
836
892
|
|
|
837
893
|
const handleChange = React.useCallback<React.ChangeEventHandler<HTMLTextAreaElement>>(
|
|
838
894
|
(event) => {
|
|
839
|
-
|
|
840
|
-
const nextValue = el.value;
|
|
841
|
-
setValue(nextValue);
|
|
842
|
-
|
|
843
|
-
if ((expandOn === 'overflow' || expandOn === 'both') && !open) {
|
|
844
|
-
el.style.height = 'auto';
|
|
845
|
-
|
|
846
|
-
if (compactHeightRef.current === 0) {
|
|
847
|
-
recomputeMetrics();
|
|
848
|
-
}
|
|
849
|
-
const shouldExpand = el.scrollHeight > compactHeightRef.current + 1;
|
|
850
|
-
if (shouldExpand) {
|
|
851
|
-
setOpen(true);
|
|
852
|
-
// Immediately size for open state to avoid 1-line + scrollbar flash
|
|
853
|
-
updateHeight(true);
|
|
854
|
-
requestAnimationFrame(() => updateHeight(true));
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Always recalc after any input
|
|
859
|
-
requestAnimationFrame(() => updateHeight());
|
|
895
|
+
setValue(event.currentTarget.value);
|
|
860
896
|
onChange?.(event);
|
|
861
897
|
},
|
|
862
|
-
[
|
|
898
|
+
[setValue, onChange],
|
|
863
899
|
);
|
|
864
900
|
|
|
865
901
|
const handlePaste = React.useCallback<React.ClipboardEventHandler<HTMLTextAreaElement>>(
|
|
866
902
|
(event) => {
|
|
867
|
-
// Attach files from clipboard if enabled
|
|
868
|
-
if (paste) {
|
|
869
|
-
const items = Array.from(event.clipboardData?.items ?? []);
|
|
870
|
-
const files = items
|
|
871
|
-
.filter((i) => i.kind === 'file')
|
|
872
|
-
.map((i) => i.getAsFile())
|
|
873
|
-
.filter((f): f is File => !!f);
|
|
874
|
-
if (files.length > 0) {
|
|
875
|
-
// Prevent pasting the file name or any text representation when files are present
|
|
876
|
-
event.preventDefault();
|
|
877
|
-
appendFilesFromPaste(files);
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
setTimeout(() => {
|
|
881
|
-
// If pasting in compact mode, force sizing as open if content overflowed
|
|
882
|
-
if (!open) {
|
|
883
|
-
updateHeight(true);
|
|
884
|
-
} else {
|
|
885
|
-
updateHeight();
|
|
886
|
-
}
|
|
887
|
-
}, 0);
|
|
888
903
|
onPaste?.(event);
|
|
904
|
+
if (event.defaultPrevented || !paste || disabled || readOnly) return;
|
|
905
|
+
const files = Array.from(event.clipboardData?.items ?? [])
|
|
906
|
+
.filter((item) => item.kind === 'file')
|
|
907
|
+
.map((item) => item.getAsFile())
|
|
908
|
+
.filter((f): f is File => !!f);
|
|
909
|
+
if (files.length === 0) return;
|
|
910
|
+
// Suppress the filename/text representation only when a file was actually attached
|
|
911
|
+
if (appendFilesFromPaste(files) > 0) event.preventDefault();
|
|
912
|
+
},
|
|
913
|
+
[paste, disabled, readOnly, onPaste, appendFilesFromPaste],
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
const handleCompositionStart = React.useCallback<React.CompositionEventHandler<HTMLTextAreaElement>>(
|
|
917
|
+
(event) => {
|
|
918
|
+
composingRef.current = true;
|
|
919
|
+
onCompositionStart?.(event);
|
|
920
|
+
},
|
|
921
|
+
[onCompositionStart],
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
const handleCompositionEnd = React.useCallback<React.CompositionEventHandler<HTMLTextAreaElement>>(
|
|
925
|
+
(event) => {
|
|
926
|
+
composingRef.current = false;
|
|
927
|
+
onCompositionEnd?.(event);
|
|
889
928
|
},
|
|
890
|
-
[
|
|
929
|
+
[onCompositionEnd],
|
|
891
930
|
);
|
|
892
931
|
|
|
893
932
|
const handleKeyDown = React.useCallback<React.KeyboardEventHandler<HTMLTextAreaElement>>(
|
|
894
933
|
(event) => {
|
|
895
|
-
if (!submitOnEnter) {
|
|
896
|
-
onKeyDown?.(event);
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && !event.nativeEvent.isComposing) {
|
|
900
|
-
if (disabled || readOnly) {
|
|
901
|
-
onKeyDown?.(event);
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
if (sendMode === 'never') {
|
|
905
|
-
onKeyDown?.(event);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
const trimmed = value.trim();
|
|
909
|
-
const hasContent = trimmed.length > 0 || ctx.attachments.length > 0;
|
|
910
|
-
if (sendMode === 'whenDirty' && !hasContent) {
|
|
911
|
-
onKeyDown?.(event);
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
event.preventDefault();
|
|
915
|
-
ctx.onSubmit?.({ value, attachments: ctx.attachments });
|
|
916
|
-
if (ctx.clearOnSubmit) {
|
|
917
|
-
if (!isValueControlled) setValue('');
|
|
918
|
-
ctx.setAttachments([]);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
934
|
onKeyDown?.(event);
|
|
935
|
+
if (event.defaultPrevented || !submitOnEnter) return;
|
|
936
|
+
if (event.key !== 'Enter' || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
937
|
+
// Safari fires the IME-confirming Enter after compositionend with keyCode 229
|
|
938
|
+
if (composingRef.current || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
|
|
939
|
+
// Plain Enter never inserts a newline when submitOnEnter is on, even with nothing to send
|
|
940
|
+
event.preventDefault();
|
|
941
|
+
submit(clearOnSubmit);
|
|
922
942
|
},
|
|
923
|
-
[submitOnEnter,
|
|
943
|
+
[submitOnEnter, onKeyDown, submit, clearOnSubmit],
|
|
924
944
|
);
|
|
925
945
|
|
|
926
|
-
const Comp = asChild ? Slot :
|
|
946
|
+
const Comp = asChild ? Slot : 'textarea';
|
|
927
947
|
return (
|
|
928
|
-
<m.div
|
|
929
|
-
className={classNames('rt-ChatbarField', 'rt-ChatbarTextarea', className)}
|
|
930
|
-
layout="position"
|
|
931
|
-
transition={{
|
|
932
|
-
layout: {
|
|
933
|
-
type: 'spring',
|
|
934
|
-
visualDuration: 0.15,
|
|
935
|
-
bounce: 0.1,
|
|
936
|
-
},
|
|
937
|
-
}}
|
|
938
|
-
>
|
|
948
|
+
<m.div className={classNames('rt-ChatbarField', 'rt-ChatbarTextarea', className)} layout="position" transition={LAYOUT_TRANSITION}>
|
|
939
949
|
<Comp
|
|
940
950
|
{...textareaProps}
|
|
941
|
-
ref={
|
|
942
|
-
if (typeof forwardedRef === 'function') forwardedRef(node);
|
|
943
|
-
else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
|
|
944
|
-
(textareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
|
|
945
|
-
}}
|
|
951
|
+
ref={composedRef}
|
|
946
952
|
className="rt-ChatbarInput"
|
|
947
953
|
value={value}
|
|
948
|
-
onInput={onInput}
|
|
949
954
|
onChange={handleChange}
|
|
950
955
|
onFocus={handleFocus}
|
|
951
956
|
onPaste={handlePaste}
|
|
952
957
|
onKeyDown={handleKeyDown}
|
|
958
|
+
onCompositionStart={handleCompositionStart}
|
|
959
|
+
onCompositionEnd={handleCompositionEnd}
|
|
953
960
|
disabled={disabled}
|
|
954
961
|
readOnly={readOnly}
|
|
955
962
|
rows={open ? minLines : 1}
|
|
@@ -968,12 +975,12 @@ interface InlineSlotProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'c
|
|
|
968
975
|
}
|
|
969
976
|
|
|
970
977
|
const InlineStart = React.forwardRef<HTMLDivElement, InlineSlotProps>((props, forwardedRef) => {
|
|
971
|
-
const { children, asChild,
|
|
978
|
+
const { children, asChild, className, ...divProps } = props;
|
|
972
979
|
const ctx = useChatbarContext();
|
|
973
980
|
if (ctx.open) return null;
|
|
974
|
-
const Comp = asChild ? Slot :
|
|
981
|
+
const Comp = asChild ? Slot : 'div';
|
|
975
982
|
return (
|
|
976
|
-
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarInlineStart', className)}
|
|
983
|
+
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarInlineStart', className)}>
|
|
977
984
|
{children}
|
|
978
985
|
</Comp>
|
|
979
986
|
);
|
|
@@ -981,13 +988,12 @@ const InlineStart = React.forwardRef<HTMLDivElement, InlineSlotProps>((props, fo
|
|
|
981
988
|
InlineStart.displayName = 'Chatbar.InlineStart';
|
|
982
989
|
|
|
983
990
|
const InlineEnd = React.forwardRef<HTMLDivElement, InlineSlotProps>((props, forwardedRef) => {
|
|
984
|
-
const { children, asChild,
|
|
991
|
+
const { children, asChild, className, ...divProps } = props;
|
|
985
992
|
const ctx = useChatbarContext();
|
|
986
|
-
// Use CSS to hide when open instead of unmounting to avoid layout animation scale
|
|
987
993
|
if (ctx.open) return null;
|
|
988
|
-
const Comp = asChild ? Slot :
|
|
994
|
+
const Comp = asChild ? Slot : 'div';
|
|
989
995
|
return (
|
|
990
|
-
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarInlineEnd', className)}
|
|
996
|
+
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarInlineEnd', className)}>
|
|
991
997
|
{children}
|
|
992
998
|
</Comp>
|
|
993
999
|
);
|
|
@@ -1006,25 +1012,15 @@ interface AttachmentsRowProps extends Omit<React.ComponentPropsWithoutRef<'div'>
|
|
|
1006
1012
|
}
|
|
1007
1013
|
|
|
1008
1014
|
const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((props, forwardedRef) => {
|
|
1009
|
-
const { asChild, forceMount, renderAttachment, className,
|
|
1015
|
+
const { asChild, forceMount, renderAttachment, className, ...divProps } = props;
|
|
1010
1016
|
const ctx = useChatbarContext();
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const Comp = asChild ? Slot : ('div' as any);
|
|
1017
|
+
if (ctx.attachments.length === 0 && !forceMount) return null;
|
|
1018
|
+
const Comp = asChild ? Slot : 'div';
|
|
1014
1019
|
return (
|
|
1015
|
-
<m.div
|
|
1016
|
-
|
|
1017
|
-
transition={{
|
|
1018
|
-
layout: {
|
|
1019
|
-
type: 'spring',
|
|
1020
|
-
visualDuration: 0.15,
|
|
1021
|
-
bounce: 0.1,
|
|
1022
|
-
},
|
|
1023
|
-
}}
|
|
1024
|
-
>
|
|
1025
|
-
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarAttachmentsRow', className)} style={style} role="list" aria-label={divProps['aria-label'] ?? 'Attachments'}>
|
|
1020
|
+
<m.div layout="position" transition={LAYOUT_TRANSITION}>
|
|
1021
|
+
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarAttachmentsRow', className)} role="list" aria-label={divProps['aria-label'] ?? 'Attachments'}>
|
|
1026
1022
|
<ScrollArea className="rt-ChatbarScrollArea" scrollbars="horizontal" size="1">
|
|
1027
|
-
<Flex align="center" gap="2"
|
|
1023
|
+
<Flex align="center" gap="2" minWidth="fit-content">
|
|
1028
1024
|
{ctx.attachments.map((att) => (
|
|
1029
1025
|
<Attachment key={att.id} attachment={att} asChild={!!renderAttachment}>
|
|
1030
1026
|
{renderAttachment?.(att)}
|
|
@@ -1044,43 +1040,74 @@ interface AttachmentProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
1044
1040
|
asChild?: boolean;
|
|
1045
1041
|
}
|
|
1046
1042
|
|
|
1043
|
+
const STATUS_LABEL: Partial<Record<AttachmentStatus, string>> = { uploading: 'uploading', error: 'failed to upload' };
|
|
1044
|
+
|
|
1047
1045
|
const Attachment = React.forwardRef<HTMLDivElement, AttachmentProps>((props, forwardedRef) => {
|
|
1048
|
-
const { attachment, asChild, className,
|
|
1046
|
+
const { attachment, asChild, className, children, ...divProps } = props;
|
|
1049
1047
|
const ctx = useChatbarContext();
|
|
1050
|
-
const Comp = asChild ? Slot :
|
|
1051
|
-
const isImage = !!attachment.url && attachment.type
|
|
1048
|
+
const Comp = asChild ? Slot : 'div';
|
|
1049
|
+
const isImage = !!attachment.url && isImageLike(attachment.name, attachment.type);
|
|
1050
|
+
const sizeLabel = `${Math.ceil(attachment.size / 1024)} KB`;
|
|
1051
|
+
const statusLabel = attachment.status ? STATUS_LABEL[attachment.status] : undefined;
|
|
1052
|
+
const locked = ctx.disabled || ctx.readOnly;
|
|
1053
|
+
|
|
1054
|
+
const handleRemove = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1055
|
+
// Move focus before the tile unmounts so keyboard users are not dropped to <body>
|
|
1056
|
+
const tile = event.currentTarget.closest('[role="listitem"]');
|
|
1057
|
+
const tiles = tile?.parentElement ? Array.from(tile.parentElement.children) : [];
|
|
1058
|
+
const index = tile ? tiles.indexOf(tile) : -1;
|
|
1059
|
+
const neighbour = tiles[index + 1] ?? tiles[index - 1];
|
|
1060
|
+
const nextButton = neighbour?.querySelector<HTMLButtonElement>('.rt-ChatbarAttachmentRemove');
|
|
1061
|
+
if (nextButton) nextButton.focus();
|
|
1062
|
+
else ctx.textareaRef.current?.focus({ preventScroll: true });
|
|
1063
|
+
ctx.removeAttachment(attachment.id);
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1052
1066
|
return (
|
|
1053
|
-
<Comp
|
|
1067
|
+
<Comp
|
|
1068
|
+
{...divProps}
|
|
1069
|
+
ref={forwardedRef}
|
|
1070
|
+
className={classNames('rt-ChatbarAttachment', className)}
|
|
1071
|
+
role="listitem"
|
|
1072
|
+
data-kind={isImage ? 'image' : 'file'}
|
|
1073
|
+
data-status={attachment.status}
|
|
1074
|
+
title={attachment.name}
|
|
1075
|
+
>
|
|
1054
1076
|
{children ?? (
|
|
1055
|
-
// <Card size={ctx.size} variant="surface">
|
|
1056
1077
|
<Flex align="center" gap="2" pr={!isImage ? '6' : undefined}>
|
|
1057
1078
|
<div className="rt-ChatbarAttachmentPreview" aria-hidden>
|
|
1058
1079
|
{isImage ? <img className="rt-ChatbarAttachmentImage" src={attachment.url} alt="" /> : <FileTextIcon />}
|
|
1059
1080
|
</div>
|
|
1060
|
-
{
|
|
1061
|
-
<
|
|
1062
|
-
|
|
1081
|
+
{isImage ? (
|
|
1082
|
+
<VisuallyHidden>
|
|
1083
|
+
{attachment.name}, {sizeLabel}
|
|
1084
|
+
{statusLabel ? `, ${statusLabel}` : ''}
|
|
1085
|
+
</VisuallyHidden>
|
|
1086
|
+
) : (
|
|
1087
|
+
<Flex direction="column" gap="0" minWidth="0">
|
|
1088
|
+
<Text size={ctx.size} weight="medium" truncate>
|
|
1063
1089
|
{attachment.name}
|
|
1064
1090
|
</Text>
|
|
1065
|
-
<Text size="1" color=
|
|
1066
|
-
{
|
|
1091
|
+
<Text size="1" color={attachment.status === 'error' ? 'red' : 'gray'}>
|
|
1092
|
+
{sizeLabel}
|
|
1093
|
+
{statusLabel ? ` · ${statusLabel}` : ''}
|
|
1067
1094
|
</Text>
|
|
1068
1095
|
</Flex>
|
|
1069
1096
|
)}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1097
|
+
{!locked && (
|
|
1098
|
+
<IconButton
|
|
1099
|
+
className="rt-ChatbarAttachmentRemove"
|
|
1100
|
+
aria-label={`Remove ${attachment.name}`}
|
|
1101
|
+
size="1"
|
|
1102
|
+
variant="classic"
|
|
1103
|
+
highContrast
|
|
1104
|
+
color="gray"
|
|
1105
|
+
onClick={handleRemove}
|
|
1106
|
+
>
|
|
1107
|
+
<CloseIcon />
|
|
1108
|
+
</IconButton>
|
|
1109
|
+
)}
|
|
1082
1110
|
</Flex>
|
|
1083
|
-
// </Card>
|
|
1084
1111
|
)}
|
|
1085
1112
|
</Comp>
|
|
1086
1113
|
);
|
|
@@ -1089,99 +1116,51 @@ Attachment.displayName = 'Chatbar.Attachment';
|
|
|
1089
1116
|
|
|
1090
1117
|
interface AttachTriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
1091
1118
|
asChild?: boolean;
|
|
1119
|
+
/** Overrides Root `accept` for files chosen through this trigger */
|
|
1092
1120
|
accept?: string | string[];
|
|
1093
1121
|
multiple?: boolean;
|
|
1094
1122
|
}
|
|
1095
1123
|
|
|
1096
1124
|
const AttachTrigger = React.forwardRef<HTMLButtonElement, AttachTriggerProps>((props, forwardedRef) => {
|
|
1097
|
-
const { asChild, accept, multiple,
|
|
1125
|
+
const { asChild, accept, multiple, disabled: disabledProp, onPointerDown, onClick, ...buttonProps } = props;
|
|
1098
1126
|
const ctx = useChatbarContext();
|
|
1099
|
-
const
|
|
1100
|
-
const Comp = asChild ? Slot :
|
|
1101
|
-
// Prefer Chatbar.Root's accept when a local accept is not provided
|
|
1102
|
-
const mergedAccept = accept ?? ctx.accept;
|
|
1103
|
-
const actualAccept = (Array.isArray(mergedAccept) ? mergedAccept : (mergedAccept?.split(',') ?? [])).join(',');
|
|
1104
|
-
React.useEffect(() => {
|
|
1105
|
-
const handleWindowFocus = () => {
|
|
1106
|
-
// Reset guard when window regains focus after file dialog closes
|
|
1107
|
-
ctx.fileDialogOpenRef.current = false;
|
|
1108
|
-
};
|
|
1109
|
-
window.addEventListener('focus', handleWindowFocus);
|
|
1110
|
-
return () => window.removeEventListener('focus', handleWindowFocus);
|
|
1111
|
-
}, [ctx.fileDialogOpenRef]);
|
|
1127
|
+
const disabled = disabledProp || ctx.disabled || ctx.readOnly;
|
|
1128
|
+
const Comp = asChild ? Slot : 'button';
|
|
1112
1129
|
return (
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
onPointerDown
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
}}
|
|
1131
|
-
onClick={(e: any) => {
|
|
1132
|
-
// Ensure file input opens reliably by clicking it first
|
|
1133
|
-
ctx.fileDialogOpenRef.current = true;
|
|
1134
|
-
if (inputRef.current) {
|
|
1135
|
-
inputRef.current.click();
|
|
1136
|
-
}
|
|
1137
|
-
// Then call user's onClick if provided
|
|
1138
|
-
buttonProps.onClick?.(e);
|
|
1139
|
-
}}
|
|
1140
|
-
/>
|
|
1141
|
-
<input
|
|
1142
|
-
ref={inputRef}
|
|
1143
|
-
type="file"
|
|
1144
|
-
accept={actualAccept}
|
|
1145
|
-
multiple={multiple ?? ctx.multiple}
|
|
1146
|
-
tabIndex={-1}
|
|
1147
|
-
style={{ display: 'none' }}
|
|
1148
|
-
onChange={(e) => {
|
|
1149
|
-
const files = Array.from(e.currentTarget.files ?? []);
|
|
1150
|
-
if (files.length > 0) {
|
|
1151
|
-
ctx.appendFiles(files);
|
|
1152
|
-
}
|
|
1153
|
-
// File dialog closed; allow normal blur handling
|
|
1154
|
-
ctx.fileDialogOpenRef.current = false;
|
|
1155
|
-
// Reset input value to allow selecting the same file again
|
|
1156
|
-
e.currentTarget.value = '';
|
|
1157
|
-
}}
|
|
1158
|
-
/>
|
|
1159
|
-
</>
|
|
1130
|
+
<Comp
|
|
1131
|
+
{...buttonProps}
|
|
1132
|
+
ref={forwardedRef}
|
|
1133
|
+
className={classNames('rt-ChatbarAttachTrigger', buttonProps.className)}
|
|
1134
|
+
type={buttonProps.type ?? 'button'}
|
|
1135
|
+
aria-label={buttonProps['aria-label'] ?? 'Add attachments'}
|
|
1136
|
+
disabled={disabled}
|
|
1137
|
+
onPointerDown={(event: React.PointerEvent<HTMLButtonElement>) => {
|
|
1138
|
+
onPointerDown?.(event);
|
|
1139
|
+
if (!disabled && !event.defaultPrevented) ctx.beginFilePickerPress();
|
|
1140
|
+
}}
|
|
1141
|
+
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1142
|
+
onClick?.(event);
|
|
1143
|
+
if (disabled || event.defaultPrevented) return;
|
|
1144
|
+
ctx.openFilePicker({ accept, multiple });
|
|
1145
|
+
}}
|
|
1146
|
+
/>
|
|
1160
1147
|
);
|
|
1161
1148
|
});
|
|
1162
1149
|
AttachTrigger.displayName = 'Chatbar.AttachTrigger';
|
|
1150
|
+
|
|
1163
1151
|
interface RowProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
1164
1152
|
asChild?: boolean;
|
|
1165
1153
|
children?: React.ReactNode;
|
|
1166
1154
|
}
|
|
1167
1155
|
|
|
1168
1156
|
const Row = React.forwardRef<HTMLDivElement, RowProps>((props, forwardedRef) => {
|
|
1169
|
-
const { asChild, children, className,
|
|
1157
|
+
const { asChild, children, className, ...divProps } = props;
|
|
1170
1158
|
const ctx = useChatbarContext();
|
|
1171
1159
|
if (!ctx.open) return null;
|
|
1172
|
-
const Comp = asChild ? Slot :
|
|
1160
|
+
const Comp = asChild ? Slot : 'div';
|
|
1173
1161
|
return (
|
|
1174
|
-
<m.div
|
|
1175
|
-
|
|
1176
|
-
transition={{
|
|
1177
|
-
layout: {
|
|
1178
|
-
type: 'spring',
|
|
1179
|
-
visualDuration: 0.15,
|
|
1180
|
-
bounce: 0.1,
|
|
1181
|
-
},
|
|
1182
|
-
}}
|
|
1183
|
-
>
|
|
1184
|
-
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRow', className)} style={style}>
|
|
1162
|
+
<m.div layout="position" transition={LAYOUT_TRANSITION}>
|
|
1163
|
+
<Comp {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRow', className)}>
|
|
1185
1164
|
<Flex align="center" justify="between" width="100%">
|
|
1186
1165
|
{children}
|
|
1187
1166
|
</Flex>
|
|
@@ -1191,15 +1170,17 @@ const Row = React.forwardRef<HTMLDivElement, RowProps>((props, forwardedRef) =>
|
|
|
1191
1170
|
});
|
|
1192
1171
|
Row.displayName = 'Chatbar.Row';
|
|
1193
1172
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1173
|
+
type RowSlotProps = React.ComponentPropsWithoutRef<'div'>;
|
|
1174
|
+
|
|
1175
|
+
const RowStart = React.forwardRef<HTMLDivElement, RowSlotProps>((props, forwardedRef) => {
|
|
1176
|
+
const { className, ...divProps } = props;
|
|
1177
|
+
return <div {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRowStart', className)} />;
|
|
1197
1178
|
});
|
|
1198
1179
|
RowStart.displayName = 'Chatbar.RowStart';
|
|
1199
1180
|
|
|
1200
|
-
const RowEnd = React.forwardRef<HTMLDivElement,
|
|
1201
|
-
const { className,
|
|
1202
|
-
return <div {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRowEnd', className)}
|
|
1181
|
+
const RowEnd = React.forwardRef<HTMLDivElement, RowSlotProps>((props, forwardedRef) => {
|
|
1182
|
+
const { className, ...divProps } = props;
|
|
1183
|
+
return <div {...divProps} ref={forwardedRef} className={classNames('rt-ChatbarRowEnd', className)} />;
|
|
1203
1184
|
});
|
|
1204
1185
|
RowEnd.displayName = 'Chatbar.RowEnd';
|
|
1205
1186
|
|
|
@@ -1208,59 +1189,65 @@ type SendProps = Omit<IconButtonProps, 'aria-label' | 'aria-labelledby'> & {
|
|
|
1208
1189
|
'aria-label'?: string;
|
|
1209
1190
|
'aria-labelledby'?: string;
|
|
1210
1191
|
asChild?: boolean;
|
|
1192
|
+
/** Clear message and attachments after sending. Defaults to Root `clearOnSubmit`. */
|
|
1211
1193
|
clearOnSend?: boolean;
|
|
1212
1194
|
};
|
|
1213
1195
|
|
|
1214
1196
|
const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef) => {
|
|
1215
|
-
const {
|
|
1197
|
+
const {
|
|
1198
|
+
asChild,
|
|
1199
|
+
clearOnSend,
|
|
1200
|
+
disabled,
|
|
1201
|
+
children,
|
|
1202
|
+
style,
|
|
1203
|
+
className,
|
|
1204
|
+
size: sizeProp,
|
|
1205
|
+
variant: variantProp,
|
|
1206
|
+
tabIndex,
|
|
1207
|
+
onClick,
|
|
1208
|
+
'aria-label': ariaLabel,
|
|
1209
|
+
'aria-labelledby': ariaLabelledby,
|
|
1210
|
+
...buttonProps
|
|
1211
|
+
} = props;
|
|
1216
1212
|
const ctx = useChatbarContext();
|
|
1213
|
+
const value = React.useContext(ChatbarValueContext);
|
|
1217
1214
|
|
|
1218
|
-
const trimmed = ctx.value.trim();
|
|
1219
|
-
const hasContent = trimmed.length > 0 || ctx.attachments.length > 0;
|
|
1220
|
-
const visible = ctx.sendMode === 'always' || (ctx.sendMode === 'whenDirty' && hasContent);
|
|
1221
1215
|
if (ctx.sendMode === 'never') return null;
|
|
1222
1216
|
|
|
1217
|
+
const hasContent = hasText(value) || ctx.attachments.length > 0;
|
|
1218
|
+
const visible = ctx.sendMode === 'always' || hasContent;
|
|
1219
|
+
const inactive = disabled || ctx.disabled || ctx.readOnly || !hasContent;
|
|
1220
|
+
|
|
1221
|
+
const labelProps: { 'aria-label': string } | { 'aria-labelledby': string } = ariaLabelledby
|
|
1222
|
+
? { 'aria-labelledby': ariaLabelledby }
|
|
1223
|
+
: { 'aria-label': ariaLabel ?? 'Send' };
|
|
1224
|
+
|
|
1223
1225
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
buttonProps.onClick?.(event);
|
|
1226
|
+
onClick?.(event);
|
|
1227
|
+
if (event.defaultPrevented || inactive) return;
|
|
1228
|
+
const clear = clearOnSend ?? ctx.clearOnSubmit;
|
|
1229
|
+
// Clearing hides and disables this button; keep keyboard focus in the composer
|
|
1230
|
+
if (ctx.submit(clear) && clear) ctx.textareaRef.current?.focus({ preventScroll: true });
|
|
1231
1231
|
};
|
|
1232
1232
|
|
|
1233
1233
|
return (
|
|
1234
1234
|
<IconButton
|
|
1235
|
-
{...
|
|
1236
|
-
ref={forwardedRef
|
|
1235
|
+
{...buttonProps}
|
|
1236
|
+
ref={forwardedRef}
|
|
1237
1237
|
size={sizeProp ?? ctx.size}
|
|
1238
1238
|
variant={variantProp ?? (ctx.open ? 'solid' : 'ghost')}
|
|
1239
|
-
disabled={
|
|
1239
|
+
disabled={inactive}
|
|
1240
1240
|
className={classNames('rt-ChatbarSend', className)}
|
|
1241
|
-
style={{
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
}}
|
|
1241
|
+
style={{ opacity: visible ? 1 : 0, pointerEvents: visible ? undefined : 'none', ...style }}
|
|
1242
|
+
// Hidden Send must not be reachable by keyboard or announced
|
|
1243
|
+
aria-hidden={visible ? undefined : true}
|
|
1244
|
+
tabIndex={visible ? tabIndex : -1}
|
|
1246
1245
|
asChild={asChild}
|
|
1247
1246
|
onClick={handleClick}
|
|
1248
|
-
|
|
1249
|
-
aria-labelledby={ariaLabelledby}
|
|
1247
|
+
{...labelProps}
|
|
1250
1248
|
>
|
|
1251
1249
|
{children ?? (
|
|
1252
|
-
<svg
|
|
1253
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
1254
|
-
width="24"
|
|
1255
|
-
height="24"
|
|
1256
|
-
viewBox="0 0 24 24"
|
|
1257
|
-
fill="none"
|
|
1258
|
-
stroke="currentColor"
|
|
1259
|
-
strokeWidth="2"
|
|
1260
|
-
strokeLinecap="round"
|
|
1261
|
-
strokeLinejoin="round"
|
|
1262
|
-
className="lucide lucide-arrow-right-icon lucide-arrow-right"
|
|
1263
|
-
>
|
|
1250
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
|
|
1264
1251
|
<path d="M5 12h14" />
|
|
1265
1252
|
<path d="m12 5 7 7-7 7" />
|
|
1266
1253
|
</svg>
|
|
@@ -1271,4 +1258,21 @@ const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef
|
|
|
1271
1258
|
Send.displayName = 'Chatbar.Send';
|
|
1272
1259
|
|
|
1273
1260
|
export { Root, Textarea, InlineStart, InlineEnd, AttachmentsRow, Attachment, AttachTrigger, Row, RowStart, RowEnd, Send };
|
|
1274
|
-
export type {
|
|
1261
|
+
export type {
|
|
1262
|
+
RootProps as ChatbarRootProps,
|
|
1263
|
+
TextareaProps as ChatbarTextareaProps,
|
|
1264
|
+
InlineSlotProps as ChatbarInlineSlotProps,
|
|
1265
|
+
AttachmentsRowProps as ChatbarAttachmentsRowProps,
|
|
1266
|
+
AttachmentProps as ChatbarAttachmentProps,
|
|
1267
|
+
AttachTriggerProps as ChatbarAttachTriggerProps,
|
|
1268
|
+
RowProps as ChatbarRowProps,
|
|
1269
|
+
RowSlotProps as ChatbarRowSlotProps,
|
|
1270
|
+
SendProps as ChatbarSendProps,
|
|
1271
|
+
ChatbarAttachment,
|
|
1272
|
+
ChatbarAttachmentRejection,
|
|
1273
|
+
ChatbarRejectReason,
|
|
1274
|
+
ChatbarSubmitPayload,
|
|
1275
|
+
AttachmentStatus as ChatbarAttachmentStatus,
|
|
1276
|
+
ExpandOn as ChatbarExpandOn,
|
|
1277
|
+
SendMode as ChatbarSendMode,
|
|
1278
|
+
};
|