flexdesk 0.15.2 → 0.16.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/LICENSE +0 -0
- package/README.md +70 -0
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +0 -0
- package/css/flexdesk.css +153 -0
- package/css/overrides.css +0 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +0 -0
- package/dist/canvas.js.map +0 -0
- package/dist/charts.js +0 -0
- package/dist/charts.js.map +0 -0
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-6JSOVNID.js +0 -0
- package/dist/chunk-6JSOVNID.js.map +0 -0
- package/dist/chunk-B5Q53LAJ.js +111 -0
- package/dist/chunk-B5Q53LAJ.js.map +7 -0
- package/dist/chunk-FBGP5KN5.js +0 -0
- package/dist/chunk-FBGP5KN5.js.map +0 -0
- package/dist/chunk-FL5KFNQH.js +0 -0
- package/dist/chunk-FL5KFNQH.js.map +0 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/chunk-NLTOY57M.js +0 -0
- package/dist/chunk-NLTOY57M.js.map +0 -0
- package/dist/chunk-NNFTETK3.js +0 -0
- package/dist/chunk-NNFTETK3.js.map +0 -0
- package/dist/chunk-O5OHMWBB.js +0 -0
- package/dist/chunk-O5OHMWBB.js.map +0 -0
- package/dist/chunk-RQM5Z2MU.js +0 -0
- package/dist/chunk-RQM5Z2MU.js.map +0 -0
- package/dist/chunk-SN4LSOAR.js +0 -0
- package/dist/chunk-SN4LSOAR.js.map +0 -0
- package/dist/{chunk-CWPMLX67.js → chunk-W6FBJLPW.js} +2 -110
- package/dist/{chunk-CWPMLX67.js.map → chunk-W6FBJLPW.js.map} +3 -3
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +0 -0
- package/dist/core.js.map +0 -0
- package/dist/editor.js +0 -0
- package/dist/editor.js.map +0 -0
- package/dist/flexdesk.css +153 -0
- package/dist/flow.js +0 -0
- package/dist/flow.js.map +0 -0
- package/dist/host.js +80 -1
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +471 -16
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +73 -9
- package/dist/widgets.js.map +3 -3
- package/dist/wm.js +4 -2
- package/dist/wm.js.map +1 -1
- package/package.json +1 -1
- package/src/canvas/canvas_adapter.js +0 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +0 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +0 -0
- package/src/charts/plot_config.js +0 -0
- package/src/charts/plot_popout_window.js +0 -0
- package/src/charts/plotly_wrapper.js +0 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +0 -0
- package/src/core/settings.js +0 -0
- package/src/core/state_guard.js +0 -0
- package/src/core/state_guard_config.js +0 -0
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +0 -0
- package/src/editor/monaco_editor_factory.js +0 -0
- package/src/editor/monaco_loader.js +0 -0
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +0 -0
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +0 -0
- package/src/flow/canvas_editor/editor.js +0 -0
- package/src/flow/canvas_editor/index.js +0 -0
- package/src/flow/canvas_editor/layout.js +0 -0
- package/src/flow/canvas_editor/strings.js +0 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +0 -0
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +0 -0
- package/src/flow/kit/keys.js +0 -0
- package/src/flow/kit/logic_graph.js +0 -0
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +0 -0
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +0 -0
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +0 -0
- package/src/flow/lanes/editor.js +0 -0
- package/src/flow/lanes/index.js +0 -0
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +0 -0
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +0 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +0 -0
- package/src/help/help_registry.js +0 -0
- package/src/host/external_links.js +94 -0
- package/src/host/host.js +4 -1
- package/src/host/photino_host.js +7 -0
- package/src/host/photino_host.md +18 -0
- package/src/host/pywebview_host.js +0 -0
- package/src/tiles/chart_tiles.js +0 -0
- package/src/tiles/config_schema.js +0 -0
- package/src/tiles/layout_persistence.js +0 -0
- package/src/tiles/list_tiles.js +422 -0
- package/src/tiles/scope_picker.js +96 -0
- package/src/tiles/tile_base.js +11 -0
- package/src/tiles/tile_grid.js +36 -1
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +0 -0
- package/src/tiling/command_palette.js +0 -0
- package/src/tiling/content_registry.js +0 -0
- package/src/tiling/desktops.js +0 -0
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +0 -0
- package/src/tiling/kind_taxonomy.js +0 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +0 -0
- package/src/tiling/page_factory.js +0 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +0 -0
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +0 -0
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +0 -0
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +0 -0
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/computing_status_window.js +0 -0
- package/src/ui/components/confirm_dialog.js +0 -0
- package/src/ui/components/context_menu.js +0 -0
- package/src/ui/components/data_table.js +0 -0
- package/src/ui/components/detail_header.js +0 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/gallery_picker.js +0 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/managed_window.js +0 -0
- package/src/ui/components/modal.js +0 -0
- package/src/ui/components/notification_history.js +0 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/slide_out_panel.js +0 -0
- package/src/ui/components/slider_field.js +0 -0
- package/src/ui/components/sortable_list.js +26 -4
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/toast.js +0 -0
- package/src/ui/components/tree_view.js +0 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/controllers/panel_state_machine.js +0 -0
- package/src/ui/controllers/window_chrome_controller.js +0 -0
- package/src/ui/notification_center.js +0 -0
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/native_context_menu.js +61 -0
- package/src/ui/utils/overlay_scrollbar.js +0 -0
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/tooltip_service.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/help/help_registry.js", "../src/help/help_modal.js"
|
|
4
|
-
"sourcesContent": ["/**\n * Where the help modal gets its help.\n *\n * HelpModal used to `import { getHelpService, HELP_TOPICS } from './help_service.js'` \u2014\n * and help_service.js is 249 domain words of EcoAgent prose about sectors,\n * currencies, agents and markets. That import was the last framework -> domain\n * edge in the core: a generic modal reaching into one application's manual.\n *\n * The modal now asks this registry, and the application fills it. If nobody\n * fills it, the modal opens and says there is no help \u2014 which is the correct\n * behaviour for a library that ships no documentation about your app.\n *\n * @typedef {Object} HelpProvider\n * @property {() => string[]} getCategories\n * @property {(category: string) => Array<{id: string, title: string}>} getTopicsByCategory\n * @property {(id: string) => Promise<Object|null>} loadTopic\n * @property {(query: string) => Array<Object>} searchTopics\n *\n * @typedef {Object.<string, {label: string, icon: string}>} HelpCategories\n * Keyed by category id; ITERATION ORDER IS DISPLAY ORDER.\n *\n * @typedef {{title: string, welcome: string}} HelpCopy\n * The landing heading. The modal used to hardcode \"EcoAgent Help\" and\n * \"Welcome to EcoAgent!\".\n */\n\n/** A provider with nothing in it. Not an error \u2014 just an app with no help. */\nconst EMPTY_PROVIDER = Object.freeze({\n getCategories: () => [],\n getTopicsByCategory: () => [],\n loadTopic: async () => null,\n searchTopics: () => [],\n});\n\nconst DEFAULT_COPY = Object.freeze({\n title: 'Help',\n welcome: 'Select a topic from the sidebar or browse the categories below.',\n});\n\nlet _provider = EMPTY_PROVIDER;\nlet _categories = Object.freeze({});\nlet _copy = DEFAULT_COPY;\n\n/**\n * @param {HelpProvider} provider\n * @param {HelpCategories} [categories] Display labels + icons, in display order.\n * @param {Partial<HelpCopy>} [copy] Landing heading and welcome line.\n */\nexport function setHelpProvider(provider, categories = {}, copy = {}) {\n _provider = provider ?? EMPTY_PROVIDER;\n _categories = Object.freeze({ ...categories });\n _copy = Object.freeze({ ...DEFAULT_COPY, ...copy });\n}\n\nexport function helpProvider() {\n return _provider;\n}\n\nexport function helpCategories() {\n return _categories;\n}\n\nexport function helpCopy() {\n return _copy;\n}\n", "/**\n * HelpModal - Modal dialog for displaying help content\n *\n * Features:\n * - Markdown rendering with syntax highlighting\n * - Table of contents navigation\n * - Topic search\n * - Category browsing\n * - Keyboard navigation (Escape to close)\n */\n\nimport { helpProvider, helpCategories, helpCopy } from './help_registry.js';\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// HelpModal Class\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nlet _activeModal = null;\n\nexport class HelpModal {\n /** The application's help, resolved at use time. */\n get _service() { return helpProvider(); }\n\n get _categories() { return helpCategories(); }\n\n get _copy() { return helpCopy(); }\n\n constructor() {\n // Read LAZILY, not here. A HelpModal can be constructed at boot (the\n // command palette holds one) before the application has registered its\n // help, and a constructor read would freeze the empty default forever.\n this._overlay = null;\n this._modal = null;\n this._currentTopic = null;\n this._searchQuery = '';\n this._boundKeyHandler = this._handleKeyDown.bind(this);\n }\n\n /**\n * Open the help modal.\n * @param {string} [topicId] - Initial topic to display\n */\n async open(topicId = null) {\n // Close any existing modal\n if (_activeModal && _activeModal !== this) {\n _activeModal.close();\n }\n _activeModal = this;\n\n this._createModal();\n document.body.appendChild(this._overlay);\n\n // Add keyboard handler\n document.addEventListener('keydown', this._boundKeyHandler);\n\n // Focus search input\n const searchInput = this._modal.querySelector('.help-search-input');\n if (searchInput) {\n setTimeout(() => searchInput.focus(), 100);\n }\n\n // Load initial topic or show index\n if (topicId) {\n await this._loadTopic(topicId);\n } else {\n this._showIndex();\n }\n }\n\n /**\n * Close the help modal.\n */\n close() {\n if (this._overlay) {\n document.removeEventListener('keydown', this._boundKeyHandler);\n this._overlay.remove();\n this._overlay = null;\n this._modal = null;\n }\n if (_activeModal === this) {\n _activeModal = null;\n }\n }\n\n /**\n * Create the modal DOM structure.\n * @private\n */\n _createModal() {\n // Overlay\n this._overlay = document.createElement('div');\n this._overlay.className = 'help-modal-overlay';\n this._overlay.addEventListener('click', (e) => {\n if (e.target === this._overlay) this.close();\n });\n\n // Modal container\n this._modal = document.createElement('div');\n this._modal.className = 'help-modal';\n this._modal.innerHTML = `\n <div class=\"help-modal__header\">\n <div class=\"help-modal__title\">\n <span class=\"material-symbols-outlined\">help</span>\n <span>Help</span>\n </div>\n <button class=\"help-modal__close\" title=\"Close (Esc)\">\n <span class=\"material-symbols-outlined\">close</span>\n </button>\n </div>\n <div class=\"help-modal__search\">\n <span class=\"material-symbols-outlined\">search</span>\n <input type=\"text\" class=\"help-search-input\" placeholder=\"Search help topics...\" autocomplete=\"off\">\n </div>\n <div class=\"help-modal__body\">\n <nav class=\"help-modal__sidebar\">\n <div class=\"help-sidebar__categories\"></div>\n </nav>\n <main class=\"help-modal__content\">\n <div class=\"help-content__loading\">Loading...</div>\n </main>\n </div>\n `;\n\n // Event handlers\n const closeBtn = this._modal.querySelector('.help-modal__close');\n closeBtn.addEventListener('click', () => this.close());\n\n const searchInput = this._modal.querySelector('.help-search-input');\n searchInput.addEventListener('input', (e) => this._handleSearch(e.target.value));\n searchInput.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') {\n if (e.target.value) {\n e.target.value = '';\n this._handleSearch('');\n e.stopPropagation();\n }\n }\n });\n\n // Build sidebar\n this._buildSidebar();\n\n this._overlay.appendChild(this._modal);\n }\n\n /**\n * Build the sidebar navigation.\n * @private\n */\n _buildSidebar() {\n const container = this._modal.querySelector('.help-sidebar__categories');\n const categories = this._service.getCategories();\n\n // Sort categories by order\n const categoryOrder = Object.keys(this._categories);\n categories.sort((a, b) => {\n const orderA = categoryOrder.indexOf(a);\n const orderB = categoryOrder.indexOf(b);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n\n container.innerHTML = categories.map(category => {\n const topics = this._service.getTopicsByCategory(category);\n const label = this._categories[category]?.label || category;\n const icon = this._categories[category]?.icon || 'folder';\n\n return `\n <div class=\"help-category\" data-category=\"${category}\">\n <div class=\"help-category__header\">\n <span class=\"material-symbols-outlined\">${icon}</span>\n <span>${label}</span>\n </div>\n <ul class=\"help-category__topics\">\n ${topics.map(topic => `\n <li class=\"help-topic-item\" data-topic=\"${topic.id}\">\n ${topic.title}\n </li>\n `).join('')}\n </ul>\n </div>\n `;\n }).join('');\n\n // Add click handlers\n container.querySelectorAll('.help-topic-item').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n\n container.querySelectorAll('.help-category__header').forEach(header => {\n header.addEventListener('click', () => {\n header.parentElement.classList.toggle('collapsed');\n });\n });\n }\n\n /**\n * Show the help index page.\n * @private\n */\n _showIndex() {\n const content = this._modal.querySelector('.help-modal__content');\n const categories = this._service.getCategories();\n\n const categoryOrder = Object.keys(this._categories);\n categories.sort((a, b) => {\n const orderA = categoryOrder.indexOf(a);\n const orderB = categoryOrder.indexOf(b);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n\n content.innerHTML = `\n <div class=\"help-index\">\n <h1>${this._copy.title}</h1>\n <p>${this._copy.welcome}</p>\n\n <div class=\"help-index__grid\">\n ${categories.map(category => {\n const topics = this._service.getTopicsByCategory(category);\n const label = this._categories[category]?.label || category;\n const icon = this._categories[category]?.icon || 'folder';\n\n return `\n <div class=\"help-index__category\" data-category=\"${category}\">\n <div class=\"help-index__category-header\">\n <span class=\"material-symbols-outlined\">${icon}</span>\n <span>${label}</span>\n </div>\n <ul class=\"help-index__topics\">\n ${topics.slice(0, 3).map(topic => `\n <li class=\"help-index__topic\" data-topic=\"${topic.id}\">\n ${topic.title}\n </li>\n `).join('')}\n ${topics.length > 3 ? `<li class=\"help-index__more\">+${topics.length - 3} more</li>` : ''}\n </ul>\n </div>\n `;\n }).join('')}\n </div>\n </div>\n `;\n\n // Add click handlers\n content.querySelectorAll('.help-index__topic').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n }\n\n /**\n * Load and display a topic.\n * @param {string} topicId - Topic ID\n * @private\n */\n async _loadTopic(topicId) {\n const content = this._modal.querySelector('.help-modal__content');\n content.innerHTML = '<div class=\"help-content__loading\">Loading...</div>';\n\n // Update sidebar selection\n this._modal.querySelectorAll('.help-topic-item').forEach(item => {\n item.classList.toggle('active', item.dataset.topic === topicId);\n });\n\n try {\n const topic = await this._service.loadTopic(topicId);\n this._currentTopic = topicId;\n\n content.innerHTML = `\n <div class=\"help-content__article\">\n <div class=\"help-content__breadcrumb\">\n <a href=\"#\" class=\"help-breadcrumb__home\" title=\"Help Index\">\n <span class=\"material-symbols-outlined\">home</span>\n </a>\n <span class=\"help-breadcrumb__separator\">/</span>\n <span class=\"help-breadcrumb__title\">${topic.title}</span>\n </div>\n <article class=\"help-article\">\n ${topic.html}\n </article>\n </div>\n `;\n\n // Add breadcrumb handler\n content.querySelector('.help-breadcrumb__home').addEventListener('click', (e) => {\n e.preventDefault();\n this._showIndex();\n });\n } catch (error) {\n content.innerHTML = `\n <div class=\"help-content__error\">\n <span class=\"material-symbols-outlined\">error</span>\n <p>Failed to load help topic.</p>\n </div>\n `;\n }\n }\n\n /**\n * Handle search input.\n * @param {string} query - Search query\n * @private\n */\n _handleSearch(query) {\n this._searchQuery = query;\n const content = this._modal.querySelector('.help-modal__content');\n\n if (!query.trim()) {\n this._showIndex();\n return;\n }\n\n const results = this._service.searchTopics(query);\n\n if (results.length === 0) {\n content.innerHTML = `\n <div class=\"help-search-results\">\n <h2>Search Results</h2>\n <p class=\"help-search-empty\">No results found for \"${query}\"</p>\n </div>\n `;\n return;\n }\n\n content.innerHTML = `\n <div class=\"help-search-results\">\n <h2>Search Results</h2>\n <p class=\"help-search-count\">${results.length} result${results.length !== 1 ? 's' : ''} for \"${query}\"</p>\n <ul class=\"help-search-list\">\n ${results.map(topic => `\n <li class=\"help-search-item\" data-topic=\"${topic.id}\">\n <span class=\"help-search-item__title\">${topic.title}</span>\n <span class=\"help-search-item__category\">${this._categories[topic.category]?.label || topic.category}</span>\n </li>\n `).join('')}\n </ul>\n </div>\n `;\n\n // Add click handlers\n content.querySelectorAll('.help-search-item').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n }\n\n /**\n * Handle keyboard events.\n * @param {KeyboardEvent} event\n * @private\n */\n _handleKeyDown(event) {\n if (event.key === 'Escape') {\n this.close();\n }\n }\n\n /**\n * Static method to open help modal.\n * @param {string} [topicId] - Initial topic\n * @returns {HelpModal}\n */\n static open(topicId = null) {\n const modal = new HelpModal();\n modal.open(topicId);\n return modal;\n }\n\n /**\n * Get the currently active modal.\n * @returns {HelpModal|null}\n */\n static getActive() {\n return _activeModal;\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global Help Shortcut\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Open help modal with the `?` key. (F1 is reserved for top-nav Home.)\n */\nfunction setupGlobalHelpShortcut() {\n document.addEventListener('keydown', (event) => {\n if (event.key !== '?') return;\n // Don't hijack `?` typed into a field (search boxes, editors).\n const t = event.target;\n if (t?.closest?.(\n 'input, textarea, select, [contenteditable=\"true\"]')) return;\n event.preventDefault();\n if (_activeModal) {\n _activeModal.close();\n } else {\n HelpModal.open();\n }\n });\n}\n\n// Initialize global shortcut\nif (typeof document !== 'undefined') {\n setupGlobalHelpShortcut();\n}\n\nexport default HelpModal;\n", "/**\n * context_menu.js \u2014 small popup menu that matches Ecosim's `.context-menu`\n * idiom (see main_new.css line 9713 and ui/js/ui/components/data_table.js).\n *\n * Usage:\n * showContextMenu(x, y, [\n * { label: 'Open', icon: 'open_in_new', action: 'open' },\n * { separator: true },\n * { label: 'Delete', icon: 'delete', action: 'delete', danger: true },\n * ], (action) => { ... });\n *\n * The menu auto-closes on outside click / scroll / Escape.\n *\n * \u2500\u2500 It is operable by keyboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Items were bare `<div>`s with a click listener: no role, no tab stop, no\n * arrow-key movement, and `showContextMenu` never moved focus into the menu.\n * A consumer that opened this from Shift+F10 \u2014 the rail in Tables does \u2014 put a\n * menu on screen that the keyboard could only dismiss. Every command in it was\n * unreachable without a pointer, and nothing said so.\n *\n * So an item is a `<button role=\"menuitem\">`, the first enabled one takes focus\n * when the menu opens, Up/Down/Home/End move between them and wrap, Enter and\n * Space activate, Escape closes, and focus returns to whatever had it before.\n * Disabled items are skipped by the arrows rather than focusable-but-inert.\n * `<button>` rather than a div with `tabindex`: the browser then gives Enter and\n * Space for free and screen readers announce it without further help.\n */\n\nimport { modalHost } from './modal.js';\n\nlet _activeMenu = null;\nlet _returnFocusTo = null;\n\nexport function showContextMenu(x, y, items, onAction) {\n hideContextMenu();\n\n // Whatever had focus when the menu opened gets it back when the menu\n // closes. Without this a keyboard user who presses Escape is returned to\n // `document.body` and has to tab back to where they were.\n _returnFocusTo = document.activeElement;\n\n const menu = document.createElement('div');\n menu.className = 'twm-context-menu ea-context-menu';\n menu.setAttribute('role', 'menu');\n\n for (const it of items) {\n if (it.separator) {\n const sep = document.createElement('div');\n sep.className = 'twm-context-menu__separator';\n sep.setAttribute('role', 'separator');\n menu.appendChild(sep);\n continue;\n }\n const row = document.createElement('button');\n row.type = 'button';\n row.setAttribute('role', 'menuitem');\n let cls = 'twm-context-menu-item';\n if (it.danger) cls += ' twm-delete-node';\n if (it.disabled) cls += ' disabled';\n row.className = cls;\n if (it.disabled) {\n row.disabled = true;\n row.setAttribute('aria-disabled', 'true');\n }\n // A DISABLED ROW WITH NO EXPLANATION IS A DEAD CONTROL. `title` was\n // accepted by callers and rendered by nothing \u2014 the item was built with\n // one, the row silently dropped it, and the user got a greyed line with\n // no way to learn why. It is the same shape as the classes this\n // repository keeps finding: no error, no throw, and invisible to every\n // test that checks the row is disabled.\n if (it.title) row.title = it.title;\n row.innerHTML = `\n <span class=\"material-symbols-outlined\">${it.icon || ''}</span>\n <span>${escapeHtml(it.label)}</span>\n `;\n if (!it.disabled) {\n row.addEventListener('click', (e) => {\n e.stopPropagation();\n hideContextMenu();\n onAction?.(it.action);\n });\n }\n menu.appendChild(row);\n }\n\n // C25. THE SAME QUESTION A MODAL ASKS: which window is the user in? A\n // consumer spanning two browser windows sets the host when its focus moves;\n // null \u2014 every consumer today \u2014 is `document.body`, exactly as before. A\n // menu in the wrong window is worse than a modal in the wrong window,\n // because it is positioned at coordinates from the OTHER one.\n (modalHost() || document.body).appendChild(menu);\n menu.style.display = 'block';\n _activeMenu = menu;\n\n // Position with viewport clamping.\n const rect = menu.getBoundingClientRect();\n const left = Math.min(x, window.innerWidth - rect.width - 8);\n const top = Math.min(y, window.innerHeight - rect.height - 8);\n menu.style.left = `${Math.max(0, left)}px`;\n menu.style.top = `${Math.max(0, top)}px`;\n\n // Close on outside click / scroll / Escape.\n setTimeout(() => {\n document.addEventListener('mousedown', _outsideHandler, { once: true, capture: true });\n }, 0);\n document.addEventListener('keydown', _keyHandler);\n window.addEventListener('scroll', hideContextMenu, { once: true, capture: true });\n\n // Focus the first item the keyboard can actually use. `preventScroll` so a\n // menu opened near the bottom of a long page does not jump it.\n _enabledItems(menu)[0]?.focus({ preventScroll: true });\n}\n\nexport function hideContextMenu() {\n if (!_activeMenu) return;\n const returnTo = _returnFocusTo;\n const held = _activeMenu.contains(document.activeElement);\n _activeMenu.remove();\n _activeMenu = null;\n _returnFocusTo = null;\n document.removeEventListener('keydown', _keyHandler);\n // Only take focus back if the menu still had it. A click elsewhere has\n // already moved focus deliberately and must not be undone.\n if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });\n}\n\nfunction _enabledItems(menu) {\n return [...menu.querySelectorAll('.twm-context-menu-item:not(.disabled)')];\n}\n\n/** Up/Down/Home/End move; the list WRAPS, which is what a menu of four items\n * wants and what every desktop menu does. Enter and Space are the button's\n * own, so they are not bound here. */\nfunction _keyHandler(e) {\n if (!_activeMenu) return;\n if (e.key === 'Escape') { e.preventDefault(); hideContextMenu(); return; }\n const items = _enabledItems(_activeMenu);\n if (items.length === 0) return;\n const at = items.indexOf(document.activeElement);\n let next = null;\n if (e.key === 'ArrowDown') next = items[(at + 1 + items.length) % items.length];\n else if (e.key === 'ArrowUp') next = items[(at - 1 + items.length) % items.length];\n else if (e.key === 'Home') next = items[0];\n else if (e.key === 'End') next = items[items.length - 1];\n if (!next) return;\n e.preventDefault();\n next.focus({ preventScroll: true });\n}\n\nfunction _outsideHandler(e) {\n if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();\n}\n\nfunction escapeHtml(s) {\n return String(s).replace(/[&<>\"']/g, (c) => ({\n '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''',\n }[c]));\n}\n"],
|
|
5
|
-
"mappings": "
|
|
3
|
+
"sources": ["../src/help/help_registry.js", "../src/help/help_modal.js"],
|
|
4
|
+
"sourcesContent": ["/**\n * Where the help modal gets its help.\n *\n * HelpModal used to `import { getHelpService, HELP_TOPICS } from './help_service.js'` \u2014\n * and help_service.js is 249 domain words of EcoAgent prose about sectors,\n * currencies, agents and markets. That import was the last framework -> domain\n * edge in the core: a generic modal reaching into one application's manual.\n *\n * The modal now asks this registry, and the application fills it. If nobody\n * fills it, the modal opens and says there is no help \u2014 which is the correct\n * behaviour for a library that ships no documentation about your app.\n *\n * @typedef {Object} HelpProvider\n * @property {() => string[]} getCategories\n * @property {(category: string) => Array<{id: string, title: string}>} getTopicsByCategory\n * @property {(id: string) => Promise<Object|null>} loadTopic\n * @property {(query: string) => Array<Object>} searchTopics\n *\n * @typedef {Object.<string, {label: string, icon: string}>} HelpCategories\n * Keyed by category id; ITERATION ORDER IS DISPLAY ORDER.\n *\n * @typedef {{title: string, welcome: string}} HelpCopy\n * The landing heading. The modal used to hardcode \"EcoAgent Help\" and\n * \"Welcome to EcoAgent!\".\n */\n\n/** A provider with nothing in it. Not an error \u2014 just an app with no help. */\nconst EMPTY_PROVIDER = Object.freeze({\n getCategories: () => [],\n getTopicsByCategory: () => [],\n loadTopic: async () => null,\n searchTopics: () => [],\n});\n\nconst DEFAULT_COPY = Object.freeze({\n title: 'Help',\n welcome: 'Select a topic from the sidebar or browse the categories below.',\n});\n\nlet _provider = EMPTY_PROVIDER;\nlet _categories = Object.freeze({});\nlet _copy = DEFAULT_COPY;\n\n/**\n * @param {HelpProvider} provider\n * @param {HelpCategories} [categories] Display labels + icons, in display order.\n * @param {Partial<HelpCopy>} [copy] Landing heading and welcome line.\n */\nexport function setHelpProvider(provider, categories = {}, copy = {}) {\n _provider = provider ?? EMPTY_PROVIDER;\n _categories = Object.freeze({ ...categories });\n _copy = Object.freeze({ ...DEFAULT_COPY, ...copy });\n}\n\nexport function helpProvider() {\n return _provider;\n}\n\nexport function helpCategories() {\n return _categories;\n}\n\nexport function helpCopy() {\n return _copy;\n}\n", "/**\n * HelpModal - Modal dialog for displaying help content\n *\n * Features:\n * - Markdown rendering with syntax highlighting\n * - Table of contents navigation\n * - Topic search\n * - Category browsing\n * - Keyboard navigation (Escape to close)\n */\n\nimport { helpProvider, helpCategories, helpCopy } from './help_registry.js';\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// HelpModal Class\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nlet _activeModal = null;\n\nexport class HelpModal {\n /** The application's help, resolved at use time. */\n get _service() { return helpProvider(); }\n\n get _categories() { return helpCategories(); }\n\n get _copy() { return helpCopy(); }\n\n constructor() {\n // Read LAZILY, not here. A HelpModal can be constructed at boot (the\n // command palette holds one) before the application has registered its\n // help, and a constructor read would freeze the empty default forever.\n this._overlay = null;\n this._modal = null;\n this._currentTopic = null;\n this._searchQuery = '';\n this._boundKeyHandler = this._handleKeyDown.bind(this);\n }\n\n /**\n * Open the help modal.\n * @param {string} [topicId] - Initial topic to display\n */\n async open(topicId = null) {\n // Close any existing modal\n if (_activeModal && _activeModal !== this) {\n _activeModal.close();\n }\n _activeModal = this;\n\n this._createModal();\n document.body.appendChild(this._overlay);\n\n // Add keyboard handler\n document.addEventListener('keydown', this._boundKeyHandler);\n\n // Focus search input\n const searchInput = this._modal.querySelector('.help-search-input');\n if (searchInput) {\n setTimeout(() => searchInput.focus(), 100);\n }\n\n // Load initial topic or show index\n if (topicId) {\n await this._loadTopic(topicId);\n } else {\n this._showIndex();\n }\n }\n\n /**\n * Close the help modal.\n */\n close() {\n if (this._overlay) {\n document.removeEventListener('keydown', this._boundKeyHandler);\n this._overlay.remove();\n this._overlay = null;\n this._modal = null;\n }\n if (_activeModal === this) {\n _activeModal = null;\n }\n }\n\n /**\n * Create the modal DOM structure.\n * @private\n */\n _createModal() {\n // Overlay\n this._overlay = document.createElement('div');\n this._overlay.className = 'help-modal-overlay';\n this._overlay.addEventListener('click', (e) => {\n if (e.target === this._overlay) this.close();\n });\n\n // Modal container\n this._modal = document.createElement('div');\n this._modal.className = 'help-modal';\n this._modal.innerHTML = `\n <div class=\"help-modal__header\">\n <div class=\"help-modal__title\">\n <span class=\"material-symbols-outlined\">help</span>\n <span>Help</span>\n </div>\n <button class=\"help-modal__close\" title=\"Close (Esc)\">\n <span class=\"material-symbols-outlined\">close</span>\n </button>\n </div>\n <div class=\"help-modal__search\">\n <span class=\"material-symbols-outlined\">search</span>\n <input type=\"text\" class=\"help-search-input\" placeholder=\"Search help topics...\" autocomplete=\"off\">\n </div>\n <div class=\"help-modal__body\">\n <nav class=\"help-modal__sidebar\">\n <div class=\"help-sidebar__categories\"></div>\n </nav>\n <main class=\"help-modal__content\">\n <div class=\"help-content__loading\">Loading...</div>\n </main>\n </div>\n `;\n\n // Event handlers\n const closeBtn = this._modal.querySelector('.help-modal__close');\n closeBtn.addEventListener('click', () => this.close());\n\n const searchInput = this._modal.querySelector('.help-search-input');\n searchInput.addEventListener('input', (e) => this._handleSearch(e.target.value));\n searchInput.addEventListener('keydown', (e) => {\n if (e.key === 'Escape') {\n if (e.target.value) {\n e.target.value = '';\n this._handleSearch('');\n e.stopPropagation();\n }\n }\n });\n\n // Build sidebar\n this._buildSidebar();\n\n this._overlay.appendChild(this._modal);\n }\n\n /**\n * Build the sidebar navigation.\n * @private\n */\n _buildSidebar() {\n const container = this._modal.querySelector('.help-sidebar__categories');\n const categories = this._service.getCategories();\n\n // Sort categories by order\n const categoryOrder = Object.keys(this._categories);\n categories.sort((a, b) => {\n const orderA = categoryOrder.indexOf(a);\n const orderB = categoryOrder.indexOf(b);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n\n container.innerHTML = categories.map(category => {\n const topics = this._service.getTopicsByCategory(category);\n const label = this._categories[category]?.label || category;\n const icon = this._categories[category]?.icon || 'folder';\n\n return `\n <div class=\"help-category\" data-category=\"${category}\">\n <div class=\"help-category__header\">\n <span class=\"material-symbols-outlined\">${icon}</span>\n <span>${label}</span>\n </div>\n <ul class=\"help-category__topics\">\n ${topics.map(topic => `\n <li class=\"help-topic-item\" data-topic=\"${topic.id}\">\n ${topic.title}\n </li>\n `).join('')}\n </ul>\n </div>\n `;\n }).join('');\n\n // Add click handlers\n container.querySelectorAll('.help-topic-item').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n\n container.querySelectorAll('.help-category__header').forEach(header => {\n header.addEventListener('click', () => {\n header.parentElement.classList.toggle('collapsed');\n });\n });\n }\n\n /**\n * Show the help index page.\n * @private\n */\n _showIndex() {\n const content = this._modal.querySelector('.help-modal__content');\n const categories = this._service.getCategories();\n\n const categoryOrder = Object.keys(this._categories);\n categories.sort((a, b) => {\n const orderA = categoryOrder.indexOf(a);\n const orderB = categoryOrder.indexOf(b);\n return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);\n });\n\n content.innerHTML = `\n <div class=\"help-index\">\n <h1>${this._copy.title}</h1>\n <p>${this._copy.welcome}</p>\n\n <div class=\"help-index__grid\">\n ${categories.map(category => {\n const topics = this._service.getTopicsByCategory(category);\n const label = this._categories[category]?.label || category;\n const icon = this._categories[category]?.icon || 'folder';\n\n return `\n <div class=\"help-index__category\" data-category=\"${category}\">\n <div class=\"help-index__category-header\">\n <span class=\"material-symbols-outlined\">${icon}</span>\n <span>${label}</span>\n </div>\n <ul class=\"help-index__topics\">\n ${topics.slice(0, 3).map(topic => `\n <li class=\"help-index__topic\" data-topic=\"${topic.id}\">\n ${topic.title}\n </li>\n `).join('')}\n ${topics.length > 3 ? `<li class=\"help-index__more\">+${topics.length - 3} more</li>` : ''}\n </ul>\n </div>\n `;\n }).join('')}\n </div>\n </div>\n `;\n\n // Add click handlers\n content.querySelectorAll('.help-index__topic').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n }\n\n /**\n * Load and display a topic.\n * @param {string} topicId - Topic ID\n * @private\n */\n async _loadTopic(topicId) {\n const content = this._modal.querySelector('.help-modal__content');\n content.innerHTML = '<div class=\"help-content__loading\">Loading...</div>';\n\n // Update sidebar selection\n this._modal.querySelectorAll('.help-topic-item').forEach(item => {\n item.classList.toggle('active', item.dataset.topic === topicId);\n });\n\n try {\n const topic = await this._service.loadTopic(topicId);\n this._currentTopic = topicId;\n\n content.innerHTML = `\n <div class=\"help-content__article\">\n <div class=\"help-content__breadcrumb\">\n <a href=\"#\" class=\"help-breadcrumb__home\" title=\"Help Index\">\n <span class=\"material-symbols-outlined\">home</span>\n </a>\n <span class=\"help-breadcrumb__separator\">/</span>\n <span class=\"help-breadcrumb__title\">${topic.title}</span>\n </div>\n <article class=\"help-article\">\n ${topic.html}\n </article>\n </div>\n `;\n\n // Add breadcrumb handler\n content.querySelector('.help-breadcrumb__home').addEventListener('click', (e) => {\n e.preventDefault();\n this._showIndex();\n });\n } catch (error) {\n content.innerHTML = `\n <div class=\"help-content__error\">\n <span class=\"material-symbols-outlined\">error</span>\n <p>Failed to load help topic.</p>\n </div>\n `;\n }\n }\n\n /**\n * Handle search input.\n * @param {string} query - Search query\n * @private\n */\n _handleSearch(query) {\n this._searchQuery = query;\n const content = this._modal.querySelector('.help-modal__content');\n\n if (!query.trim()) {\n this._showIndex();\n return;\n }\n\n const results = this._service.searchTopics(query);\n\n if (results.length === 0) {\n content.innerHTML = `\n <div class=\"help-search-results\">\n <h2>Search Results</h2>\n <p class=\"help-search-empty\">No results found for \"${query}\"</p>\n </div>\n `;\n return;\n }\n\n content.innerHTML = `\n <div class=\"help-search-results\">\n <h2>Search Results</h2>\n <p class=\"help-search-count\">${results.length} result${results.length !== 1 ? 's' : ''} for \"${query}\"</p>\n <ul class=\"help-search-list\">\n ${results.map(topic => `\n <li class=\"help-search-item\" data-topic=\"${topic.id}\">\n <span class=\"help-search-item__title\">${topic.title}</span>\n <span class=\"help-search-item__category\">${this._categories[topic.category]?.label || topic.category}</span>\n </li>\n `).join('')}\n </ul>\n </div>\n `;\n\n // Add click handlers\n content.querySelectorAll('.help-search-item').forEach(item => {\n item.addEventListener('click', () => {\n this._loadTopic(item.dataset.topic);\n });\n });\n }\n\n /**\n * Handle keyboard events.\n * @param {KeyboardEvent} event\n * @private\n */\n _handleKeyDown(event) {\n if (event.key === 'Escape') {\n this.close();\n }\n }\n\n /**\n * Static method to open help modal.\n * @param {string} [topicId] - Initial topic\n * @returns {HelpModal}\n */\n static open(topicId = null) {\n const modal = new HelpModal();\n modal.open(topicId);\n return modal;\n }\n\n /**\n * Get the currently active modal.\n * @returns {HelpModal|null}\n */\n static getActive() {\n return _activeModal;\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global Help Shortcut\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Open help modal with the `?` key. (F1 is reserved for top-nav Home.)\n */\nfunction setupGlobalHelpShortcut() {\n document.addEventListener('keydown', (event) => {\n if (event.key !== '?') return;\n // Don't hijack `?` typed into a field (search boxes, editors).\n const t = event.target;\n if (t?.closest?.(\n 'input, textarea, select, [contenteditable=\"true\"]')) return;\n event.preventDefault();\n if (_activeModal) {\n _activeModal.close();\n } else {\n HelpModal.open();\n }\n });\n}\n\n// Initialize global shortcut\nif (typeof document !== 'undefined') {\n setupGlobalHelpShortcut();\n}\n\nexport default HelpModal;\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/chunk-WVFGV5FT.js
CHANGED
|
File without changes
|
|
File without changes
|
package/dist/core.js
CHANGED
|
File without changes
|
package/dist/core.js.map
CHANGED
|
File without changes
|
package/dist/editor.js
CHANGED
|
File without changes
|
package/dist/editor.js.map
CHANGED
|
File without changes
|
package/dist/flexdesk.css
CHANGED
|
@@ -3565,6 +3565,159 @@ body.twm-window-maximized .twm-resize-handle {
|
|
|
3565
3565
|
font-size: var(--font-size-xs);
|
|
3566
3566
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
|
|
3567
3567
|
}
|
|
3568
|
+
/* ── the list and people tiles (0.16.1, list_tiles.js) ──────────────────
|
|
3569
|
+
A bar (count, button), the rows, a note. The rows scroll inside the tile;
|
|
3570
|
+
the bar and the note stay put. */
|
|
3571
|
+
.fd-list-tile {
|
|
3572
|
+
flex: 1 1 auto;
|
|
3573
|
+
display: flex;
|
|
3574
|
+
flex-direction: column;
|
|
3575
|
+
min-height: 0;
|
|
3576
|
+
gap: var(--space-xs);
|
|
3577
|
+
margin: calc(-1 * var(--space-sm)) 0;
|
|
3578
|
+
}
|
|
3579
|
+
.fd-list-tile__bar {
|
|
3580
|
+
flex: 0 0 auto;
|
|
3581
|
+
display: flex;
|
|
3582
|
+
align-items: center;
|
|
3583
|
+
gap: var(--space-sm);
|
|
3584
|
+
min-height: 24px;
|
|
3585
|
+
}
|
|
3586
|
+
.fd-list-tile__count {
|
|
3587
|
+
color: var(--text-muted);
|
|
3588
|
+
font-size: var(--font-size-xs);
|
|
3589
|
+
font-variant-numeric: tabular-nums;
|
|
3590
|
+
}
|
|
3591
|
+
.fd-list-tile__spacer { flex: 1 1 auto; }
|
|
3592
|
+
.fd-list-tile__action {
|
|
3593
|
+
display: inline-flex;
|
|
3594
|
+
align-items: center;
|
|
3595
|
+
gap: 4px;
|
|
3596
|
+
padding: 2px 8px;
|
|
3597
|
+
border: 1px solid var(--border-subtle);
|
|
3598
|
+
border-radius: var(--radius-sm);
|
|
3599
|
+
background: transparent;
|
|
3600
|
+
color: var(--text-secondary);
|
|
3601
|
+
font: inherit;
|
|
3602
|
+
font-size: var(--font-size-xs);
|
|
3603
|
+
cursor: pointer;
|
|
3604
|
+
}
|
|
3605
|
+
.fd-list-tile__action:hover { color: var(--text-bright); border-color: var(--text-muted); }
|
|
3606
|
+
.fd-list-tile__action .material-symbols-outlined { font-size: 15px; }
|
|
3607
|
+
.fd-list-tile__rows {
|
|
3608
|
+
flex: 1 1 auto;
|
|
3609
|
+
min-height: 0;
|
|
3610
|
+
overflow-y: auto;
|
|
3611
|
+
overflow-x: hidden;
|
|
3612
|
+
}
|
|
3613
|
+
.fd-list-tile__empty {
|
|
3614
|
+
padding: var(--space-sm) 2px;
|
|
3615
|
+
color: var(--text-muted);
|
|
3616
|
+
font-size: var(--font-size-sm);
|
|
3617
|
+
}
|
|
3618
|
+
.fd-list-tile__row {
|
|
3619
|
+
display: flex;
|
|
3620
|
+
align-items: center;
|
|
3621
|
+
gap: var(--space-sm);
|
|
3622
|
+
min-width: 0;
|
|
3623
|
+
padding: 3px 4px;
|
|
3624
|
+
border-radius: var(--radius-sm);
|
|
3625
|
+
font-size: var(--font-size-sm);
|
|
3626
|
+
cursor: pointer;
|
|
3627
|
+
outline: none;
|
|
3628
|
+
}
|
|
3629
|
+
.fd-list-tile__row:hover { background: rgba(255, 255, 255, 0.05); }
|
|
3630
|
+
.fd-list-tile__row:focus-visible { box-shadow: inset 0 0 0 1px var(--accent-bright); }
|
|
3631
|
+
.fd-list-tile__icon { flex: 0 0 auto; font-size: 16px; color: var(--text-muted); }
|
|
3632
|
+
.fd-list-tile__lead {
|
|
3633
|
+
flex: 0 0 auto;
|
|
3634
|
+
color: var(--text-muted);
|
|
3635
|
+
font-variant-numeric: tabular-nums;
|
|
3636
|
+
white-space: nowrap;
|
|
3637
|
+
}
|
|
3638
|
+
.fd-list-tile__title {
|
|
3639
|
+
flex: 1 1 auto;
|
|
3640
|
+
min-width: 0;
|
|
3641
|
+
color: var(--text-bright);
|
|
3642
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3643
|
+
}
|
|
3644
|
+
.fd-list-tile__cell {
|
|
3645
|
+
flex: 0 0 auto;
|
|
3646
|
+
display: inline-flex;
|
|
3647
|
+
align-items: center;
|
|
3648
|
+
gap: 2px;
|
|
3649
|
+
max-width: 40%;
|
|
3650
|
+
color: var(--text-secondary);
|
|
3651
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3652
|
+
}
|
|
3653
|
+
.fd-list-tile__cell .material-symbols-outlined { font-size: 14px; }
|
|
3654
|
+
.fd-list-tile__cell--danger, .fd-people-tile__pill--danger { color: var(--color-danger); }
|
|
3655
|
+
.fd-list-tile__cell--warning, .fd-people-tile__pill--warning { color: var(--color-warning); }
|
|
3656
|
+
.fd-list-tile__cell--ok, .fd-people-tile__pill--ok { color: var(--color-success); }
|
|
3657
|
+
.fd-list-tile__cell--muted, .fd-people-tile__pill--muted { color: var(--text-muted); }
|
|
3658
|
+
.fd-list-tile__note {
|
|
3659
|
+
flex: 0 0 auto;
|
|
3660
|
+
font-size: var(--font-size-xs);
|
|
3661
|
+
color: var(--text-muted);
|
|
3662
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3663
|
+
}
|
|
3664
|
+
.fd-people-tile__name {
|
|
3665
|
+
flex: 0 1 auto;
|
|
3666
|
+
min-width: 0;
|
|
3667
|
+
color: var(--text-bright);
|
|
3668
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3669
|
+
}
|
|
3670
|
+
.fd-people-tile__name--muted { color: var(--text-muted); font-style: italic; }
|
|
3671
|
+
.fd-people-tile__pills {
|
|
3672
|
+
flex: 1 1 auto;
|
|
3673
|
+
display: flex;
|
|
3674
|
+
flex-wrap: nowrap;
|
|
3675
|
+
gap: 4px;
|
|
3676
|
+
min-width: 0;
|
|
3677
|
+
overflow: hidden;
|
|
3678
|
+
}
|
|
3679
|
+
.fd-people-tile__pill {
|
|
3680
|
+
flex: 0 0 auto;
|
|
3681
|
+
padding: 0 6px;
|
|
3682
|
+
border: 1px solid var(--border-subtle);
|
|
3683
|
+
border-radius: 9px;
|
|
3684
|
+
font-size: var(--font-size-xs);
|
|
3685
|
+
line-height: 16px;
|
|
3686
|
+
color: var(--text-secondary);
|
|
3687
|
+
white-space: nowrap;
|
|
3688
|
+
}
|
|
3689
|
+
.fd-people-tile__count {
|
|
3690
|
+
flex: 0 0 auto;
|
|
3691
|
+
color: var(--text-secondary);
|
|
3692
|
+
font-size: var(--font-size-xs);
|
|
3693
|
+
font-variant-numeric: tabular-nums;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
/* ── the scope picker (0.16.1, scope_picker.js): one pressed button of a row */
|
|
3697
|
+
.fd-scope-picker {
|
|
3698
|
+
display: inline-flex;
|
|
3699
|
+
align-items: stretch;
|
|
3700
|
+
border: 1px solid var(--border-subtle);
|
|
3701
|
+
border-radius: var(--radius-sm);
|
|
3702
|
+
overflow: hidden;
|
|
3703
|
+
}
|
|
3704
|
+
.fd-scope-picker__btn {
|
|
3705
|
+
padding: 3px 10px;
|
|
3706
|
+
border: 0;
|
|
3707
|
+
border-left: 1px solid var(--border-subtle);
|
|
3708
|
+
background: transparent;
|
|
3709
|
+
color: var(--text-secondary);
|
|
3710
|
+
font: inherit;
|
|
3711
|
+
font-size: var(--font-size-sm);
|
|
3712
|
+
white-space: nowrap;
|
|
3713
|
+
cursor: pointer;
|
|
3714
|
+
}
|
|
3715
|
+
.fd-scope-picker__btn:first-child { border-left: 0; }
|
|
3716
|
+
.fd-scope-picker__btn:hover { color: var(--text-bright); background: rgba(255, 255, 255, 0.05); }
|
|
3717
|
+
.fd-scope-picker__btn--on,
|
|
3718
|
+
.fd-scope-picker__btn--on:hover { background: var(--accent); color: #fff; }
|
|
3719
|
+
.fd-scope-picker__btn:focus-visible { outline: 1px solid var(--accent-bright); outline-offset: -2px; }
|
|
3720
|
+
|
|
3568
3721
|
/* ===================================================================
|
|
3569
3722
|
REUSABLE ACTION DROPDOWN COMPONENT
|
|
3570
3723
|
Used by Add Scenario, Add Widget, and other action dropdowns
|
package/dist/flow.js
CHANGED
|
File without changes
|
package/dist/flow.js.map
CHANGED
|
File without changes
|
package/dist/host.js
CHANGED
|
@@ -18,7 +18,10 @@ var HOST_CONTRACT = Object.freeze({
|
|
|
18
18
|
optional: Object.freeze(["startNativeDrag", "startNativeResize"])
|
|
19
19
|
}),
|
|
20
20
|
dialogs: Object.freeze({ required: Object.freeze(["saveFile"]), optional: Object.freeze([]) }),
|
|
21
|
-
state: Object.freeze({ required: Object.freeze(["read", "write"]), optional: Object.freeze([]) })
|
|
21
|
+
state: Object.freeze({ required: Object.freeze(["read", "write"]), optional: Object.freeze([]) }),
|
|
22
|
+
// `openExternal(url)` hands a web address to the OS's default browser and
|
|
23
|
+
// resolves true when it did. See external_links.js.
|
|
24
|
+
shell: Object.freeze({ required: Object.freeze(["openExternal"]), optional: Object.freeze([]) })
|
|
22
25
|
});
|
|
23
26
|
function validateCapability(name, cap) {
|
|
24
27
|
const spec = HOST_CONTRACT[name];
|
|
@@ -256,6 +259,12 @@ function createPhotinoHost(opts = {}) {
|
|
|
256
259
|
return r && typeof r === "object" ? r : null;
|
|
257
260
|
}
|
|
258
261
|
},
|
|
262
|
+
shell: {
|
|
263
|
+
// True when the native side opened it in the system browser; false
|
|
264
|
+
// when it refused (a scheme it won't open, an older native side
|
|
265
|
+
// without the op): the caller falls back to window.open.
|
|
266
|
+
openExternal: async (url) => await soft(false, "shell", "openExternal", String(url || "")) === true
|
|
267
|
+
},
|
|
259
268
|
state: {
|
|
260
269
|
async read(key) {
|
|
261
270
|
const v = await soft(null, "state", "read", key);
|
|
@@ -279,6 +288,75 @@ function createPhotinoHost(opts = {}) {
|
|
|
279
288
|
}
|
|
280
289
|
return createHost(caps);
|
|
281
290
|
}
|
|
291
|
+
|
|
292
|
+
// src/host/external_links.js
|
|
293
|
+
var SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
|
|
294
|
+
function outbound(href, base) {
|
|
295
|
+
let url;
|
|
296
|
+
try {
|
|
297
|
+
url = new URL(String(href), base);
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return SCHEMES.has(url.protocol) ? url : null;
|
|
302
|
+
}
|
|
303
|
+
function routeExternalLinks({ host, root, win } = {}) {
|
|
304
|
+
const w = win ?? (typeof window !== "undefined" ? window : null);
|
|
305
|
+
const r = root ?? (typeof document !== "undefined" ? document : null);
|
|
306
|
+
const shell = host?.shell;
|
|
307
|
+
if (!shell || typeof shell.openExternal !== "function" || !w || !r) return { dispose() {
|
|
308
|
+
} };
|
|
309
|
+
const base = () => w.location?.href;
|
|
310
|
+
const origin = () => {
|
|
311
|
+
try {
|
|
312
|
+
return new URL(base()).origin;
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const nativeOpen = w.open;
|
|
318
|
+
const fallback = (href) => {
|
|
319
|
+
try {
|
|
320
|
+
nativeOpen?.call(w, href, "_blank", "noopener");
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
const send = (href) => {
|
|
325
|
+
Promise.resolve().then(() => shell.openExternal(href)).then((opened) => {
|
|
326
|
+
if (opened !== true) fallback(href);
|
|
327
|
+
}, () => fallback(href));
|
|
328
|
+
};
|
|
329
|
+
const onClick = (e) => {
|
|
330
|
+
if (e.defaultPrevented) return;
|
|
331
|
+
if (e.type === "click" ? e.button !== 0 : e.button !== 1) return;
|
|
332
|
+
const a = e.target?.closest?.("a[href]");
|
|
333
|
+
if (!a || a.hasAttribute("download")) return;
|
|
334
|
+
const url = outbound(a.getAttribute("href"), base());
|
|
335
|
+
if (!url) return;
|
|
336
|
+
const leaves = url.protocol === "mailto:" || (a.getAttribute("target") || "").toLowerCase() === "_blank" || url.origin !== origin() || e.button === 1;
|
|
337
|
+
if (!leaves) return;
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
send(url.href);
|
|
340
|
+
};
|
|
341
|
+
const routedOpen = function open(href, target, features) {
|
|
342
|
+
const url = href == null ? null : outbound(href, base());
|
|
343
|
+
if (url && (url.protocol === "mailto:" || url.origin !== origin())) {
|
|
344
|
+
send(url.href);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
return nativeOpen.call(w, href, target, features);
|
|
348
|
+
};
|
|
349
|
+
r.addEventListener("click", onClick);
|
|
350
|
+
r.addEventListener("auxclick", onClick);
|
|
351
|
+
w.open = routedOpen;
|
|
352
|
+
return {
|
|
353
|
+
dispose() {
|
|
354
|
+
r.removeEventListener("click", onClick);
|
|
355
|
+
r.removeEventListener("auxclick", onClick);
|
|
356
|
+
if (w.open === routedOpen) w.open = nativeOpen;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
282
360
|
export {
|
|
283
361
|
HOST_CONTRACT,
|
|
284
362
|
NULL_HOST,
|
|
@@ -287,6 +365,7 @@ export {
|
|
|
287
365
|
createPhotinoTransport,
|
|
288
366
|
createPywebviewHost,
|
|
289
367
|
isPhotinoWindow,
|
|
368
|
+
routeExternalLinks,
|
|
290
369
|
usesCustomWindowChrome,
|
|
291
370
|
validateCapability
|
|
292
371
|
};
|
package/dist/host.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/host/host.js", "../src/host/pywebview_host.js", "../src/host/photino_host.js"],
|
|
4
|
-
"sourcesContent": ["/**\n * host.js \u2014 the host port.\n *\n * The framework declares what it needs from its embedder. A Host is a bag of\n * INDEPENDENTLY OPTIONAL capability objects:\n *\n * - absent capability => absent key (never a stub)\n * - present capability => must be complete (validated at boot)\n *\n * There is NO module-level default host and NO getHost(). Hosts are injected \u2014\n * a module singleton here would just be `window.pywebview` wearing a hat, and\n * would make a second host (mock, browser tab, VS Code webview) impossible.\n *\n * Zero pywebview. Zero EcoAgent. See pywebview_host.js for the reference\n * adapter that implements this port against the pywebview / HTTP transports.\n */\n\nexport const HOST_CONTRACT = Object.freeze({\n window: Object.freeze({\n required: Object.freeze(['chrome', 'minimize', 'close', 'isMaximized', 'setMaximized',\n 'isFullscreen', 'toggleFullscreen', 'getBounds', 'setBounds']),\n // `chrome()` answers 'native' or 'custom', or a promise of one.\n optional: Object.freeze(['startNativeDrag', 'startNativeResize']),\n }),\n dialogs: Object.freeze({ required: Object.freeze(['saveFile']), optional: Object.freeze([]) }),\n state: Object.freeze({ required: Object.freeze(['read', 'write']), optional: Object.freeze([]) }),\n});\n\n/**\n * Validate ONE capability object. We validate the capability the adapter\n * authored \u2014 a plain object literal with real closures \u2014 not the raw bridge:\n * `index.html`'s browser-mode polyfill installs a `new Proxy({}, {get})` that\n * returns a function for EVERY property name, so duck-typing the bridge is\n * vacuously true and validates nothing. A Proxy cannot forge a missing key in\n * an object literal.\n *\n * @returns {{ ok: boolean, missing: string[] }}\n */\nexport function validateCapability(name, cap) {\n const spec = HOST_CONTRACT[name];\n if (!spec) return { ok: false, missing: [`unknown capability '${name}'`] };\n if (!cap || typeof cap !== 'object') return { ok: false, missing: [...spec.required] };\n const missing = spec.required.filter((m) => typeof cap[m] !== 'function');\n return { ok: missing.length === 0, missing };\n}\n\n/**\n * Validate + freeze a set of capabilities into a Host.\n * Throws at boot on an incomplete capability \u2014 a half-wired host is a bug,\n * and a bug at boot beats a silent degrade at click time.\n *\n * @param {{ window?: object, dialogs?: object, state?: object }} caps\n * @returns {Readonly<object>} the Host\n */\nexport function createHost(caps = {}) {\n const host = {};\n for (const name of Object.keys(HOST_CONTRACT)) {\n const cap = caps[name];\n if (cap == null) continue; // legitimately absent\n const v = validateCapability(name, cap);\n if (!v.ok) {\n throw new Error(`Host capability '${name}' is incomplete: missing ${v.missing.join(', ')}`);\n }\n host[name] = Object.freeze(cap);\n }\n return Object.freeze(host);\n}\n\n/** The zero-capability host. Every framework consumer MUST survive it. */\nexport const NULL_HOST = createHost({});\n", "/**\n * pywebview_host.js \u2014 the REFERENCE host adapter.\n *\n * Knows pywebview. Knows NOTHING about EcoAgent: every domain fact (which\n * bridge ref to use, where a state key lands on disk) arrives as a parameter.\n * This is the adapter a standalone consumer copies; EcoAgent is just its first\n * caller (see ui/js/bootstrap/app_bootstrap.js).\n *\n * It speaks to whatever `window.pywebview.api` currently is \u2014 the real\n * pywebview bridge in the desktop app, or the `POST /api/<method>` Proxy\n * polyfill that ui/index.html installs in --browser mode. Both transports are\n * positional-on-the-wire, which is why setBounds() spreads its object back out\n * into positional args.\n */\n\nimport { createHost } from './host.js';\n\nconst FRAMELESS_BACKENDS = new Set(['edgechromium', 'mshtml']);\n\n/**\n * Custom (frameless) window chrome \u2014 the min/max/close buttons, edge resize\n * handles and drag-to-move top bar \u2014 only makes sense when our top bar IS the\n * window's title bar. That holds on a native pywebview window the OS draws\n * *without* decorations of its own: Windows (frameless). On Linux/macOS the\n * window manager already draws a native title bar above our top bar, and in\n * `--browser` mode the browser supplies its own chrome \u2014 in both cases the\n * custom controls are a redundant second set, so we neither render nor wire\n * them (which also stops us swallowing native F11 fullscreen and showing dead\n * resize cursors at the window edges).\n *\n * Detection keys off the pywebview backend string, which is unambiguous: the\n * browser-mode polyfill in index.html injects only `.api`, never `.platform`,\n * so any `.platform` at all \u21D2 running as a native pywebview app. The frameless\n * (custom-title-bar) backends are the Windows WebViews \u2014 'edgechromium' /\n * 'mshtml'. Linux ('gtkwebkit2' / 'qtwebengine') and macOS ('cocoa') draw\n * native decorations, so they return false. Verified live: WebKitGTK reports\n * platform 'gtkwebkit2'.\n */\nexport function usesCustomWindowChrome() {\n // Authoritative signal from the Python host: run_pywebview_mode() appends\n // `?frameless=1` to the window URL exactly when it created a frameless native\n // window (Windows). Prefer it over the backend sniff below because it's\n // present at document parse \u2014 before pywebview finishes injecting\n // `window.pywebview` (and before the browser-mode polyfill, which sets `.api`\n // but no `.platform`, can clobber it). Reading the sniff at shell-build time\n // otherwise races and the whole custom chrome silently fails to render.\n try {\n if (new URLSearchParams(window.location.search).get('frameless') === '1') {\n return true;\n }\n } catch (_) { /* location unavailable (non-browser env) \u2014 fall through */ }\n const platform = window.pywebview && window.pywebview.platform;\n return !!platform && FRAMELESS_BACKENDS.has(platform);\n}\n\n/**\n * Build a Host backed by the pywebview bridge.\n *\n * @param {object} opts\n * @param {{current: object|null}} [opts.bridgeRef] LIVE ref to the bridge.\n * MUST be a ref, never a snapshot: `setupHostBridgeListener` mutates\n * `.current` when a late bridge arrives, and `resolveHostBridge` can\n * resolve `null` after its timeout. Snapshotting the bridge here would\n * freeze that null and reproduce as an intermittent boot failure.\n * @param {(key: string) => string} [opts.resolvePath] logical state key ->\n * storage path. THE domain seam: the framework only ever names keys\n * ('desktops', 'datatable_state'); the embedder decides where they land.\n * @param {object} [opts.logger]\n */\nexport function createPywebviewHost({ bridgeRef = null, resolvePath = (k) => `${k}.json`, logger = console } = {}) {\n const api = () => bridgeRef?.current ?? window.pywebview?.api ?? null;\n\n const call = async (name, args = []) => {\n const a = api();\n if (!a || typeof a[name] !== 'function') return null;\n try { return await a[name](...args); }\n catch (e) { logger.warn?.('[host] call failed', name, e); return null; }\n };\n\n // Envelope unwrapping, in ONE place. Transcribed from the call sites this\n // replaces, so app.py's catch-all `{ok: true}` reply (which carries no\n // `maximized` key) still reads as false, exactly as it does today.\n const okMax = (r) => Boolean(r && r.ok && r.maximized === true);\n const okFs = (r) => Boolean(r && r.ok && r.fullscreen);\n\n // ALL THREE capabilities are ALWAYS advertised. Do NOT gate them on api()\n // being non-null at construction time: the methods late-bind through the\n // ref, and gating here would permanently strand a late-arriving bridge on\n // the fallback paths (Blob download instead of the native save dialog).\n return createHost({\n window: {\n chrome: () => (usesCustomWindowChrome() ? 'custom' : 'native'),\n minimize: () => { void call('window_minimize'); },\n close: () => { void call('window_close'); },\n isMaximized: async () => okMax(await call('window_is_maximized')),\n setMaximized: async (on) => okMax(await call('window_set_maximized', [on])),\n isFullscreen: async () => okFs(await call('window_is_fullscreen')),\n toggleFullscreen: async () => okFs(await call('window_toggle_fullscreen')),\n async getBounds() {\n const r = await call('window_get_bounds');\n if (!r || !r.ok) return null;\n return { x: r.x, y: r.y, width: r.width, height: r.height };\n },\n // POSITIONAL on the wire \u2014 both transports depend on it. Not async:\n // this is a rAF hot path during a window drag/resize; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n void call('window_set_bounds', [x, y, width, height]);\n },\n startNativeDrag: async () => Boolean((await call('window_start_native_drag'))?.ok),\n },\n dialogs: {\n // Resolves to null when the transport has no save_file_dialog behind it.\n // Per host.js that is the contract for \"unavailable\", and every caller\n // treats it exactly like an absent `dialogs` capability: fall back to a\n // Blob download, never report a failed save. Covered by\n // tests/ui/test_host_save_fallback.mjs.\n saveFile: ({ data, filename, kind }) => call('save_file_dialog', [data, filename, kind]),\n },\n state: {\n async read(key) {\n const blob = await call('workspace_state_read', [{ path: resolvePath(key) }]);\n if (!blob) return null;\n try { return typeof blob === 'string' ? JSON.parse(blob) : blob; }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n const r = await call('workspace_state_write',\n [{ path: resolvePath(key), data: JSON.stringify(value) }]);\n return r !== null;\n },\n },\n });\n}\n", "/**\n * photino_host.js \u2014 the host port over a Photino.NET window.\n *\n * Photino gives the page exactly two things: `window.external.sendMessage(text)`\n * and `window.external.receiveMessage(callback)`. Strings in, strings out, no\n * replies. This adapter puts a request/reply protocol over them, matched by id:\n *\n * page \u2192 host {\"id\": 7, \"cap\": \"window\", \"op\": \"setBounds\", \"args\": [{\"x\":0,\"y\":0,\"width\":1200,\"height\":800}]}\n * host \u2192 page {\"id\": 7, \"ok\": true, \"value\": null}\n * host \u2192 page {\"id\": 8, \"ok\": false, \"error\": \"no such op\"}\n * host \u2192 page {\"event\": \"window.maximizedChanged\", \"value\": true} // unsolicited, no id\n *\n * and offers the port's `window`, `dialogs` and `state` capabilities on top.\n * The C# side \u2014 one dispatcher on the window's web-message handler, every op\n * with its args and reply \u2014 is documented in `photino_host.md` beside this file.\n *\n * Like pywebview_host.js it knows the transport and NOTHING about the app: an\n * embedder that keeps some capability elsewhere (BugDesk keeps `state` on its\n * own server) passes that capability in, or `null` to leave it out.\n *\n * In a browser tab there is no `window.external.sendMessage`, and\n * createPhotinoHost() returns NULL_HOST \u2014 the fallback every FlexDesk consumer\n * already has to survive.\n */\n\nimport { createHost, NULL_HOST } from './host.js';\n\n/** True when `ext` (default: `window.external`) is a Photino bridge. */\nexport function isPhotinoWindow(ext = (typeof window !== 'undefined' ? window.external : undefined)) {\n return !!ext && typeof ext.sendMessage === 'function' && typeof ext.receiveMessage === 'function';\n}\n\n/**\n * The bare request/reply layer. `call(cap, op, ...args)` sends one request and\n * resolves with the reply's `value` (`request(cap, op, args, {wait})` is the\n * same with a timeout of its own). It REJECTS when the host answers\n * `ok: false` (with the host's error) and when no reply arrives within\n * `timeoutMs`. A reply for an id nobody is waiting on, a message that is not\n * JSON, and anything that is neither a reply nor an event are dropped.\n *\n * @param {object} opts\n * @param {{sendMessage: Function, receiveMessage: Function}} opts.external\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent] id-less `{event, value}` messages\n * @param {object} [opts.logger]\n */\nexport function createPhotinoTransport({ external, timeoutMs = 10000, onEvent = null, logger = console }) {\n const pending = new Map();\n let seq = 0;\n external.receiveMessage((text) => {\n let m;\n try { m = typeof text === 'string' ? JSON.parse(text) : text; } catch { return; }\n if (!m || typeof m !== 'object') return;\n if (m.id == null) {\n if (typeof m.event === 'string' && onEvent) {\n try { onEvent(m.event, m.value); }\n catch (e) { logger.warn?.('[host] photino event handler failed', m.event, e); }\n }\n return;\n }\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n clearTimeout(p.timer);\n if (m.ok) p.resolve(m.value === undefined ? null : m.value);\n else p.reject(new Error(m.error || `${p.what}: refused by the host`));\n });\n // `wait` is this request's timeout in ms; 0 waits for as long as it takes.\n const request = (cap, op, args = [], { wait = timeoutMs } = {}) => new Promise((resolve, reject) => {\n const id = ++seq;\n const what = `${cap}.${op}`;\n const timer = wait > 0 ? setTimeout(() => {\n if (pending.delete(id)) reject(new Error(`${what}: no answer from the host within ${wait} ms`));\n }, wait) : null;\n pending.set(id, { resolve, reject, timer, what });\n try {\n external.sendMessage(JSON.stringify({ id, cap, op, args }));\n } catch (e) {\n pending.delete(id);\n clearTimeout(timer);\n reject(e);\n }\n });\n const call = (cap, op, ...args) => request(cap, op, args);\n return { call, request, pending: () => pending.size };\n}\n\n/**\n * Build a Host backed by a Photino window.\n *\n * The capability methods follow pywebview_host.js's contract, so a consumer\n * cannot tell the two apart: they DEGRADE instead of rejecting. A failed query\n * reads as false / null, a failed fire-and-forget is logged, and a save that\n * the host refused or never answered resolves `null` \u2014 the host.js meaning of\n * \"unavailable\", which every caller turns into a Blob download.\n *\n * @param {object} [opts]\n * @param {object} [opts.external=window.external] the Photino bridge (a fake one in tests)\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent]\n * @param {object} [opts.logger=console]\n * @param {'auto'|'native'|'custom'} [opts.chrome='auto'] who draws the window's\n * title bar and borders. `auto` asks the native side once (`host.describe`,\n * its `frame` field), the first time somebody needs to know; `native`\n * or `custom` fixes the answer without asking. See photino_host.md.\n * @param {object|null} [opts.window] replace the window capability, or `null` to leave it out\n * @param {object|null} [opts.dialogs] replace the dialogs capability, or `null` to leave it out\n * @param {object|null} [opts.state] replace the state capability, or `null` to leave it out\n * @returns {Readonly<object>} a Host, or NULL_HOST outside a Photino window\n */\nexport function createPhotinoHost(opts = {}) {\n const {\n external = (typeof window !== 'undefined' ? window.external : undefined),\n timeoutMs = 10000,\n onEvent = null,\n logger = console,\n chrome = 'auto',\n } = opts;\n if (!isPhotinoWindow(external)) return NULL_HOST;\n\n const { call, request } = createPhotinoTransport({ external, timeoutMs, onEvent, logger });\n const warn = (cap, op, e) => logger.warn?.('[host] photino call failed', `${cap}.${op}`, e);\n const soft = async (fallback, cap, op, ...args) => {\n try { return await call(cap, op, ...args); }\n catch (e) { warn(cap, op, e); return fallback; }\n };\n const fire = (cap, op, ...args) => { void soft(null, cap, op, ...args); };\n\n // Who draws the frame. Asked once and remembered: a string once known, the\n // pending question before that. A native side that predates `frame`, or\n // doesn't answer within 3 s, has native chrome.\n let frame = chrome === 'custom' || chrome === 'native' ? chrome : null;\n let asking = null;\n const askFrame = () => {\n if (frame) return frame;\n asking ??= request('host', 'describe', [], { wait: 3000 })\n .then((d) => (d && d.frame === 'custom' ? 'custom' : 'native'), () => 'native')\n .then((f) => { frame = f; return f; });\n return asking;\n };\n // A drag or resize handed to the OS: the reply comes when the mouse is\n // released, however long that takes. Refused (a native side without the op,\n // or a window with native chrome) means \"not handled\": the caller does it in JS.\n const gesture = async (op, ...args) => {\n try { await request('window', op, args, { wait: 0 }); return true; }\n catch (e) { return false; }\n };\n\n const defaults = {\n window: {\n // 'native' (the OS draws the frame: FlexDesk's own buttons would be\n // a second set) or 'custom' (the native side made the window\n // frameless). A promise until the native side has said which.\n chrome: () => askFrame(),\n minimize: () => { fire('window', 'minimize'); },\n close: () => { fire('window', 'close'); },\n isMaximized: async () => (await soft(false, 'window', 'isMaximized')) === true,\n setMaximized: async (on) => (await soft(false, 'window', 'setMaximized', !!on)) === true,\n isFullscreen: async () => (await soft(false, 'window', 'isFullscreen')) === true,\n toggleFullscreen: async () => (await soft(false, 'window', 'toggleFullscreen')) === true,\n async getBounds() {\n const b = await soft(null, 'window', 'getBounds');\n if (!b || typeof b !== 'object') return null;\n return { x: b.x, y: b.y, width: b.width, height: b.height };\n },\n startNativeDrag: () => gesture('startDrag'),\n startNativeResize: (edge) => gesture('startResize', String(edge || '')),\n // Not async, like pywebview's: a rAF hot path during a drag; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n fire('window', 'setBounds', { x, y, width, height });\n },\n },\n dialogs: {\n // `{ok: true, path}` | `{ok: false, cancelled: true}` | `{ok: false, error}`,\n // or null when the host refused: fall back to a download. NO TIMEOUT \u2014\n // the dialog is open for as long as the user takes, and giving up on it\n // would download the file a second time behind the dialog.\n async saveFile({ data, filename, kind } = {}) {\n let r = null;\n try { r = await request('dialogs', 'saveFile', [{ data, filename, kind }], { wait: 0 }); }\n catch (e) { warn('dialogs', 'saveFile', e); }\n return r && typeof r === 'object' ? r : null;\n },\n },\n state: {\n async read(key) {\n const v = await soft(null, 'state', 'read', key);\n if (typeof v !== 'string') return v ?? null;\n try { return JSON.parse(v); }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n return (await soft(false, 'state', 'write', key, value)) !== false;\n },\n },\n };\n\n const caps = {};\n for (const name of Object.keys(defaults)) {\n if (!(name in opts)) caps[name] = defaults[name];\n else if (opts[name] != null) caps[name] = opts[name];\n }\n return createHost(caps);\n}\n"],
|
|
5
|
-
"mappings": ";;;AAiBO,IAAM,gBAAgB,OAAO,OAAO;AAAA,EACvC,QAAQ,OAAO,OAAO;AAAA,IAClB,UAAU,OAAO,OAAO;AAAA,MAAC;AAAA,MAAU;AAAA,MAAY;AAAA,MAAS;AAAA,MAAe;AAAA,MAC9C;AAAA,MAAgB;AAAA,MAAoB;AAAA,MAAa;AAAA,IAAW,CAAC;AAAA;AAAA,IAEtF,UAAU,OAAO,OAAO,CAAC,mBAAmB,mBAAmB,CAAC;AAAA,EACpE,CAAC;AAAA,EACD,SAAS,OAAO,OAAO,EAAE,UAAU,OAAO,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,OAAO,OAAO,CAAC,CAAC,EAAE,CAAC;AAAA,EAC7F,OAAS,OAAO,OAAO,EAAE,UAAU,OAAO,OAAO,CAAC,QAAQ,OAAO,CAAC,GAAG,UAAU,OAAO,OAAO,CAAC,CAAC,EAAE,CAAC;
|
|
3
|
+
"sources": ["../src/host/host.js", "../src/host/pywebview_host.js", "../src/host/photino_host.js", "../src/host/external_links.js"],
|
|
4
|
+
"sourcesContent": ["/**\n * host.js \u2014 the host port.\n *\n * The framework declares what it needs from its embedder. A Host is a bag of\n * INDEPENDENTLY OPTIONAL capability objects:\n *\n * - absent capability => absent key (never a stub)\n * - present capability => must be complete (validated at boot)\n *\n * There is NO module-level default host and NO getHost(). Hosts are injected \u2014\n * a module singleton here would just be `window.pywebview` wearing a hat, and\n * would make a second host (mock, browser tab, VS Code webview) impossible.\n *\n * Zero pywebview. Zero EcoAgent. See pywebview_host.js for the reference\n * adapter that implements this port against the pywebview / HTTP transports.\n */\n\nexport const HOST_CONTRACT = Object.freeze({\n window: Object.freeze({\n required: Object.freeze(['chrome', 'minimize', 'close', 'isMaximized', 'setMaximized',\n 'isFullscreen', 'toggleFullscreen', 'getBounds', 'setBounds']),\n // `chrome()` answers 'native' or 'custom', or a promise of one.\n optional: Object.freeze(['startNativeDrag', 'startNativeResize']),\n }),\n dialogs: Object.freeze({ required: Object.freeze(['saveFile']), optional: Object.freeze([]) }),\n state: Object.freeze({ required: Object.freeze(['read', 'write']), optional: Object.freeze([]) }),\n // `openExternal(url)` hands a web address to the OS's default browser and\n // resolves true when it did. See external_links.js.\n shell: Object.freeze({ required: Object.freeze(['openExternal']), optional: Object.freeze([]) }),\n});\n\n/**\n * Validate ONE capability object. We validate the capability the adapter\n * authored \u2014 a plain object literal with real closures \u2014 not the raw bridge:\n * `index.html`'s browser-mode polyfill installs a `new Proxy({}, {get})` that\n * returns a function for EVERY property name, so duck-typing the bridge is\n * vacuously true and validates nothing. A Proxy cannot forge a missing key in\n * an object literal.\n *\n * @returns {{ ok: boolean, missing: string[] }}\n */\nexport function validateCapability(name, cap) {\n const spec = HOST_CONTRACT[name];\n if (!spec) return { ok: false, missing: [`unknown capability '${name}'`] };\n if (!cap || typeof cap !== 'object') return { ok: false, missing: [...spec.required] };\n const missing = spec.required.filter((m) => typeof cap[m] !== 'function');\n return { ok: missing.length === 0, missing };\n}\n\n/**\n * Validate + freeze a set of capabilities into a Host.\n * Throws at boot on an incomplete capability \u2014 a half-wired host is a bug,\n * and a bug at boot beats a silent degrade at click time.\n *\n * @param {{ window?: object, dialogs?: object, state?: object, shell?: object }} caps\n * @returns {Readonly<object>} the Host\n */\nexport function createHost(caps = {}) {\n const host = {};\n for (const name of Object.keys(HOST_CONTRACT)) {\n const cap = caps[name];\n if (cap == null) continue; // legitimately absent\n const v = validateCapability(name, cap);\n if (!v.ok) {\n throw new Error(`Host capability '${name}' is incomplete: missing ${v.missing.join(', ')}`);\n }\n host[name] = Object.freeze(cap);\n }\n return Object.freeze(host);\n}\n\n/** The zero-capability host. Every framework consumer MUST survive it. */\nexport const NULL_HOST = createHost({});\n", "/**\n * pywebview_host.js \u2014 the REFERENCE host adapter.\n *\n * Knows pywebview. Knows NOTHING about EcoAgent: every domain fact (which\n * bridge ref to use, where a state key lands on disk) arrives as a parameter.\n * This is the adapter a standalone consumer copies; EcoAgent is just its first\n * caller (see ui/js/bootstrap/app_bootstrap.js).\n *\n * It speaks to whatever `window.pywebview.api` currently is \u2014 the real\n * pywebview bridge in the desktop app, or the `POST /api/<method>` Proxy\n * polyfill that ui/index.html installs in --browser mode. Both transports are\n * positional-on-the-wire, which is why setBounds() spreads its object back out\n * into positional args.\n */\n\nimport { createHost } from './host.js';\n\nconst FRAMELESS_BACKENDS = new Set(['edgechromium', 'mshtml']);\n\n/**\n * Custom (frameless) window chrome \u2014 the min/max/close buttons, edge resize\n * handles and drag-to-move top bar \u2014 only makes sense when our top bar IS the\n * window's title bar. That holds on a native pywebview window the OS draws\n * *without* decorations of its own: Windows (frameless). On Linux/macOS the\n * window manager already draws a native title bar above our top bar, and in\n * `--browser` mode the browser supplies its own chrome \u2014 in both cases the\n * custom controls are a redundant second set, so we neither render nor wire\n * them (which also stops us swallowing native F11 fullscreen and showing dead\n * resize cursors at the window edges).\n *\n * Detection keys off the pywebview backend string, which is unambiguous: the\n * browser-mode polyfill in index.html injects only `.api`, never `.platform`,\n * so any `.platform` at all \u21D2 running as a native pywebview app. The frameless\n * (custom-title-bar) backends are the Windows WebViews \u2014 'edgechromium' /\n * 'mshtml'. Linux ('gtkwebkit2' / 'qtwebengine') and macOS ('cocoa') draw\n * native decorations, so they return false. Verified live: WebKitGTK reports\n * platform 'gtkwebkit2'.\n */\nexport function usesCustomWindowChrome() {\n // Authoritative signal from the Python host: run_pywebview_mode() appends\n // `?frameless=1` to the window URL exactly when it created a frameless native\n // window (Windows). Prefer it over the backend sniff below because it's\n // present at document parse \u2014 before pywebview finishes injecting\n // `window.pywebview` (and before the browser-mode polyfill, which sets `.api`\n // but no `.platform`, can clobber it). Reading the sniff at shell-build time\n // otherwise races and the whole custom chrome silently fails to render.\n try {\n if (new URLSearchParams(window.location.search).get('frameless') === '1') {\n return true;\n }\n } catch (_) { /* location unavailable (non-browser env) \u2014 fall through */ }\n const platform = window.pywebview && window.pywebview.platform;\n return !!platform && FRAMELESS_BACKENDS.has(platform);\n}\n\n/**\n * Build a Host backed by the pywebview bridge.\n *\n * @param {object} opts\n * @param {{current: object|null}} [opts.bridgeRef] LIVE ref to the bridge.\n * MUST be a ref, never a snapshot: `setupHostBridgeListener` mutates\n * `.current` when a late bridge arrives, and `resolveHostBridge` can\n * resolve `null` after its timeout. Snapshotting the bridge here would\n * freeze that null and reproduce as an intermittent boot failure.\n * @param {(key: string) => string} [opts.resolvePath] logical state key ->\n * storage path. THE domain seam: the framework only ever names keys\n * ('desktops', 'datatable_state'); the embedder decides where they land.\n * @param {object} [opts.logger]\n */\nexport function createPywebviewHost({ bridgeRef = null, resolvePath = (k) => `${k}.json`, logger = console } = {}) {\n const api = () => bridgeRef?.current ?? window.pywebview?.api ?? null;\n\n const call = async (name, args = []) => {\n const a = api();\n if (!a || typeof a[name] !== 'function') return null;\n try { return await a[name](...args); }\n catch (e) { logger.warn?.('[host] call failed', name, e); return null; }\n };\n\n // Envelope unwrapping, in ONE place. Transcribed from the call sites this\n // replaces, so app.py's catch-all `{ok: true}` reply (which carries no\n // `maximized` key) still reads as false, exactly as it does today.\n const okMax = (r) => Boolean(r && r.ok && r.maximized === true);\n const okFs = (r) => Boolean(r && r.ok && r.fullscreen);\n\n // ALL THREE capabilities are ALWAYS advertised. Do NOT gate them on api()\n // being non-null at construction time: the methods late-bind through the\n // ref, and gating here would permanently strand a late-arriving bridge on\n // the fallback paths (Blob download instead of the native save dialog).\n return createHost({\n window: {\n chrome: () => (usesCustomWindowChrome() ? 'custom' : 'native'),\n minimize: () => { void call('window_minimize'); },\n close: () => { void call('window_close'); },\n isMaximized: async () => okMax(await call('window_is_maximized')),\n setMaximized: async (on) => okMax(await call('window_set_maximized', [on])),\n isFullscreen: async () => okFs(await call('window_is_fullscreen')),\n toggleFullscreen: async () => okFs(await call('window_toggle_fullscreen')),\n async getBounds() {\n const r = await call('window_get_bounds');\n if (!r || !r.ok) return null;\n return { x: r.x, y: r.y, width: r.width, height: r.height };\n },\n // POSITIONAL on the wire \u2014 both transports depend on it. Not async:\n // this is a rAF hot path during a window drag/resize; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n void call('window_set_bounds', [x, y, width, height]);\n },\n startNativeDrag: async () => Boolean((await call('window_start_native_drag'))?.ok),\n },\n dialogs: {\n // Resolves to null when the transport has no save_file_dialog behind it.\n // Per host.js that is the contract for \"unavailable\", and every caller\n // treats it exactly like an absent `dialogs` capability: fall back to a\n // Blob download, never report a failed save. Covered by\n // tests/ui/test_host_save_fallback.mjs.\n saveFile: ({ data, filename, kind }) => call('save_file_dialog', [data, filename, kind]),\n },\n state: {\n async read(key) {\n const blob = await call('workspace_state_read', [{ path: resolvePath(key) }]);\n if (!blob) return null;\n try { return typeof blob === 'string' ? JSON.parse(blob) : blob; }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n const r = await call('workspace_state_write',\n [{ path: resolvePath(key), data: JSON.stringify(value) }]);\n return r !== null;\n },\n },\n });\n}\n", "/**\n * photino_host.js \u2014 the host port over a Photino.NET window.\n *\n * Photino gives the page exactly two things: `window.external.sendMessage(text)`\n * and `window.external.receiveMessage(callback)`. Strings in, strings out, no\n * replies. This adapter puts a request/reply protocol over them, matched by id:\n *\n * page \u2192 host {\"id\": 7, \"cap\": \"window\", \"op\": \"setBounds\", \"args\": [{\"x\":0,\"y\":0,\"width\":1200,\"height\":800}]}\n * host \u2192 page {\"id\": 7, \"ok\": true, \"value\": null}\n * host \u2192 page {\"id\": 8, \"ok\": false, \"error\": \"no such op\"}\n * host \u2192 page {\"event\": \"window.maximizedChanged\", \"value\": true} // unsolicited, no id\n *\n * and offers the port's `window`, `dialogs` and `state` capabilities on top.\n * The C# side \u2014 one dispatcher on the window's web-message handler, every op\n * with its args and reply \u2014 is documented in `photino_host.md` beside this file.\n *\n * Like pywebview_host.js it knows the transport and NOTHING about the app: an\n * embedder that keeps some capability elsewhere (BugDesk keeps `state` on its\n * own server) passes that capability in, or `null` to leave it out.\n *\n * In a browser tab there is no `window.external.sendMessage`, and\n * createPhotinoHost() returns NULL_HOST \u2014 the fallback every FlexDesk consumer\n * already has to survive.\n */\n\nimport { createHost, NULL_HOST } from './host.js';\n\n/** True when `ext` (default: `window.external`) is a Photino bridge. */\nexport function isPhotinoWindow(ext = (typeof window !== 'undefined' ? window.external : undefined)) {\n return !!ext && typeof ext.sendMessage === 'function' && typeof ext.receiveMessage === 'function';\n}\n\n/**\n * The bare request/reply layer. `call(cap, op, ...args)` sends one request and\n * resolves with the reply's `value` (`request(cap, op, args, {wait})` is the\n * same with a timeout of its own). It REJECTS when the host answers\n * `ok: false` (with the host's error) and when no reply arrives within\n * `timeoutMs`. A reply for an id nobody is waiting on, a message that is not\n * JSON, and anything that is neither a reply nor an event are dropped.\n *\n * @param {object} opts\n * @param {{sendMessage: Function, receiveMessage: Function}} opts.external\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent] id-less `{event, value}` messages\n * @param {object} [opts.logger]\n */\nexport function createPhotinoTransport({ external, timeoutMs = 10000, onEvent = null, logger = console }) {\n const pending = new Map();\n let seq = 0;\n external.receiveMessage((text) => {\n let m;\n try { m = typeof text === 'string' ? JSON.parse(text) : text; } catch { return; }\n if (!m || typeof m !== 'object') return;\n if (m.id == null) {\n if (typeof m.event === 'string' && onEvent) {\n try { onEvent(m.event, m.value); }\n catch (e) { logger.warn?.('[host] photino event handler failed', m.event, e); }\n }\n return;\n }\n const p = pending.get(m.id);\n if (!p) return;\n pending.delete(m.id);\n clearTimeout(p.timer);\n if (m.ok) p.resolve(m.value === undefined ? null : m.value);\n else p.reject(new Error(m.error || `${p.what}: refused by the host`));\n });\n // `wait` is this request's timeout in ms; 0 waits for as long as it takes.\n const request = (cap, op, args = [], { wait = timeoutMs } = {}) => new Promise((resolve, reject) => {\n const id = ++seq;\n const what = `${cap}.${op}`;\n const timer = wait > 0 ? setTimeout(() => {\n if (pending.delete(id)) reject(new Error(`${what}: no answer from the host within ${wait} ms`));\n }, wait) : null;\n pending.set(id, { resolve, reject, timer, what });\n try {\n external.sendMessage(JSON.stringify({ id, cap, op, args }));\n } catch (e) {\n pending.delete(id);\n clearTimeout(timer);\n reject(e);\n }\n });\n const call = (cap, op, ...args) => request(cap, op, args);\n return { call, request, pending: () => pending.size };\n}\n\n/**\n * Build a Host backed by a Photino window.\n *\n * The capability methods follow pywebview_host.js's contract, so a consumer\n * cannot tell the two apart: they DEGRADE instead of rejecting. A failed query\n * reads as false / null, a failed fire-and-forget is logged, and a save that\n * the host refused or never answered resolves `null` \u2014 the host.js meaning of\n * \"unavailable\", which every caller turns into a Blob download.\n *\n * @param {object} [opts]\n * @param {object} [opts.external=window.external] the Photino bridge (a fake one in tests)\n * @param {number} [opts.timeoutMs=10000]\n * @param {(name: string, value: any) => void} [opts.onEvent]\n * @param {object} [opts.logger=console]\n * @param {'auto'|'native'|'custom'} [opts.chrome='auto'] who draws the window's\n * title bar and borders. `auto` asks the native side once (`host.describe`,\n * its `frame` field), the first time somebody needs to know; `native`\n * or `custom` fixes the answer without asking. See photino_host.md.\n * @param {object|null} [opts.window] replace the window capability, or `null` to leave it out\n * @param {object|null} [opts.dialogs] replace the dialogs capability, or `null` to leave it out\n * @param {object|null} [opts.state] replace the state capability, or `null` to leave it out\n * @param {object|null} [opts.shell] replace the shell capability, or `null` to leave it out\n * @returns {Readonly<object>} a Host, or NULL_HOST outside a Photino window\n */\nexport function createPhotinoHost(opts = {}) {\n const {\n external = (typeof window !== 'undefined' ? window.external : undefined),\n timeoutMs = 10000,\n onEvent = null,\n logger = console,\n chrome = 'auto',\n } = opts;\n if (!isPhotinoWindow(external)) return NULL_HOST;\n\n const { call, request } = createPhotinoTransport({ external, timeoutMs, onEvent, logger });\n const warn = (cap, op, e) => logger.warn?.('[host] photino call failed', `${cap}.${op}`, e);\n const soft = async (fallback, cap, op, ...args) => {\n try { return await call(cap, op, ...args); }\n catch (e) { warn(cap, op, e); return fallback; }\n };\n const fire = (cap, op, ...args) => { void soft(null, cap, op, ...args); };\n\n // Who draws the frame. Asked once and remembered: a string once known, the\n // pending question before that. A native side that predates `frame`, or\n // doesn't answer within 3 s, has native chrome.\n let frame = chrome === 'custom' || chrome === 'native' ? chrome : null;\n let asking = null;\n const askFrame = () => {\n if (frame) return frame;\n asking ??= request('host', 'describe', [], { wait: 3000 })\n .then((d) => (d && d.frame === 'custom' ? 'custom' : 'native'), () => 'native')\n .then((f) => { frame = f; return f; });\n return asking;\n };\n // A drag or resize handed to the OS: the reply comes when the mouse is\n // released, however long that takes. Refused (a native side without the op,\n // or a window with native chrome) means \"not handled\": the caller does it in JS.\n const gesture = async (op, ...args) => {\n try { await request('window', op, args, { wait: 0 }); return true; }\n catch (e) { return false; }\n };\n\n const defaults = {\n window: {\n // 'native' (the OS draws the frame: FlexDesk's own buttons would be\n // a second set) or 'custom' (the native side made the window\n // frameless). A promise until the native side has said which.\n chrome: () => askFrame(),\n minimize: () => { fire('window', 'minimize'); },\n close: () => { fire('window', 'close'); },\n isMaximized: async () => (await soft(false, 'window', 'isMaximized')) === true,\n setMaximized: async (on) => (await soft(false, 'window', 'setMaximized', !!on)) === true,\n isFullscreen: async () => (await soft(false, 'window', 'isFullscreen')) === true,\n toggleFullscreen: async () => (await soft(false, 'window', 'toggleFullscreen')) === true,\n async getBounds() {\n const b = await soft(null, 'window', 'getBounds');\n if (!b || typeof b !== 'object') return null;\n return { x: b.x, y: b.y, width: b.width, height: b.height };\n },\n startNativeDrag: () => gesture('startDrag'),\n startNativeResize: (edge) => gesture('startResize', String(edge || '')),\n // Not async, like pywebview's: a rAF hot path during a drag; nobody awaits.\n setBounds({ x = null, y = null, width = null, height = null } = {}) {\n fire('window', 'setBounds', { x, y, width, height });\n },\n },\n dialogs: {\n // `{ok: true, path}` | `{ok: false, cancelled: true}` | `{ok: false, error}`,\n // or null when the host refused: fall back to a download. NO TIMEOUT \u2014\n // the dialog is open for as long as the user takes, and giving up on it\n // would download the file a second time behind the dialog.\n async saveFile({ data, filename, kind } = {}) {\n let r = null;\n try { r = await request('dialogs', 'saveFile', [{ data, filename, kind }], { wait: 0 }); }\n catch (e) { warn('dialogs', 'saveFile', e); }\n return r && typeof r === 'object' ? r : null;\n },\n },\n shell: {\n // True when the native side opened it in the system browser; false\n // when it refused (a scheme it won't open, an older native side\n // without the op): the caller falls back to window.open.\n openExternal: async (url) => (await soft(false, 'shell', 'openExternal', String(url || ''))) === true,\n },\n state: {\n async read(key) {\n const v = await soft(null, 'state', 'read', key);\n if (typeof v !== 'string') return v ?? null;\n try { return JSON.parse(v); }\n catch (e) { logger.warn?.('[host] bad JSON for state key', key, e); return null; }\n },\n async write(key, value) {\n return (await soft(false, 'state', 'write', key, value)) !== false;\n },\n },\n };\n\n const caps = {};\n for (const name of Object.keys(defaults)) {\n if (!(name in opts)) caps[name] = defaults[name];\n else if (opts[name] != null) caps[name] = opts[name];\n }\n return createHost(caps);\n}\n", "/**\n * external_links.js \u2014 links that leave the app open in the system browser.\n *\n * In a browser tab, `target=\"_blank\"` and `window.open()` open a new tab, which\n * is what everyone expects. In a native window (Photino, a webview) they open\n * either nothing or a bare popup with no address bar and no way back. The host\n * knows how to hand a URL to the OS (`host.shell.openExternal`); this routes\n * the page's outbound links there.\n *\n * Without a `shell` capability it changes nothing, so an app can call it\n * unconditionally at boot: in a browser tab it is a no-op.\n */\n\nconst SCHEMES = new Set(['http:', 'https:', 'mailto:']);\n\n/** The URL as an absolute URL when it is one we would send out, else null. */\nfunction outbound(href, base) {\n let url;\n try { url = new URL(String(href), base); } catch { return null; }\n return SCHEMES.has(url.protocol) ? url : null;\n}\n\n/**\n * Send external link clicks and `window.open` calls to `host.shell.openExternal`.\n *\n * A link goes out when it is http(s) or mailto AND it has `target=\"_blank\"`,\n * points to another origin, or is mailto. A same-origin link without a target\n * is the app's own navigation and is left alone; a same-origin `_blank` goes\n * out too, because in a native window the alternative is a bare popup. A\n * handler that already called `preventDefault()` on the click keeps it.\n *\n * When `openExternal` resolves false (the native side refused it), the original\n * `window.open` runs instead, so the click still does something.\n *\n * @param {object} opts\n * @param {object} opts.host a Host; does nothing without `host.shell`\n * @param {EventTarget} [opts.root=document] where clicks are listened for\n * @param {Window} [opts.win=window] whose `open` is wrapped\n * @returns {{ dispose: () => void }}\n */\nexport function routeExternalLinks({ host, root, win } = {}) {\n const w = win ?? (typeof window !== 'undefined' ? window : null);\n const r = root ?? (typeof document !== 'undefined' ? document : null);\n const shell = host?.shell;\n if (!shell || typeof shell.openExternal !== 'function' || !w || !r) return { dispose() {} };\n\n const base = () => w.location?.href;\n const origin = () => { try { return new URL(base()).origin; } catch { return null; } };\n const nativeOpen = w.open;\n const fallback = (href) => { try { nativeOpen?.call(w, href, '_blank', 'noopener'); } catch { /* blocked */ } };\n const send = (href) => {\n Promise.resolve()\n .then(() => shell.openExternal(href))\n .then((opened) => { if (opened !== true) fallback(href); }, () => fallback(href));\n };\n\n const onClick = (e) => {\n if (e.defaultPrevented) return;\n // Left click, or a middle click (auxclick): both open a new window.\n if (e.type === 'click' ? e.button !== 0 : e.button !== 1) return;\n const a = e.target?.closest?.('a[href]');\n if (!a || a.hasAttribute('download')) return;\n const url = outbound(a.getAttribute('href'), base());\n if (!url) return;\n const leaves = url.protocol === 'mailto:'\n || (a.getAttribute('target') || '').toLowerCase() === '_blank'\n || url.origin !== origin()\n || e.button === 1;\n if (!leaves) return;\n e.preventDefault();\n send(url.href);\n };\n\n const routedOpen = function open(href, target, features) {\n const url = href == null ? null : outbound(href, base());\n if (url && (url.protocol === 'mailto:' || url.origin !== origin())) {\n send(url.href);\n return null;\n }\n return nativeOpen.call(w, href, target, features);\n };\n\n r.addEventListener('click', onClick);\n r.addEventListener('auxclick', onClick);\n w.open = routedOpen;\n\n return {\n dispose() {\n r.removeEventListener('click', onClick);\n r.removeEventListener('auxclick', onClick);\n if (w.open === routedOpen) w.open = nativeOpen;\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/reset.css
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|