@artooi/agents-web-components 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +4397 -2
- package/dist/agents-web-components.bundle.js +5708 -0
- package/dist/agents-web-components.bundle.js.map +7 -0
- package/dist/bundle.d.ts +3 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/chat.d.ts +3 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +16373 -0
- package/dist/chat.js.map +7 -0
- package/dist/chunk-ARNLFGCC.js +7715 -0
- package/dist/chunk-ARNLFGCC.js.map +7 -0
- package/dist/constants.d.ts +575 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/core/activity_registration.d.ts +19 -0
- package/dist/core/activity_registration.d.ts.map +1 -0
- package/dist/core/activity_registry.d.ts +46 -0
- package/dist/core/activity_registry.d.ts.map +1 -0
- package/dist/core/activity_renderer.d.ts +20 -0
- package/dist/core/activity_renderer.d.ts.map +1 -0
- package/dist/core/ag_ui_chat.d.ts +836 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -0
- package/dist/core/agui_client.d.ts +370 -0
- package/dist/core/agui_client.d.ts.map +1 -0
- package/dist/core/attachment.d.ts +42 -0
- package/dist/core/attachment.d.ts.map +1 -0
- package/dist/core/chat_registry.d.ts +23 -0
- package/dist/core/chat_registry.d.ts.map +1 -0
- package/dist/core/client_seed.d.ts +27 -0
- package/dist/core/client_seed.d.ts.map +1 -0
- package/dist/core/conversation_store.d.ts +195 -0
- package/dist/core/conversation_store.d.ts.map +1 -0
- package/dist/core/create_http_agent.d.ts +57 -0
- package/dist/core/create_http_agent.d.ts.map +1 -0
- package/dist/core/define_chat.d.ts +13 -0
- package/dist/core/define_chat.d.ts.map +1 -0
- package/dist/core/events/attachments_detail.d.ts +9 -0
- package/dist/core/events/attachments_detail.d.ts.map +1 -0
- package/dist/core/events/bar_height_detail.d.ts +6 -0
- package/dist/core/events/bar_height_detail.d.ts.map +1 -0
- package/dist/core/events/custom_agent_detail.d.ts +8 -0
- package/dist/core/events/custom_agent_detail.d.ts.map +1 -0
- package/dist/core/events/dock_width_detail.d.ts +8 -0
- package/dist/core/events/dock_width_detail.d.ts.map +1 -0
- package/dist/core/events/feedback_detail.d.ts +28 -0
- package/dist/core/events/feedback_detail.d.ts.map +1 -0
- package/dist/core/events/invalidate_detail.d.ts +16 -0
- package/dist/core/events/invalidate_detail.d.ts.map +1 -0
- package/dist/core/events/maximize_detail.d.ts +5 -0
- package/dist/core/events/maximize_detail.d.ts.map +1 -0
- package/dist/core/events/run_finished_detail.d.ts +36 -0
- package/dist/core/events/run_finished_detail.d.ts.map +1 -0
- package/dist/core/events/state_detail.d.ts +5 -0
- package/dist/core/events/state_detail.d.ts.map +1 -0
- package/dist/core/events/submit_detail.d.ts +8 -0
- package/dist/core/events/submit_detail.d.ts.map +1 -0
- package/dist/core/events/thread_detail.d.ts +27 -0
- package/dist/core/events/thread_detail.d.ts.map +1 -0
- package/dist/core/events/thread_title_detail.d.ts +8 -0
- package/dist/core/events/thread_title_detail.d.ts.map +1 -0
- package/dist/core/events/toggle_detail.d.ts +5 -0
- package/dist/core/events/toggle_detail.d.ts.map +1 -0
- package/dist/core/events/tool_run.d.ts +11 -0
- package/dist/core/events/tool_run.d.ts.map +1 -0
- package/dist/core/events/unread_detail.d.ts +11 -0
- package/dist/core/events/unread_detail.d.ts.map +1 -0
- package/dist/core/message_role.d.ts +4 -0
- package/dist/core/message_role.d.ts.map +1 -0
- package/dist/core/read_flag.d.ts +11 -0
- package/dist/core/read_flag.d.ts.map +1 -0
- package/dist/core/read_json_attribute.d.ts +15 -0
- package/dist/core/read_json_attribute.d.ts.map +1 -0
- package/dist/core/read_max_tool_rounds.d.ts +9 -0
- package/dist/core/read_max_tool_rounds.d.ts.map +1 -0
- package/dist/core/remote_conversation_store.d.ts +71 -0
- package/dist/core/remote_conversation_store.d.ts.map +1 -0
- package/dist/core/run_handlers.d.ts +85 -0
- package/dist/core/run_handlers.d.ts.map +1 -0
- package/dist/core/run_index.d.ts +58 -0
- package/dist/core/run_index.d.ts.map +1 -0
- package/dist/core/storage_scope.d.ts +118 -0
- package/dist/core/storage_scope.d.ts.map +1 -0
- package/dist/core/tool_outcome.d.ts +27 -0
- package/dist/core/tool_outcome.d.ts.map +1 -0
- package/dist/core/transcribe_audio.d.ts +32 -0
- package/dist/core/transcribe_audio.d.ts.map +1 -0
- package/dist/core/upload_attachment.d.ts +44 -0
- package/dist/core/upload_attachment.d.ts.map +1 -0
- package/dist/core/utils.d.ts +117 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/dom/animations.d.ts +122 -0
- package/dist/dom/animations.d.ts.map +1 -0
- package/dist/dom/dom_driver.d.ts +28 -0
- package/dist/dom/dom_driver.d.ts.map +1 -0
- package/dist/dom/highlight_overlay.d.ts +39 -0
- package/dist/dom/highlight_overlay.d.ts.map +1 -0
- package/dist/dom/native_setter.d.ts +5 -0
- package/dist/dom/native_setter.d.ts.map +1 -0
- package/dist/dom/ring_color.d.ts +16 -0
- package/dist/dom/ring_color.d.ts.map +1 -0
- package/dist/dom/utils.d.ts +3 -0
- package/dist/dom/utils.d.ts.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +7 -0
- package/dist/skills/fill_template.d.ts +16 -0
- package/dist/skills/fill_template.d.ts.map +1 -0
- package/dist/skills/parse_skills.d.ts +8 -0
- package/dist/skills/parse_skills.d.ts.map +1 -0
- package/dist/skills/skill.d.ts +33 -0
- package/dist/skills/skill.d.ts.map +1 -0
- package/dist/skills/skill_catalog.d.ts +69 -0
- package/dist/skills/skill_catalog.d.ts.map +1 -0
- package/dist/skills/skill_name_from.d.ts +12 -0
- package/dist/skills/skill_name_from.d.ts.map +1 -0
- package/dist/tools/chat_surface_tools.d.ts +77 -0
- package/dist/tools/chat_surface_tools.d.ts.map +1 -0
- package/dist/tools/client_tool_registry.d.ts +58 -0
- package/dist/tools/client_tool_registry.d.ts.map +1 -0
- package/dist/tools/is_destructive.d.ts +14 -0
- package/dist/tools/is_destructive.d.ts.map +1 -0
- package/dist/tools/is_navigates.d.ts +9 -0
- package/dist/tools/is_navigates.d.ts.map +1 -0
- package/dist/tools/page_action_tools.d.ts +30 -0
- package/dist/tools/page_action_tools.d.ts.map +1 -0
- package/dist/tools/page_map.d.ts +16 -0
- package/dist/tools/page_map.d.ts.map +1 -0
- package/dist/tools/page_state.d.ts +27 -0
- package/dist/tools/page_state.d.ts.map +1 -0
- package/dist/tools/parse_tool_catalog.d.ts +21 -0
- package/dist/tools/parse_tool_catalog.d.ts.map +1 -0
- package/dist/tools/route_map.d.ts +37 -0
- package/dist/tools/route_map.d.ts.map +1 -0
- package/dist/tools/tool_catalog.d.ts +86 -0
- package/dist/tools/tool_catalog.d.ts.map +1 -0
- package/dist/tools/tool_dispatch.d.ts +82 -0
- package/dist/tools/tool_dispatch.d.ts.map +1 -0
- package/dist/ui/charts/chart_block.d.ts +57 -0
- package/dist/ui/charts/chart_block.d.ts.map +1 -0
- package/dist/ui/charts/chart_spec_from.d.ts +12 -0
- package/dist/ui/charts/chart_spec_from.d.ts.map +1 -0
- package/dist/ui/charts/chart_styles.d.ts +2 -0
- package/dist/ui/charts/chart_styles.d.ts.map +1 -0
- package/dist/ui/charts/chart_tool.d.ts +15 -0
- package/dist/ui/charts/chart_tool.d.ts.map +1 -0
- package/dist/ui/composer/attachment_styles.d.ts +2 -0
- package/dist/ui/composer/attachment_styles.d.ts.map +1 -0
- package/dist/ui/composer/attachment_tray.d.ts +53 -0
- package/dist/ui/composer/attachment_tray.d.ts.map +1 -0
- package/dist/ui/composer/auto_grow.d.ts +10 -0
- package/dist/ui/composer/auto_grow.d.ts.map +1 -0
- package/dist/ui/composer/build_composer.d.ts +32 -0
- package/dist/ui/composer/build_composer.d.ts.map +1 -0
- package/dist/ui/composer/composer_attachments.d.ts +57 -0
- package/dist/ui/composer/composer_attachments.d.ts.map +1 -0
- package/dist/ui/composer/composer_styles.d.ts +2 -0
- package/dist/ui/composer/composer_styles.d.ts.map +1 -0
- package/dist/ui/composer/composer_voice.d.ts +48 -0
- package/dist/ui/composer/composer_voice.d.ts.map +1 -0
- package/dist/ui/composer/skill_styles.d.ts +2 -0
- package/dist/ui/composer/skill_styles.d.ts.map +1 -0
- package/dist/ui/composer/skills_menu.d.ts +34 -0
- package/dist/ui/composer/skills_menu.d.ts.map +1 -0
- package/dist/ui/composer/voice_input.d.ts +46 -0
- package/dist/ui/composer/voice_input.d.ts.map +1 -0
- package/dist/ui/excerpts/attach_copy_buttons.d.ts +13 -0
- package/dist/ui/excerpts/attach_copy_buttons.d.ts.map +1 -0
- package/dist/ui/excerpts/copy_payload.d.ts +30 -0
- package/dist/ui/excerpts/copy_payload.d.ts.map +1 -0
- package/dist/ui/excerpts/page_quote_offer.d.ts +33 -0
- package/dist/ui/excerpts/page_quote_offer.d.ts.map +1 -0
- package/dist/ui/excerpts/place_offer.d.ts +9 -0
- package/dist/ui/excerpts/place_offer.d.ts.map +1 -0
- package/dist/ui/excerpts/quote_selection.d.ts +66 -0
- package/dist/ui/excerpts/quote_selection.d.ts.map +1 -0
- package/dist/ui/excerpts/transcript_quote_offer.d.ts +57 -0
- package/dist/ui/excerpts/transcript_quote_offer.d.ts.map +1 -0
- package/dist/ui/fill_ui_string.d.ts +32 -0
- package/dist/ui/fill_ui_string.d.ts.map +1 -0
- package/dist/ui/history/checkpoint_menu.d.ts +41 -0
- package/dist/ui/history/checkpoint_menu.d.ts.map +1 -0
- package/dist/ui/history/conversation_history.d.ts +317 -0
- package/dist/ui/history/conversation_history.d.ts.map +1 -0
- package/dist/ui/history/history_styles.d.ts +2 -0
- package/dist/ui/history/history_styles.d.ts.map +1 -0
- package/dist/ui/history/relative_time.d.ts +23 -0
- package/dist/ui/history/relative_time.d.ts.map +1 -0
- package/dist/ui/history/thread_drawer.d.ts +64 -0
- package/dist/ui/history/thread_drawer.d.ts.map +1 -0
- package/dist/ui/history/utils.d.ts +2 -0
- package/dist/ui/history/utils.d.ts.map +1 -0
- package/dist/ui/interrupts/approval_card.d.ts +64 -0
- package/dist/ui/interrupts/approval_card.d.ts.map +1 -0
- package/dist/ui/interrupts/approval_styles.d.ts +2 -0
- package/dist/ui/interrupts/approval_styles.d.ts.map +1 -0
- package/dist/ui/interrupts/confirmation_card.d.ts +51 -0
- package/dist/ui/interrupts/confirmation_card.d.ts.map +1 -0
- package/dist/ui/interrupts/confirmation_styles.d.ts +2 -0
- package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -0
- package/dist/ui/interrupts/pending_decision.d.ts +22 -0
- package/dist/ui/interrupts/pending_decision.d.ts.map +1 -0
- package/dist/ui/interrupts/question_card.d.ts +49 -0
- package/dist/ui/interrupts/question_card.d.ts.map +1 -0
- package/dist/ui/interrupts/question_styles.d.ts +2 -0
- package/dist/ui/interrupts/question_styles.d.ts.map +1 -0
- package/dist/ui/place/place.d.ts +17 -0
- package/dist/ui/place/place.d.ts.map +1 -0
- package/dist/ui/place/place_chip.d.ts +26 -0
- package/dist/ui/place/place_chip.d.ts.map +1 -0
- package/dist/ui/place/place_from_route.d.ts +9 -0
- package/dist/ui/place/place_from_route.d.ts.map +1 -0
- package/dist/ui/place/place_styles.d.ts +2 -0
- package/dist/ui/place/place_styles.d.ts.map +1 -0
- package/dist/ui/placement/clamp_launcher.d.ts +20 -0
- package/dist/ui/placement/clamp_launcher.d.ts.map +1 -0
- package/dist/ui/placement/clamp_panel.d.ts +13 -0
- package/dist/ui/placement/clamp_panel.d.ts.map +1 -0
- package/dist/ui/placement/collapse_styles.d.ts +2 -0
- package/dist/ui/placement/collapse_styles.d.ts.map +1 -0
- package/dist/ui/placement/dock_styles.d.ts +2 -0
- package/dist/ui/placement/dock_styles.d.ts.map +1 -0
- package/dist/ui/placement/embedded_styles.d.ts +2 -0
- package/dist/ui/placement/embedded_styles.d.ts.map +1 -0
- package/dist/ui/placement/launcher_drag.d.ts +49 -0
- package/dist/ui/placement/launcher_drag.d.ts.map +1 -0
- package/dist/ui/placement/launcher_placement.d.ts +77 -0
- package/dist/ui/placement/launcher_placement.d.ts.map +1 -0
- package/dist/ui/placement/launcher_styles.d.ts +2 -0
- package/dist/ui/placement/launcher_styles.d.ts.map +1 -0
- package/dist/ui/placement/page_styles.d.ts +2 -0
- package/dist/ui/placement/page_styles.d.ts.map +1 -0
- package/dist/ui/placement/panel_drag.d.ts +40 -0
- package/dist/ui/placement/panel_drag.d.ts.map +1 -0
- package/dist/ui/placement/panel_placement.d.ts +173 -0
- package/dist/ui/placement/panel_placement.d.ts.map +1 -0
- package/dist/ui/placement/place_widget.d.ts +31 -0
- package/dist/ui/placement/place_widget.d.ts.map +1 -0
- package/dist/ui/placement/preset_styles.d.ts +2 -0
- package/dist/ui/placement/preset_styles.d.ts.map +1 -0
- package/dist/ui/placement/resize_handle.d.ts +78 -0
- package/dist/ui/placement/resize_handle.d.ts.map +1 -0
- package/dist/ui/placement/resize_styles.d.ts +2 -0
- package/dist/ui/placement/resize_styles.d.ts.map +1 -0
- package/dist/ui/placement/sidebar_styles.d.ts +2 -0
- package/dist/ui/placement/sidebar_styles.d.ts.map +1 -0
- package/dist/ui/placement/small_viewport_styles.d.ts +2 -0
- package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -0
- package/dist/ui/placement/surface_for.d.ts +93 -0
- package/dist/ui/placement/surface_for.d.ts.map +1 -0
- package/dist/ui/progress/prettify_tool_name.d.ts +11 -0
- package/dist/ui/progress/prettify_tool_name.d.ts.map +1 -0
- package/dist/ui/progress/run_announcer.d.ts +41 -0
- package/dist/ui/progress/run_announcer.d.ts.map +1 -0
- package/dist/ui/progress/run_notice.d.ts +21 -0
- package/dist/ui/progress/run_notice.d.ts.map +1 -0
- package/dist/ui/progress/run_notice_styles.d.ts +2 -0
- package/dist/ui/progress/run_notice_styles.d.ts.map +1 -0
- package/dist/ui/progress/subagent_panel.d.ts +101 -0
- package/dist/ui/progress/subagent_panel.d.ts.map +1 -0
- package/dist/ui/progress/subagent_progress.d.ts +57 -0
- package/dist/ui/progress/subagent_progress.d.ts.map +1 -0
- package/dist/ui/progress/subagent_update.d.ts +28 -0
- package/dist/ui/progress/subagent_update.d.ts.map +1 -0
- package/dist/ui/progress/thoughts_block.d.ts +57 -0
- package/dist/ui/progress/thoughts_block.d.ts.map +1 -0
- package/dist/ui/progress/thoughts_styles.d.ts +2 -0
- package/dist/ui/progress/thoughts_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_call_card.d.ts +167 -0
- package/dist/ui/progress/tool_call_card.d.ts.map +1 -0
- package/dist/ui/progress/tool_group_styles.d.ts +2 -0
- package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_group.d.ts +50 -0
- package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
- package/dist/ui/progress/tool_step_styles.d.ts +2 -0
- package/dist/ui/progress/tool_step_styles.d.ts.map +1 -0
- package/dist/ui/shell/adopt_styles.d.ts +28 -0
- package/dist/ui/shell/adopt_styles.d.ts.map +1 -0
- package/dist/ui/shell/build_header.d.ts +45 -0
- package/dist/ui/shell/build_header.d.ts.map +1 -0
- package/dist/ui/shell/build_launcher.d.ts +33 -0
- package/dist/ui/shell/build_launcher.d.ts.map +1 -0
- package/dist/ui/shell/collapse_glyph.d.ts +14 -0
- package/dist/ui/shell/collapse_glyph.d.ts.map +1 -0
- package/dist/ui/shell/frame_styles.d.ts +2 -0
- package/dist/ui/shell/frame_styles.d.ts.map +1 -0
- package/dist/ui/shell/glyph_slot.d.ts +7 -0
- package/dist/ui/shell/glyph_slot.d.ts.map +1 -0
- package/dist/ui/shell/header_button.d.ts +11 -0
- package/dist/ui/shell/header_button.d.ts.map +1 -0
- package/dist/ui/shell/icon_element.d.ts +12 -0
- package/dist/ui/shell/icon_element.d.ts.map +1 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts +10 -0
- package/dist/ui/shell/is_unread_badge_enabled.d.ts.map +1 -0
- package/dist/ui/shell/read_launcher_icon_url.d.ts +8 -0
- package/dist/ui/shell/read_launcher_icon_url.d.ts.map +1 -0
- package/dist/ui/shell/theme_styles.d.ts +2 -0
- package/dist/ui/shell/theme_styles.d.ts.map +1 -0
- package/dist/ui/shell/token_styles.d.ts +2 -0
- package/dist/ui/shell/token_styles.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +2 -0
- package/dist/ui/styles.d.ts.map +1 -0
- package/dist/ui/themes/classic_styles.d.ts +2 -0
- package/dist/ui/themes/classic_styles.d.ts.map +1 -0
- package/dist/ui/themes/classic_tokens.d.ts +2 -0
- package/dist/ui/themes/classic_tokens.d.ts.map +1 -0
- package/dist/ui/themes/code_tokens.d.ts +2 -0
- package/dist/ui/themes/code_tokens.d.ts.map +1 -0
- package/dist/ui/themes/ledger_styles.d.ts +2 -0
- package/dist/ui/themes/ledger_styles.d.ts.map +1 -0
- package/dist/ui/themes/ledger_tokens.d.ts +2 -0
- package/dist/ui/themes/ledger_tokens.d.ts.map +1 -0
- package/dist/ui/themes/modern_styles.d.ts +2 -0
- package/dist/ui/themes/modern_styles.d.ts.map +1 -0
- package/dist/ui/themes/quiet_styles.d.ts +2 -0
- package/dist/ui/themes/quiet_styles.d.ts.map +1 -0
- package/dist/ui/themes/scheme_styles.d.ts +2 -0
- package/dist/ui/themes/scheme_styles.d.ts.map +1 -0
- package/dist/ui/themes/soft_styles.d.ts +2 -0
- package/dist/ui/themes/soft_styles.d.ts.map +1 -0
- package/dist/ui/themes/soft_tokens.d.ts +2 -0
- package/dist/ui/themes/soft_tokens.d.ts.map +1 -0
- package/dist/ui/themes/theme_hosts.d.ts +15 -0
- package/dist/ui/themes/theme_hosts.d.ts.map +1 -0
- package/dist/ui/transcript/answer_actions.d.ts +50 -0
- package/dist/ui/transcript/answer_actions.d.ts.map +1 -0
- package/dist/ui/transcript/answer_stream.d.ts +56 -0
- package/dist/ui/transcript/answer_stream.d.ts.map +1 -0
- package/dist/ui/transcript/attachment_chip_styles.d.ts +2 -0
- package/dist/ui/transcript/attachment_chip_styles.d.ts.map +1 -0
- package/dist/ui/transcript/attachment_chips.d.ts +21 -0
- package/dist/ui/transcript/attachment_chips.d.ts.map +1 -0
- package/dist/ui/transcript/build_transcript.d.ts +21 -0
- package/dist/ui/transcript/build_transcript.d.ts.map +1 -0
- package/dist/ui/transcript/greeting_styles.d.ts +2 -0
- package/dist/ui/transcript/greeting_styles.d.ts.map +1 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
- package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
- package/dist/ui/transcript/message_action_styles.d.ts +2 -0
- package/dist/ui/transcript/message_action_styles.d.ts.map +1 -0
- package/dist/ui/transcript/message_actions.d.ts +74 -0
- package/dist/ui/transcript/message_actions.d.ts.map +1 -0
- package/dist/ui/transcript/message_styles.d.ts +2 -0
- package/dist/ui/transcript/message_styles.d.ts.map +1 -0
- package/dist/ui/transcript/render_markdown.d.ts +40 -0
- package/dist/ui/transcript/render_markdown.d.ts.map +1 -0
- package/dist/ui/transcript/render_or_warn.d.ts +16 -0
- package/dist/ui/transcript/render_or_warn.d.ts.map +1 -0
- package/dist/ui/transcript/reveal_words.d.ts +8 -0
- package/dist/ui/transcript/reveal_words.d.ts.map +1 -0
- package/dist/ui/transcript/starter_chips.d.ts +21 -0
- package/dist/ui/transcript/starter_chips.d.ts.map +1 -0
- package/dist/ui/transcript/stick_to_bottom.d.ts +55 -0
- package/dist/ui/transcript/stick_to_bottom.d.ts.map +1 -0
- package/dist/ui/transcript/suggestion_chips.d.ts +29 -0
- package/dist/ui/transcript/suggestion_chips.d.ts.map +1 -0
- package/dist/ui/transcript/transcript.d.ts +196 -0
- package/dist/ui/transcript/transcript.d.ts.map +1 -0
- package/dist/ui/transcript/transcript_styles.d.ts +2 -0
- package/dist/ui/transcript/transcript_styles.d.ts.map +1 -0
- package/dist/ui/ui_strings.d.ts +352 -0
- package/dist/ui/ui_strings.d.ts.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/package.json +86 -3
- package/src/bundle.ts +2 -0
- package/src/chat.ts +2 -0
- package/src/constants.ts +650 -0
- package/src/core/activity_registration.ts +19 -0
- package/src/core/activity_registry.ts +133 -0
- package/src/core/activity_renderer.ts +19 -0
- package/src/core/ag_ui_chat.ts +3966 -0
- package/src/core/agui_client.ts +1028 -0
- package/src/core/attachment.ts +62 -0
- package/src/core/chat_registry.ts +53 -0
- package/src/core/client_seed.ts +27 -0
- package/src/core/conversation_store.ts +478 -0
- package/src/core/create_http_agent.ts +97 -0
- package/src/core/define_chat.ts +25 -0
- package/src/core/events/attachments_detail.ts +9 -0
- package/src/core/events/bar_height_detail.ts +5 -0
- package/src/core/events/custom_agent_detail.ts +7 -0
- package/src/core/events/dock_width_detail.ts +7 -0
- package/src/core/events/feedback_detail.ts +27 -0
- package/src/core/events/invalidate_detail.ts +15 -0
- package/src/core/events/maximize_detail.ts +4 -0
- package/src/core/events/run_finished_detail.ts +36 -0
- package/src/core/events/state_detail.ts +4 -0
- package/src/core/events/submit_detail.ts +8 -0
- package/src/core/events/thread_detail.ts +26 -0
- package/src/core/events/thread_title_detail.ts +7 -0
- package/src/core/events/toggle_detail.ts +4 -0
- package/src/core/events/tool_run.ts +10 -0
- package/src/core/events/unread_detail.ts +10 -0
- package/src/core/message_role.ts +4 -0
- package/src/core/read_flag.ts +13 -0
- package/src/core/read_json_attribute.ts +30 -0
- package/src/core/read_max_tool_rounds.ts +13 -0
- package/src/core/remote_conversation_store.ts +279 -0
- package/src/core/run_handlers.ts +524 -0
- package/src/core/run_index.ts +114 -0
- package/src/core/storage_scope.ts +291 -0
- package/src/core/tool_outcome.ts +39 -0
- package/src/core/transcribe_audio.ts +76 -0
- package/src/core/upload_attachment.ts +130 -0
- package/src/core/utils.ts +230 -0
- package/src/dom/animations.ts +341 -0
- package/src/dom/dom_driver.ts +91 -0
- package/src/dom/highlight_overlay.ts +204 -0
- package/src/dom/native_setter.ts +40 -0
- package/src/dom/ring_color.ts +206 -0
- package/src/dom/utils.ts +5 -0
- package/src/index.ts +220 -0
- package/src/skills/fill_template.ts +31 -0
- package/src/skills/parse_skills.ts +31 -0
- package/src/skills/skill.ts +32 -0
- package/src/skills/skill_catalog.ts +172 -0
- package/src/skills/skill_name_from.ts +19 -0
- package/src/tools/chat_surface_tools.ts +183 -0
- package/src/tools/client_tool_registry.ts +80 -0
- package/src/tools/is_destructive.ts +17 -0
- package/src/tools/is_navigates.ts +12 -0
- package/src/tools/page_action_tools.ts +123 -0
- package/src/tools/page_map.ts +26 -0
- package/src/tools/page_state.ts +57 -0
- package/src/tools/parse_tool_catalog.ts +41 -0
- package/src/tools/route_map.ts +132 -0
- package/src/tools/tool_catalog.ts +334 -0
- package/src/tools/tool_dispatch.ts +453 -0
- package/src/ui/charts/chart_block.ts +519 -0
- package/src/ui/charts/chart_spec_from.ts +125 -0
- package/src/ui/charts/chart_styles.ts +63 -0
- package/src/ui/charts/chart_tool.ts +72 -0
- package/src/ui/composer/attachment_styles.ts +71 -0
- package/src/ui/composer/attachment_tray.ts +295 -0
- package/src/ui/composer/auto_grow.ts +12 -0
- package/src/ui/composer/build_composer.ts +114 -0
- package/src/ui/composer/composer_attachments.ts +324 -0
- package/src/ui/composer/composer_styles.ts +177 -0
- package/src/ui/composer/composer_voice.ts +93 -0
- package/src/ui/composer/skill_styles.ts +120 -0
- package/src/ui/composer/skills_menu.ts +188 -0
- package/src/ui/composer/voice_input.ts +282 -0
- package/src/ui/excerpts/attach_copy_buttons.ts +73 -0
- package/src/ui/excerpts/copy_payload.ts +156 -0
- package/src/ui/excerpts/page_quote_offer.ts +203 -0
- package/src/ui/excerpts/place_offer.ts +24 -0
- package/src/ui/excerpts/quote_selection.ts +345 -0
- package/src/ui/excerpts/transcript_quote_offer.ts +175 -0
- package/src/ui/fill_ui_string.ts +41 -0
- package/src/ui/history/checkpoint_menu.ts +300 -0
- package/src/ui/history/conversation_history.ts +1169 -0
- package/src/ui/history/history_styles.ts +492 -0
- package/src/ui/history/relative_time.ts +50 -0
- package/src/ui/history/thread_drawer.ts +471 -0
- package/src/ui/history/utils.ts +11 -0
- package/src/ui/interrupts/approval_card.ts +202 -0
- package/src/ui/interrupts/approval_styles.ts +52 -0
- package/src/ui/interrupts/confirmation_card.ts +138 -0
- package/src/ui/interrupts/confirmation_styles.ts +114 -0
- package/src/ui/interrupts/pending_decision.ts +33 -0
- package/src/ui/interrupts/question_card.ts +213 -0
- package/src/ui/interrupts/question_styles.ts +73 -0
- package/src/ui/place/place.ts +16 -0
- package/src/ui/place/place_chip.ts +146 -0
- package/src/ui/place/place_from_route.ts +27 -0
- package/src/ui/place/place_styles.ts +108 -0
- package/src/ui/placement/clamp_launcher.ts +38 -0
- package/src/ui/placement/clamp_panel.ts +30 -0
- package/src/ui/placement/collapse_styles.ts +472 -0
- package/src/ui/placement/dock_styles.ts +116 -0
- package/src/ui/placement/embedded_styles.ts +14 -0
- package/src/ui/placement/launcher_drag.ts +206 -0
- package/src/ui/placement/launcher_placement.ts +133 -0
- package/src/ui/placement/launcher_styles.ts +189 -0
- package/src/ui/placement/page_styles.ts +47 -0
- package/src/ui/placement/panel_drag.ts +138 -0
- package/src/ui/placement/panel_placement.ts +1975 -0
- package/src/ui/placement/place_widget.ts +64 -0
- package/src/ui/placement/preset_styles.ts +49 -0
- package/src/ui/placement/resize_handle.ts +229 -0
- package/src/ui/placement/resize_styles.ts +201 -0
- package/src/ui/placement/sidebar_styles.ts +113 -0
- package/src/ui/placement/small_viewport_styles.ts +70 -0
- package/src/ui/placement/surface_for.ts +183 -0
- package/src/ui/progress/prettify_tool_name.ts +16 -0
- package/src/ui/progress/run_announcer.ts +67 -0
- package/src/ui/progress/run_notice.ts +61 -0
- package/src/ui/progress/run_notice_styles.ts +52 -0
- package/src/ui/progress/subagent_panel.ts +227 -0
- package/src/ui/progress/subagent_progress.ts +200 -0
- package/src/ui/progress/subagent_update.ts +89 -0
- package/src/ui/progress/thoughts_block.ts +165 -0
- package/src/ui/progress/thoughts_styles.ts +74 -0
- package/src/ui/progress/tool_call_card.ts +429 -0
- package/src/ui/progress/tool_group_styles.ts +313 -0
- package/src/ui/progress/tool_step_group.ts +317 -0
- package/src/ui/progress/tool_step_styles.ts +486 -0
- package/src/ui/shell/adopt_styles.ts +34 -0
- package/src/ui/shell/build_header.ts +121 -0
- package/src/ui/shell/build_launcher.ts +79 -0
- package/src/ui/shell/collapse_glyph.ts +42 -0
- package/src/ui/shell/frame_styles.ts +147 -0
- package/src/ui/shell/glyph_slot.ts +12 -0
- package/src/ui/shell/header_button.ts +22 -0
- package/src/ui/shell/icon_element.ts +33 -0
- package/src/ui/shell/is_unread_badge_enabled.ts +11 -0
- package/src/ui/shell/read_launcher_icon_url.ts +9 -0
- package/src/ui/shell/theme_styles.ts +11 -0
- package/src/ui/shell/token_styles.ts +480 -0
- package/src/ui/styles.ts +110 -0
- package/src/ui/themes/classic_styles.ts +32 -0
- package/src/ui/themes/classic_tokens.ts +51 -0
- package/src/ui/themes/code_tokens.ts +21 -0
- package/src/ui/themes/ledger_styles.ts +169 -0
- package/src/ui/themes/ledger_tokens.ts +33 -0
- package/src/ui/themes/modern_styles.ts +80 -0
- package/src/ui/themes/quiet_styles.ts +118 -0
- package/src/ui/themes/scheme_styles.ts +26 -0
- package/src/ui/themes/soft_styles.ts +149 -0
- package/src/ui/themes/soft_tokens.ts +29 -0
- package/src/ui/themes/theme_hosts.ts +14 -0
- package/src/ui/transcript/answer_actions.ts +165 -0
- package/src/ui/transcript/answer_stream.ts +123 -0
- package/src/ui/transcript/attachment_chip_styles.ts +116 -0
- package/src/ui/transcript/attachment_chips.ts +82 -0
- package/src/ui/transcript/build_transcript.ts +67 -0
- package/src/ui/transcript/greeting_styles.ts +171 -0
- package/src/ui/transcript/groups_tool_steps.ts +16 -0
- package/src/ui/transcript/message_action_styles.ts +155 -0
- package/src/ui/transcript/message_actions.ts +234 -0
- package/src/ui/transcript/message_styles.ts +262 -0
- package/src/ui/transcript/render_markdown.ts +181 -0
- package/src/ui/transcript/render_or_warn.ts +22 -0
- package/src/ui/transcript/reveal_words.ts +41 -0
- package/src/ui/transcript/starter_chips.ts +42 -0
- package/src/ui/transcript/stick_to_bottom.ts +126 -0
- package/src/ui/transcript/suggestion_chips.ts +73 -0
- package/src/ui/transcript/transcript.ts +555 -0
- package/src/ui/transcript/transcript_styles.ts +153 -0
- package/src/ui/ui_strings.ts +550 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,1975 @@
|
|
|
1
|
+
import { EDGE_MARGIN, SCREEN_EDGE_MARGIN, STORAGE_ROOT } from "../../constants.js";
|
|
2
|
+
import type { ChatCorner, ChatSurfaceReport } from "../../tools/chat_surface_tools.js";
|
|
3
|
+
import type { UiStrings } from "../ui_strings.js";
|
|
4
|
+
import { clampLauncher } from "./clamp_launcher.js";
|
|
5
|
+
import { clampPanel } from "./clamp_panel.js";
|
|
6
|
+
import { enableLauncherDrag } from "./launcher_drag.js";
|
|
7
|
+
import {
|
|
8
|
+
type ExpandCorner,
|
|
9
|
+
type Extent,
|
|
10
|
+
type LauncherBox,
|
|
11
|
+
launcherPlacement,
|
|
12
|
+
type ViewportBox,
|
|
13
|
+
} from "./launcher_placement.js";
|
|
14
|
+
import { enablePanelDrag } from "./panel_drag.js";
|
|
15
|
+
import { placeWidget } from "./place_widget.js";
|
|
16
|
+
import {
|
|
17
|
+
createResizeHandle,
|
|
18
|
+
gripName,
|
|
19
|
+
type PanelRect,
|
|
20
|
+
type ResizeAnchor,
|
|
21
|
+
type ResizeAxis,
|
|
22
|
+
type ResizeGrip,
|
|
23
|
+
type ResizeSize,
|
|
24
|
+
} from "./resize_handle.js";
|
|
25
|
+
import { type Surface, surfaceFor } from "./surface_for.js";
|
|
26
|
+
|
|
27
|
+
/** Persistence key for the dragged panel sizes, one per placement in one record. */
|
|
28
|
+
const SIZE_KEY = `${STORAGE_ROOT}:size`;
|
|
29
|
+
|
|
30
|
+
/** The stored record: a dragged size per placement attribute value. */
|
|
31
|
+
interface StoredSizes {
|
|
32
|
+
readonly v: 2;
|
|
33
|
+
readonly placements: Readonly<Record<string, ResizeSize>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The private properties a dragged size lives in. They are not the public
|
|
38
|
+
* `--ag-ui-width` / `--ag-ui-height` tokens, which belong to the host: an
|
|
39
|
+
* inline write there outranked every host rule and was read back by the
|
|
40
|
+
* small-viewport block, so a size dragged in a wide window leaked into the
|
|
41
|
+
* phone shape. The placement rules read these before the host token; the
|
|
42
|
+
* small-viewport block does not read them at all.
|
|
43
|
+
*/
|
|
44
|
+
const DRAG_WIDTH = "--_drag-width";
|
|
45
|
+
const DRAG_HEIGHT = "--_drag-height";
|
|
46
|
+
/**
|
|
47
|
+
* How far above the bottom of the layout viewport a docked panel stands, and
|
|
48
|
+
* how far in from its right edge: the bar's height, or the host anchor's top
|
|
49
|
+
* and right. Private and written inline, like the dragged size, and read only
|
|
50
|
+
* by the dock rules; never an inline --_inset, which would outrank the phone
|
|
51
|
+
* block and keep a docked panel off full-bleed on a phone.
|
|
52
|
+
*/
|
|
53
|
+
const DOCK_BOTTOM = "--_dock-bottom";
|
|
54
|
+
const DOCK_RIGHT = "--_dock-right";
|
|
55
|
+
const DOCK_TOP = "--_dock-top";
|
|
56
|
+
|
|
57
|
+
/** The side of its anchor a panel under collapse-to="none" opens on. */
|
|
58
|
+
type AnchorSide = "above" | "below";
|
|
59
|
+
|
|
60
|
+
/** Per-tab persistence key for a dragged launcher position. */
|
|
61
|
+
const LAUNCHER_KEY = `${STORAGE_ROOT}:launcher`;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Persistence key for where the user dragged a docked panel. Its own key, not
|
|
65
|
+
* the launcher's: a spot stored under the corner launcher describes another
|
|
66
|
+
* surface, and reading it here would open every panel away from the bar for a
|
|
67
|
+
* host that switched to the bar after its users had dragged their corner
|
|
68
|
+
* panels.
|
|
69
|
+
*/
|
|
70
|
+
const DOCK_KEY = `${STORAGE_ROOT}:dock`;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where the user last put a corner panel's launcher, and the panel itself when
|
|
74
|
+
* they stated that too. The shape the launcher's record is stored in.
|
|
75
|
+
*/
|
|
76
|
+
interface CornerSpot {
|
|
77
|
+
readonly left: number;
|
|
78
|
+
readonly top: number;
|
|
79
|
+
readonly panel?: { readonly left: number; readonly top: number };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A stored `{ left, top }` pair, or null for anything that is not one. */
|
|
83
|
+
function asPoint(value: unknown): { readonly left: number; readonly top: number } | null {
|
|
84
|
+
if (typeof value !== "object" || value === null) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const { left, top } = value as { left?: unknown; top?: unknown };
|
|
88
|
+
return typeof left === "number" && typeof top === "number" ? { left, top } : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether a box covers the usable viewport, which is what full-bleed means.
|
|
93
|
+
*
|
|
94
|
+
* A pixel of slack on each axis, because a box sized from `100vw` and one sized
|
|
95
|
+
* from the visual viewport disagree by sub-pixel rounding, and a panel that
|
|
96
|
+
* fills the screen must not read as one that could still be moved within it.
|
|
97
|
+
*/
|
|
98
|
+
function coversViewport(box: Extent, viewport: Extent): boolean {
|
|
99
|
+
return box.width >= viewport.width - 1 && box.height >= viewport.height - 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One edge the host reserved, read off the probe's padding. */
|
|
103
|
+
function reservedEdge(style: CSSStyleDeclaration, name: string): number {
|
|
104
|
+
const value = Number.parseFloat(style.getPropertyValue(name));
|
|
105
|
+
// A detached or not-yet-rendered probe resolves to nothing at all, and
|
|
106
|
+
// reserving NaN is worse than reserving zero in every case.
|
|
107
|
+
return Number.isFinite(value) ? value : 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether a box that covers the usable viewport stands off any edge of it.
|
|
112
|
+
*
|
|
113
|
+
* The far edges are read against the layout viewport rather than the usable box,
|
|
114
|
+
* because a keyboard shortens the second and pans the panel down inside the
|
|
115
|
+
* first: a phone's full-bleed panel is correctly past the usable box's foot then,
|
|
116
|
+
* and pinning it back would put its header above the screen.
|
|
117
|
+
*/
|
|
118
|
+
function hangsOff(box: PanelRect, viewport: ViewportBox, screen: Extent): boolean {
|
|
119
|
+
const slack = 0.5;
|
|
120
|
+
return (
|
|
121
|
+
box.left < viewport.left - slack ||
|
|
122
|
+
box.top < viewport.top - slack ||
|
|
123
|
+
box.right > screen.width + slack ||
|
|
124
|
+
box.bottom > screen.height + slack
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A box of the same size as `box`, standing at the start of the usable viewport. */
|
|
129
|
+
function atStartOf(viewport: ViewportBox, box: PanelRect): PanelRect {
|
|
130
|
+
return {
|
|
131
|
+
left: viewport.left,
|
|
132
|
+
top: viewport.top,
|
|
133
|
+
right: viewport.left + (box.right - box.left),
|
|
134
|
+
bottom: viewport.top + (box.bottom - box.top),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The `--ag-ui-inset` value that pins `box` by `corner`.
|
|
140
|
+
*
|
|
141
|
+
* Only the two sides the corner names get a length; the other two are `auto`,
|
|
142
|
+
* so the box keeps its size and grows away from that corner. Right and bottom
|
|
143
|
+
* are measured from `screen`, the layout viewport, because that is what the
|
|
144
|
+
* browser measures a fixed element's inset from.
|
|
145
|
+
*/
|
|
146
|
+
function insetFrom(corner: ResizeAnchor, box: PanelRect, screen: Extent): string {
|
|
147
|
+
const side = (value: number): string => `${Math.round(value)}px`;
|
|
148
|
+
return [
|
|
149
|
+
corner.y === "top" ? side(box.top) : "auto",
|
|
150
|
+
corner.x === "right" ? side(screen.width - box.right) : "auto",
|
|
151
|
+
corner.y === "bottom" ? side(screen.height - box.bottom) : "auto",
|
|
152
|
+
corner.x === "left" ? side(box.left) : "auto",
|
|
153
|
+
].join(" ");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Every edge and corner the panel can be dragged by. Corners last, so they sit
|
|
158
|
+
* above the edge strips they overlap and win the pointer at the corners.
|
|
159
|
+
*/
|
|
160
|
+
const RESIZE_GRIPS: readonly ResizeGrip[] = [
|
|
161
|
+
{ y: "top" },
|
|
162
|
+
{ y: "bottom" },
|
|
163
|
+
{ x: "left" },
|
|
164
|
+
{ x: "right" },
|
|
165
|
+
{ x: "left", y: "top" },
|
|
166
|
+
{ x: "right", y: "top" },
|
|
167
|
+
{ x: "left", y: "bottom" },
|
|
168
|
+
{ x: "right", y: "bottom" },
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* What the placement needs from the element that owns it.
|
|
173
|
+
*
|
|
174
|
+
* Everything the element keeps private is reached through a thunk here, the
|
|
175
|
+
* same shape `PanelDragOptions` and `ResizeOptions` already give their hosts:
|
|
176
|
+
* read per call, so a value the element replaces later (the resolved string
|
|
177
|
+
* table, the connected flag) is never captured stale.
|
|
178
|
+
*/
|
|
179
|
+
export interface PlacementHost {
|
|
180
|
+
/** The custom element itself: its inline style, attributes and box. */
|
|
181
|
+
readonly element: HTMLElement;
|
|
182
|
+
/** The collapsed widget's button, which a drag moves and a resize carries. */
|
|
183
|
+
readonly launcher: HTMLButtonElement;
|
|
184
|
+
/** The shadow root, searched for a header mid-drag. */
|
|
185
|
+
readonly root: ShadowRoot;
|
|
186
|
+
/** Whether the element is in the document. */
|
|
187
|
+
readonly connected: () => boolean;
|
|
188
|
+
/** Whether the widget is collapsed. */
|
|
189
|
+
readonly collapsed: () => boolean;
|
|
190
|
+
/** Whether the current placement has a collapsed state at all. */
|
|
191
|
+
readonly collapsible: () => boolean;
|
|
192
|
+
/** The resolved string table. */
|
|
193
|
+
readonly strings: () => UiStrings;
|
|
194
|
+
/** Read a layout preference, durable store first. */
|
|
195
|
+
readonly readPreference: (base: string) => string | null;
|
|
196
|
+
/** Persist a layout preference to both stores. */
|
|
197
|
+
readonly writePreference: (base: string, value: string) => void;
|
|
198
|
+
/** Drop a layout preference from both stores. */
|
|
199
|
+
readonly clearPreference: (base: string) => void;
|
|
200
|
+
/** Tell the reader the agent moved their window, with the way back. */
|
|
201
|
+
readonly announceSurfaceChange: (text: string, undo: () => void) => void;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Where the panel and its launcher sit, and how big the panel is: the dragged
|
|
206
|
+
* launcher, the dragged header, the eight resize grips, the corner the panel
|
|
207
|
+
* opens from, and the persisted record of all of it.
|
|
208
|
+
*
|
|
209
|
+
* Owned one-to-one by an `<ag-ui-chat>`, and holding no state outside the
|
|
210
|
+
* instance, so two elements on a page place themselves independently.
|
|
211
|
+
*/
|
|
212
|
+
export class PanelPlacement {
|
|
213
|
+
readonly #host: PlacementHost;
|
|
214
|
+
|
|
215
|
+
constructor(host: PlacementHost) {
|
|
216
|
+
this.#host = host;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Make the header a title bar. Only while open: a collapsed widget has no
|
|
221
|
+
* header on screen, and the launcher is the handle then.
|
|
222
|
+
*/
|
|
223
|
+
enablePanelDrag(header: HTMLElement): void {
|
|
224
|
+
this.#header = header;
|
|
225
|
+
enablePanelDrag(header, {
|
|
226
|
+
enabled: () => !this.#host.collapsed() && this.#canDragPanel(),
|
|
227
|
+
rect: () => this.#host.element.getBoundingClientRect(),
|
|
228
|
+
apply: (box, from) => this.#movePanel(box, from),
|
|
229
|
+
commit: (box, from) => this.#commitPanel(box, from),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Let the launcher be dragged. Only while collapsed: the launcher is scaled
|
|
235
|
+
* away and unclickable behind the open panel, so a drag there would move
|
|
236
|
+
* something nobody can see.
|
|
237
|
+
*/
|
|
238
|
+
enableLauncherDrag(signal: AbortSignal): void {
|
|
239
|
+
enableLauncherDrag(this.#host.launcher, {
|
|
240
|
+
signal,
|
|
241
|
+
enabled: () => this.#host.collapsed() && this.#launcherDraggable(),
|
|
242
|
+
rect: () => this.#launcherBox(),
|
|
243
|
+
viewport: () => this.#viewport(),
|
|
244
|
+
apply: (left, top) => this.#moveLauncher(left, top),
|
|
245
|
+
commit: (left, top) => this.#commitLauncher(left, top),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Build the eight resize grips into `chat`, with one of them in the tab order. */
|
|
250
|
+
mountResizeGrips(chat: HTMLElement): void {
|
|
251
|
+
// The grips a key gesture was on are gone, and a key that comes up on nothing
|
|
252
|
+
// ends nothing, so an element taken out mid-gesture would otherwise come back
|
|
253
|
+
// deaf to every window resize.
|
|
254
|
+
this.#resizing = false;
|
|
255
|
+
for (const grip of RESIZE_GRIPS) {
|
|
256
|
+
const handle = createResizeHandle(grip, {
|
|
257
|
+
axis: () => this.#resizeAxis(),
|
|
258
|
+
rect: () => this.#host.element.getBoundingClientRect(),
|
|
259
|
+
apply: (box) => {
|
|
260
|
+
this.#resizing = true;
|
|
261
|
+
this.#applyResize(grip, box);
|
|
262
|
+
},
|
|
263
|
+
commit: (box) => {
|
|
264
|
+
this.#resizing = false;
|
|
265
|
+
this.#commitResize(grip, box);
|
|
266
|
+
},
|
|
267
|
+
label: this.#host.strings().resizePanel,
|
|
268
|
+
});
|
|
269
|
+
// Only one of the eight is in the tab order: eight separators between the
|
|
270
|
+
// transcript and the composer would be an obstacle, and one grip reaches
|
|
271
|
+
// both axes. syncResizeAnchor picks the free corner, so an arrow key grows
|
|
272
|
+
// the panel rather than moving it.
|
|
273
|
+
handle.tabIndex = -1;
|
|
274
|
+
handle.setAttribute("aria-hidden", "true");
|
|
275
|
+
this.#resizeHandles.set(gripName(grip), handle);
|
|
276
|
+
chat.appendChild(handle);
|
|
277
|
+
}
|
|
278
|
+
this.#focusableGrip();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Restore the current placement's dragged size before the panel paints. */
|
|
282
|
+
restoreSize(): void {
|
|
283
|
+
// A reconnect may find the stored size gone, and a size kept inline would outlive it.
|
|
284
|
+
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
285
|
+
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
286
|
+
this.#applySize(this.#readSize());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Swap the dragged size for the one stored under the placement just set, so a
|
|
291
|
+
* floating drag never sizes a sidebar. Between two corner placements a stated
|
|
292
|
+
* spot moves with the size, as on resetSize, so the launcher keeps its corner.
|
|
293
|
+
* Before connect the namespace is unclaimed, and connect restores the size.
|
|
294
|
+
*/
|
|
295
|
+
switchPlacement(previous: string | null): void {
|
|
296
|
+
const element = this.#host.element;
|
|
297
|
+
const sized = element.getBoundingClientRect();
|
|
298
|
+
element.style.removeProperty(DRAG_WIDTH);
|
|
299
|
+
element.style.removeProperty(DRAG_HEIGHT);
|
|
300
|
+
if (!this.#host.connected()) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.#applySize(this.#readSize());
|
|
304
|
+
if (surfaceFor(previous, element.getAttribute("collapse-to")).launcherDraggable) {
|
|
305
|
+
this.#carrySpotWithSize(sized);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Drop the current placement's dragged size and its stored copy, so the
|
|
311
|
+
* placement's own size and the host's tokens apply again; other placements
|
|
312
|
+
* keep theirs. For a host entering a sized state of its
|
|
313
|
+
* own, which a user's drag would otherwise outrank.
|
|
314
|
+
*
|
|
315
|
+
* A docked panel the user dragged off its dock goes back to it as well, and
|
|
316
|
+
* the stored spot with it: there the position is part of the same resting
|
|
317
|
+
* state the host is asking for. A corner panel keeps a dragged position, as
|
|
318
|
+
* it always has.
|
|
319
|
+
*/
|
|
320
|
+
resetSize(): void {
|
|
321
|
+
const sized = this.#host.element.getBoundingClientRect();
|
|
322
|
+
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
323
|
+
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
324
|
+
this.#writeSizes(this.#bucket(), null);
|
|
325
|
+
if (this.#docked()) {
|
|
326
|
+
if (this.#dockPos !== null) {
|
|
327
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
328
|
+
}
|
|
329
|
+
this.#dockPos = null;
|
|
330
|
+
this.#storeDockPosition(null);
|
|
331
|
+
// A size the host holds does not change here, so no observer is coming to
|
|
332
|
+
// stand a panel that fills the window back on the screen.
|
|
333
|
+
this.#placeDocked();
|
|
334
|
+
} else {
|
|
335
|
+
this.#carrySpotWithSize(sized);
|
|
336
|
+
}
|
|
337
|
+
// The anchor was measured against the dragged box; the resting one may
|
|
338
|
+
// pin different edges.
|
|
339
|
+
this.syncResizeAnchor();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Stand the larger panel back on the screen once the element has stamped the
|
|
344
|
+
* maximized state, whose rules size the box outright, over any dragged size
|
|
345
|
+
* still held inline. The stored size and every stored spot are kept: restore
|
|
346
|
+
* puts them back.
|
|
347
|
+
*/
|
|
348
|
+
enterMaximized(): void {
|
|
349
|
+
this.#replace();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Put the stored size back once the element has left the maximized state. */
|
|
353
|
+
leaveMaximized(): void {
|
|
354
|
+
this.restoreSize();
|
|
355
|
+
this.#replace();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Re-place the panel from the record after its size changed under it. */
|
|
359
|
+
#replace(): void {
|
|
360
|
+
if (this.#docked()) {
|
|
361
|
+
this.#placeDocked();
|
|
362
|
+
} else {
|
|
363
|
+
this.restoreLauncherPosition();
|
|
364
|
+
}
|
|
365
|
+
this.syncResizeAnchor();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Carry a stated corner spot along with a dragged size coming off.
|
|
370
|
+
*
|
|
371
|
+
* The size comes off toward the corner the layout holds still, so the top-left
|
|
372
|
+
* the spot is kept as moves with it, and a restore from the old one would swing
|
|
373
|
+
* the panel away from its launcher. Only that movement goes onto the spot, not
|
|
374
|
+
* where the box stands: in a window narrower than the spot the box is held
|
|
375
|
+
* short of it, and that is not something the user stated.
|
|
376
|
+
*
|
|
377
|
+
* Left alone while a host's own size is in force, because that cannot be told
|
|
378
|
+
* from the dragged size coming off, and it may be a state the host is about to
|
|
379
|
+
* leave: counting it would throw the spot off by as much as that state is
|
|
380
|
+
* bigger. A panel that is the screen needs no case of its own: the small-viewport
|
|
381
|
+
* block ignores a dragged size, so its box does not move.
|
|
382
|
+
*
|
|
383
|
+
* Left alone too under data-launcher-drag="false", where a spot left over from
|
|
384
|
+
* before the attribute is a record kept for the day it comes off, and nothing
|
|
385
|
+
* that happens to the box now is a statement about it.
|
|
386
|
+
*/
|
|
387
|
+
#carrySpotWithSize(sized: DOMRect): void {
|
|
388
|
+
const element = this.#host.element;
|
|
389
|
+
const box = element.getBoundingClientRect();
|
|
390
|
+
const spot = this.#cornerSpot ?? this.#readLauncherPosition();
|
|
391
|
+
const style = getComputedStyle(element);
|
|
392
|
+
if (
|
|
393
|
+
spot?.panel === undefined ||
|
|
394
|
+
!this.#launcherDraggable() ||
|
|
395
|
+
style.getPropertyValue("--ag-ui-width") !== "" ||
|
|
396
|
+
style.getPropertyValue("--ag-ui-height") !== ""
|
|
397
|
+
) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
this.#writeCornerSpot({
|
|
401
|
+
...spot,
|
|
402
|
+
panel: {
|
|
403
|
+
left: spot.panel.left + box.left - sized.left,
|
|
404
|
+
top: spot.panel.top + box.top - sized.top,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* A zero-sized box carrying the host's viewport insets as padding, so they
|
|
411
|
+
* can be read back as used pixel lengths. See the `.viewport-probe` rule for
|
|
412
|
+
* why a custom property cannot be read directly.
|
|
413
|
+
*/
|
|
414
|
+
readonly probe = document.createElement("div");
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Where the user dragged the launcher, in viewport coordinates, or null
|
|
418
|
+
* while the host's own CSS still places it. Set means this element owns its
|
|
419
|
+
* position -- see #applyLauncherPlacement for what that costs the host.
|
|
420
|
+
*/
|
|
421
|
+
#launcherPos: { readonly left: number; readonly top: number } | null = null;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Where the user dragged the *panel*, in viewport coordinates, or null while
|
|
425
|
+
* its position is still derived from the launcher's.
|
|
426
|
+
*
|
|
427
|
+
* The two gestures state different things and are restored differently. A
|
|
428
|
+
* launcher drag says where the bubble goes and leaves the panel to open into
|
|
429
|
+
* whatever space the viewport has, so it is re-derived every time -- which is
|
|
430
|
+
* what lets a widget re-decide its direction when the window changes under
|
|
431
|
+
* it. A header drag states the panel's own position, and re-deriving that
|
|
432
|
+
* from the launcher would move the panel the user just placed.
|
|
433
|
+
*/
|
|
434
|
+
#panelPos: { readonly left: number; readonly top: number } | null = null;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The spot #launcherPos and #panelPos were last stated as, or null while
|
|
438
|
+
* nothing has been stated since this element connected.
|
|
439
|
+
*
|
|
440
|
+
* Those two say where the launcher and the panel stand now, held inside the
|
|
441
|
+
* viewport, because that is where a press finds them and a drag starts from.
|
|
442
|
+
* This is what the user asked for, kept apart so that a window narrower than
|
|
443
|
+
* the spot, or a phone that took the panel full-bleed, gives it back unchanged
|
|
444
|
+
* once there is room again. Only a gesture, a move or a dragged size coming
|
|
445
|
+
* off changes it; re-applying it against a viewport never does, so a clamped
|
|
446
|
+
* position never quietly becomes the record.
|
|
447
|
+
*/
|
|
448
|
+
#cornerSpot: CornerSpot | null = null;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The inset a grip wrote on a corner panel with no spot, or null. A grip on an
|
|
452
|
+
* edge the layout holds moves the panel without stating a spot, so this is the
|
|
453
|
+
* only way to tell its inline inset from one a host wrote there, which stays,
|
|
454
|
+
* and to give it back once a phone has taken the panel full-bleed.
|
|
455
|
+
*/
|
|
456
|
+
#gripInset: string | null = null;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The inset standing a panel at the start of a window a host's size fills, or
|
|
460
|
+
* null: a corner panel with no spot, or a docked one with or without one. Told
|
|
461
|
+
* apart from a host's own for the same reason, and it lasts only as long as that
|
|
462
|
+
* size does.
|
|
463
|
+
*/
|
|
464
|
+
#fillInset: string | null = null;
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Where the user dragged a docked panel, in viewport coordinates, or null
|
|
468
|
+
* while it stands on the bar or the anchor.
|
|
469
|
+
*
|
|
470
|
+
* Apart from the two corner positions above because it moves nothing else:
|
|
471
|
+
* a docked panel has no launcher to carry, and the corner it opens from is
|
|
472
|
+
* the dock's. Held as the user stated it and never as clamped, so a bar that
|
|
473
|
+
* grew, or a phone that took the panel full-bleed, gives the spot back
|
|
474
|
+
* unchanged once there is room for it again.
|
|
475
|
+
*/
|
|
476
|
+
#dockPos: { readonly left: number; readonly top: number } | null = null;
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* The line a docked panel stays above, in viewport coordinates: the bar's
|
|
480
|
+
* top, or the anchor's. Infinite where neither is drawn. Measured by
|
|
481
|
+
* syncDock, which runs on every change either can make.
|
|
482
|
+
*/
|
|
483
|
+
#dockTop = Number.POSITIVE_INFINITY;
|
|
484
|
+
/**
|
|
485
|
+
* The line a panel docked below its anchor stays below: the anchor's bottom.
|
|
486
|
+
* Negative infinity on every other dock.
|
|
487
|
+
*/
|
|
488
|
+
#dockFloor = Number.NEGATIVE_INFINITY;
|
|
489
|
+
/** The side of the anchor the panel last stood on, held while a gesture runs. */
|
|
490
|
+
#anchorSide: AnchorSide = "above";
|
|
491
|
+
/** The bar's height, as last measured: what a fill takes away when it stamps. */
|
|
492
|
+
#barHeight = 0;
|
|
493
|
+
/**
|
|
494
|
+
* What the usable box and the window measured when a fill took the bar away, and what
|
|
495
|
+
* the bar was. Null while no fill holds it. See {@link #fillRoom}.
|
|
496
|
+
*/
|
|
497
|
+
#barTaken: { readonly room: number; readonly window: number; readonly bar: number } | null = null;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The corner the panel opens away from, once this element is placing itself.
|
|
501
|
+
* Null means the host's layout still decides, and the anchor is measured.
|
|
502
|
+
*/
|
|
503
|
+
#expandCorner: ExpandCorner | null = null;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The edges the layout is holding still, as last measured. Cached because a
|
|
507
|
+
* resize reads it per pointer move and measuring forces a reflow -- thirty a
|
|
508
|
+
* second while the panel is already being laid out on every one of them.
|
|
509
|
+
*/
|
|
510
|
+
#anchor: ResizeAnchor = { x: "right", y: "bottom" };
|
|
511
|
+
|
|
512
|
+
/** The header the panel drag listens on, stamped by #stampHeader. */
|
|
513
|
+
#header: HTMLElement | null = null;
|
|
514
|
+
|
|
515
|
+
/** Whether a grip has applied a box it has not committed yet. */
|
|
516
|
+
#resizing = false;
|
|
517
|
+
|
|
518
|
+
/** The eight grips, by name, so the keyboard-reachable one can be moved. */
|
|
519
|
+
readonly #resizeHandles = new Map<string, HTMLDivElement>();
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Hold a resized box inside the part of the screen the host left free.
|
|
523
|
+
*
|
|
524
|
+
* Each edge on its own, unlike the drag's clamp: a drag moves a box of fixed
|
|
525
|
+
* size, so pushing it back in is right, while a resize is anchored on the
|
|
526
|
+
* opposite edge and pushing it back would move the edge the user is not
|
|
527
|
+
* touching. Bounding each edge instead leaves the grip stopped at the limit
|
|
528
|
+
* -- the gesture keeps going and the panel simply stops growing, which is
|
|
529
|
+
* what dragging already does.
|
|
530
|
+
*
|
|
531
|
+
* The minimum size is the grip's own concern and is applied before this, so
|
|
532
|
+
* a panel that cannot fit the space is left at its minimum and overflowing
|
|
533
|
+
* rather than collapsed to nothing.
|
|
534
|
+
*/
|
|
535
|
+
#withinViewport(box: PanelRect): PanelRect {
|
|
536
|
+
// A docked panel that moves has its room stop at the dock, so no grip
|
|
537
|
+
// pulls it over the bar it minimises to. A placement that owns its
|
|
538
|
+
// position keeps the bound it always had.
|
|
539
|
+
const viewport = this.#docked() && this.#panelMovable() ? this.#dockRoom() : this.#viewport();
|
|
540
|
+
// The same bound a drag stops at, so a grip pulled to the edge and a panel
|
|
541
|
+
// dragged to it come to rest on the same line. The inner Math.max/min pair
|
|
542
|
+
// keeps an already-inverted box from turning inside out.
|
|
543
|
+
const left = viewport.left + SCREEN_EDGE_MARGIN;
|
|
544
|
+
const top = viewport.top + SCREEN_EDGE_MARGIN;
|
|
545
|
+
const right = viewport.left + viewport.width - SCREEN_EDGE_MARGIN;
|
|
546
|
+
const bottom = viewport.top + viewport.height - SCREEN_EDGE_MARGIN;
|
|
547
|
+
return {
|
|
548
|
+
left: Math.min(Math.max(box.left, left), box.right),
|
|
549
|
+
top: Math.min(Math.max(box.top, top), box.bottom),
|
|
550
|
+
right: Math.max(Math.min(box.right, right), box.left),
|
|
551
|
+
bottom: Math.max(Math.min(box.bottom, bottom), box.top),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Whether a pointer or key gesture is currently placing the widget.
|
|
557
|
+
*
|
|
558
|
+
* Read from the stamp the drag helpers already set, rather than tracked
|
|
559
|
+
* separately: one source of truth, and it clears on `pointercancel` as well
|
|
560
|
+
* as `pointerup`, which is the end a touch gesture usually gets. The one
|
|
561
|
+
* gesture that stamps nothing is an arrow key on a grip, which applies a box
|
|
562
|
+
* per key and states it only when the key comes up, hence #resizing.
|
|
563
|
+
*/
|
|
564
|
+
dragging(): boolean {
|
|
565
|
+
return (
|
|
566
|
+
this.#resizing ||
|
|
567
|
+
this.#host.launcher.hasAttribute("data-dragging") ||
|
|
568
|
+
this.#host.root.querySelector(".header[data-dragging], .resize-handle[data-dragging]") !==
|
|
569
|
+
null
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** The report behind `AgUiChat.describeSurface`, whose doc is the contract. */
|
|
574
|
+
describeSurface(): ChatSurfaceReport {
|
|
575
|
+
const box = this.#host.element.getBoundingClientRect();
|
|
576
|
+
const viewport = this.#viewport();
|
|
577
|
+
const fullBleed = coversViewport(box, this.#fillRoom());
|
|
578
|
+
return {
|
|
579
|
+
placement: this.#host.element.getAttribute("placement"),
|
|
580
|
+
collapseTo: this.#surface().collapseTo,
|
|
581
|
+
collapsed: this.#host.collapsed(),
|
|
582
|
+
collapsible: this.#host.collapsible(),
|
|
583
|
+
maximized: this.#host.element.hasAttribute("maximized"),
|
|
584
|
+
movable: this.#panelMovable() && !fullBleed,
|
|
585
|
+
// The page's setting alone, true unless `data-launcher-drag="false"`, so
|
|
586
|
+
// it does not change with `collapse-to`; whether this panel moves is `movable`.
|
|
587
|
+
draggable: this.#host.element.getAttribute("data-launcher-drag") !== "false",
|
|
588
|
+
fullBleed,
|
|
589
|
+
box: {
|
|
590
|
+
left: Math.round(box.left),
|
|
591
|
+
top: Math.round(box.top),
|
|
592
|
+
width: Math.round(box.width),
|
|
593
|
+
height: Math.round(box.height),
|
|
594
|
+
},
|
|
595
|
+
viewport: {
|
|
596
|
+
left: Math.round(viewport.left),
|
|
597
|
+
top: Math.round(viewport.top),
|
|
598
|
+
width: Math.round(viewport.width),
|
|
599
|
+
height: Math.round(viewport.height),
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** The move behind `AgUiChat.moveTo`, whose doc is the contract. */
|
|
605
|
+
moveTo(corner: ChatCorner, options: { readonly announce?: boolean } = {}): boolean {
|
|
606
|
+
if (!this.#panelMovable()) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
const restore = options.announce === true ? this.#captureGeometry() : null;
|
|
610
|
+
const viewport = this.#viewport();
|
|
611
|
+
const box = this.#host.element.getBoundingClientRect();
|
|
612
|
+
if (coversViewport(box, this.#fillRoom())) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
const [edgeY, edgeX] = corner.split("-");
|
|
616
|
+
// A docked panel's bottom corners are above its dock, not on the bar.
|
|
617
|
+
const docked = this.#docked();
|
|
618
|
+
const room = docked ? this.#dockRoom() : viewport;
|
|
619
|
+
// Every term is an absolute screen coordinate, because that is what the
|
|
620
|
+
// clamps and the insets both speak. The usable box carries an origin, so
|
|
621
|
+
// its near edge is `viewport.left`, not zero, and its far edge is
|
|
622
|
+
// `viewport.left + viewport.width` -- a margin applied to the extents
|
|
623
|
+
// alone would send the agent's own move under the chrome the host
|
|
624
|
+
// reserved, which is the failure the usable box exists to prevent.
|
|
625
|
+
const nearX = room.left + EDGE_MARGIN;
|
|
626
|
+
const nearY = room.top + EDGE_MARGIN;
|
|
627
|
+
const left =
|
|
628
|
+
edgeX === "left" ? nearX : Math.max(nearX, room.left + room.width - EDGE_MARGIN - box.width);
|
|
629
|
+
const top =
|
|
630
|
+
edgeY === "top" ? nearY : Math.max(nearY, room.top + room.height - EDGE_MARGIN - box.height);
|
|
631
|
+
const host = { left, top, right: left + box.width, bottom: top + box.height };
|
|
632
|
+
if (docked) {
|
|
633
|
+
this.#dockPos = { left, top };
|
|
634
|
+
this.#writeDockInset(host);
|
|
635
|
+
this.#storeDockPosition(this.#dockPos);
|
|
636
|
+
if (restore !== null) {
|
|
637
|
+
this.#host.announceSurfaceChange(this.#host.strings().chatMoved, restore);
|
|
638
|
+
}
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
// Both axes measured, rather than one read twice: a host can restyle the
|
|
642
|
+
// launcher as a pill, and squaring it here would put it off the corner.
|
|
643
|
+
const launcherWidth = this.#host.launcher.offsetWidth;
|
|
644
|
+
const launcherHeight = this.#host.launcher.offsetHeight;
|
|
645
|
+
this.#placePanelAndLauncher(host, {
|
|
646
|
+
left: edgeX === "left" ? host.left : host.right - launcherWidth,
|
|
647
|
+
top: edgeY === "top" ? host.top : host.bottom - launcherHeight,
|
|
648
|
+
width: launcherWidth,
|
|
649
|
+
height: launcherHeight,
|
|
650
|
+
});
|
|
651
|
+
this.#storeLauncherPosition();
|
|
652
|
+
if (restore !== null) {
|
|
653
|
+
this.#host.announceSurfaceChange(this.#host.strings().chatMoved, restore);
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Snapshot the panel's stated position, and return a function that puts it
|
|
660
|
+
* back.
|
|
661
|
+
*
|
|
662
|
+
* Both insets and the expand corner, because they are one decision: the
|
|
663
|
+
* corner is what the panel grows from, so restoring a position without it
|
|
664
|
+
* puts the box back and animates it out of the wrong side. Absent values are
|
|
665
|
+
* captured as absent and removed on the way back, rather than written as
|
|
666
|
+
* empty strings that would outrank the placement. And what the inset is, because
|
|
667
|
+
* one put back with no record of having come from a grip or a fill reads as a
|
|
668
|
+
* host's own, and a phone would leave it holding the panel off the screen.
|
|
669
|
+
*
|
|
670
|
+
* The surface may have changed by the time of the undo, so the two halves are
|
|
671
|
+
* put back apart. Both records are, always, each to its own store, so the
|
|
672
|
+
* memory and the storage cannot disagree and the surface they belong to finds
|
|
673
|
+
* them when it comes back. The inline values only mean something to the surface
|
|
674
|
+
* they were read from: written on any other, they pin a sidebar to where a
|
|
675
|
+
* floating panel used to be.
|
|
676
|
+
*/
|
|
677
|
+
#captureGeometry(): () => void {
|
|
678
|
+
const inset = this.#host.element.style.getPropertyValue("--ag-ui-inset");
|
|
679
|
+
const launcherInset = this.#host.element.style.getPropertyValue("--ag-ui-launcher-inset");
|
|
680
|
+
const corner = this.#host.element.getAttribute("data-expand-corner");
|
|
681
|
+
const launcherPos = this.#launcherPos;
|
|
682
|
+
const panelPos = this.#panelPos;
|
|
683
|
+
const dockPos = this.#dockPos ?? this.#readDockPosition();
|
|
684
|
+
const expandCorner = this.#expandCorner;
|
|
685
|
+
const gripInset = this.#gripInset;
|
|
686
|
+
const fillInset = this.#fillInset;
|
|
687
|
+
const cornerSpot = this.#cornerSpot ?? this.#readLauncherPosition();
|
|
688
|
+
const owner = this.#positionOwner();
|
|
689
|
+
return () => {
|
|
690
|
+
this.#dockPos = dockPos;
|
|
691
|
+
this.#storeDockPosition(dockPos);
|
|
692
|
+
// Erased rather than rewritten when there was nothing to go back to,
|
|
693
|
+
// which would leave the move this is undoing sitting in storage -- and
|
|
694
|
+
// since that store outlives the tab, the next resize or reload would
|
|
695
|
+
// quietly put the panel back in the corner the user had just rejected.
|
|
696
|
+
if (cornerSpot === null) {
|
|
697
|
+
this.#cornerSpot = null;
|
|
698
|
+
this.#host.clearPreference(LAUNCHER_KEY);
|
|
699
|
+
} else {
|
|
700
|
+
this.#writeCornerSpot(cornerSpot);
|
|
701
|
+
}
|
|
702
|
+
if (owner === this.#positionOwner()) {
|
|
703
|
+
this.#restoreProperty("--ag-ui-inset", inset);
|
|
704
|
+
this.#restoreProperty("--ag-ui-launcher-inset", launcherInset);
|
|
705
|
+
if (corner === null) {
|
|
706
|
+
this.#host.element.removeAttribute("data-expand-corner");
|
|
707
|
+
} else {
|
|
708
|
+
this.#host.element.setAttribute("data-expand-corner", corner);
|
|
709
|
+
}
|
|
710
|
+
this.#launcherPos = launcherPos;
|
|
711
|
+
this.#panelPos = panelPos;
|
|
712
|
+
this.#expandCorner = expandCorner;
|
|
713
|
+
this.#gripInset = gripInset;
|
|
714
|
+
this.#fillInset = fillInset;
|
|
715
|
+
} else {
|
|
716
|
+
this.#forgetCornerPosition();
|
|
717
|
+
}
|
|
718
|
+
// Against the viewport there is now, which may be a phone's: the insets
|
|
719
|
+
// put back above were written for the one the move was made in.
|
|
720
|
+
this.restoreLauncherPosition();
|
|
721
|
+
this.syncResizeAnchor();
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Which of the three kinds of surface decides where the panel stands. */
|
|
726
|
+
#positionOwner(): "dock" | "corner" | "placement" {
|
|
727
|
+
if (this.#docked()) {
|
|
728
|
+
return "dock";
|
|
729
|
+
}
|
|
730
|
+
return this.#surface().launcherDraggable ? "corner" : "placement";
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Which axes the current placement allows.
|
|
735
|
+
*
|
|
736
|
+
* A full-bleed layout is `100vw`/`100vh` by definition and cannot be resized
|
|
737
|
+
* at all; a docked panel owns its height, leaving only its inner edge. Read
|
|
738
|
+
* per interaction, because `placement` is a live attribute.
|
|
739
|
+
*/
|
|
740
|
+
#resizeAxis(): ResizeAxis {
|
|
741
|
+
// Refused while maximized, so restore always lands on the size stored before it.
|
|
742
|
+
return this.#host.element.hasAttribute("maximized") ? "none" : this.#surface().resizeAxis;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Which edges the layout is holding still, by measuring rather than guessing:
|
|
747
|
+
* nudge the size by a pixel, see which edges stayed put, and undo. One forced
|
|
748
|
+
* reflow per drag.
|
|
749
|
+
*
|
|
750
|
+
* `placement` cannot answer this — an embedded panel goes wherever the page's
|
|
751
|
+
* CSS puts it — and see {@link createResizeHandle} for why guessing produces
|
|
752
|
+
* a visibly broken control.
|
|
753
|
+
*/
|
|
754
|
+
#measureAnchor(): ResizeAnchor {
|
|
755
|
+
const before = this.#host.element.getBoundingClientRect();
|
|
756
|
+
const width = this.#host.element.style.getPropertyValue(DRAG_WIDTH);
|
|
757
|
+
const height = this.#host.element.style.getPropertyValue(DRAG_HEIGHT);
|
|
758
|
+
// Shrink first: at a size resting against max-width or max-height a grown
|
|
759
|
+
// box does not change, and every clamped axis would read as pinned on its
|
|
760
|
+
// free side (the default panel is born clamped under 428px). Shrinking is
|
|
761
|
+
// measured from the used width, so it always moves an edge.
|
|
762
|
+
const shrunk = this.#probeAnchor(before, -1);
|
|
763
|
+
// Unless a host rule sets a minimum, in which case that axis is asked the
|
|
764
|
+
// opposite question rather than left to a guess.
|
|
765
|
+
const grown = shrunk.x === null || shrunk.y === null ? this.#probeAnchor(before, 1) : shrunk;
|
|
766
|
+
// Restore exactly what was there, including "nothing" — leaving a probe
|
|
767
|
+
// value behind would pin a panel that had been sizing itself.
|
|
768
|
+
this.#restoreProperty(DRAG_WIDTH, width);
|
|
769
|
+
this.#restoreProperty(DRAG_HEIGHT, height);
|
|
770
|
+
return {
|
|
771
|
+
// Neither direction moved it: the axis cannot be resized at all, so the
|
|
772
|
+
// floating default is the best answer available and is the one the
|
|
773
|
+
// stylesheet would have used with no measurement at all.
|
|
774
|
+
x: shrunk.x ?? grown.x ?? "right",
|
|
775
|
+
y: shrunk.y ?? grown.y ?? "bottom",
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Which edge each axis holds still when the panel changes size by `delta`.
|
|
781
|
+
*
|
|
782
|
+
* Null for an axis whose size did not change: nothing moved, so nothing was
|
|
783
|
+
* learned, and reporting the unmoved edge as the pinned one would be exactly
|
|
784
|
+
* backwards.
|
|
785
|
+
*/
|
|
786
|
+
#probeAnchor(
|
|
787
|
+
before: DOMRect,
|
|
788
|
+
delta: number,
|
|
789
|
+
): { x: "left" | "right" | null; y: "top" | "bottom" | null } {
|
|
790
|
+
this.#applySize({ width: before.width + delta, height: before.height + delta });
|
|
791
|
+
const after = this.#host.element.getBoundingClientRect();
|
|
792
|
+
const moved = (a: number, b: number): boolean => Math.abs(a - b) >= 0.5;
|
|
793
|
+
return {
|
|
794
|
+
x: moved(after.width, before.width)
|
|
795
|
+
? moved(after.left, before.left)
|
|
796
|
+
? "right"
|
|
797
|
+
: "left"
|
|
798
|
+
: null,
|
|
799
|
+
y: moved(after.height, before.height)
|
|
800
|
+
? moved(after.top, before.top)
|
|
801
|
+
? "bottom"
|
|
802
|
+
: "top"
|
|
803
|
+
: null,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Stamp the measured anchor so the shadow CSS can place the grip. */
|
|
808
|
+
syncResizeAnchor(): void {
|
|
809
|
+
if (!this.#host.connected()) {
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
// When this element owns its position it knows which edges are pinned --
|
|
813
|
+
// it just wrote them -- so there is nothing to probe. The probe is also
|
|
814
|
+
// unreliable at a size resting against max-width or max-height, where a
|
|
815
|
+
// nudge moves no edge and every axis reads as pinned on the wrong side.
|
|
816
|
+
const anchor = this.#expandCorner ?? this.#measureAnchor();
|
|
817
|
+
this.#anchor = anchor;
|
|
818
|
+
this.#host.element.setAttribute("data-resize-anchor", `${anchor.y}-${anchor.x}`);
|
|
819
|
+
this.#focusableGrip();
|
|
820
|
+
this.#stampHeader();
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Tell the stylesheet whether the panel is the screen, for the header's cursor.
|
|
825
|
+
*
|
|
826
|
+
* Only this half of what refuses a header drag: the resting rules already answer
|
|
827
|
+
* for the placement and for data-launcher-drag="false", and neither of those
|
|
828
|
+
* re-applies anything when it changes, so a stamp taking them in would go on
|
|
829
|
+
* refusing, in the cursor, a drag the header would take.
|
|
830
|
+
*/
|
|
831
|
+
#stampHeader(): void {
|
|
832
|
+
this.#header?.toggleAttribute("data-fills-screen", this.#fillsScreen());
|
|
833
|
+
// And for the bar and the grips, which go while it does. The next frame, because
|
|
834
|
+
// this runs inside the transcript's observer, and the bar it takes away sits
|
|
835
|
+
// above that in the tree: its own observer would be left undelivered, which the
|
|
836
|
+
// browser reports on the window, where a host's error tracking counts it.
|
|
837
|
+
const element = this.#host.element;
|
|
838
|
+
if (this.#fillsWindow() !== element.hasAttribute("data-fills-window")) {
|
|
839
|
+
requestAnimationFrame(() => {
|
|
840
|
+
const fills = this.#fillsWindow();
|
|
841
|
+
if (fills !== element.hasAttribute("data-fills-window")) {
|
|
842
|
+
this.#barTaken = fills
|
|
843
|
+
? { room: this.#viewport().height, window: this.#screen().height, bar: this.#barHeight }
|
|
844
|
+
: null;
|
|
845
|
+
element.toggleAttribute("data-fills-window", fills);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Whether the panel fills the window in a placement it can move in. Not one that owns
|
|
853
|
+
* its position: full stops at the bar, so taking the bar away would make it fill the
|
|
854
|
+
* window, and that would take the bar away for good.
|
|
855
|
+
*/
|
|
856
|
+
#fillsWindow(): boolean {
|
|
857
|
+
return this.#fillsScreen() && this.#surface().panelMovable;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** Put a custom property back to a previous value, or remove it if there was none. */
|
|
861
|
+
#restoreProperty(name: string, value: string): void {
|
|
862
|
+
if (value === "") {
|
|
863
|
+
this.#host.element.style.removeProperty(name);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.#host.element.style.setProperty(name, value);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Write a dragged size onto the host, on the axes this placement leaves free.
|
|
871
|
+
*
|
|
872
|
+
* Writing a custom property rather than inline `width` / `height` does not
|
|
873
|
+
* by itself leave placement in charge: an inline custom property still
|
|
874
|
+
* outranks a `:host([placement=…])` rule setting the same property, so a
|
|
875
|
+
* height dragged while floating would cap a docked sidebar asking for
|
|
876
|
+
* `100vh`. The cascade cannot arbitrate this, so the axis check must — a
|
|
877
|
+
* placement owns the axes it fixes, and a persisted size is applied only to
|
|
878
|
+
* the ones it leaves free.
|
|
879
|
+
*/
|
|
880
|
+
#applySize(size: ResizeSize): void {
|
|
881
|
+
const axis = this.#resizeAxis();
|
|
882
|
+
if (axis === "none") {
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
// The placement's max-width and max-height are left alone, so a grip pushed
|
|
886
|
+
// against the edge the placement is not anchored to stops one gutter short:
|
|
887
|
+
// moving the cap would shift several resting sizes by a pixel. Staying inside
|
|
888
|
+
// what the host left free is enforced by #withinViewport.
|
|
889
|
+
if (size.width !== undefined) {
|
|
890
|
+
this.#host.element.style.setProperty(DRAG_WIDTH, `${size.width}px`);
|
|
891
|
+
}
|
|
892
|
+
if (size.height !== undefined && axis === "both") {
|
|
893
|
+
this.#host.element.style.setProperty(DRAG_HEIGHT, `${size.height}px`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Drop any dragged size the new placement has taken ownership of.
|
|
899
|
+
*
|
|
900
|
+
* Without this a size survives the switch as an inline property and silently
|
|
901
|
+
* overrides the placement it moved to — the panel keeps a floating height
|
|
902
|
+
* while docked, and reads as a component that cannot do full height.
|
|
903
|
+
*/
|
|
904
|
+
releaseOwnedAxes(): void {
|
|
905
|
+
const axis = this.#resizeAxis();
|
|
906
|
+
if (axis !== "both") {
|
|
907
|
+
this.#host.element.style.removeProperty(DRAG_HEIGHT);
|
|
908
|
+
}
|
|
909
|
+
if (axis === "none") {
|
|
910
|
+
this.#host.element.style.removeProperty(DRAG_WIDTH);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** The surface the live placement and collapse-to attributes describe. */
|
|
915
|
+
#surface(): Surface {
|
|
916
|
+
const element = this.#host.element;
|
|
917
|
+
return surfaceFor(element.getAttribute("placement"), element.getAttribute("collapse-to"));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Whether the panel rises from the bottom bar or the host's anchor, until the
|
|
922
|
+
* user drags it somewhere else.
|
|
923
|
+
*/
|
|
924
|
+
#docked(): boolean {
|
|
925
|
+
const target = this.#surface().collapseTo;
|
|
926
|
+
return target === "bar" || target === "none";
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Write where a docked panel stands, and return the bar's height.
|
|
931
|
+
*
|
|
932
|
+
* Under the bar it is the strip's border box, which is 0 whenever the strip
|
|
933
|
+
* is not drawn (a phone with the panel open, where the panel is the screen).
|
|
934
|
+
* Under a host anchor it is the anchor's top, or its bottom on the side below,
|
|
935
|
+
* and its right, measured against the
|
|
936
|
+
* layout viewport a fixed element is placed in; an anchor of null leaves the
|
|
937
|
+
* panel in its placement's own corner. Every other surface carries neither
|
|
938
|
+
* property, so nothing stale is left behind by a live switch.
|
|
939
|
+
*/
|
|
940
|
+
syncDock(bar: HTMLElement, anchor: Element | null): number {
|
|
941
|
+
const element = this.#host.element;
|
|
942
|
+
const target = this.#surface().collapseTo;
|
|
943
|
+
const height = target === "bar" ? bar.offsetHeight : 0;
|
|
944
|
+
const rect = target === "none" && anchor !== null ? anchor.getBoundingClientRect() : null;
|
|
945
|
+
const side = rect === null ? null : this.#resolveSide(rect);
|
|
946
|
+
if (target === "bar") {
|
|
947
|
+
element.style.setProperty(DOCK_BOTTOM, `${height}px`);
|
|
948
|
+
} else if (rect !== null && side === "above") {
|
|
949
|
+
element.style.setProperty(DOCK_BOTTOM, `${Math.max(0, this.#screen().height - rect.top)}px`);
|
|
950
|
+
} else {
|
|
951
|
+
element.style.removeProperty(DOCK_BOTTOM);
|
|
952
|
+
}
|
|
953
|
+
if (rect !== null && side === "below") {
|
|
954
|
+
element.style.setProperty(DOCK_TOP, `${Math.max(0, rect.bottom)}px`);
|
|
955
|
+
} else {
|
|
956
|
+
element.style.removeProperty(DOCK_TOP);
|
|
957
|
+
}
|
|
958
|
+
if (rect !== null) {
|
|
959
|
+
element.style.setProperty(DOCK_RIGHT, `${this.#dockRight(rect)}px`);
|
|
960
|
+
} else {
|
|
961
|
+
element.style.removeProperty(DOCK_RIGHT);
|
|
962
|
+
}
|
|
963
|
+
if (side === null) {
|
|
964
|
+
element.removeAttribute("data-dock-anchored");
|
|
965
|
+
} else {
|
|
966
|
+
element.setAttribute("data-dock-anchored", side);
|
|
967
|
+
}
|
|
968
|
+
this.#barHeight = height;
|
|
969
|
+
if (target !== "bar") {
|
|
970
|
+
this.#barTaken = null;
|
|
971
|
+
}
|
|
972
|
+
this.#dockTop =
|
|
973
|
+
target === "bar" && height > 0
|
|
974
|
+
? bar.getBoundingClientRect().top
|
|
975
|
+
: rect !== null && side === "above"
|
|
976
|
+
? rect.top
|
|
977
|
+
: Number.POSITIVE_INFINITY;
|
|
978
|
+
this.#dockFloor = rect !== null && side === "below" ? rect.bottom : Number.NEGATIVE_INFINITY;
|
|
979
|
+
// Every change the dock can make passes through here -- the bar's height
|
|
980
|
+
// arrives by a ResizeObserver after the first restore, and a bar that grew
|
|
981
|
+
// since the drag would otherwise cover the panel's foot -- so the dragged
|
|
982
|
+
// spot is held against it here rather than only when it is restored.
|
|
983
|
+
this.#placeDocked();
|
|
984
|
+
return height;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* The part of the usable viewport a docked panel may stand in: all of it
|
|
989
|
+
* above the dock's line. The bar is what it minimises to, and the anchor is
|
|
990
|
+
* the host's own way back, so a dragged panel never covers either.
|
|
991
|
+
*/
|
|
992
|
+
#dockRoom(): ViewportBox {
|
|
993
|
+
const viewport = this.#viewport();
|
|
994
|
+
const top = Math.max(viewport.top, this.#dockFloor);
|
|
995
|
+
const bottom = Math.min(viewport.top + viewport.height, this.#dockTop);
|
|
996
|
+
return { ...viewport, top, height: Math.max(0, bottom - top) };
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* The side of the anchor the panel opens on: the host's anchor-side, or under
|
|
1001
|
+
* auto the side with more of the usable viewport, kept while a gesture runs.
|
|
1002
|
+
*/
|
|
1003
|
+
#resolveSide(rect: DOMRect): AnchorSide {
|
|
1004
|
+
const stated = this.#host.element.getAttribute("anchor-side");
|
|
1005
|
+
if (stated === "above" || stated === "below") {
|
|
1006
|
+
this.#anchorSide = stated;
|
|
1007
|
+
} else if (!this.dragging()) {
|
|
1008
|
+
const viewport = this.#viewport();
|
|
1009
|
+
const below = viewport.top + viewport.height - rect.bottom;
|
|
1010
|
+
this.#anchorSide = below > rect.top - viewport.top ? "below" : "above";
|
|
1011
|
+
}
|
|
1012
|
+
return this.#anchorSide;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* The panel's distance from the right edge: on the anchor's right, unless a
|
|
1017
|
+
* panel that wide would then cross the gutter on the left of the usable box,
|
|
1018
|
+
* and never inside the gutter on the right of it.
|
|
1019
|
+
*/
|
|
1020
|
+
#dockRight(rect: DOMRect): number {
|
|
1021
|
+
const screen = this.#screen();
|
|
1022
|
+
const fits =
|
|
1023
|
+
screen.width - this.#viewport().left - EDGE_MARGIN - this.#host.element.offsetWidth;
|
|
1024
|
+
const floor = EDGE_MARGIN + reservedEdge(getComputedStyle(this.probe), "padding-right");
|
|
1025
|
+
return Math.max(floor, Math.min(screen.width - rect.right, fits));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Put a docked panel back where the user dragged it, held inside its room.
|
|
1030
|
+
*
|
|
1031
|
+
* The inline inset comes off first, which is both how the panel's resting
|
|
1032
|
+
* box is measured and what leaves a phone full-bleed: the small-viewport
|
|
1033
|
+
* block reads the host's inset before its own, so a dragged spot written
|
|
1034
|
+
* there would hold the panel off the screen's edges. The spot itself stays,
|
|
1035
|
+
* for the next time there is room for it.
|
|
1036
|
+
*
|
|
1037
|
+
* A host's size that makes the panel the window hangs it off the resting inset's
|
|
1038
|
+
* far side, past the top of the window with the header on it under the bar, so
|
|
1039
|
+
* it stands at the start of the room instead, moved or not, as a corner panel
|
|
1040
|
+
* does, and the stylesheet takes the bar away for as long as it does. A placement
|
|
1041
|
+
* that owns its position keeps it, whatever the host's size.
|
|
1042
|
+
*
|
|
1043
|
+
* Under data-launcher-drag="false" a spot from before the attribute is left alone
|
|
1044
|
+
* while the panel is on the screen, as a corner panel's is, and the record stays
|
|
1045
|
+
* for the day the attribute comes off.
|
|
1046
|
+
*/
|
|
1047
|
+
#placeDocked(): void {
|
|
1048
|
+
if (!this.#docked() || !this.#surface().panelMovable || this.dragging()) {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const element = this.#host.element;
|
|
1052
|
+
const at = this.#panelMovable() ? (this.#dockPos ?? this.#readDockPosition()) : null;
|
|
1053
|
+
if (at === null) {
|
|
1054
|
+
if (this.#dockPos !== null) {
|
|
1055
|
+
const filling = element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset;
|
|
1056
|
+
if (!filling && !this.#offScreen()) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
this.#dockPos = null;
|
|
1060
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1061
|
+
}
|
|
1062
|
+
this.#placeUnstated();
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
this.#dockPos = at;
|
|
1066
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1067
|
+
const rest = element.getBoundingClientRect();
|
|
1068
|
+
const viewport = this.#viewport();
|
|
1069
|
+
if (coversViewport(rest, this.#fillRoom())) {
|
|
1070
|
+
if (hangsOff(rest, viewport, this.#screen())) {
|
|
1071
|
+
this.#standAtStart(rest);
|
|
1072
|
+
}
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
// Measured at the dock, so the inset below is written from the corner the
|
|
1076
|
+
// dock holds still and a resize grows the panel away from it.
|
|
1077
|
+
this.syncResizeAnchor();
|
|
1078
|
+
this.#writeDockInset(
|
|
1079
|
+
clampPanel(
|
|
1080
|
+
{ left: at.left, top: at.top, right: at.left + rest.width, bottom: at.top + rest.height },
|
|
1081
|
+
this.#dockRoom(),
|
|
1082
|
+
SCREEN_EDGE_MARGIN,
|
|
1083
|
+
),
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** Write the inset that stands a docked panel at `box`. */
|
|
1088
|
+
#writeDockInset(box: PanelRect): void {
|
|
1089
|
+
this.#host.element.style.setProperty(
|
|
1090
|
+
"--ag-ui-inset",
|
|
1091
|
+
insetFrom(this.#anchor, box, this.#screen()),
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/** Remember a docked panel's dragged spot, or forget it for null. */
|
|
1096
|
+
#storeDockPosition(at: { readonly left: number; readonly top: number } | null): void {
|
|
1097
|
+
if (at === null) {
|
|
1098
|
+
this.#host.clearPreference(DOCK_KEY);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
this.#host.writePreference(DOCK_KEY, JSON.stringify({ left: at.left, top: at.top }));
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** The stored spot of a docked panel, or null for none or anything corrupt. */
|
|
1105
|
+
#readDockPosition(): { readonly left: number; readonly top: number } | null {
|
|
1106
|
+
const raw = this.#host.readPreference(DOCK_KEY);
|
|
1107
|
+
if (raw === null) {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
return asPoint(JSON.parse(raw));
|
|
1112
|
+
} catch {
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Whether the header drag and moveTo may move the open panel: the
|
|
1119
|
+
* placement's answer, unless the host turned moving off.
|
|
1120
|
+
*/
|
|
1121
|
+
#panelMovable(): boolean {
|
|
1122
|
+
return (
|
|
1123
|
+
this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
|
|
1124
|
+
this.#surface().panelMovable
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Whether a header drag may start now. A panel that is the screen, on a phone
|
|
1130
|
+
* or by a host's own sizing, has nowhere to go, and a drag there would pull it
|
|
1131
|
+
* off the edges it fills.
|
|
1132
|
+
*/
|
|
1133
|
+
#canDragPanel(): boolean {
|
|
1134
|
+
return this.#panelMovable() && !this.#fillsScreen();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** Whether the panel covers the usable viewport, as it stands now. */
|
|
1138
|
+
#fillsScreen(): boolean {
|
|
1139
|
+
return coversViewport(this.#host.element.getBoundingClientRect(), this.#fillRoom());
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* The box a fill is judged against: the usable one, unless taking the bar away is
|
|
1144
|
+
* what grew it.
|
|
1145
|
+
*
|
|
1146
|
+
* A host can write the bar's height into the inset it reserves. The panel fills the
|
|
1147
|
+
* box that leaves, so the bar goes, the height says 0, the host writes that, the box
|
|
1148
|
+
* grows by what the bar took, and the panel no longer fills it. Judged against that,
|
|
1149
|
+
* the bar comes back and it all goes round again every frame without end. So on the
|
|
1150
|
+
* same window, a box grown by the bar's height is the box it was, and the panel
|
|
1151
|
+
* stands where it stood with the bar gone. Grown by anything else, or on a window of
|
|
1152
|
+
* another size, it is a change of the host's or the reader's and is judged as one.
|
|
1153
|
+
* The slack is the pixel a host rounding the height up can add.
|
|
1154
|
+
*/
|
|
1155
|
+
#fillRoom(): ViewportBox {
|
|
1156
|
+
const viewport = this.#viewport();
|
|
1157
|
+
const taken = this.#barTaken;
|
|
1158
|
+
if (
|
|
1159
|
+
taken !== null &&
|
|
1160
|
+
taken.window === this.#screen().height &&
|
|
1161
|
+
Math.abs(viewport.height - taken.room - taken.bar) <= 1
|
|
1162
|
+
) {
|
|
1163
|
+
return { ...viewport, height: taken.room };
|
|
1164
|
+
}
|
|
1165
|
+
return viewport;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Whether the panel stands off an edge of the screen, as it stands now.
|
|
1170
|
+
*
|
|
1171
|
+
* The screen rather than the usable box: a panel under chrome the host reserved
|
|
1172
|
+
* has its controls where the host can still bring them back, and is not what a
|
|
1173
|
+
* position kept under data-launcher-drag="false" has to give way to.
|
|
1174
|
+
*/
|
|
1175
|
+
#offScreen(): boolean {
|
|
1176
|
+
const screen = this.#screen();
|
|
1177
|
+
return hangsOff(
|
|
1178
|
+
this.#host.element.getBoundingClientRect(),
|
|
1179
|
+
{ left: 0, top: 0, ...screen },
|
|
1180
|
+
screen,
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Whether this placement lets the launcher be dragged, read per interaction
|
|
1186
|
+
* because `placement` is a live attribute. `data-launcher-drag="false"` opts
|
|
1187
|
+
* a host out without giving up the launcher itself.
|
|
1188
|
+
*/
|
|
1189
|
+
#launcherDraggable(): boolean {
|
|
1190
|
+
return (
|
|
1191
|
+
this.#host.element.getAttribute("data-launcher-drag") !== "false" &&
|
|
1192
|
+
this.#surface().launcherDraggable
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* The viewport the launcher and the panel both have to fit inside.
|
|
1198
|
+
*
|
|
1199
|
+
* The *visual* viewport, not the layout one, because they come apart exactly
|
|
1200
|
+
* when this matters. An on-screen keyboard shrinks the visual viewport and
|
|
1201
|
+
* leaves the layout viewport alone, so clamping against `innerHeight` parks
|
|
1202
|
+
* the launcher behind the keyboard and decides which corner to open into
|
|
1203
|
+
* using space that is not on the screen. Pinch-zoom does the same on both
|
|
1204
|
+
* axes.
|
|
1205
|
+
*
|
|
1206
|
+
* Falls back where the API is absent, which keeps this working in the
|
|
1207
|
+
* happy-dom project as well as in an old browser.
|
|
1208
|
+
*/
|
|
1209
|
+
#viewport(): ViewportBox {
|
|
1210
|
+
const visual = window.visualViewport;
|
|
1211
|
+
const width = visual?.width ?? window.innerWidth;
|
|
1212
|
+
const height = visual?.height ?? window.innerHeight;
|
|
1213
|
+
// And minus whatever the host reserved for its own chrome. Without this a
|
|
1214
|
+
// panel is clamped against the whole screen and settles happily underneath
|
|
1215
|
+
// a sticky header, where it cannot be reached -- and where collapsing it,
|
|
1216
|
+
// the one thing a user tries, hides it completely rather than rescuing it.
|
|
1217
|
+
// Read as padding off the probe, not as custom properties off this
|
|
1218
|
+
// element. `getPropertyValue` on an unregistered custom property returns
|
|
1219
|
+
// the token stream rather than a length, so a host stating `4rem` reserves
|
|
1220
|
+
// four pixels here and sixty-four in the stylesheet, and one stating
|
|
1221
|
+
// `calc(56px + env(safe-area-inset-top))` -- which is the natural spelling
|
|
1222
|
+
// of what the token's own documentation recommends -- parses as NaN and
|
|
1223
|
+
// takes the panel's whole inset down with it.
|
|
1224
|
+
const style = getComputedStyle(this.probe);
|
|
1225
|
+
const left = reservedEdge(style, "padding-left");
|
|
1226
|
+
const top = reservedEdge(style, "padding-top");
|
|
1227
|
+
return {
|
|
1228
|
+
left,
|
|
1229
|
+
top,
|
|
1230
|
+
width: Math.max(0, width - left - reservedEdge(style, "padding-right")),
|
|
1231
|
+
height: Math.max(0, height - top - reservedEdge(style, "padding-bottom")),
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* The whole viewport, before anything the host reserved is taken out of it.
|
|
1237
|
+
*
|
|
1238
|
+
* Distinct from {@link #viewport} on purpose, and the two must not be
|
|
1239
|
+
* swapped. The usable box decides where the widget may rest; this is what a
|
|
1240
|
+
* CSS `inset` on a fixed element is measured from, because that is what the
|
|
1241
|
+
* browser measures it from.
|
|
1242
|
+
*/
|
|
1243
|
+
#screen(): Extent {
|
|
1244
|
+
// The *layout* viewport, and `clientWidth`/`clientHeight` rather than
|
|
1245
|
+
// `innerWidth`/`innerHeight`, because those two disagree by the width of a
|
|
1246
|
+
// classic scrollbar and it is the smaller one a fixed element is laid out
|
|
1247
|
+
// against. Reading the visual viewport here would be the same mistake one
|
|
1248
|
+
// level up as clamping against the whole screen was one level down: a
|
|
1249
|
+
// keyboard shrinks the visual viewport without moving the box CSS measures
|
|
1250
|
+
// an inset from, so a panel the clamp had just held inside the visible
|
|
1251
|
+
// band would be written back out behind the keyboard.
|
|
1252
|
+
//
|
|
1253
|
+
// The zero checks are for a detached or not-yet-laid-out document, where
|
|
1254
|
+
// `clientWidth` is 0 and no viewport ever is.
|
|
1255
|
+
const root = document.documentElement;
|
|
1256
|
+
return {
|
|
1257
|
+
width: root.clientWidth || window.innerWidth,
|
|
1258
|
+
height: root.clientHeight || window.innerHeight,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Publish the measured viewport height so the stylesheet can size a
|
|
1264
|
+
* full-bleed panel to what the user can see.
|
|
1265
|
+
*
|
|
1266
|
+
* No CSS length carries this. An on-screen keyboard has no effect on any
|
|
1267
|
+
* viewport-percentage unit, so a panel sized from `100dvh` puts its composer
|
|
1268
|
+
* behind the keyboard being typed into. Written inline, and read through a
|
|
1269
|
+
* token the host's own `--ag-ui-viewport-height` still outranks.
|
|
1270
|
+
*
|
|
1271
|
+
* Removed rather than frozen when the two viewports agree, so a desktop that
|
|
1272
|
+
* never diverges carries no inline override at all and the declared fallback
|
|
1273
|
+
* stays in charge.
|
|
1274
|
+
*/
|
|
1275
|
+
publishVisualViewport(): void {
|
|
1276
|
+
const visual = window.visualViewport;
|
|
1277
|
+
if (visual === null || visual === undefined) {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
// The layout viewport from the box a fixed element is laid out against,
|
|
1281
|
+
// for the reasons #screen gives -- and one more that only a phone shows.
|
|
1282
|
+
// While iOS Safari has the visible area panned down to show a focused
|
|
1283
|
+
// field, innerHeight reads the layout height less the pan, and goes on
|
|
1284
|
+
// reading it until a window scroll the element never hears about. Measured
|
|
1285
|
+
// from that, the band below came out as nothing, and at the deepest pan the
|
|
1286
|
+
// browser makes -- the one that shows a composer docked at the foot of the
|
|
1287
|
+
// screen -- the shorter number equals the visible height, so the check
|
|
1288
|
+
// below took every measurement back and put the panel at the layout top at
|
|
1289
|
+
// full height, its header above the screen. clientHeight held the layout
|
|
1290
|
+
// height throughout.
|
|
1291
|
+
const layout = this.#screen().height;
|
|
1292
|
+
if (Math.abs(visual.height - layout) < 1) {
|
|
1293
|
+
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-height");
|
|
1294
|
+
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-inset-bottom");
|
|
1295
|
+
this.#host.element.style.removeProperty("--ag-ui-visual-viewport-inset-top");
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
this.#host.element.style.setProperty(
|
|
1299
|
+
"--ag-ui-visual-viewport-height",
|
|
1300
|
+
`${Math.round(visual.height)}px`,
|
|
1301
|
+
);
|
|
1302
|
+
// What is hidden below the visible area, which is where a keyboard is. A
|
|
1303
|
+
// shorter panel does not help anything anchored to the bottom: a floating
|
|
1304
|
+
// widget is positioned against the layout viewport, so its bottom edge and
|
|
1305
|
+
// the launcher at that corner stay behind the keyboard until this lifts
|
|
1306
|
+
// them. Never negative -- a visual viewport panned up past the layout one
|
|
1307
|
+
// would otherwise pull the panel down off the screen.
|
|
1308
|
+
const hidden = layout - visual.height - visual.offsetTop;
|
|
1309
|
+
this.#host.element.style.setProperty(
|
|
1310
|
+
"--ag-ui-visual-viewport-inset-bottom",
|
|
1311
|
+
`${Math.max(0, Math.round(hidden))}px`,
|
|
1312
|
+
);
|
|
1313
|
+
// And what is hidden above it. To show a field the keyboard would cover, a
|
|
1314
|
+
// browser pans the visual viewport down the layout one, and a fixed panel
|
|
1315
|
+
// anchored at the top stays at the layout top: sized to the visible height,
|
|
1316
|
+
// it then shows only its lower part, from the pan down, with its header off
|
|
1317
|
+
// the screen and page background under it. Never negative, for the same
|
|
1318
|
+
// reason as the band below.
|
|
1319
|
+
this.#host.element.style.setProperty(
|
|
1320
|
+
"--ag-ui-visual-viewport-inset-top",
|
|
1321
|
+
`${Math.max(0, Math.round(visual.offsetTop))}px`,
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* The launcher's box in viewport coordinates, with its transform divided out.
|
|
1327
|
+
*
|
|
1328
|
+
* The launcher is scaled in four states -- the collapse animation, hover,
|
|
1329
|
+
* press, and the resting scale(0.4) it sits at while the panel is open -- and
|
|
1330
|
+
* `getBoundingClientRect` reports every one of them. A drag that started from
|
|
1331
|
+
* that rect would begin a couple of pixels off, because a press is one of
|
|
1332
|
+
* those states.
|
|
1333
|
+
*
|
|
1334
|
+
* So the size comes from `offsetWidth`/`offsetHeight`, which are layout
|
|
1335
|
+
* metrics no transform reaches, and the position from the rect's *centre*,
|
|
1336
|
+
* which a centred scale is the one point that cannot move.
|
|
1337
|
+
*/
|
|
1338
|
+
#launcherBox(): LauncherBox {
|
|
1339
|
+
const width = this.#host.launcher.offsetWidth;
|
|
1340
|
+
const height = this.#host.launcher.offsetHeight;
|
|
1341
|
+
const dragged = this.#launcherPos;
|
|
1342
|
+
if (dragged !== null) {
|
|
1343
|
+
return { left: dragged.left, top: dragged.top, width, height };
|
|
1344
|
+
}
|
|
1345
|
+
const rect = this.#host.launcher.getBoundingClientRect();
|
|
1346
|
+
return {
|
|
1347
|
+
left: rect.left + rect.width / 2 - width / 2,
|
|
1348
|
+
top: rect.top + rect.height / 2 - height / 2,
|
|
1349
|
+
width,
|
|
1350
|
+
height,
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Place the host box and the launcher for the position the user dragged to.
|
|
1356
|
+
*
|
|
1357
|
+
* This writes `--ag-ui-inset`, which is a host-facing property: an inline
|
|
1358
|
+
* value outranks the page's own rule for it, exactly as a dragged width
|
|
1359
|
+
* outranks a placement's. That is the intent -- the user moved it -- and it
|
|
1360
|
+
* is why switching to a placement that owns its position hands the property
|
|
1361
|
+
* back rather than leaving a stale inline one behind.
|
|
1362
|
+
*/
|
|
1363
|
+
#applyLauncherPlacement(at: { readonly left: number; readonly top: number }): void {
|
|
1364
|
+
this.#launcherPos = at;
|
|
1365
|
+
// Dropping the bubble hands the panel's position back to the placement.
|
|
1366
|
+
// Keeping a stated one would pin the panel where it was dragged and leave
|
|
1367
|
+
// the launcher deriving nothing, which is the gesture doing half its job.
|
|
1368
|
+
this.#panelPos = null;
|
|
1369
|
+
// The host box keeps its expanded size while collapsed, so its own rect is
|
|
1370
|
+
// the panel's size in either state and needs no separate bookkeeping.
|
|
1371
|
+
const panel = this.#host.element.getBoundingClientRect();
|
|
1372
|
+
const placement = launcherPlacement(
|
|
1373
|
+
this.#launcherBox(),
|
|
1374
|
+
{ width: panel.width, height: panel.height },
|
|
1375
|
+
this.#viewport(),
|
|
1376
|
+
this.#screen(),
|
|
1377
|
+
);
|
|
1378
|
+
this.#host.element.style.setProperty("--ag-ui-inset", placement.hostInset);
|
|
1379
|
+
this.#host.element.style.setProperty("--ag-ui-launcher-inset", placement.launcherInset);
|
|
1380
|
+
this.#expandCorner = placement.corner;
|
|
1381
|
+
// The corner the panel grows from, for the open/close animation's origin.
|
|
1382
|
+
this.#host.element.setAttribute(
|
|
1383
|
+
"data-expand-corner",
|
|
1384
|
+
`${placement.corner.y}-${placement.corner.x}`,
|
|
1385
|
+
);
|
|
1386
|
+
this.syncResizeAnchor();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** Move the launcher live during a drag, without persisting. */
|
|
1390
|
+
#moveLauncher(left: number, top: number): void {
|
|
1391
|
+
// Stated from the first move, not the last: a key gesture only ends at its
|
|
1392
|
+
// keyup, and an Enter pressed before then expands by way of a restore.
|
|
1393
|
+
this.#cornerSpot = { left, top };
|
|
1394
|
+
this.#applyLauncherPlacement({ left, top });
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
/** Move the launcher and remember where, per tab, like the dragged size. */
|
|
1398
|
+
#commitLauncher(left: number, top: number): void {
|
|
1399
|
+
this.#moveLauncher(left, top);
|
|
1400
|
+
this.#storeLauncherPosition();
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Move the panel live during a header drag, without persisting.
|
|
1405
|
+
*
|
|
1406
|
+
* Only the host box is written, and that is the whole trick: the launcher is
|
|
1407
|
+
* positioned *inside* that box, so leaving its own inset alone carries it
|
|
1408
|
+
* along by exactly the distance the panel travelled -- which is what a person
|
|
1409
|
+
* dragging a window expects of the thing it collapses into. Placing it on the
|
|
1410
|
+
* panel's pinned corner instead, as an earlier version did, sent it leaping
|
|
1411
|
+
* across the panel the moment the drag re-picked that corner.
|
|
1412
|
+
*
|
|
1413
|
+
* The corner is therefore held for the length of the gesture. Both insets are
|
|
1414
|
+
* measured from it, and rewriting one of them from a new corner while the
|
|
1415
|
+
* other still names the old one would move the launcher for no reason.
|
|
1416
|
+
*/
|
|
1417
|
+
#movePanel(box: PanelRect, from: PanelRect): { held: PanelRect; launcher: LauncherBox | null } {
|
|
1418
|
+
if (this.#docked()) {
|
|
1419
|
+
// Inside the room above the dock, with nothing carried along.
|
|
1420
|
+
const held = clampPanel(box, this.#dockRoom(), SCREEN_EDGE_MARGIN);
|
|
1421
|
+
this.#dockPos = { left: held.left, top: held.top };
|
|
1422
|
+
this.#writeDockInset(held);
|
|
1423
|
+
return { held, launcher: null };
|
|
1424
|
+
}
|
|
1425
|
+
if (!this.#launcherDraggable()) {
|
|
1426
|
+
return { held: box, launcher: null };
|
|
1427
|
+
}
|
|
1428
|
+
// Where the launcher rests, recorded before the first move writes anything.
|
|
1429
|
+
// From here on the DOM shows it mid-gesture, so this is the last moment it
|
|
1430
|
+
// can be read rather than derived.
|
|
1431
|
+
if (this.#launcherPos === null) {
|
|
1432
|
+
const resting = this.#launcherBox();
|
|
1433
|
+
this.#launcherPos = { left: resting.left, top: resting.top };
|
|
1434
|
+
}
|
|
1435
|
+
// The launcher as it was when the gesture began. Held for the whole drag:
|
|
1436
|
+
// every move measures from here, so the two halves cannot drift apart.
|
|
1437
|
+
const start = this.#launcherPos;
|
|
1438
|
+
// The screen-edge bound, not the resting gutter: the 24px margin is where a
|
|
1439
|
+
// placement rests one, not where a person may put it. Not zero either: the
|
|
1440
|
+
// launcher is held the same distance off, and the restore below agrees.
|
|
1441
|
+
const held = clampPanel(box, this.#viewport(), SCREEN_EDGE_MARGIN);
|
|
1442
|
+
const corner = this.#expandCorner ?? this.#anchor;
|
|
1443
|
+
// The usable box decides where the panel may rest, above; these insets are
|
|
1444
|
+
// measured from the screen, as the browser measures a fixed element's.
|
|
1445
|
+
this.#host.element.style.setProperty("--ag-ui-inset", insetFrom(corner, held, this.#screen()));
|
|
1446
|
+
this.#panelPos = { left: held.left, top: held.top };
|
|
1447
|
+
|
|
1448
|
+
// Where the launcher has ended up, derived rather than read. During a
|
|
1449
|
+
// header drag it rides inside the host box with its own inset untouched,
|
|
1450
|
+
// so the DOM shows it moving while `#launcherPos` still holds where it
|
|
1451
|
+
// started -- reading it back mid-gesture returns the stale value, and
|
|
1452
|
+
// adding the panel's travel to that a second time on release is a jump.
|
|
1453
|
+
// Measured from the box the press started on, so a long drag cannot
|
|
1454
|
+
// accumulate the rounding each move writes.
|
|
1455
|
+
const carried = {
|
|
1456
|
+
...this.#launcherBox(),
|
|
1457
|
+
left: start.left + (held.left - from.left),
|
|
1458
|
+
top: start.top + (held.top - from.top),
|
|
1459
|
+
};
|
|
1460
|
+
// A bubble carried into an edge the host reserved is one nobody can press.
|
|
1461
|
+
// Clamping it live rather than at the end is what makes releasing the drag
|
|
1462
|
+
// change nothing: leaving it until then parked it under a nav bar for the
|
|
1463
|
+
// whole gesture and hopped it out on pointerup.
|
|
1464
|
+
const launcher = { ...carried, ...clampLauncher(carried, this.#viewport()) };
|
|
1465
|
+
this.#host.element.style.setProperty(
|
|
1466
|
+
"--ag-ui-launcher-inset",
|
|
1467
|
+
placeWidget(held, launcher, corner, this.#screen()).launcherInset,
|
|
1468
|
+
);
|
|
1469
|
+
return { held, launcher };
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* Finish a header drag: settle where both halves ended up, and remember it.
|
|
1474
|
+
*
|
|
1475
|
+
* The launcher travels the distance the panel actually travelled, which is
|
|
1476
|
+
* the clamped distance rather than the pointer's -- a panel held against the
|
|
1477
|
+
* viewport margin stops, and so does the bubble attached to it. Measured from
|
|
1478
|
+
* the box the press started on, so a long drag cannot accumulate the rounding
|
|
1479
|
+
* each move writes into the inset.
|
|
1480
|
+
*
|
|
1481
|
+
* Only now is the corner re-picked, from where the launcher has ended up, so
|
|
1482
|
+
* the panel opens into clear space next time. Re-picking it moves nothing:
|
|
1483
|
+
* both insets are rewritten from positions that are already decided.
|
|
1484
|
+
*/
|
|
1485
|
+
#commitPanel(box: PanelRect, from: PanelRect): void {
|
|
1486
|
+
// Exactly what the last move applied, rather than the same sum computed
|
|
1487
|
+
// again. Recomputing it is how the two came apart: releasing the drag
|
|
1488
|
+
// moved the bubble by the panel's whole travel a second time.
|
|
1489
|
+
const { held, launcher } = this.#movePanel(box, from);
|
|
1490
|
+
if (this.#docked()) {
|
|
1491
|
+
this.#storeDockPosition({ left: held.left, top: held.top });
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (launcher === null) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
this.#placePanelAndLauncher(held, launcher);
|
|
1498
|
+
this.#storeLauncherPosition();
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Write both insets for a panel and launcher that are already positioned,
|
|
1503
|
+
* re-picking the corner they are measured from.
|
|
1504
|
+
*/
|
|
1505
|
+
#placePanelAndLauncher(host: PanelRect, launcher: LauncherBox): void {
|
|
1506
|
+
const viewport = this.#viewport();
|
|
1507
|
+
const screen = this.#screen();
|
|
1508
|
+
const size = { width: host.right - host.left, height: host.bottom - host.top };
|
|
1509
|
+
const { corner } = launcherPlacement(launcher, size, viewport, screen);
|
|
1510
|
+
// The screen again, not the usable box: these are CSS insets on a fixed
|
|
1511
|
+
// element and the browser measures them from the real edges.
|
|
1512
|
+
const insets = placeWidget(host, launcher, corner, screen);
|
|
1513
|
+
this.#host.element.style.setProperty("--ag-ui-inset", insets.hostInset);
|
|
1514
|
+
this.#host.element.style.setProperty("--ag-ui-launcher-inset", insets.launcherInset);
|
|
1515
|
+
this.#launcherPos = { left: launcher.left, top: launcher.top };
|
|
1516
|
+
this.#panelPos = { left: host.left, top: host.top };
|
|
1517
|
+
this.#expandCorner = corner;
|
|
1518
|
+
this.#host.element.setAttribute("data-expand-corner", `${corner.y}-${corner.x}`);
|
|
1519
|
+
this.syncResizeAnchor();
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Re-apply a panel position the user stated, against the current viewport.
|
|
1524
|
+
*
|
|
1525
|
+
* The launcher keeps its offset from the panel through the clamp -- it was
|
|
1526
|
+
* put where it is relative to the panel, and a viewport that has since shrunk
|
|
1527
|
+
* is no reason to move one without the other -- and is then held on screen in
|
|
1528
|
+
* its own right.
|
|
1529
|
+
*/
|
|
1530
|
+
#restorePanelPosition(at: { readonly left: number; readonly top: number }): void {
|
|
1531
|
+
const rect = this.#host.element.getBoundingClientRect();
|
|
1532
|
+
// The same bound the drag itself used. Taking the default here instead is
|
|
1533
|
+
// what made a panel dragged to an edge jump a whole resting gutter inward
|
|
1534
|
+
// on the next resize, reload or expand -- re-placing a position the user
|
|
1535
|
+
// had stated, against a limit they had never been shown.
|
|
1536
|
+
const held = clampPanel(
|
|
1537
|
+
{ left: at.left, top: at.top, right: at.left + rect.width, bottom: at.top + rect.height },
|
|
1538
|
+
this.#viewport(),
|
|
1539
|
+
SCREEN_EDGE_MARGIN,
|
|
1540
|
+
);
|
|
1541
|
+
const launcher = this.#launcherBox();
|
|
1542
|
+
const carried = {
|
|
1543
|
+
...launcher,
|
|
1544
|
+
left: launcher.left + (held.left - at.left),
|
|
1545
|
+
top: launcher.top + (held.top - at.top),
|
|
1546
|
+
};
|
|
1547
|
+
this.#placePanelAndLauncher(held, {
|
|
1548
|
+
...carried,
|
|
1549
|
+
...clampLauncher(carried, this.#viewport()),
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Write the current position, if this element owns one.
|
|
1555
|
+
*
|
|
1556
|
+
* The panel's own position rides along only when the user stated it, because
|
|
1557
|
+
* its presence is what tells a restore which of the two gestures to honour:
|
|
1558
|
+
* with it, the panel goes back where it was put; without it, the panel is
|
|
1559
|
+
* re-derived from the launcher and opens into whatever room the viewport has
|
|
1560
|
+
* now.
|
|
1561
|
+
*
|
|
1562
|
+
* Every caller is a gesture ending or a move being made, so where the two
|
|
1563
|
+
* stand now is what was stated. A restore never calls this.
|
|
1564
|
+
*/
|
|
1565
|
+
#storeLauncherPosition(): void {
|
|
1566
|
+
const position = this.#launcherPos;
|
|
1567
|
+
if (position === null) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const panel = this.#panelPos;
|
|
1571
|
+
this.#writeCornerSpot(panel === null ? position : { ...position, panel });
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Make `spot` the stated one, and remember it. */
|
|
1575
|
+
#writeCornerSpot(spot: CornerSpot): void {
|
|
1576
|
+
this.#cornerSpot = spot;
|
|
1577
|
+
this.#host.writePreference(LAUNCHER_KEY, JSON.stringify(spot));
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Re-apply the dragged position against the current viewport: on connect,
|
|
1582
|
+
* whenever the window resizes, and before an expand. The viewport that
|
|
1583
|
+
* stored a position may since have shrunk, and a launcher past the edge is
|
|
1584
|
+
* unreachable -- it is the only way back to a collapsed conversation.
|
|
1585
|
+
*
|
|
1586
|
+
* The one gate on whether a stored position is this element's to honour: the
|
|
1587
|
+
* gestures check their own before they ever reach the placing below.
|
|
1588
|
+
*/
|
|
1589
|
+
restoreLauncherPosition(): void {
|
|
1590
|
+
this.#placeFromRecord(true);
|
|
1591
|
+
this.#stampHeader();
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* Re-apply it for a change in the panel's own layout, which needs no window or
|
|
1596
|
+
* reopen to happen: a line typed in the composer is one. A position waiting to
|
|
1597
|
+
* be handed back under data-launcher-drag="false" waits on, because a panel that leaves for the
|
|
1598
|
+
* host's corner as the reader starts typing is a jump with no cause they can see.
|
|
1599
|
+
* It waits no longer than the panel stays on the screen: a host's size can stand
|
|
1600
|
+
* it off an edge, where its header is out of reach until something else happens.
|
|
1601
|
+
*/
|
|
1602
|
+
settleLauncherPosition(): void {
|
|
1603
|
+
this.#placeFromRecord(false);
|
|
1604
|
+
this.#stampHeader();
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/** The placing behind {@link restoreLauncherPosition}. */
|
|
1608
|
+
#placeFromRecord(handBack: boolean): void {
|
|
1609
|
+
// Before the launcher's record is read at all: it belongs to the corner
|
|
1610
|
+
// surface, and seeding anything from it would put a spot dragged there on
|
|
1611
|
+
// a panel that should stand on its dock.
|
|
1612
|
+
if (this.#docked()) {
|
|
1613
|
+
this.#placeDocked();
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (!this.#surface().launcherDraggable) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
// Under data-launcher-drag="false" the widget is wherever the host's CSS puts
|
|
1620
|
+
// it, so a position this element took before the attribute went on is handed
|
|
1621
|
+
// back here, at the first window resize or reopen, and the record stays for the
|
|
1622
|
+
// day the attribute comes off. Nothing watches the attribute: it is read per
|
|
1623
|
+
// interaction, like every other answer here.
|
|
1624
|
+
const draggable = this.#launcherDraggable();
|
|
1625
|
+
if (!draggable && this.#launcherPos !== null) {
|
|
1626
|
+
if (!handBack && !this.#offScreen()) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
this.releaseLauncherPosition();
|
|
1630
|
+
}
|
|
1631
|
+
const spot = draggable ? (this.#cornerSpot ?? this.#readLauncherPosition()) : null;
|
|
1632
|
+
if (spot === null) {
|
|
1633
|
+
this.#placeUnstated();
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (this.#standFullBleed(spot)) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (spot.panel !== undefined) {
|
|
1640
|
+
// Stated rather than derived: the launcher is only read here to keep the
|
|
1641
|
+
// offset the two were left with, so it has to be seeded before the panel
|
|
1642
|
+
// is placed around it.
|
|
1643
|
+
this.#launcherPos = { left: spot.left, top: spot.top };
|
|
1644
|
+
this.#restorePanelPosition(spot.panel);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
const box = this.#launcherBox();
|
|
1648
|
+
this.#applyLauncherPlacement(
|
|
1649
|
+
clampLauncher({ ...box, left: spot.left, top: spot.top }, this.#viewport()),
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* Leave a corner panel full-bleed if it has become the screen, and say whether
|
|
1655
|
+
* it has.
|
|
1656
|
+
*
|
|
1657
|
+
* The inline inset comes off, because the small-viewport block reads the
|
|
1658
|
+
* host's inset before its own: a spot written there holds the panel off the
|
|
1659
|
+
* edges it should fill, and past them by the same amount, since its size is
|
|
1660
|
+
* the screen's. What the panel covers is read before that, from the size
|
|
1661
|
+
* alone, which no inset changes.
|
|
1662
|
+
*
|
|
1663
|
+
* The launcher stays a place of its own. It is positioned from the host box,
|
|
1664
|
+
* so it is measured again from the box that has just moved, and it is held on
|
|
1665
|
+
* the screen, because it is the way back from a collapsed conversation. The
|
|
1666
|
+
* spot itself is untouched, for the next time there is room for it.
|
|
1667
|
+
*
|
|
1668
|
+
* Above the breakpoint nothing in the stylesheet takes the panel to the edges.
|
|
1669
|
+
* A host's own size makes it the screen while the resting inset still spends a
|
|
1670
|
+
* gutter on one corner, which hangs the box off the opposite one, so there it
|
|
1671
|
+
* stands at the start of the room the host left free and keeps that inset. A
|
|
1672
|
+
* box the placement already holds on the screen has no inset to keep, which is
|
|
1673
|
+
* every phone, and a host's rule that placed the box itself.
|
|
1674
|
+
*/
|
|
1675
|
+
#standFullBleed(spot: CornerSpot): boolean {
|
|
1676
|
+
const element = this.#host.element;
|
|
1677
|
+
const viewport = this.#viewport();
|
|
1678
|
+
if (!coversViewport(element.getBoundingClientRect(), viewport)) {
|
|
1679
|
+
return false;
|
|
1680
|
+
}
|
|
1681
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1682
|
+
const rest = element.getBoundingClientRect();
|
|
1683
|
+
const hanging = hangsOff(rest, viewport, this.#screen());
|
|
1684
|
+
const box = hanging ? atStartOf(viewport, rest) : rest;
|
|
1685
|
+
const launcher = { ...this.#launcherBox(), left: spot.left, top: spot.top };
|
|
1686
|
+
this.#placePanelAndLauncher(box, { ...launcher, ...clampLauncher(launcher, viewport) });
|
|
1687
|
+
if (!hanging) {
|
|
1688
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1689
|
+
}
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Place a panel that has no spot, a corner one or a docked one, the half of
|
|
1695
|
+
* {@link #standFullBleed} with nothing stated to keep and no launcher of its own
|
|
1696
|
+
* to measure.
|
|
1697
|
+
*
|
|
1698
|
+
* Only an inset this element wrote comes off or goes back. One the host wrote
|
|
1699
|
+
* inline is a token a host may state on a phone as anywhere else, and with no
|
|
1700
|
+
* spot it never came from here.
|
|
1701
|
+
*/
|
|
1702
|
+
#placeUnstated(): void {
|
|
1703
|
+
const element = this.#host.element;
|
|
1704
|
+
const inline = element.style.getPropertyValue("--ag-ui-inset");
|
|
1705
|
+
const viewport = this.#viewport();
|
|
1706
|
+
if (!coversViewport(element.getBoundingClientRect(), this.#fillRoom())) {
|
|
1707
|
+
if (inline === "" || inline === this.#fillInset) {
|
|
1708
|
+
this.#restoreProperty("--ag-ui-inset", this.#gripInset ?? "");
|
|
1709
|
+
this.syncResizeAnchor();
|
|
1710
|
+
}
|
|
1711
|
+
this.#fillInset = null;
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (inline === this.#gripInset || inline === this.#fillInset) {
|
|
1715
|
+
element.style.removeProperty("--ag-ui-inset");
|
|
1716
|
+
}
|
|
1717
|
+
const rest = element.getBoundingClientRect();
|
|
1718
|
+
const screen = this.#screen();
|
|
1719
|
+
if (
|
|
1720
|
+
element.style.getPropertyValue("--ag-ui-inset") === "" &&
|
|
1721
|
+
hangsOff(rest, viewport, screen)
|
|
1722
|
+
) {
|
|
1723
|
+
this.#standAtStart(rest);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** Stand a box a host's size fills at the start of the room the host left free. */
|
|
1728
|
+
#standAtStart(rest: PanelRect): void {
|
|
1729
|
+
this.#fillInset = insetFrom(
|
|
1730
|
+
{ x: "left", y: "top" },
|
|
1731
|
+
atStartOf(this.#viewport(), rest),
|
|
1732
|
+
this.#screen(),
|
|
1733
|
+
);
|
|
1734
|
+
this.#host.element.style.setProperty("--ag-ui-inset", this.#fillInset);
|
|
1735
|
+
this.syncResizeAnchor();
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/** The persisted position for this instance, or null. */
|
|
1739
|
+
#readLauncherPosition(): CornerSpot | null {
|
|
1740
|
+
const raw = this.#host.readPreference(LAUNCHER_KEY);
|
|
1741
|
+
if (raw === null) {
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
const parsed: unknown = JSON.parse(raw);
|
|
1746
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
const { left, top, panel } = parsed as { left?: unknown; top?: unknown; panel?: unknown };
|
|
1750
|
+
if (typeof left !== "number" || typeof top !== "number") {
|
|
1751
|
+
return null;
|
|
1752
|
+
}
|
|
1753
|
+
// A record written before the panel could be dragged has no panel half,
|
|
1754
|
+
// and one written by a launcher drag never will -- both restore by
|
|
1755
|
+
// deriving the panel, which is what they meant.
|
|
1756
|
+
const at = asPoint(panel);
|
|
1757
|
+
return at === null ? { left, top } : { left, top, panel: at };
|
|
1758
|
+
} catch {
|
|
1759
|
+
// A corrupt entry is not worth failing a mount over; fall back to the
|
|
1760
|
+
// placement's own corner.
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/**
|
|
1766
|
+
* Give the position back to the host when the new placement owns it, the way
|
|
1767
|
+
* releaseOwnedAxes gives back a size. Without this a dragged inset survives
|
|
1768
|
+
* the switch inline and pins a sidebar to wherever the floating launcher was.
|
|
1769
|
+
*/
|
|
1770
|
+
releaseLauncherPosition(): void {
|
|
1771
|
+
// A dragged dock spot belongs to a docked panel that moves. Any other
|
|
1772
|
+
// surface hands it back; a docked one keeps it, and syncDock holds it
|
|
1773
|
+
// against the new dock.
|
|
1774
|
+
if (this.#dockPos !== null && !(this.#docked() && this.#panelMovable())) {
|
|
1775
|
+
this.#dockPos = null;
|
|
1776
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
1777
|
+
}
|
|
1778
|
+
if (this.#launcherDraggable()) {
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
this.#forgetCornerPosition();
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/** Take everything off that placed a corner panel, leaving its record alone. */
|
|
1785
|
+
#forgetCornerPosition(): void {
|
|
1786
|
+
this.#launcherPos = null;
|
|
1787
|
+
this.#panelPos = null;
|
|
1788
|
+
this.#expandCorner = null;
|
|
1789
|
+
this.#gripInset = null;
|
|
1790
|
+
this.#fillInset = null;
|
|
1791
|
+
this.#host.element.style.removeProperty("--ag-ui-inset");
|
|
1792
|
+
this.#host.element.style.removeProperty("--ag-ui-launcher-inset");
|
|
1793
|
+
this.#host.element.removeAttribute("data-expand-corner");
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/**
|
|
1797
|
+
* Apply the box a grip is asking for.
|
|
1798
|
+
*
|
|
1799
|
+
* The size is the easy half. The other half is that **dragging the edge the
|
|
1800
|
+
* layout is holding still moves the panel as well as resizing it**, and the
|
|
1801
|
+
* layout cannot express that on its own: a floating panel pinned bottom-right
|
|
1802
|
+
* cannot grow rightward, because its right edge is what the placement fixed.
|
|
1803
|
+
* So a grip on a pinned edge takes the position over -- which is the same
|
|
1804
|
+
* ownership the launcher drag takes, written the same way.
|
|
1805
|
+
*
|
|
1806
|
+
* A grip on a free edge writes nothing but the size, exactly as before, so a
|
|
1807
|
+
* host positioning the panel with its own rule keeps that rule until someone
|
|
1808
|
+
* drags the edge it was holding.
|
|
1809
|
+
*/
|
|
1810
|
+
#applyResize(grip: ResizeGrip, box: PanelRect): PanelRect {
|
|
1811
|
+
box = this.#withinViewport(box);
|
|
1812
|
+
if (this.#docked() && this.#dockPos === null) {
|
|
1813
|
+
// A panel standing on its dock keeps it through a resize: the edges it
|
|
1814
|
+
// stands on are the bar's or the anchor's, and a grip on one of them
|
|
1815
|
+
// would write the position below and take the panel off its dock. So
|
|
1816
|
+
// the pinned edges stay where they are and only the free ones follow.
|
|
1817
|
+
// Once dragged off, it resizes like a corner panel, below.
|
|
1818
|
+
const now = this.#host.element.getBoundingClientRect();
|
|
1819
|
+
box = {
|
|
1820
|
+
left: grip.x === this.#anchor.x ? now.left : box.left,
|
|
1821
|
+
right: grip.x === this.#anchor.x ? now.right : box.right,
|
|
1822
|
+
top: grip.y === this.#anchor.y ? now.top : box.top,
|
|
1823
|
+
bottom: grip.y === this.#anchor.y ? now.bottom : box.bottom,
|
|
1824
|
+
};
|
|
1825
|
+
this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
|
|
1826
|
+
return box;
|
|
1827
|
+
}
|
|
1828
|
+
this.#applySize({ width: box.right - box.left, height: box.bottom - box.top });
|
|
1829
|
+
if (this.#docked()) {
|
|
1830
|
+
// Any grip moves the top-left corner the spot is kept as, including one
|
|
1831
|
+
// on a free edge, which leaves the inset alone.
|
|
1832
|
+
this.#dockPos = { left: box.left, top: box.top };
|
|
1833
|
+
}
|
|
1834
|
+
if (grip.x === this.#anchor.x || grip.y === this.#anchor.y) {
|
|
1835
|
+
// The whole screen, not the usable box: a CSS inset on a fixed element is
|
|
1836
|
+
// measured from the real edges, so expressing a right or bottom against a
|
|
1837
|
+
// box the host has inset comes out short by exactly that inset.
|
|
1838
|
+
const anchor = this.#anchor;
|
|
1839
|
+
const inset = insetFrom(anchor, box, this.#screen());
|
|
1840
|
+
this.#host.element.style.setProperty("--ag-ui-inset", inset);
|
|
1841
|
+
this.#gripInset = this.#launcherPos === null && !this.#docked() ? inset : null;
|
|
1842
|
+
// The launcher lives at this corner of the panel, so a corner that moved
|
|
1843
|
+
// takes it along. Without this the next expand would re-derive the panel's
|
|
1844
|
+
// position from a launcher still standing where the panel used to be, and
|
|
1845
|
+
// undo the move.
|
|
1846
|
+
if (this.#launcherPos !== null) {
|
|
1847
|
+
const size = this.#host.launcher.offsetWidth;
|
|
1848
|
+
this.#launcherPos = {
|
|
1849
|
+
left: anchor.x === "left" ? box.left : box.right - size,
|
|
1850
|
+
top: anchor.y === "top" ? box.top : box.bottom - size,
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
// A stated corner panel's spot is a claim about this box, so it moves with
|
|
1855
|
+
// it, by a free edge as well as by a pinned one. Read from what was drawn,
|
|
1856
|
+
// not what the pointer asked for, because a host's maximum stops the box short
|
|
1857
|
+
// of the pointer. Not while docked: this position belongs to the corner.
|
|
1858
|
+
if (this.#panelPos !== null && !this.#docked()) {
|
|
1859
|
+
const drawn = this.#host.element.getBoundingClientRect();
|
|
1860
|
+
this.#panelPos = { left: drawn.left, top: drawn.top };
|
|
1861
|
+
}
|
|
1862
|
+
return box;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/** Finish a resize: keep the box, remember it, and re-read the pinned edges. */
|
|
1866
|
+
#commitResize(grip: ResizeGrip, box: PanelRect): void {
|
|
1867
|
+
// The bounded box, not the one the pointer asked for. Persisting the raw
|
|
1868
|
+
// one would store a size the panel never had and restore it on the next
|
|
1869
|
+
// mount, which is the same disagreement between apply and commit that made
|
|
1870
|
+
// the header drag jump on release.
|
|
1871
|
+
const held = this.#applyResize(grip, box);
|
|
1872
|
+
// A grip on the free edges of a panel standing at the start of a filled window
|
|
1873
|
+
// leaves that inset alone, and the size it stated takes the panel off the
|
|
1874
|
+
// screen's shape. It has to stay where the grip left it, so the inset stops
|
|
1875
|
+
// being one that lasts only as long as the host's size does.
|
|
1876
|
+
if (this.#host.element.style.getPropertyValue("--ag-ui-inset") === this.#fillInset) {
|
|
1877
|
+
this.#gripInset = this.#fillInset;
|
|
1878
|
+
this.#fillInset = null;
|
|
1879
|
+
}
|
|
1880
|
+
this.#persistSize({ width: held.right - held.left, height: held.bottom - held.top });
|
|
1881
|
+
this.#storeLauncherPosition();
|
|
1882
|
+
if (this.#dockPos !== null) {
|
|
1883
|
+
this.#storeDockPosition(this.#dockPos);
|
|
1884
|
+
}
|
|
1885
|
+
// Re-stamp after the drag: a host whose layout changed underneath us would
|
|
1886
|
+
// otherwise keep the tab-reachable grip in the old corner, which reads as
|
|
1887
|
+
// the control being in the wrong place even though the drag was right.
|
|
1888
|
+
this.syncResizeAnchor();
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* Put the tab stop on the grip diagonally opposite the pinned corner.
|
|
1893
|
+
*
|
|
1894
|
+
* That is the corner a resize grows the panel from, so an arrow key there
|
|
1895
|
+
* changes the size and never the position -- the behaviour the single grip
|
|
1896
|
+
* this replaced had, kept for the one path that cannot simply grab a
|
|
1897
|
+
* different edge.
|
|
1898
|
+
*/
|
|
1899
|
+
#focusableGrip(): void {
|
|
1900
|
+
const free = `${this.#anchor.y === "top" ? "bottom" : "top"}-${
|
|
1901
|
+
this.#anchor.x === "left" ? "right" : "left"
|
|
1902
|
+
}`;
|
|
1903
|
+
for (const [name, handle] of this.#resizeHandles) {
|
|
1904
|
+
const reachable = name === free;
|
|
1905
|
+
handle.tabIndex = reachable ? 0 : -1;
|
|
1906
|
+
if (reachable) {
|
|
1907
|
+
handle.removeAttribute("aria-hidden");
|
|
1908
|
+
} else {
|
|
1909
|
+
handle.setAttribute("aria-hidden", "true");
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* The stored-size bucket for the live placement: its attribute value, with
|
|
1916
|
+
* absent and empty read as floating, as the stylesheet treats them.
|
|
1917
|
+
*/
|
|
1918
|
+
#bucket(): string {
|
|
1919
|
+
const placement = this.#host.element.getAttribute("placement");
|
|
1920
|
+
return placement === null || placement === "" ? "floating" : placement;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/** Persist a dragged size under the current placement, keeping the others. */
|
|
1924
|
+
#persistSize(size: ResizeSize): void {
|
|
1925
|
+
this.#writeSizes(this.#bucket(), { ...this.#readSize(), ...size });
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/** Store one placement's size, or drop it with null, clearing an emptied record. */
|
|
1929
|
+
#writeSizes(bucket: string, size: ResizeSize | null): void {
|
|
1930
|
+
const placements: Record<string, ResizeSize> = { ...this.#readSizes() };
|
|
1931
|
+
if (size === null) {
|
|
1932
|
+
delete placements[bucket];
|
|
1933
|
+
} else {
|
|
1934
|
+
placements[bucket] = size;
|
|
1935
|
+
}
|
|
1936
|
+
if (Object.keys(placements).length === 0) {
|
|
1937
|
+
this.#host.clearPreference(SIZE_KEY);
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const stored: StoredSizes = { v: 2, placements };
|
|
1941
|
+
this.#host.writePreference(SIZE_KEY, JSON.stringify(stored));
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/** The persisted size for the current placement, or an empty record. */
|
|
1945
|
+
#readSize(): ResizeSize {
|
|
1946
|
+
return this.#readSizes()[this.#bucket()] ?? {};
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
/**
|
|
1950
|
+
* Every persisted size by placement. A record from before sizes were kept per
|
|
1951
|
+
* placement is one flat size, read as floating's, which is where it was
|
|
1952
|
+
* nearly always dragged.
|
|
1953
|
+
*/
|
|
1954
|
+
#readSizes(): Readonly<Record<string, ResizeSize>> {
|
|
1955
|
+
const raw = this.#host.readPreference(SIZE_KEY);
|
|
1956
|
+
if (raw === null) {
|
|
1957
|
+
return {};
|
|
1958
|
+
}
|
|
1959
|
+
try {
|
|
1960
|
+
const parsed: unknown = JSON.parse(raw);
|
|
1961
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1962
|
+
return {};
|
|
1963
|
+
}
|
|
1964
|
+
const record = parsed as Partial<StoredSizes>;
|
|
1965
|
+
if (record.v === 2 && typeof record.placements === "object" && record.placements !== null) {
|
|
1966
|
+
return record.placements;
|
|
1967
|
+
}
|
|
1968
|
+
return { floating: parsed as ResizeSize };
|
|
1969
|
+
} catch {
|
|
1970
|
+
// A corrupt entry is not worth failing a mount over; fall back to the
|
|
1971
|
+
// placement's own size.
|
|
1972
|
+
return {};
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|