web-mojo 2.4.4 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/admin-models.cjs.js +1 -1
- package/dist/admin-models.cjs.js.map +1 -1
- package/dist/admin-models.es.js +1 -1
- package/dist/admin-models.es.js.map +1 -1
- package/dist/admin.cjs.js +1 -1
- package/dist/admin.css +258 -6
- package/dist/admin.es.js +1 -1
- package/dist/auth.cjs.js +1 -1
- package/dist/auth.es.js +1 -1
- package/dist/charts.cjs.js +1 -1
- package/dist/charts.es.js +1 -1
- package/dist/chat.css +5 -0
- package/dist/chunks/AssistantPanelView-CkQEcaFk.js +2 -0
- package/dist/chunks/AssistantPanelView-CkQEcaFk.js.map +1 -0
- package/dist/chunks/AssistantPanelView-D1wEbgtM.js +2 -0
- package/dist/chunks/AssistantPanelView-D1wEbgtM.js.map +1 -0
- package/dist/chunks/ChatView-D2WOSxPu.js +2 -0
- package/dist/chunks/ChatView-D2WOSxPu.js.map +1 -0
- package/dist/chunks/ChatView-kWguc444.js +2 -0
- package/dist/chunks/ChatView-kWguc444.js.map +1 -0
- package/dist/chunks/{Collection-7F3lsq4z.js → Collection-C0pHSKDH.js} +2 -2
- package/dist/chunks/{Collection-7F3lsq4z.js.map → Collection-C0pHSKDH.js.map} +1 -1
- package/dist/chunks/{Collection-Cgxbmj8G.js → Collection-DNmr743A.js} +2 -2
- package/dist/chunks/{Collection-Cgxbmj8G.js.map → Collection-DNmr743A.js.map} +1 -1
- package/dist/chunks/{ContextMenu-afYD0lFk.js → ContextMenu-BeveGkJr.js} +2 -2
- package/dist/chunks/{ContextMenu-afYD0lFk.js.map → ContextMenu-BeveGkJr.js.map} +1 -1
- package/dist/chunks/{ContextMenu-Ba4fjHxg.js → ContextMenu-T3yDdsIe.js} +2 -2
- package/dist/chunks/{ContextMenu-Ba4fjHxg.js.map → ContextMenu-T3yDdsIe.js.map} +1 -1
- package/dist/chunks/{DataView-DFGIE3wK.js → DataView-VZIXSsZa.js} +2 -2
- package/dist/chunks/{DataView-DFGIE3wK.js.map → DataView-VZIXSsZa.js.map} +1 -1
- package/dist/chunks/{DataView-fA6qQbvN.js → DataView-z2rxXk4L.js} +2 -2
- package/dist/chunks/{DataView-fA6qQbvN.js.map → DataView-z2rxXk4L.js.map} +1 -1
- package/dist/chunks/{FormView-CgnaTPkQ.js → FormView-COIPtbrd.js} +2 -2
- package/dist/chunks/{FormView-CgnaTPkQ.js.map → FormView-COIPtbrd.js.map} +1 -1
- package/dist/chunks/{FormView-LRb8scDI.js → FormView-DUXQruUZ.js} +2 -2
- package/dist/chunks/{FormView-LRb8scDI.js.map → FormView-DUXQruUZ.js.map} +1 -1
- package/dist/chunks/{ListView-Dsezts8J.js → ListView-BjsNHuZ1.js} +2 -2
- package/dist/chunks/{ListView-Dsezts8J.js.map → ListView-BjsNHuZ1.js.map} +1 -1
- package/dist/chunks/{ListView-Cu6iQ0aG.js → ListView-BxcxIwC3.js} +2 -2
- package/dist/chunks/{ListView-Cu6iQ0aG.js.map → ListView-BxcxIwC3.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-BsJoEsUE.js → MetricsCountryMapView-Bp3qoVHp.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-BsJoEsUE.js.map → MetricsCountryMapView-Bp3qoVHp.js.map} +1 -1
- package/dist/chunks/{MetricsCountryMapView-BkLDonK4.js → MetricsCountryMapView-CWjIEBJB.js} +2 -2
- package/dist/chunks/{MetricsCountryMapView-BkLDonK4.js.map → MetricsCountryMapView-CWjIEBJB.js.map} +1 -1
- package/dist/chunks/Modal-Bm1OQ8Ou.js +3 -0
- package/dist/chunks/{Modal-BRKy85bz.js.map → Modal-Bm1OQ8Ou.js.map} +1 -1
- package/dist/chunks/Modal-KnJhNZ1E.js +3 -0
- package/dist/chunks/{Modal-DKjxtaZI.js.map → Modal-KnJhNZ1E.js.map} +1 -1
- package/dist/chunks/Passkeys-B1-Z4-16.js +2 -0
- package/dist/chunks/Passkeys-B1-Z4-16.js.map +1 -0
- package/dist/chunks/Passkeys-BlHx11-5.js +2 -0
- package/dist/chunks/Passkeys-BlHx11-5.js.map +1 -0
- package/dist/chunks/TicketPanelView-DVePzWyJ.js +2 -0
- package/dist/chunks/TicketPanelView-DVePzWyJ.js.map +1 -0
- package/dist/chunks/TicketPanelView-TYh5iZiR.js +2 -0
- package/dist/chunks/TicketPanelView-TYh5iZiR.js.map +1 -0
- package/dist/chunks/{TokenManager-CWRL33UM.js → TokenManager-6atX9uKB.js} +2 -2
- package/dist/chunks/TokenManager-6atX9uKB.js.map +1 -0
- package/dist/chunks/{TokenManager-CVR3ENIS.js → TokenManager-CiNtJclo.js} +2 -2
- package/dist/chunks/TokenManager-CiNtJclo.js.map +1 -0
- package/dist/chunks/User-9qvKV7G6.js +2 -0
- package/dist/chunks/User-9qvKV7G6.js.map +1 -0
- package/dist/chunks/{User-BmS8zImI.js → User-BM76Ughk.js} +2 -2
- package/dist/chunks/User-BM76Ughk.js.map +1 -0
- package/dist/chunks/{UserProfileView-CrmACQjJ.js → UserProfileView-DDflzpTa.js} +2 -2
- package/dist/chunks/{UserProfileView-CrmACQjJ.js.map → UserProfileView-DDflzpTa.js.map} +1 -1
- package/dist/chunks/{UserProfileView-B56WeGL1.js → UserProfileView-cUF8ED9n.js} +2 -2
- package/dist/chunks/{UserProfileView-B56WeGL1.js.map → UserProfileView-cUF8ED9n.js.map} +1 -1
- package/dist/chunks/{View-gAghvPMN.js → View-BxlKR1IW.js} +2 -2
- package/dist/chunks/{View-gAghvPMN.js.map → View-BxlKR1IW.js.map} +1 -1
- package/dist/chunks/{View-IgBQlwd0.js → View-CPWwS19u.js} +2 -2
- package/dist/chunks/{View-IgBQlwd0.js.map → View-CPWwS19u.js.map} +1 -1
- package/dist/chunks/{WebApp-B6wrmIaj.js → WebApp-BdxhRnnT.js} +2 -2
- package/dist/chunks/WebApp-BdxhRnnT.js.map +1 -0
- package/dist/chunks/WebApp-Bo_egO0c.js +2 -0
- package/dist/chunks/WebApp-Bo_egO0c.js.map +1 -0
- package/dist/chunks/admin-CHPo4iDn.js +2 -0
- package/dist/chunks/admin-CHPo4iDn.js.map +1 -0
- package/dist/chunks/admin-CucHFXfD.js +2 -0
- package/dist/chunks/admin-CucHFXfD.js.map +1 -0
- package/dist/chunks/admin-models-CkHjtMHf.js +2 -0
- package/dist/chunks/admin-models-CkHjtMHf.js.map +1 -0
- package/dist/chunks/admin-models-DLtFboxy.js +2 -0
- package/dist/chunks/admin-models-DLtFboxy.js.map +1 -0
- package/dist/chunks/{exportChart-CmfLeCi8.js → exportChart-BTrEOM9j.js} +2 -2
- package/dist/chunks/{exportChart-CmfLeCi8.js.map → exportChart-BTrEOM9j.js.map} +1 -1
- package/dist/chunks/{exportChart-Ue-zU0_c.js → exportChart-BfzZUb1j.js} +2 -2
- package/dist/chunks/{exportChart-Ue-zU0_c.js.map → exportChart-BfzZUb1j.js.map} +1 -1
- package/dist/chunks/{index-DTXotoXw.js → index-Cxffar1o.js} +2 -2
- package/dist/chunks/{index-DTXotoXw.js.map → index-Cxffar1o.js.map} +1 -1
- package/dist/chunks/{index-NXnA6T-5.js → index-DsID1QpB.js} +2 -2
- package/dist/chunks/{index-NXnA6T-5.js.map → index-DsID1QpB.js.map} +1 -1
- package/dist/chunks/{version-CpR8sTVV.js → version-BYy2UAT0.js} +2 -2
- package/dist/chunks/{version-CpR8sTVV.js.map → version-BYy2UAT0.js.map} +1 -1
- package/dist/chunks/{version-CsLH6aiB.js → version-BxLEknar.js} +2 -2
- package/dist/chunks/{version-CsLH6aiB.js.map → version-BxLEknar.js.map} +1 -1
- package/dist/css/web-mojo.css +1 -1
- package/dist/docit.cjs.js +1 -1
- package/dist/docit.es.js +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.es.js.map +1 -1
- package/dist/lightbox.cjs.js +1 -1
- package/dist/lightbox.es.js +1 -1
- package/dist/map.cjs.js +1 -1
- package/dist/map.es.js +1 -1
- package/dist/timeline.cjs.js +1 -1
- package/dist/timeline.es.js +1 -1
- package/dist/user-profile.cjs.js +1 -1
- package/dist/user-profile.es.js +1 -1
- package/dist/web-mojo.lite.iife.js +33 -4
- package/dist/web-mojo.lite.iife.js.map +1 -1
- package/dist/web-mojo.lite.iife.min.js +45 -45
- package/dist/web-mojo.lite.iife.min.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/AssistantPanelView-B3orPoag.js +0 -2
- package/dist/chunks/AssistantPanelView-B3orPoag.js.map +0 -1
- package/dist/chunks/AssistantPanelView-GeIHDvyN.js +0 -2
- package/dist/chunks/AssistantPanelView-GeIHDvyN.js.map +0 -1
- package/dist/chunks/ChatView-B0xHv7Nq.js +0 -2
- package/dist/chunks/ChatView-B0xHv7Nq.js.map +0 -1
- package/dist/chunks/ChatView-CUmRTKna.js +0 -2
- package/dist/chunks/ChatView-CUmRTKna.js.map +0 -1
- package/dist/chunks/Modal-BRKy85bz.js +0 -3
- package/dist/chunks/Modal-DKjxtaZI.js +0 -3
- package/dist/chunks/Passkeys-CMh9iSax.js +0 -2
- package/dist/chunks/Passkeys-CMh9iSax.js.map +0 -1
- package/dist/chunks/Passkeys-DF7mRGYj.js +0 -2
- package/dist/chunks/Passkeys-DF7mRGYj.js.map +0 -1
- package/dist/chunks/TokenManager-CVR3ENIS.js.map +0 -1
- package/dist/chunks/TokenManager-CWRL33UM.js.map +0 -1
- package/dist/chunks/User-BmS8zImI.js.map +0 -1
- package/dist/chunks/User-CayBjzMG.js +0 -2
- package/dist/chunks/User-CayBjzMG.js.map +0 -1
- package/dist/chunks/WebApp-B6wrmIaj.js.map +0 -1
- package/dist/chunks/WebApp-DeHPnmbD.js +0 -2
- package/dist/chunks/WebApp-DeHPnmbD.js.map +0 -1
- package/dist/chunks/admin-FvzwR-f7.js +0 -2
- package/dist/chunks/admin-FvzwR-f7.js.map +0 -1
- package/dist/chunks/admin-yjCkhxvU.js +0 -2
- package/dist/chunks/admin-yjCkhxvU.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatView-D2WOSxPu.js","sources":["../../src/core/models/Metrics.js","../../src/core/models/System.js","../../src/core/pages/TablePage.js","../../src/core/views/navigation/TabView.js","../../src/core/views/navigation/SideNavView.js","../../src/core/views/data/FilePreviewView.js","../../src/core/models/ShortLink.js","../../src/core/views/data/FileView.js","../../src/core/views/chat/ChatMessageView.js","../../src/core/views/chat/ChatInputView.js","../../src/core/views/chat/ChatView.js"],"sourcesContent":["import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\nclass MetricsPermission extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/metrics/permissions',\n id_key: 'account'\n });\n }\n}\n\nclass MetricsPermissionList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: MetricsPermission,\n endpoint: '/api/metrics/permissions',\n ...options,\n });\n }\n\n}\n\nconst MetricsForms = {\n edit: {\n title: 'Edit Metrics Permissions',\n fields: [\n { name: 'account', type: 'text', label: 'Account', columns:12 },\n { name: 'view_permissions', type: 'tags', label: 'View Permissions', help: 'Enter permissions or \"public\"', columns:12 },\n { name: 'write_permissions', type: 'tags', label: 'Write Permissions', help: 'Enter permissions', columns:12 },\n ]\n }\n};\n\nexport { MetricsPermission, MetricsPermissionList, MetricsForms };\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/* =========================\n * GeoLocatedIP\n * ========================= */\n\nconst GeoIPForms = {\n editLocation: {\n title: 'Edit Location',\n size: 'lg',\n fields: [\n { name: 'ip_address', label: 'IP Address', type: 'text', required: true, readonly: true, cols: 6 },\n { name: 'subnet', label: 'Subnet', type: 'text', cols: 6 },\n { name: 'country_name', label: 'Country', type: 'text', cols: 6 },\n { name: 'country_code', label: 'Country Code', type: 'text', cols: 6 },\n { name: 'region', label: 'Region', type: 'text', cols: 6 },\n { name: 'city', label: 'City', type: 'text', cols: 6 },\n { name: 'postal_code', label: 'Postal Code', type: 'text', cols: 6 },\n { name: 'timezone', label: 'Timezone', type: 'text', cols: 6 },\n { name: 'latitude', label: 'Latitude', type: 'number', step: 'any', cols: 6 },\n { name: 'longitude', label: 'Longitude', type: 'number', step: 'any', cols: 6 },\n ]\n },\n editSecurity: {\n title: 'Edit Security',\n size: 'md',\n fields: [\n { \n name: 'threat_level', \n label: 'Threat Level', \n type: 'select', \n cols: 12,\n options: [\n { value: '', label: 'None' },\n { value: 'low', label: 'Low' },\n { value: 'medium', label: 'Medium' },\n { value: 'high', label: 'High' },\n { value: 'critical', label: 'Critical' }\n ]\n },\n { name: 'is_threat', label: 'Threat', type: 'switch', cols: 6 },\n { name: 'is_suspicious', label: 'Suspicious', type: 'switch', cols: 6 },\n { name: 'is_known_attacker', label: 'Known Attacker', type: 'switch', cols: 6 },\n { name: 'is_known_abuser', label: 'Known Abuser', type: 'switch', cols: 6 },\n { name: 'risk_score', label: 'Risk Score', type: 'number', cols: 6 },\n { name: 'is_tor', label: 'TOR Exit Node', type: 'switch', cols: 6 },\n { name: 'is_vpn', label: 'VPN', type: 'switch', cols: 6 },\n { name: 'is_proxy', label: 'Proxy', type: 'switch', cols: 6 },\n { name: 'is_cloud', label: 'Cloud Provider', type: 'switch', cols: 6 },\n { name: 'is_datacenter', label: 'Datacenter', type: 'switch', cols: 6 }\n ]\n },\n editNetwork: {\n title: 'Edit Network',\n size: 'md',\n fields: [\n { name: 'asn', label: 'ASN', type: 'text', cols: 6 },\n { name: 'asn_org', label: 'ASN Organization', type: 'text', cols: 6 },\n { name: 'isp', label: 'ISP', type: 'text', cols: 12 },\n { name: 'connection_type', label: 'Connection Type', type: 'text', cols: 6 },\n { name: 'provider', label: 'Provider', type: 'text', cols: 6 },\n { name: 'is_mobile', label: 'Mobile Connection', type: 'switch', cols: 6 },\n { name: 'mobile_carrier', label: 'Mobile Carrier', type: 'text', cols: 6 },\n { name: 'last_seen', label: 'Last Seen', type: 'datetime', cols: 12 }\n ]\n }\n};\n\nclass GeoLocatedIP extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/system/geoip',\n });\n }\n\n static async lookup(ip) {\n const model = new GeoLocatedIP();\n const resp = await model.rest.GET('/api/system/geoip/lookup', { ip });\n if (resp.success && resp.data && resp.data.data) {\n return new GeoLocatedIP(resp.data.data);\n }\n return null;\n }\n}\n\n// Attach forms to model (use Location as default EDIT_FORM for TableView)\nGeoLocatedIP.EDIT_FORM = GeoIPForms.editLocation;\nGeoLocatedIP.EDIT_LOCATION_FORM = GeoIPForms.editLocation;\nGeoLocatedIP.EDIT_SECURITY_FORM = GeoIPForms.editSecurity;\nGeoLocatedIP.EDIT_NETWORK_FORM = GeoIPForms.editNetwork;\n\nclass GeoLocatedIPList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: GeoLocatedIP,\n endpoint: '/api/system/geoip',\n ...options,\n });\n }\n}\n\nexport { GeoLocatedIP, GeoLocatedIPList };\n","/**\n * TablePage - Page component that manages a TableView with URL parameter synchronization\n *\n * A clean, simplified implementation using the new TableView component.\n * Automatically syncs pagination, sorting, and filtering with URL parameters.\n *\n * @example\n * const usersPage = new TablePage({\n * pageName: 'users',\n * title: 'User Management',\n * collection: userCollection,\n * columns: [\n * { key: 'name', label: 'Name', sortable: true },\n * { key: 'email', label: 'Email' },\n * { key: 'role', label: 'Role', type: 'badge' }\n * ],\n * actions: ['view', 'edit', 'delete']\n * });\n */\n\nimport Page from '@core/Page.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport TableView from '@core/views/table/TableView.js';\nimport Collection from '@core/Collection.js';\nimport { parseFilterKey } from '@core/utils/DjangoLookups.js';\n\nclass TablePage extends Page {\n constructor(options = {}) {\n super({\n ...options,\n pageName: options.pageName || options.name || 'table'\n });\n\n // Page configuration\n this.title = options.title || this.pageName;\n this.description = options.description || '';\n\n // Collection setup\n this.Collection = options.Collection || null;\n this.collection = options.collection || null;\n\n // our default collection query\n this.defaultQuery = options.defaultQuery || {};\n\n // Group field configuration - defaults to \"group\"\n this.groupField = options.groupField || 'group';\n\n // Store configuration for TableView\n // Map legacy property names to new ones\n this.tableViewConfig = {\n // Core table properties\n columns: options.columns || [],\n actions: options.actions || null,\n contextMenu: options.contextMenu || null,\n batchActions: options.batchActions || null,\n batchBarLocation: options.batchBarLocation || 'top',\n clickAction: options.clickAction || 'view',\n // Map legacy form properties to new names\n addForm: options.addForm || options.formFields || options.formCreate,\n editForm: options.editForm || options.formEdit || options.formFields,\n\n // Model operation configurations\n itemView: options.itemView || options.itemViewClass,\n deleteTemplate: options.deleteTemplate,\n formDialogConfig: options.formDialogConfig,\n viewDialogOptions: options.viewDialogOptions,\n\n // Features\n searchable: options.searchable !== false,\n sortable: options.sortable !== false,\n filterable: options.filterable !== false,\n paginated: options.paginated !== false,\n\n // Selection mode\n selectionMode: options.selectionMode || (options.selectable ? 'multiple' : 'none'),\n\n // Filter configuration\n filters: options.filters || options.additionalFilters || [],\n hideActivePills: options.hideActivePills || false,\n hideActivePillNames: options.hideActivePillNames || [],\n searchPlacement: options.searchPlacement || 'toolbar',\n\n // Display options for the HTML table element\n tableOptions: {\n striped: true,\n bordered: false,\n hover: true,\n responsive: false,\n ...options.tableOptions\n },\n\n // Additional options\n emptyMessage: options.emptyMessage || 'No data available',\n searchPlaceholder: options.searchPlaceholder || 'Search...',\n showAdd: options.showAdd !== false,\n showExport: options.showExport !== false,\n\n // Custom handlers\n onItemView: options.onItemView,\n onItemEdit: options.onItemEdit,\n onItemDelete: options.onItemDelete,\n onAdd: options.onAdd,\n onExport: options.onExport,\n\n // Override with tableViewOptions if provided\n ...options.tableViewOptions\n };\n\n // URL synchronization\n this.urlSyncEnabled = options.urlSyncEnabled !== false;\n\n // Status tracking\n this.lastUpdated = null;\n this.isLoading = false;\n\n // Set up template\n this.template = options.template || this.buildTemplate();\n }\n\n /**\n * Build the page template\n */\n buildTemplate() {\n return `\n <div class=\"table-page-container\">\n\n <div class=\"table-container\" data-container=\"table\"></div>\n\n {{#showStatus}}\n <div class=\"table-status-bar table-status-top\">\n <div class=\"status-info\">\n <div class=\"d-flex justify-content-between w-100\">\n <span class=\"text-muted\">\n <i class=\"bi bi-clock me-1\"></i>\n Last updated: <span data-status=\"last-updated\">{{lastUpdated}}</span>\n </span>\n <span class=\"text-muted\">\n <i class=\"bi bi-list-ol me-1\"></i>\n Total records: <span data-status=\"record-count\">0</span>\n </span>\n </div>\n </div>\n </div>\n {{/showStatus}}\n\n </div>\n `;\n }\n\n /**\n * Initialize the page\n */\n async onInit() {\n await super.onInit();\n\n // Create collection if needed\n if (!this.collection) {\n if (this.Collection) {\n this.collection = new this.Collection();\n } else {\n this.collection = new Collection();\n }\n }\n\n // Apply URL query parameters to collection\n this.applyQueryToCollection();\n\n // Create TableView instance with all configuration\n // Intercept item view to add deep-link URL support\n this.tableView = new TableView({\n collection: this.collection,\n containerId: 'table',\n fetchOnMount: true,\n ...this.tableViewConfig,\n onItemView: async (model, event) => {\n // Allow subclass onItemView to fully override\n if (this.tableViewConfig.onItemView) {\n return this.tableViewConfig.onItemView(model, event);\n }\n await this.showItemDialog(model);\n }\n });\n\n // Add as child view\n this.addChild(this.tableView);\n\n // Set up event listeners\n this.setupEventListeners();\n }\n\n /**\n * Set up event listeners\n */\n setupEventListeners() {\n // Listen for collection changes to sync URL\n if (this.urlSyncEnabled && this.collection) {\n // Sync URL when collection params change\n this.collection.on('fetch:start', () => {\n this.isLoading = true;\n });\n\n this.collection.on('fetch:end', () => {\n this.isLoading = false;\n this.lastUpdated = new Date().toLocaleTimeString();\n this.updateStatusDisplay();\n });\n }\n\n // Listen for params-changed event from TableView to sync URL\n this.tableView.on('params-changed', () => {\n if (this.urlSyncEnabled) {\n this.syncUrl();\n }\n });\n\n // // Listen for table events (these also emit params-changed, but keep for backwards compatibility)\n // this.tableView.on('table:search', ({ searchTerm }) => {\n // // params-changed will handle URL sync\n // });\n\n // this.tableView.on('table:sort', ({ field }) => {\n // // params-changed will handle URL sync\n // });\n\n // this.tableView.on('table:page', ({ page }) => {\n // // params-changed will handle URL sync\n // });\n\n // Filter events - params-changed will handle URL sync\n this.tableView.on('filter:edit', async ({ key }) => {\n await this.handleFilterEdit(key);\n });\n\n // Row action events\n this.tableView.on('row:view', async ({ model }) => {\n if (this.onItemView) {\n await this.onItemView(model);\n }\n });\n\n this.tableView.on('row:edit', async ({ model }) => {\n if (this.onItemEdit) {\n await this.onItemEdit(model);\n }\n });\n\n this.tableView.on('row:delete', async ({ model }) => {\n if (this.onItemDelete) {\n await this.onItemDelete(model);\n }\n });\n\n // Table action events\n // Note: TableView will call options.onAdd if provided, but we still listen to the event\n // for backwards compatibility and to support event-based patterns\n this.tableView.on('table:add', async ({ event }) => {\n // The handler was already called by TableView if options.onAdd is set,\n // but we keep this listener for external code that might listen to 'table:add'\n // We don't call this.tableViewConfig.onAdd here to avoid double execution\n });\n\n this.tableView.on('table:export', async ({ data }) => {\n if (this.tableViewConfig.onExport) {\n await this.tableViewConfig.onExport(data);\n }\n });\n }\n\n /**\n * Apply URL query parameters to collection\n */\n applyQueryToCollection() {\n const params = {};\n const query = { ...this.defaultQuery, ...this.query };\n if (!query || Object.keys(query).length === 0) {\n return;\n }\n // Pagination\n if (query.start !== undefined) params.start = parseInt(query.start) || 0;\n if (query.size !== undefined) params.size = parseInt(query.size) || 10;\n\n // Sorting\n if (query.sort !== undefined) params.sort = query.sort;\n\n // Search\n if (query.search !== undefined) params.search = query.search;\n\n // Process all other params as potential filters\n const reservedParams = ['start', 'size', 'sort', 'search', 'page', '_item'];\n Object.entries(query).forEach(([key, value]) => {\n if (!reservedParams.includes(key) && value !== undefined && value !== '') {\n // Parse value if it looks like JSON\n if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {\n try {\n params[key] = JSON.parse(value);\n } catch (e) {\n params[key] = value;\n }\n } else {\n params[key] = value;\n }\n }\n });\n\n // Update collection params\n if (Object.keys(params).length > 0) {\n // Deduplicate simple + __in filters (prefer __in / not_in when both are present)\n Object.keys(params).forEach(key => {\n const { field, lookup } = parseFilterKey(key);\n if ((lookup === 'in' || lookup === 'not_in') && params.hasOwnProperty(field)) {\n delete params[field];\n }\n });\n\n this.collection.setParams({\n ...this.collection.params,\n ...params\n });\n }\n }\n\n /**\n * Sync URL with current table state\n */\n syncUrl(force = true) {\n if (!this.urlSyncEnabled || !this.collection || !this.getApp()?.router) {\n return;\n }\n\n // Get current URL params\n const currentUrl = new URL(window.location);\n const currentParams = {};\n for (const [key, value] of currentUrl.searchParams) {\n if (key !== 'page') {\n currentParams[key] = value;\n }\n }\n\n // Get desired params from collection\n const desiredParams = {};\n const collectionParams = this.collection.params || {};\n\n // Only include non-default values\n if (collectionParams.start) {\n desiredParams.start = collectionParams.start;\n }\n if (collectionParams.size) {\n desiredParams.size = collectionParams.size;\n }\n if (collectionParams.sort) {\n desiredParams.sort = collectionParams.sort;\n }\n if (collectionParams.search) {\n desiredParams.search = collectionParams.search;\n }\n\n // Include other filters\n Object.entries(collectionParams).forEach(([key, value]) => {\n if (!['start', 'size', 'sort', 'search'].includes(key) && value !== undefined && value !== '') {\n // Stringify complex values for URL\n if (typeof value === 'object') {\n desiredParams[key] = JSON.stringify(value);\n } else {\n desiredParams[key] = value;\n }\n }\n });\n\n // Check if there are any changes\n const hasChanges =\n Object.keys(desiredParams).some(key =>\n String(currentParams[key] || '') !== String(desiredParams[key] || '')\n ) ||\n Object.keys(currentParams).some(key =>\n !(key in desiredParams)\n );\n\n // Preserve _item param if currently set (deep-link, not a collection param)\n if (this.query._item) {\n desiredParams._item = this.query._item;\n }\n\n this.query = desiredParams;\n if (!hasChanges && !force) return;\n\n // Update URL\n this.updateBrowserUrl(desiredParams, true, false);\n }\n\n /**\n * Update status display\n */\n updateStatusDisplay() {\n if (!this.element) return;\n\n // Update last updated time\n const updatedElement = this.element.querySelector('[data-status=\"last-updated\"]');\n if (updatedElement) {\n updatedElement.textContent = this.lastUpdated || 'Never';\n }\n\n // Update record count\n const countElement = this.element.querySelector('[data-status=\"record-count\"]');\n if (countElement && this.collection) {\n const count = this.collection.meta?.count || this.collection.length();\n countElement.textContent = count;\n }\n }\n\n /**\n * Called when entering this page\n */\n async onEnter() {\n await super.onEnter();\n\n if (this.options.requiresGroup && !this.query[this.groupField] && this.getApp().activeGroup) {\n this.query[this.groupField] = this.getApp().activeGroup.id;\n }\n\n this.applyQueryToCollection();\n\n // Ensure filter pills are shown if there are active filters from URL\n if (this.tableView && this.tableView.element) {\n setTimeout(() => {\n this.tableView.updateFilterPills();\n this.tableView.updateSortIcons();\n }, 100);\n }\n\n // Deep-link: auto-open item dialog if _item param is present\n if (this.query._item) {\n this._openDeepLinkedItem(this.query._item);\n }\n }\n\n /**\n * Open a deep-linked item dialog by fetching the model and showing it\n */\n async _openDeepLinkedItem(itemId) {\n try {\n const model = await this.collection.fetchOne(itemId);\n if (model) {\n await this.showItemDialog(model);\n }\n } catch (e) {\n // Item not found or fetch failed — silently clear the param\n this._clearItemParam();\n }\n }\n\n /**\n * Show the item view dialog with deep-link URL support\n */\n async showItemDialog(model) {\n // Update URL with _item param\n this._setItemParam(model.id);\n\n // Fetch latest model data before showing the dialog\n if (this.tableView.fetchOnView) {\n try {\n Modal.loading();\n await model.fetch();\n } catch (error) {\n Modal.hideLoading(true);\n Modal.showError(error?.data?.error || error?.message || 'Failed to load item details');\n this._clearItemParam();\n return;\n } finally {\n Modal.hideLoading(true);\n }\n }\n\n const ViewClass = this.tableView.getItemViewClass(model);\n\n if (ViewClass) {\n const viewInstance = new ViewClass({ model, collection: this.collection });\n await Modal.dialog({\n header: false,\n body: viewInstance,\n size: 'lg',\n centered: false,\n ...this.tableView.getFormDialogConfig(this.tableView.getModelClass(model)),\n ...this.tableView.viewDialogOptions\n });\n } else {\n await Modal.data({\n title: `View ${this.tableView.getModelName(model)} #${model.id}`,\n model\n });\n }\n\n // Dialog closed — remove _item from URL\n this._clearItemParam();\n }\n\n /**\n * Add _item param to URL\n */\n _setItemParam(itemId) {\n const params = { ...this.query, _item: itemId };\n this.query = params;\n this.updateBrowserUrl(params, true, false);\n }\n\n /**\n * Remove _item param from URL\n */\n _clearItemParam() {\n delete this.query._item;\n this.updateBrowserUrl(this.query, true, false);\n }\n\n /**\n * Public method to refresh the table\n */\n async refresh() {\n await this.tableView.refresh();\n }\n\n /**\n * Public method to get selected items\n */\n getSelectedItems() {\n return this.tableView.getSelectedItems();\n }\n\n /**\n * Public method to clear selection\n */\n clearSelection() {\n this.tableView.clearSelection();\n }\n\n /**\n * Handle filter edit dialog\n */\n async handleFilterEdit(filterKey) {\n const filterConfig = this.tableView.getAllAvailableFilters().find(f => f.key === filterKey);\n const currentValue = this.collection.params[filterKey];\n\n if (!filterConfig) return;\n\n // Build form field for the filter\n const field = {\n name: 'filter_value',\n label: filterConfig.label || filterKey,\n value: currentValue,\n ...filterConfig.config\n };\n\n const result = await Modal.form({\n title: `Edit ${field.label} Filter`,\n size: 'md',\n fields: [field]\n });\n\n if (result && result.filter_value !== undefined) {\n this.tableView.setFilter(filterKey, result.filter_value);\n\n if (this.collection.restEnabled) {\n this.collection.fetch();\n }\n await this.tableView.render();\n this.syncUrl();\n }\n }\n\n /**\n * Clear all filters\n */\n clearAllFilters() {\n if (!this.collection) return;\n\n // Keep only pagination and sort params\n const { start, size, sort } = this.collection.params;\n this.collection.params = { start, size };\n if (sort) this.collection.params.sort = sort;\n\n this.syncUrl();\n\n if (this.collection.restEnabled) {\n this.collection.fetch();\n } else {\n this.tableView.render();\n }\n }\n\n async onGroupChange(group) {\n if (!group || !this.collection || !this.options.requiresGroup) return;\n this.query[this.groupField] = group.id;\n this.applyQueryToCollection();\n if (this.collection && this.collection.restEnabled) {\n this.collection.fetch();\n }\n }\n\n /**\n * Cleanup on destroy\n */\n async onBeforeDestroy() {\n // Remove event listeners\n if (this.collection) {\n this.collection.off('fetch:start');\n this.collection.off('fetch:end');\n }\n\n if (this.tableView) {\n this.tableView.off('params-changed');\n this.tableView.off('table:search');\n this.tableView.off('table:sort');\n this.tableView.off('table:page');\n this.tableView.off('filter:edit');\n this.tableView.off('row:view');\n this.tableView.off('row:edit');\n this.tableView.off('row:delete');\n this.tableView.off('table:add');\n this.tableView.off('table:export');\n }\n\n await super.onBeforeDestroy();\n }\n\n /**\n * Show/hide status display\n */\n get showStatus() {\n return this.options.showStatus === true;\n }\n\n /**\n * Static factory method\n */\n static create(options = {}) {\n return new this(options);\n }\n}\n\nexport default TablePage;\n","/**\n * TabView - Responsive tabbed interface component with smooth transitions\n * Displays multiple views in a tabbed interface with automatic responsive adaptation\n *\n * Features:\n * - Responsive tab navigation with automatic dropdown mode\n * - Smooth fade transitions between tabs (Bootstrap compatible)\n * - Child view management with proper mounting/unmounting\n * - Bootstrap 5 styling\n * - Keyboard navigation support\n * - Event-driven tab switching\n * - ResizeObserver for efficient responsive detection\n *\n * Example Usage:\n * ```javascript\n * const tabView = new TabView({\n * tabs: {\n * 'Profile': new UserProfileView({ data: userData }),\n * 'Settings': new UserSettingsView({ data: settings }),\n * 'Activity': new UserActivityView({ data: activity })\n * },\n * activeTab: 'Profile', // Optional: set initial active tab\n * enableTransitions: true, // Optional: enable fade transitions (default: true)\n * transitionDuration: 150, // Optional: transition duration in ms (default: 150)\n * enableResponsive: true, // Optional: enable responsive mode (default: true)\n * dropdownStyle: 'select' // Optional: 'select' or 'button' (default: 'select')\n * });\n * ```\n */\n\nimport View from '@core/View.js';\n\nclass TabView extends View {\n constructor(options = {}) {\n const {\n tabs,\n activeTab,\n tabsClass,\n contentClass,\n minWidth,\n enableResponsive,\n tabPadding,\n dropdownStyle, // 'button' (default) or 'select'\n enableTransitions, // Enable fade transitions (default: true)\n transitionDuration, // Transition duration in ms (default: 150, Bootstrap default)\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'tab-view',\n ...viewOptions\n });\n\n // Tab configuration\n this.tabs = {};\n this.tabLabels = Object.keys(this.tabs);\n this.activeTab = activeTab || this.tabLabels[0] || null;\n\n // CSS classes\n this.tabsClass = tabsClass || 'nav nav-tabs mb-3';\n this.contentClass = contentClass || 'tab-content';\n\n // Transition configuration\n this.enableTransitions = enableTransitions !== false; // Default to enabled\n this.transitionDuration = transitionDuration || 150; // Bootstrap default\n\n // Responsive configuration\n this.dropdownStyle = dropdownStyle || 'select'; // 'button' or 'select'\n this.minWidth = minWidth || 300; // Minimum width before switching to dropdown\n this.enableResponsive = enableResponsive !== false; // Default to enabled\n this.tabPadding = tabPadding || 80; // Estimated padding per tab (16px * 2)\n this.currentMode = 'tabs'; // 'tabs' or 'dropdown'\n\n // Width calculation cache\n this.tabWidthCache = new Map();\n this.lastContainerWidth = 0;\n this.resizeObserver = null;\n this._measurementSpan = null; // Reusable measurement element\n this._tabComputedStyle = null; // To store computed styles\n\n\n\n // State tracking\n this.isMobileMode = false;\n this.hasOverflow = false;\n\n // Initialize tabs by adding them as child views\n for (const [label, view] of Object.entries(tabs)) {\n this.addTab(label, view);\n }\n\n // Bind resize handler\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Render the tab navigation and content areas\n */\n async renderTemplate() {\n const tabNavigation = this.buildTabNavigation();\n const tabContent = this.buildTabContent();\n\n return `\n <div class=\"tab-view-container\">\n ${tabNavigation}\n ${tabContent}\n </div>\n `;\n }\n\n /**\n * Build the tab navigation HTML - supports both tab and dropdown modes\n * @returns {string} Tab navigation HTML\n */\n buildTabNavigation() {\n if (this.tabLabels.length === 0) {\n return '';\n }\n\n if (this.currentMode === 'dropdown') {\n return this.buildDropdownNavigation();\n } else {\n return this.buildTabsNavigation();\n }\n }\n\n /**\n * Build traditional tab navigation\n * @returns {string} Tab navigation HTML\n */\n buildTabsNavigation() {\n const tabItems = this.tabLabels.map(label => {\n const isActive = label === this.activeTab;\n const tabId = this.getTabId(label);\n\n return `\n <li class=\"nav-item\" role=\"presentation\">\n <button class=\"nav-link ${isActive ? 'active' : ''}\"\n id=\"${tabId}-tab\"\n data-action=\"show-tab\"\n data-tab-label=\"${this.escapeHtml(label)}\"\n type=\"button\"\n role=\"tab\"\n aria-controls=\"${tabId}\"\n aria-selected=\"${isActive}\">\n ${this.escapeHtml(label)}\n </button>\n </li>\n `;\n }).join('');\n\n return `\n <ul class=\"${this.tabsClass}\" role=\"tablist\" data-tab-mode=\"tabs\">\n ${tabItems}\n </ul>\n `;\n }\n\n /**\n * Build dropdown navigation for mobile/narrow screens\n * @returns {string} Dropdown navigation HTML\n */\n buildDropdownNavigation() {\n const activeLabel = this.activeTab || this.tabLabels[0];\n const dropdownItems = this.tabLabels.map(label => {\n const isActive = label === this.activeTab;\n return `\n <li>\n <button class=\"dropdown-item ${isActive ? 'active' : ''}\"\n data-action=\"show-tab\"\n data-tab-label=\"${this.escapeHtml(label)}\"\n type=\"button\">\n ${this.escapeHtml(label)}\n ${isActive ? '<i class=\"bi bi-check-lg ms-2\"></i>' : ''}\n </button>\n </li>\n `;\n }).join('');\n\n let buttonHtml;\n if (this.dropdownStyle === 'select') {\n // New \"select\" style button\n buttonHtml = `\n <button class=\"btn tab-view-select-style dropdown-toggle\"\n type=\"button\"\n data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\"\n id=\"tab-dropdown-${this.id}\">\n <span class=\"tab-view-select-label\">${this.escapeHtml(activeLabel)}</span>\n </button>\n `;\n } else {\n // Original \"button\" style\n buttonHtml = `\n <button class=\"btn btn-outline-secondary dropdown-toggle w-100 w-sm-auto\"\n type=\"button\"\n data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\"\n id=\"tab-dropdown-${this.id}\">\n <i class=\"bi bi-list me-2\"></i>\n ${this.escapeHtml(activeLabel)}\n </button>\n `;\n }\n\n return `\n <div class=\"dropdown mb-3\" data-tab-mode=\"dropdown\">\n ${buttonHtml}\n <ul class=\"dropdown-menu\" aria-labelledby=\"tab-dropdown-${this.id}\">\n ${dropdownItems}\n </ul>\n </div>\n `;\n }\n\n /**\n * Build mobile dropdown navigation\n * @returns {string} Mobile dropdown HTML\n */\n buildMobileDropdownNavigation() {\n const activeLabel = this.activeTab || this.tabLabels[0];\n const dropdownItems = this.tabLabels.map(label => {\n const isActive = label === this.activeTab;\n return `\n <li>\n <button class=\"dropdown-item ${isActive ? 'active' : ''}\"\n data-action=\"show-tab\"\n data-tab-label=\"${this.escapeHtml(label)}\"\n type=\"button\">\n ${this.escapeHtml(label)}\n ${isActive ? '<i class=\"bi bi-check ms-2\"></i>' : ''}\n </button>\n </li>\n `;\n }).join('');\n\n return `\n <div class=\"dropdown mb-3\" data-tab-navigation=\"mobile\">\n <button class=\"btn btn-outline-secondary dropdown-toggle w-100 text-start\"\n type=\"button\"\n data-bs-toggle=\"dropdown\"\n aria-expanded=\"false\">\n <i class=\"bi bi-list me-2\"></i>\n ${this.escapeHtml(activeLabel)}\n </button>\n <ul class=\"dropdown-menu w-100\">\n ${dropdownItems}\n </ul>\n </div>\n `;\n }\n\n /**\n * Determine if mobile dropdown should be used\n * @returns {boolean} True if mobile dropdown should be used\n */\n shouldUseMobileDropdown() {\n if (!this.enableMobileDropdown) return false;\n\n // Check viewport width\n const viewportWidth = window.innerWidth;\n if (viewportWidth < this.mobileBreakpoint) {\n return true;\n }\n\n // Check for overflow in desktop mode\n return this.hasOverflow && viewportWidth < 992; // Bootstrap lg breakpoint\n }\n\n /**\n * Build the tab content area HTML\n * @returns {string} Tab content HTML\n */\n buildTabContent() {\n if (this.tabLabels.length === 0) {\n return '<div class=\"alert alert-info\">No tabs to display</div>';\n }\n\n const tabPanes = this.tabLabels.map(label => {\n const isActive = label === this.activeTab;\n const tabId = this.getTabId(label);\n\n return `\n <div class=\"tab-pane fade ${isActive ? 'show active' : ''}\"\n id=\"${tabId}\"\n role=\"tabpanel\"\n aria-labelledby=\"${tabId}-tab\"\n data-tab-label=\"${this.escapeHtml(label)}\">\n <div data-container=\"${tabId}-content\"></div>\n </div>\n `;\n }).join('');\n\n return `\n <div class=\"${this.contentClass}\">\n ${tabPanes}\n </div>\n `;\n }\n\n /**\n * Generate a safe DOM ID for a tab\n * @param {string} label - Tab label\n * @returns {string} Safe DOM ID\n */\n getTabId(label) {\n return `tab-${label.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${this.id}`;\n }\n\n /**\n * Show a specific tab\n * @param {string} tabLabel - Label of the tab to show\n * @returns {Promise<boolean>} True if tab was shown successfully\n */\n async showTab(tabLabel, options = {}) {\n const { force = false } = options;\n\n // Validate tab exists\n if (!this.tabs[tabLabel]) {\n console.warn(`TabView: Tab \"${tabLabel}\" does not exist`);\n return false;\n }\n\n // Skip if already active and not being forced\n // This prevents re-rendering the same tab on repeated clicks, but allows\n // for forced re-mounting after a parent view render.\n if (this.activeTab === tabLabel && !force) {\n // As a safeguard, ensure the view is actually in the DOM.\n // If not, proceed to re-mount it.\n const activeView = this.tabs[tabLabel];\n if (activeView && activeView.isMounted() && this.element.contains(activeView.element)) {\n return true;\n }\n }\n\n const previousTab = this.activeTab;\n this.activeTab = tabLabel;\n\n try {\n // Update tab navigation\n await this.updateTabNavigation(tabLabel, previousTab);\n\n // Update tab content\n await this.updateTabContent(tabLabel, previousTab);\n\n // Emit tab change event\n this.emit('tab:changed', {\n activeTab: tabLabel,\n previousTab: previousTab\n });\n\n return true;\n } catch (error) {\n console.error('TabView: Error showing tab:', error);\n this.activeTab = previousTab; // Revert on error\n return false;\n }\n }\n\n /**\n * Update tab navigation visual state\n * @param {string} activeTabLabel - New active tab\n * @param {string} previousTabLabel - Previous active tab\n */\n async updateTabNavigation(activeTabLabel, previousTabLabel) {\n if (!this.element) return;\n\n // In dropdown mode, re-render the navigation to update the button label and items\n if (this.currentMode === 'dropdown') {\n await this.reRenderNavigation();\n return; // Re-rendering handles all visual updates for navigation\n }\n\n // In tabs mode, just toggle classes for efficiency\n if (previousTabLabel) {\n const prevTabButton = this.element.querySelector(`[data-tab-label=\"${previousTabLabel}\"]`);\n if (prevTabButton) {\n prevTabButton.classList.remove('active');\n prevTabButton.setAttribute('aria-selected', 'false');\n }\n }\n\n // Add active state to new tab\n const activeTabButton = this.element.querySelector(`[data-tab-label=\"${activeTabLabel}\"]`);\n if (activeTabButton) {\n activeTabButton.classList.add('active');\n activeTabButton.setAttribute('aria-selected', 'true');\n }\n }\n\n /**\n * Update tab content area and manage child views\n * @param {string} activeTabLabel - New active tab\n * @param {string} previousTabLabel - Previous active tab\n */\n async updateTabContent(activeTabLabel, previousTabLabel) {\n if (!this.element) return;\n\n const activeTabId = this.getTabId(activeTabLabel);\n const previousTabId = previousTabLabel ? this.getTabId(previousTabLabel) : null;\n\n const activePane = this.element.querySelector(`#${activeTabId}`);\n const prevPane = previousTabId ? this.element.querySelector(`#${previousTabId}`) : null;\n\n // Get the active view once at the top\n const activeView = this.tabs[activeTabLabel];\n\n // Handle transitions if enabled\n if (this.enableTransitions) {\n // Start fade-out transition for previous pane\n if (prevPane && prevPane.classList.contains('show')) {\n // Remove 'show' to trigger fade-out (but keep 'active' so it stays visible during transition)\n prevPane.classList.remove('show');\n \n // Wait for fade-out transition to complete\n await new Promise(resolve => {\n const duration = this._getTransitionDuration(prevPane) || this.transitionDuration;\n setTimeout(resolve, duration);\n });\n \n // Now remove 'active' to fully hide the pane\n prevPane.classList.remove('active');\n }\n\n // Mount the active tab's view before showing it (prevents flash of empty content)\n if (activeView) {\n const container = this.element.querySelector(`[data-container=\"${activeTabId}-content\"]`);\n if (container && !activeView.isMounted()) {\n await activeView.render(true, container);\n }\n }\n\n // Start fade-in transition for active pane\n if (activePane) {\n // Add 'active' to make it the active pane (but not visible yet)\n activePane.classList.add('active');\n \n // Force reflow to ensure the 'active' class is applied before 'show'\n // This is necessary for CSS transitions to work properly\n void activePane.offsetHeight;\n \n // Add 'show' to trigger fade-in transition\n activePane.classList.add('show');\n }\n } else {\n // No transitions - instant switch\n if (prevPane) {\n prevPane.classList.remove('show', 'active');\n }\n\n // Mount the active tab's view\n if (activeView) {\n const container = this.element.querySelector(`[data-container=\"${activeTabId}-content\"]`);\n if (container && !activeView.isMounted()) {\n await activeView.render(true, container);\n }\n }\n\n if (activePane) {\n activePane.classList.add('show', 'active');\n }\n }\n\n // Call onTabActivated hook after the view is mounted and visible\n if (activeView && activeView.onTabActivated) {\n await activeView.onTabActivated();\n }\n }\n\n /**\n * Get CSS transition duration for an element\n * @param {HTMLElement} element - Element to check\n * @returns {number} Transition duration in milliseconds\n * @private\n */\n _getTransitionDuration(element) {\n if (!element || typeof window === 'undefined' || !window.getComputedStyle) {\n return 0;\n }\n\n const computedStyle = window.getComputedStyle(element);\n const duration = computedStyle.transitionDuration || '0s';\n \n // Parse duration (can be in seconds or milliseconds)\n const matches = duration.match(/^([0-9.]+)(m?s)$/);\n if (!matches) return 0;\n \n const value = parseFloat(matches[1]);\n const unit = matches[2];\n \n // Convert to milliseconds\n return unit === 's' ? value * 1000 : value;\n }\n\n /**\n * Handle tab click events\n * @param {Event} event - Click event\n * @param {HTMLElement} element - Clicked element\n */\n async onActionShowTab(event, element) {\n const tabLabel = element.getAttribute('data-tab-label');\n if (tabLabel) {\n await this.showTab(tabLabel);\n }\n }\n\n /**\n * Initialize measurement styles by reading computed styles from a rendered tab\n */\n _initializeMeasurementStyles() {\n if (!this.element || this._tabComputedStyle) return;\n\n const tabButton = this.element.querySelector('.nav-link');\n if (tabButton && typeof window.getComputedStyle === 'function') {\n const style = window.getComputedStyle(tabButton);\n this._tabComputedStyle = {\n font: style.font,\n letterSpacing: style.letterSpacing,\n };\n\n // Calculate horizontal padding from computed styles\n const paddingLeft = parseFloat(style.paddingLeft) || 0;\n const paddingRight = parseFloat(style.paddingRight) || 0;\n this.tabPadding = paddingLeft + paddingRight + 12; // Update with real value\n }\n }\n\n /**\n * Initialize active tab after rendering\n */\n async onAfterRender() {\n await super.onAfterRender();\n\n // Initialize styles for accurate width calculation before setting up responsive handling\n this._initializeMeasurementStyles();\n\n // Set up responsive behavior\n if (this.enableResponsive) {\n this.setupResponsiveHandling();\n }\n\n // Show the active tab after initial render. Forcing ensures the content is\n // correctly re-mounted if the TabView itself was re-rendered.\n if (this.activeTab && this.tabs[this.activeTab]) {\n await this.showTab(this.activeTab, { force: true });\n }\n }\n\n /**\n * Mount child views after tab view is mounted\n */\n async onAfterMount() {\n await super.onAfterMount();\n\n // The active tab's content is now mounted via the onAfterRender -> showTab flow,\n // which correctly handles both initial renders and subsequent re-renders.\n // The logic previously here was redundant.\n }\n\n /**\n * Clean up child views when destroying\n */\n async onBeforeDestroy() {\n await super.onBeforeDestroy();\n\n // Clean up resize observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n // Remove window resize listener\n if (typeof window !== 'undefined') {\n window.removeEventListener('resize', this.handleResize);\n }\n\n // Remove measurement span if it exists\n if (this._measurementSpan && this._measurementSpan.parentElement) {\n this._measurementSpan.parentElement.removeChild(this._measurementSpan);\n }\n this._measurementSpan = null;\n\n // Destroy all child tab views\n for (const [label, view] of Object.entries(this.tabs)) {\n if (view && typeof view.destroy === 'function') {\n await view.destroy();\n }\n }\n }\n\n /**\n * Called when this TabView is inside a SideNavView and the section is re-activated.\n * Propagates to the active tab's onTabActivated hook.\n */\n async onSectionActivated() {\n const activeView = this.activeTab ? this.tabs[this.activeTab] : null;\n if (activeView?.onTabActivated) {\n await activeView.onTabActivated();\n }\n }\n\n /**\n * Get the currently active tab label\n * @returns {string|null} Active tab label\n */\n getActiveTab() {\n return this.activeTab;\n }\n\n /**\n * Get all tab labels\n * @returns {string[]} Array of tab labels\n */\n getTabLabels() {\n return [...this.tabLabels];\n }\n\n /**\n * Get a specific tab's view by label\n * @param {string} label - Tab label\n * @returns {View|null} The tab's view instance or null if not found\n */\n getTab(label) {\n return this.tabs[label] || null;\n }\n\n /**\n * Add a new tab\n * @param {string} label - Tab label\n * @param {View} view - View instance for tab content\n * @param {boolean} makeActive - Whether to make this tab active\n * @returns {Promise<boolean>} True if tab was added successfully\n */\n async addTab(label, view, makeActive = false) {\n if (this.tabs[label]) {\n console.warn(`TabView: Tab \"${label}\" already exists`);\n return false;\n }\n\n if (view.options.permissions && !this.getApp().activeUser.hasPerm(view.options.permissions)) {\n return false;\n }\n\n this.tabs[label] = view;\n // this.addChild(view);\n view.containerId = this.getTabId(label);\n view.parent = this;\n this.tabLabels = Object.keys(this.tabs);\n\n // Set as active tab if it's the first tab or explicitly requested\n if (!this.activeTab || makeActive) {\n this.activeTab = label;\n }\n\n // Re-render if mounted\n if (this.isMounted()) {\n await this.render();\n\n if (makeActive || this.activeTab === label) {\n await this.showTab(label);\n }\n }\n\n this.emit('tab:added', { label, view });\n return true;\n }\n\n /**\n * Remove a tab\n * @param {string} label - Tab label to remove\n * @returns {Promise<boolean>} True if tab was removed successfully\n */\n async removeTab(label) {\n if (!this.tabs[label]) {\n console.warn(`TabView: Tab \"${label}\" does not exist`);\n return false;\n }\n\n const view = this.tabs[label];\n\n // Destroy the view\n if (view && typeof view.destroy === 'function') {\n await view.destroy();\n }\n\n // Remove from tabs\n delete this.tabs[label];\n this.tabLabels = Object.keys(this.tabs);\n\n // Handle active tab removal\n if (this.activeTab === label) {\n this.activeTab = this.tabLabels[0] || null;\n }\n\n // Re-render if mounted\n if (this.isMounted()) {\n await this.render();\n\n if (this.activeTab) {\n await this.showTab(this.activeTab);\n }\n }\n\n this.emit('tab:removed', { label, view });\n return true;\n }\n\n /**\n * Calculate estimated width needed for a tab label\n * @param {string} label - Tab label text\n * @returns {number} Estimated width in pixels\n */\n calculateTabWidth(label) {\n if (this.tabWidthCache.has(label)) {\n return this.tabWidthCache.get(label);\n }\n\n // Fallback for non-browser environments\n if (typeof document === 'undefined') {\n const estimatedWidth = label.length * 8 + this.tabPadding; // Original fallback\n this.tabWidthCache.set(label, estimatedWidth);\n return estimatedWidth;\n }\n\n // Create a reusable measurement span if it doesn't exist\n if (!this._measurementSpan) {\n this._measurementSpan = document.createElement('span');\n this._measurementSpan.style.visibility = 'hidden';\n this._measurementSpan.style.position = 'absolute';\n this._measurementSpan.style.whiteSpace = 'nowrap';\n }\n\n const span = this._measurementSpan;\n\n // Apply computed styles if available, otherwise use defaults\n if (this._tabComputedStyle) {\n span.style.font = this._tabComputedStyle.font;\n span.style.letterSpacing = this._tabComputedStyle.letterSpacing;\n } else {\n // Fallback to original hardcoded styles if computed style not yet available\n span.style.fontSize = '14px';\n span.style.fontFamily = 'system-ui, -apple-system, sans-serif';\n }\n\n span.textContent = label;\n document.body.appendChild(span);\n const width = span.offsetWidth + this.tabPadding;\n document.body.removeChild(span);\n\n this.tabWidthCache.set(label, width);\n return width;\n }\n\n /**\n * Calculate total width needed for all tabs\n * @returns {number} Total width in pixels\n */\n getTotalTabWidth() {\n return this.tabLabels.reduce((total, label) => {\n return total + this.calculateTabWidth(label);\n }, 0);\n }\n\n /**\n * Get current container width\n * @returns {number} Container width in pixels\n */\n getContainerWidth() {\n if (!this.element) {\n return this.minWidth;\n }\n\n const container = this.element.parentElement || this.element;\n return container.offsetWidth || this.minWidth;\n }\n\n /**\n * Determine if dropdown mode should be used\n * @returns {boolean} True if dropdown mode should be used\n */\n shouldUseDropdown() {\n if (!this.enableResponsive) {\n return false;\n }\n\n const containerWidth = this.getContainerWidth();\n const totalTabWidth = this.getTotalTabWidth();\n\n // Use dropdown if tabs would overflow or container is too narrow\n return containerWidth < Math.max(totalTabWidth, this.minWidth);\n }\n\n /**\n * Setup responsive handling with ResizeObserver\n */\n setupResponsiveHandling() {\n if (!this.element || !this.enableResponsive) {\n return;\n }\n\n // Set initial mode\n this.updateNavigationMode();\n\n // Use ResizeObserver for better performance\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n this.handleResize();\n });\n\n const container = this.element.parentElement || this.element;\n this.resizeObserver.observe(container);\n } else {\n // Fallback to window resize\n window.addEventListener('resize', this.handleResize);\n }\n }\n\n /**\n * Handle resize events\n */\n async handleResize() {\n const containerWidth = this.getContainerWidth();\n\n // Only update if width changed significantly (avoid excessive updates)\n if (Math.abs(containerWidth - this.lastContainerWidth) > 50) {\n this.lastContainerWidth = containerWidth;\n await this.updateNavigationMode();\n }\n }\n\n /**\n * Update navigation mode based on current space\n */\n async updateNavigationMode() {\n const shouldUseDropdown = this.shouldUseDropdown();\n const newMode = shouldUseDropdown ? 'dropdown' : 'tabs';\n\n if (newMode !== this.currentMode) {\n this.currentMode = newMode;\n\n // Re-render navigation if mounted\n if (this.isMounted()) {\n await this.reRenderNavigation();\n }\n\n // Emit mode change event\n this.emit('navigation:modeChanged', {\n mode: this.currentMode,\n containerWidth: this.getContainerWidth(),\n totalTabWidth: this.getTotalTabWidth()\n });\n }\n }\n\n /**\n * Re-render just the navigation part\n */\n async reRenderNavigation() {\n if (!this.element) return;\n\n const navigationContainer = this.element.querySelector('[data-tab-mode]');\n if (navigationContainer) {\n const newNavigation = this.buildTabNavigation();\n navigationContainer.outerHTML = newNavigation;\n }\n }\n\n /**\n * Get current navigation mode\n * @returns {string} Current mode ('tabs' or 'dropdown')\n */\n getNavigationMode() {\n return this.currentMode;\n }\n\n /**\n * Force a specific navigation mode\n * @param {string} mode - 'tabs' or 'dropdown'\n */\n async setNavigationMode(mode) {\n if (mode !== 'tabs' && mode !== 'dropdown') {\n console.warn('TabView: Invalid navigation mode. Use \"tabs\" or \"dropdown\"');\n return;\n }\n\n this.currentMode = mode;\n\n if (this.isMounted()) {\n await this.reRenderNavigation();\n }\n }\n\n /**\n * Clear the tab width cache (useful when tab labels change)\n */\n clearWidthCache() {\n this.tabWidthCache.clear();\n }\n\n /**\n * Static factory method\n * @param {object} options - TabView options\n * @returns {TabView} New TabView instance\n */\n static create(options = {}) {\n return new TabView(options);\n }\n}\n\nexport default TabView;\n","/**\n * SideNavView - Left sidebar navigation with content panel\n *\n * A reusable navigation component that displays a vertical sidebar with\n * nav links, optional group labels, and icons. The content panel mounts\n * one child view at a time, switching on nav click.\n *\n * Features:\n * - Left sidebar with nav links, icons, and group dividers\n * - Active state with accent border\n * - Mount/unmount child views on section switch\n * - Responsive: collapses to dropdown on narrow containers\n * - Permission-aware: skips sections the user lacks permission for\n * - Configurable nav width and content padding\n * - Smooth fade transitions between sections\n *\n * Example Usage:\n * ```javascript\n * const sideNav = new SideNavView({\n * sections: [\n * { key: 'profile', label: 'Profile', icon: 'bi-person', view: profileView },\n * { key: 'security', label: 'Security', icon: 'bi-shield-lock', view: securityView },\n * { type: 'divider', label: 'Activity' },\n * { key: 'sessions', label: 'Sessions', icon: 'bi-clock-history', view: sessionsView },\n * ],\n * activeSection: 'profile',\n * navWidth: 200,\n * contentPadding: '1.5rem 2.5rem',\n * enableResponsive: true\n * });\n * ```\n */\n\nimport View from '@core/View.js';\n\nclass SideNavView extends View {\n constructor(options = {}) {\n const {\n sections = [],\n activeSection,\n navWidth,\n contentPadding,\n enableResponsive,\n minWidth,\n ...viewOptions\n } = options;\n\n super({\n tagName: 'div',\n className: 'side-nav-view',\n ...viewOptions\n });\n\n // Configuration\n this.navWidth = navWidth || 200;\n this.contentPadding = contentPadding || '1.5rem 2.5rem';\n this.enableResponsive = enableResponsive !== false;\n this.minWidth = minWidth || 500;\n\n // State\n this.sectionConfigs = []; // Full config array (including dividers)\n this.sectionViews = {}; // key → view instance\n this.sectionKeys = []; // Ordered navigable section keys\n this.activeSection = null;\n this.currentMode = 'sidebar'; // 'sidebar' or 'dropdown'\n this.resizeObserver = null;\n this.lastContainerWidth = 0;\n\n // Process sections config\n for (const config of sections) {\n this._addSectionConfig(config);\n }\n\n // Set initial active section\n this.activeSection = activeSection || this.sectionKeys[0] || null;\n\n // Bind resize handler\n this.handleResize = this.handleResize.bind(this);\n }\n\n /**\n * Process and store a section config entry\n * @param {object} config - Section config (navigable or divider)\n * @private\n */\n _addSectionConfig(config) {\n if (config.type === 'divider') {\n this.sectionConfigs.push({ type: 'divider', label: config.label });\n return;\n }\n\n // Skip if user lacks required permission\n if (config.permissions && !this._hasPermission(config.permissions)) {\n return;\n }\n\n this.sectionConfigs.push(config);\n this.sectionKeys.push(config.key);\n\n if (config.view) {\n this.sectionViews[config.key] = config.view;\n config.view.parent = this;\n }\n }\n\n /**\n * Check if the current user has a permission\n * @param {string} perm - Permission string\n * @returns {boolean}\n * @private\n */\n _hasPermission(perm) {\n try {\n return this.getApp().activeUser.hasPerm(perm);\n } catch {\n return true; // If app isn't available yet, allow — will be checked at render\n }\n }\n\n // ───────────────────────────────────────────────\n // Template\n // ───────────────────────────────────────────────\n\n async renderTemplate() {\n const nav = this.currentMode === 'dropdown'\n ? this._buildDropdownNav()\n : this._buildSidebarNav();\n\n return `\n <style>\n .snv-layout { display: flex; height: 100%; min-height: 0; }\n .snv-nav {\n width: ${this.navWidth}px;\n background: #f8f9fc;\n border-right: 1px solid #e9ecef;\n padding: 0.75rem 0;\n flex-shrink: 0;\n overflow-y: auto;\n }\n .snv-nav a {\n color: #495057;\n padding: 0.45rem 1.25rem;\n font-size: 0.85rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n text-decoration: none;\n cursor: pointer;\n }\n .snv-nav a:hover { background: #e9ecef; }\n .snv-nav a.active {\n background: #e7f1ff;\n color: #0d6efd;\n font-weight: 600;\n border-right: 2px solid #0d6efd;\n }\n .snv-nav a i { width: 18px; text-align: center; font-size: 0.9rem; }\n .snv-nav-label {\n font-size: 0.65rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: #adb5bd;\n padding: 0.75rem 1.25rem 0.25rem;\n }\n .snv-content {\n flex: 1;\n overflow-y: auto;\n padding: ${this.contentPadding};\n min-width: 0;\n }\n .snv-content > .snv-section { display: none; }\n .snv-content > .snv-section.snv-active { display: block; }\n .snv-dropdown { margin-bottom: 0.75rem; }\n .snv-select-btn {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n width: 100%;\n padding: 0.5rem 1rem;\n background: #f8f9fc;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n font-size: 0.85rem;\n color: #495057;\n cursor: pointer;\n }\n .snv-select-btn:hover { background: #e9ecef; }\n .snv-select-btn::after {\n content: '';\n display: inline-block;\n margin-left: auto;\n border-top: 0.3em solid;\n border-right: 0.3em solid transparent;\n border-left: 0.3em solid transparent;\n }\n @media (max-width: 576px) {\n .snv-nav { display: none; }\n .snv-content { padding: 1.25rem; }\n }\n </style>\n ${this.currentMode === 'dropdown' ? `\n <div class=\"snv-dropdown\">${nav}</div>\n <div class=\"snv-content\" data-container=\"snv-content\"></div>\n ` : `\n <div class=\"snv-layout\">\n <nav class=\"snv-nav\">${nav}</nav>\n <div class=\"snv-content\" data-container=\"snv-content\"></div>\n </div>\n `}\n `;\n }\n\n /**\n * Build sidebar navigation HTML\n * @returns {string}\n * @private\n */\n _buildSidebarNav() {\n return this.sectionConfigs.map(config => {\n if (config.type === 'divider') {\n return `<div class=\"snv-nav-label\">${this.escapeHtml(config.label)}</div>`;\n }\n const isActive = config.key === this.activeSection;\n const icon = config.icon ? `<i class=\"bi ${config.icon}\"></i>` : '';\n return `<a role=\"button\" class=\"${isActive ? 'active' : ''}\" data-action=\"navigate\" data-section=\"${config.key}\">${icon} ${this.escapeHtml(config.label)}</a>`;\n }).join('');\n }\n\n /**\n * Build dropdown navigation HTML (responsive mode)\n * @returns {string}\n * @private\n */\n _buildDropdownNav() {\n const activeConfig = this.sectionConfigs.find(c => c.key === this.activeSection);\n const activeLabel = activeConfig ? activeConfig.label : this.sectionKeys[0];\n\n const items = this.sectionConfigs\n .filter(c => c.type !== 'divider')\n .map(config => {\n const isActive = config.key === this.activeSection;\n return `\n <li>\n <button class=\"dropdown-item ${isActive ? 'active' : ''}\"\n data-action=\"navigate\"\n data-section=\"${config.key}\"\n type=\"button\">\n ${config.icon ? `<i class=\"bi ${config.icon} me-2\"></i>` : ''}\n ${this.escapeHtml(config.label)}\n ${isActive ? '<i class=\"bi bi-check-lg ms-2\"></i>' : ''}\n </button>\n </li>\n `;\n }).join('');\n\n return `\n <div class=\"dropdown\">\n <button class=\"snv-select-btn\" type=\"button\"\n data-bs-toggle=\"dropdown\" aria-expanded=\"false\">\n ${activeConfig?.icon ? `<i class=\"bi ${activeConfig.icon}\"></i>` : ''}\n <span>${this.escapeHtml(activeLabel)}</span>\n </button>\n <ul class=\"dropdown-menu w-100\">${items}</ul>\n </div>\n `;\n }\n\n // ───────────────────────────────────────────────\n // Lifecycle\n // ───────────────────────────────────────────────\n\n async onAfterRender() {\n await super.onAfterRender();\n\n // Mount the active section\n if (this.activeSection) {\n await this._mountSection(this.activeSection);\n }\n\n // Set up responsive behavior\n if (this.enableResponsive) {\n this._setupResponsive();\n }\n }\n\n async onBeforeDestroy() {\n await super.onBeforeDestroy();\n\n // Clean up resize observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('resize', this.handleResize);\n }\n\n // Destroy all section views\n for (const view of Object.values(this.sectionViews)) {\n if (view && typeof view.destroy === 'function') {\n await view.destroy();\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Section switching\n // ───────────────────────────────────────────────\n\n /**\n * Navigate to a section\n * @param {string} key - Section key\n * @returns {Promise<boolean>}\n */\n async showSection(key) {\n if (!this.sectionViews[key]) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n if (key === this.activeSection) {\n // Already active — but ensure it's mounted\n const view = this.sectionViews[key];\n if (view && view.isMounted() && this.element?.contains(view.element)) {\n return true;\n }\n }\n\n const previousSection = this.activeSection;\n this.activeSection = key;\n\n // Unmount previous section\n if (previousSection && previousSection !== key) {\n await this._unmountSection(previousSection);\n }\n\n // Mount new section\n await this._mountSection(key);\n\n // Call onSectionActivated hook after the view is mounted and visible\n const activeView = this.sectionViews[key];\n if (activeView?.onSectionActivated) {\n await activeView.onSectionActivated();\n }\n\n // Update nav visual state\n this._updateNavState(key);\n\n this.emit('section:changed', {\n activeSection: key,\n previousSection\n });\n\n return true;\n }\n\n /**\n * Mount a section view into the content area\n * @param {string} key - Section key\n * @private\n */\n async _mountSection(key) {\n const view = this.sectionViews[key];\n if (!view) return;\n\n const container = this.element?.querySelector('[data-container=\"snv-content\"]');\n if (!container) return;\n\n if (!view.isMounted()) {\n this._showContentLoading(container);\n try {\n await view.render(true, container);\n } finally {\n this._hideContentLoading(container);\n }\n }\n }\n\n /**\n * Show a lightweight spinner in the content panel\n * @param {HTMLElement} container\n * @private\n */\n _showContentLoading(container) {\n if (!container) return;\n let spinner = container.querySelector('.snv-loading');\n if (!spinner) {\n spinner = document.createElement('div');\n spinner.className = 'snv-loading';\n spinner.innerHTML = '<div class=\"spinner-border spinner-border-sm text-secondary\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div>';\n spinner.style.cssText = 'display:flex;align-items:center;justify-content:center;padding:3rem;';\n container.prepend(spinner);\n }\n }\n\n /**\n * Remove the content panel spinner\n * @param {HTMLElement} container\n * @private\n */\n _hideContentLoading(container) {\n if (!container) return;\n const spinner = container.querySelector('.snv-loading');\n if (spinner) spinner.remove();\n }\n\n /**\n * Unmount a section view\n * @param {string} key - Section key\n * @private\n */\n async _unmountSection(key) {\n const view = this.sectionViews[key];\n if (!view || !view.isMounted()) return;\n\n await view.unmount();\n }\n\n /**\n * Update nav link active state\n * @param {string} activeKey - Active section key\n * @private\n */\n _updateNavState(activeKey) {\n if (!this.element) return;\n\n // Update sidebar links\n this.element.querySelectorAll('.snv-nav a, .dropdown-item').forEach(link => {\n const section = link.dataset.section;\n if (section) {\n link.classList.toggle('active', section === activeKey);\n }\n });\n\n // Update dropdown button label\n const selectBtn = this.element.querySelector('.snv-select-btn span');\n if (selectBtn) {\n const config = this.sectionConfigs.find(c => c.key === activeKey);\n if (config) {\n selectBtn.textContent = config.label;\n }\n }\n }\n\n // ───────────────────────────────────────────────\n // Action handlers\n // ───────────────────────────────────────────────\n\n async onActionNavigate(event, el) {\n event.preventDefault();\n const section = el.dataset.section;\n if (section) {\n await this.showSection(section);\n }\n return true;\n }\n\n // ───────────────────────────────────────────────\n // Responsive\n // ───────────────────────────────────────────────\n\n /**\n * Set up responsive width detection\n * @private\n */\n _setupResponsive() {\n if (!this.element || !this.enableResponsive) return;\n\n this._updateMode();\n\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(() => {\n this.handleResize();\n });\n const container = this.element.parentElement || this.element;\n this.resizeObserver.observe(container);\n } else {\n window.addEventListener('resize', this.handleResize);\n }\n }\n\n /**\n * Handle resize events\n */\n async handleResize() {\n const containerWidth = this._getContainerWidth();\n if (Math.abs(containerWidth - this.lastContainerWidth) > 50) {\n this.lastContainerWidth = containerWidth;\n await this._updateMode();\n }\n }\n\n /**\n * Get the container width\n * @returns {number}\n * @private\n */\n _getContainerWidth() {\n if (!this.element) return this.minWidth;\n const container = this.element.parentElement || this.element;\n return container.offsetWidth || this.minWidth;\n }\n\n /**\n * Check and switch between sidebar and dropdown modes\n * @private\n */\n async _updateMode() {\n const containerWidth = this._getContainerWidth();\n const newMode = containerWidth < this.minWidth ? 'dropdown' : 'sidebar';\n\n if (newMode !== this.currentMode) {\n this.currentMode = newMode;\n if (this.isMounted()) {\n await this.render();\n }\n this.emit('navigation:modeChanged', {\n mode: this.currentMode,\n containerWidth\n });\n }\n }\n\n // ───────────────────────────────────────────────\n // Public API\n // ───────────────────────────────────────────────\n\n /**\n * Get the active section key\n * @returns {string|null}\n */\n getActiveSection() {\n return this.activeSection;\n }\n\n /**\n * Get all navigable section keys\n * @returns {string[]}\n */\n getSectionKeys() {\n return [...this.sectionKeys];\n }\n\n /**\n * Get a section's view by key\n * @param {string} key - Section key\n * @returns {View|null}\n */\n getSection(key) {\n return this.sectionViews[key] || null;\n }\n\n /**\n * Add a section dynamically\n * @param {object} config - Section config\n * @param {boolean} makeActive - Whether to activate the section\n * @returns {Promise<boolean>}\n */\n async addSection(config, makeActive = false) {\n if (config.key && this.sectionViews[config.key]) {\n console.warn(`SideNavView: Section \"${config.key}\" already exists`);\n return false;\n }\n\n this._addSectionConfig(config);\n\n if (this.isMounted()) {\n await this.render();\n if (makeActive && config.key) {\n await this.showSection(config.key);\n }\n }\n\n this.emit('section:added', { config });\n return true;\n }\n\n /**\n * Remove a section dynamically\n * @param {string} key - Section key to remove\n * @returns {Promise<boolean>}\n */\n async removeSection(key) {\n const view = this.sectionViews[key];\n if (!view) {\n console.warn(`SideNavView: Section \"${key}\" does not exist`);\n return false;\n }\n\n // Destroy the view\n if (typeof view.destroy === 'function') {\n await view.destroy();\n }\n\n // Remove from data structures\n delete this.sectionViews[key];\n this.sectionKeys = this.sectionKeys.filter(k => k !== key);\n this.sectionConfigs = this.sectionConfigs.filter(c => c.key !== key);\n\n // Handle active section removal\n if (this.activeSection === key) {\n this.activeSection = this.sectionKeys[0] || null;\n }\n\n if (this.isMounted()) {\n await this.render();\n }\n\n this.emit('section:removed', { key });\n return true;\n }\n\n /**\n * Prevent model changes from triggering a full re-render.\n * Section views manage their own model reactivity.\n */\n _onModelChange() {\n // no-op — same pattern as UserView\n }\n\n static create(options = {}) {\n return new SideNavView(options);\n }\n}\n\nexport default SideNavView;\n","import View from '@core/View.js';\n\nclass FilePreviewView extends View {\n constructor(options = {}) {\n super({\n className: 'file-preview',\n ...options\n });\n this.file = options.file || {};\n this.isImage = this.file.content_type?.startsWith('image/');\n this.isPdf = this.file.content_type === 'application/pdf';\n }\n\n getTemplate() {\n return `\n <div class=\"file-preview-item card card-body p-2 mt-2\">\n <div class=\"d-flex align-items-center\">\n <div class=\"flex-shrink-0\">\n ${this.isImage ? `<img src=\"${this.file.thumbnailUrl || this.file.url}\" class=\"rounded\" style=\"width: 40px; height: 40px; object-fit: cover;\">` : `<i class=\"bi bi-file-earmark-text fs-2 text-secondary\"></i>`}\n </div>\n <div class=\"flex-grow-1 ms-3\">\n <div class=\"fw-bold text-truncate\">{{file.filename}}</div>\n <div class=\"small text-muted\">{{file.file_size|filesize}}</div>\n </div>\n <div class=\"flex-shrink-0\">\n <button class=\"btn btn-sm btn-outline-primary\" data-action=\"view-file\">View</button>\n </div>\n </div>\n </div>\n `;\n }\n\n async onActionViewFile() {\n if (this.isImage) {\n // Check if lightbox extension is available\n const LightboxGallery = window.MOJO?.plugins?.LightboxGallery;\n \n if (LightboxGallery) {\n LightboxGallery.show({ src: this.file.url, alt: this.file.filename });\n } else {\n // Fallback: open in new tab\n window.open(this.file.url, '_blank');\n }\n } else if (this.isPdf) {\n // Check if lightbox extension is available\n const PDFViewer = window.MOJO?.plugins?.PDFViewer;\n \n if (PDFViewer) {\n PDFViewer.showDialog(this.file.url, { title: this.file.filename });\n } else {\n // Fallback: open in new tab\n window.open(this.file.url, '_blank');\n }\n } else {\n window.open(this.file.url, '_blank');\n }\n }\n}\n\nexport default FilePreviewView;\n","import Collection from '@core/Collection.js';\nimport Model from '@core/Model.js';\n\n/* =========================\n * Constants\n * ========================= */\n\nconst SHORTLINK_SOURCE_OPTIONS = [\n { value: 'admin', label: 'Admin' },\n { value: 'email', label: 'Email' },\n { value: 'sms', label: 'SMS' },\n { value: 'push', label: 'Push' },\n { value: 'fileman', label: 'File Manager' },\n { value: 'api', label: 'API' },\n { value: 'other', label: 'Other' },\n];\n\nconst TWITTER_CARD_OPTIONS = [\n { value: '', label: '— None —' },\n { value: 'summary', label: 'summary' },\n { value: 'summary_large_image', label: 'summary_large_image' },\n];\n\n/* =========================\n * Metadata helpers\n * ========================= */\n\n/**\n * Flatten a ShortLink metadata object (keys like \"og:title\") into sibling form\n * fields (og_title, twitter_card, ...). Used to seed edit dialogs.\n */\nfunction flattenShortLinkMetadata(metadata = {}) {\n const m = metadata || {};\n return {\n og_title: m['og:title'] || '',\n og_description: m['og:description'] || '',\n og_image: m['og:image'] || '',\n twitter_card: m['twitter:card'] || '',\n twitter_title: m['twitter:title'] || '',\n twitter_description: m['twitter:description'] || '',\n twitter_image: m['twitter:image'] || '',\n };\n}\n\n/**\n * Inverse of flattenShortLinkMetadata — combines og_ and twitter_ form fields\n * into a flat metadata object with colon-keyed keys. Empty values are dropped.\n */\nfunction buildShortLinkMetadata(formData = {}) {\n const map = {\n og_title: 'og:title',\n og_description: 'og:description',\n og_image: 'og:image',\n twitter_card: 'twitter:card',\n twitter_title: 'twitter:title',\n twitter_description: 'twitter:description',\n twitter_image: 'twitter:image',\n };\n const metadata = {};\n for (const [flatKey, targetKey] of Object.entries(map)) {\n const v = formData[flatKey];\n if (v !== undefined && v !== null && v !== '') {\n metadata[targetKey] = v;\n }\n }\n return metadata;\n}\n\n/**\n * Pull og_ and twitter_ fields out of a form-data object and collapse them into\n * `metadata`. Returns a new object safe to send to the REST API. If no OG or\n * Twitter fields are set, `metadata` is omitted entirely so the backend's\n * auto-scraper can fill in the gaps.\n */\nfunction extractShortLinkPayload(formData = {}) {\n const flatKeys = [\n 'og_title', 'og_description', 'og_image',\n 'twitter_card', 'twitter_title', 'twitter_description', 'twitter_image',\n ];\n const payload = { ...formData };\n for (const k of flatKeys) delete payload[k];\n const metadata = buildShortLinkMetadata(formData);\n if (Object.keys(metadata).length > 0) {\n payload.metadata = metadata;\n }\n return payload;\n}\n\n/* =========================\n * ShortLink Model\n * ========================= */\n\nclass ShortLink extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/shortlink/link',\n });\n }\n}\n\n/* =========================\n * ShortLink Collection\n * ========================= */\n\nclass ShortLinkList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ShortLink,\n endpoint: '/api/shortlink/link',\n ...options,\n });\n }\n}\n\n/* =========================\n * ShortLinkClick (read-only)\n * ========================= */\n\nclass ShortLinkClick extends Model {\n constructor(data = {}) {\n super(data, {\n endpoint: '/api/shortlink/history',\n });\n }\n}\n\nclass ShortLinkClickList extends Collection {\n constructor(options = {}) {\n super({\n ModelClass: ShortLinkClick,\n endpoint: '/api/shortlink/history',\n ...options,\n });\n }\n}\n\n/* =========================\n * Forms\n * ========================= */\n\nconst _shortLinkSharedFields = [\n { name: 'url', type: 'url', label: 'Destination URL', required: true, placeholder: 'https://example.com/page', cols: 12 },\n { name: 'source', type: 'select', label: 'Source', options: SHORTLINK_SOURCE_OPTIONS, value: 'admin', cols: 6 },\n { name: 'expire_days', type: 'number', label: 'Expire (days)', value: 3, min: 0, cols: 3, help: '0 = never' },\n { name: 'expire_hours', type: 'number', label: 'Expire (hours)', value: 0, min: 0, cols: 3 },\n { name: 'track_clicks', type: 'switch', label: 'Track clicks', value: false, cols: 4, help: 'Record per-click history and per-link metrics.' },\n { name: 'bot_passthrough', type: 'switch', label: 'Bypass bot preview', value: false, cols: 4, help: 'Bots receive a plain redirect (use for transactional links).' },\n { name: 'is_protected', type: 'switch', label: 'Protected', value: false, cols: 4, help: 'Prevents accidental deletion.' },\n // ── OG Metadata (optional) ──\n { type: 'heading', label: 'OpenGraph Preview (optional)', cols: 12 },\n { name: 'og_title', type: 'text', label: 'og:title', placeholder: 'Shown in Slack/iMessage preview', cols: 12 },\n { name: 'og_description', type: 'textarea', label: 'og:description', rows: 2, cols: 12 },\n { name: 'og_image', type: 'url', label: 'og:image', placeholder: 'https://example.com/preview.jpg', cols: 12 },\n];\n\nconst ShortLinkForms = {\n create: {\n title: 'Create Shortlink',\n size: 'md',\n fields: [\n ..._shortLinkSharedFields,\n ],\n help: 'Leave OG fields blank to let the server scrape the destination automatically.',\n },\n\n edit: {\n title: 'Edit Shortlink',\n size: 'md',\n fields: [\n { name: 'is_active', type: 'switch', label: 'Active', cols: 4 },\n ..._shortLinkSharedFields,\n { name: 'twitter_card', type: 'select', label: 'twitter:card', options: TWITTER_CARD_OPTIONS, cols: 6 },\n { name: 'twitter_title', type: 'text', label: 'twitter:title', cols: 6 },\n { name: 'twitter_description', type: 'textarea', label: 'twitter:description', rows: 2, cols: 12 },\n { name: 'twitter_image', type: 'url', label: 'twitter:image', cols: 12 },\n ],\n },\n};\n\nShortLink.EDIT_FORM = ShortLinkForms.edit;\n\nexport {\n ShortLink,\n ShortLinkList,\n ShortLinkClick,\n ShortLinkClickList,\n ShortLinkForms,\n SHORTLINK_SOURCE_OPTIONS,\n TWITTER_CARD_OPTIONS,\n flattenShortLinkMetadata,\n buildShortLinkMetadata,\n extractShortLinkPayload,\n};\n","/**\n * FileView - Unified viewer for fileman File records\n *\n * Canonical viewer for `File` models returned by /api/fileman/file. Mirrors the\n * UserView pattern: a header block with a ContextMenu, plus a SideNavView that\n * switches between Preview / Details / Renditions / Metadata sections.\n *\n * Preview is driven by the backend `category` field (image, video, audio, pdf,\n * document, spreadsheet, presentation, archive, other). Each category has a\n * defined preview experience. Image and PDF previews delegate to the optional\n * lightbox extension (`window.MOJO.plugins.LightboxGallery` / `PDFViewer`);\n * when that extension isn't loaded, actions fall back to `window.open`.\n *\n * Features:\n * - Category-aware Preview section (inline <video>/<audio>, lightbox, pdf viewer)\n * - DataView-powered Details with all file metadata\n * - TableView-powered Renditions (hidden when there are no renditions)\n * - Auto-generated Metadata DataView from `model.metadata` (hidden when empty)\n * - ContextMenu actions: View, Download, Edit Details, Make Public/Private,\n * Copy URL, Delete\n * - Emits `file:deleted` after successful delete\n */\n\nimport View from '@core/View.js';\nimport SideNavView from '@core/views/navigation/SideNavView.js';\nimport DataView from '@core/views/data/DataView.js';\nimport TableView from '@core/views/table/TableView.js';\nimport ContextMenu from '@core/views/feedback/ContextMenu.js';\nimport Modal from '@core/views/feedback/Modal.js';\nimport { File, FileForms } from '@core/models/Files.js';\nimport { ShortLinkList } from '@core/models/ShortLink.js';\n\n// ──────────────────────────────────────────────────────────────────────────\n// Category → preview config\n// ──────────────────────────────────────────────────────────────────────────\n\nconst CATEGORY_CONFIG = {\n image: { icon: 'bi-image', previewType: 'image', badgeClass: 'bg-info' },\n video: { icon: 'bi-camera-video', previewType: 'video', badgeClass: 'bg-primary' },\n audio: { icon: 'bi-music-note-beamed', previewType: 'audio', badgeClass: 'bg-primary' },\n pdf: { icon: 'bi-file-earmark-pdf', previewType: 'pdf', badgeClass: 'bg-danger' },\n document: { icon: 'bi-file-earmark-text', previewType: 'document', badgeClass: 'bg-secondary' },\n spreadsheet: { icon: 'bi-file-earmark-spreadsheet', previewType: 'document', badgeClass: 'bg-success' },\n presentation: { icon: 'bi-file-earmark-slides', previewType: 'document', badgeClass: 'bg-warning' },\n archive: { icon: 'bi-file-earmark-zip', previewType: 'download', badgeClass: 'bg-dark' },\n other: { icon: 'bi-file-earmark', previewType: 'download', badgeClass: 'bg-secondary' }\n};\n\nfunction getCategoryConfig(model) {\n const cat = (model && typeof model.getCategory === 'function')\n ? model.getCategory()\n : (model?.get?.('category') || 'other');\n return CATEGORY_CONFIG[cat] || CATEGORY_CONFIG.other;\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FilePreviewSection — category-aware preview (inside SideNavView content)\n//\n// NOTE: SideNavView assigns `view.parent = this` to its section views\n// (SideNavView.js:102), so events bubble to SideNavView, not FileView.\n// Action handlers are therefore duplicated here; both touch only\n// `this.model` + `window.MOJO.plugins`, so there is no cross-talk risk.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FilePreviewSection extends View {\n constructor(options = {}) {\n // Pass `model` through — View.setModel wires up the 'change'\n // listener automatically, no need to duplicate it here.\n super({\n className: 'file-preview-section p-3',\n ...options\n });\n this.categoryConfig = options.categoryConfig || CATEGORY_CONFIG.other;\n }\n\n /**\n * Override the base re-render-on-model-change behavior.\n * - video/audio elements must NOT re-render: the rendition poll fires\n * model `change` every 5s while transcoding, and re-rendering would\n * destroy and recreate the media element on every tick (visible\n * reload cycle, lost playback state).\n * - image/pdf/document/archive previews are idempotent — re-render\n * safely to pick up new rendition URLs as they arrive.\n */\n _onModelChange() {\n const type = this.categoryConfig?.previewType;\n if (type === 'video' || type === 'audio') return;\n if (this.isMounted()) this.render();\n }\n\n getTemplate() {\n const type = this.categoryConfig.previewType;\n const url = this.model.get('url') || '';\n const filename = this.model.get('filename') || '';\n\n if (type === 'image') {\n const previewUrl = (this.model.getThumbnailUrl && this.model.getThumbnailUrl()) || url;\n return `\n <div class=\"text-center\">\n <img src=\"${escapeAttr(previewUrl)}\"\n alt=\"${escapeAttr(filename)}\"\n class=\"img-fluid rounded shadow-sm\"\n style=\"max-height: 70vh; cursor: zoom-in;\"\n data-action=\"view-file\" role=\"button\">\n <div class=\"text-muted small mt-2\">Click image for full view</div>\n </div>\n `;\n }\n\n if (type === 'video') {\n const poster = this.model.getThumbnailUrl && this.model.getThumbnailUrl();\n return `\n <div class=\"text-center\">\n <video controls preload=\"metadata\"\n src=\"${escapeAttr(url)}\"\n ${poster ? `poster=\"${escapeAttr(poster)}\"` : ''}\n style=\"width: 100%; max-height: 70vh; background:#000;\"></video>\n </div>\n `;\n }\n\n if (type === 'audio') {\n return `\n <div class=\"p-4 bg-light rounded text-center\">\n <i class=\"bi ${this.categoryConfig.icon} display-4 text-secondary\"></i>\n <h5 class=\"mt-3 mb-3 text-break\">${escapeHtml(filename)}</h5>\n <audio controls class=\"w-100\" src=\"${escapeAttr(url)}\"></audio>\n </div>\n `;\n }\n\n if (type === 'pdf') {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi ${this.categoryConfig.icon} text-danger\" style=\"font-size: 5rem;\"></i>\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <div class=\"mt-4\">\n <button type=\"button\" class=\"btn btn-primary me-2\" data-action=\"view-file\">\n <i class=\"bi bi-eye me-1\"></i>Open PDF Viewer\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n if (type === 'document') {\n const preview = this.model.getBestImageRendition && this.model.getBestImageRendition();\n return `\n <div class=\"text-center p-4 bg-light rounded\">\n ${preview\n ? `<img src=\"${escapeAttr(preview.url)}\" alt=\"${escapeAttr(filename)} preview\" class=\"img-fluid rounded mb-3\" style=\"max-height: 50vh;\">`\n : `<i class=\"bi ${this.categoryConfig.icon} text-secondary\" style=\"font-size: 5rem;\"></i>`}\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary me-2\" data-action=\"view-file\">\n <i class=\"bi bi-box-arrow-up-right me-1\"></i>Open\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n // archive / other / download fallback\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi ${this.categoryConfig.icon} text-secondary\" style=\"font-size: 5rem;\"></i>\n <h5 class=\"mt-3 text-break\">${escapeHtml(filename)}</h5>\n <p class=\"text-muted small\">No inline preview available for this file type.</p>\n <div class=\"mt-3\">\n <button type=\"button\" class=\"btn btn-primary\" data-action=\"download-file\">\n <i class=\"bi bi-download me-1\"></i>Download\n </button>\n </div>\n </div>\n `;\n }\n\n async onActionViewFile() {\n openFileInPreview(this.model, this.categoryConfig);\n }\n\n async onActionDownloadFile() {\n downloadFile(this.model);\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileRenditionsSection — Renditions gallery\n//\n// Renders each rendition as a card tile: inline preview (image thumbnail,\n// video poster with play overlay, or category icon), role badge, dimensions\n// + size, and three actions (Preview, Copy URL, Download).\n//\n// Three states:\n// - Gallery grid when renditions exist\n// - Processing placeholder when upload_status === 'completed' and the\n// backend worker hasn't populated renditions yet\n// - \"Upload in progress\" placeholder otherwise\n//\n// `model:` flows through to View.setModel which wires the built-in\n// 'change' → _onModelChange → render() listener (guarded by isMounted).\n// That's exactly what we want: swap placeholder for the grid, or add new\n// rendition cards, as the rendition poll brings them in.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileRenditionsSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-renditions-section p-3',\n ...options\n });\n }\n\n getTemplate() {\n if (this.model.hasRenditions && this.model.hasRenditions()) {\n return this._buildGalleryTemplate();\n }\n if (this.model.isUploadPending && this.model.isUploadPending()) {\n return this._buildWaitingTemplate();\n }\n // Upload finished, but the rendition map is empty — no work is in\n // progress (per the backend: completed === done). Show a clean empty\n // state with a Regenerate button instead of an indefinite spinner.\n return this._buildEmptyTemplate();\n }\n\n _buildGalleryTemplate() {\n const renditions = this.model.getRenditions();\n const cards = renditions.map(r => this._buildCard(r)).join('');\n const count = renditions.length;\n return `\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"text-muted small\">\n ${count} rendition${count === 1 ? '' : 's'}\n </div>\n <div class=\"btn-group btn-group-sm\" role=\"group\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"refresh-renditions\" title=\"Refresh list\">\n <i class=\"bi bi-arrow-clockwise\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-outline-secondary\" data-action=\"regenerate-from-section\" title=\"Rebuild all previews\">\n <i class=\"bi bi-arrow-repeat me-1\"></i>Regenerate\n </button>\n </div>\n </div>\n <div class=\"row g-3\">${cards}</div>\n `;\n }\n\n _buildCard(r) {\n const url = r && r.url ? r.url : '';\n const ct = r && typeof r.content_type === 'string' ? r.content_type : '';\n const role = r && r.role ? r.role : 'rendition';\n const filename = r && r.filename ? r.filename : role;\n const size = r && r.file_size ? formatBytes(r.file_size) : '';\n const dimensions = (r && r.width && r.height) ? `${r.width} × ${r.height}` : '';\n\n const viewData = [\n `data-action=\"view-rendition\"`,\n `data-url=\"${escapeAttr(url)}\"`,\n `data-ct=\"${escapeAttr(ct)}\"`,\n `data-filename=\"${escapeAttr(filename)}\"`,\n `data-role=\"${escapeAttr(role)}\"`\n ].join(' ');\n\n let preview;\n if (ct.startsWith('image/') && url) {\n preview = `<img src=\"${escapeAttr(url)}\" alt=\"${escapeAttr(role)}\"\n loading=\"lazy\"\n class=\"w-100\"\n style=\"height: 140px; object-fit: cover; background: #f8f9fa; border-top-left-radius: var(--bs-card-inner-border-radius); border-top-right-radius: var(--bs-card-inner-border-radius);\">`;\n } else if (ct.startsWith('video/')) {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center position-relative\"\n style=\"height: 140px; background: linear-gradient(135deg, #212529 0%, #343a40 100%); color: #fff;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-play-circle-fill\" style=\"font-size: 2.75rem; opacity: 0.9;\"></i>\n <span class=\"position-absolute bottom-0 start-0 end-0 text-center small py-1\"\n style=\"background: rgba(0,0,0,0.35); font-variant-numeric: tabular-nums;\">\n ${escapeHtml(ct)}\n </span>\n </div>`;\n } else if (ct.startsWith('audio/')) {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center\"\n style=\"height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-music-note-beamed text-secondary\" style=\"font-size: 2.5rem;\"></i>\n </div>`;\n } else {\n preview = `\n <div class=\"d-flex align-items-center justify-content-center\"\n style=\"height: 140px; background: #f8f9fa;\n border-top-left-radius: var(--bs-card-inner-border-radius);\n border-top-right-radius: var(--bs-card-inner-border-radius);\">\n <i class=\"bi bi-file-earmark text-secondary\" style=\"font-size: 2.5rem;\"></i>\n </div>`;\n }\n\n const footer = url ? `\n <div class=\"card-footer p-1 d-flex gap-1 bg-white border-top-0\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary flex-fill\"\n ${viewData}\n title=\"Preview\">\n <i class=\"bi bi-eye\"></i>\n </button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary flex-fill\"\n data-action=\"copy-rendition-url\" data-url=\"${escapeAttr(url)}\"\n title=\"Copy URL\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n <a href=\"${escapeAttr(url)}\" download=\"${escapeAttr(filename)}\"\n class=\"btn btn-sm btn-outline-secondary flex-fill\"\n title=\"Download\"\n data-stop-propagation>\n <i class=\"bi bi-download\"></i>\n </a>\n </div>\n ` : '';\n\n return `\n <div class=\"col-sm-6 col-md-4 col-lg-3\">\n <div class=\"card h-100 shadow-sm rendition-card\">\n <div ${url ? viewData : ''} ${url ? 'role=\"button\" style=\"cursor: pointer;\"' : ''}>\n ${preview}\n </div>\n <div class=\"card-body p-2\">\n <div class=\"d-flex justify-content-between align-items-start gap-2 mb-1\">\n <span class=\"badge bg-secondary text-truncate\" style=\"max-width: 100%;\" title=\"${escapeAttr(role)}\">${escapeHtml(role)}</span>\n ${size ? `<small class=\"text-muted flex-shrink-0\" style=\"font-variant-numeric: tabular-nums;\">${escapeHtml(size)}</small>` : ''}\n </div>\n ${dimensions ? `<div class=\"small text-muted\" style=\"font-variant-numeric: tabular-nums;\">${escapeHtml(dimensions)}</div>` : ''}\n </div>\n ${footer}\n </div>\n </div>\n `;\n }\n\n _buildEmptyTemplate() {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi bi-layers display-6 text-muted\"></i>\n <h6 class=\"mt-3 mb-1\">No renditions for this file</h6>\n <p class=\"text-muted small mb-3\">\n Click <strong>Regenerate</strong> to (re)build thumbnails and previews on the backend.\n </p>\n <div class=\"d-inline-flex gap-2\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" data-action=\"refresh-renditions\">\n <i class=\"bi bi-arrow-clockwise me-1\"></i>Refresh\n </button>\n <button type=\"button\" class=\"btn btn-sm btn-outline-primary\" data-action=\"regenerate-from-section\">\n <i class=\"bi bi-arrow-repeat me-1\"></i>Regenerate\n </button>\n </div>\n </div>\n `;\n }\n\n _buildWaitingTemplate() {\n return `\n <div class=\"text-center p-5 bg-light rounded\">\n <i class=\"bi bi-hourglass-split display-6 text-muted\"></i>\n <h6 class=\"mt-3 mb-1\">Upload still in progress</h6>\n <p class=\"text-muted small mb-0\">Renditions will be generated once the upload completes.</p>\n </div>\n `;\n }\n\n // ── Actions ─────────────────────────────────────\n\n async onActionRefreshRenditions() {\n try {\n await this.model.fetch();\n } catch (err) {\n console.warn('FileView: refresh-renditions fetch failed:', err);\n }\n }\n\n async onActionViewRendition(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const url = element.dataset.url;\n const ct = element.dataset.ct || '';\n if (!url) return;\n\n if (ct.startsWith('image/')) {\n const Lightbox = typeof window !== 'undefined' ? window.MOJO?.plugins?.LightboxGallery : null;\n if (Lightbox && typeof Lightbox.show === 'function') {\n const images = this.model.getRenditions()\n .filter(r => r && r.url && typeof r.content_type === 'string' && r.content_type.startsWith('image/'))\n .map(r => ({ src: r.url, alt: r.role || '' }));\n const startIndex = Math.max(0, images.findIndex(img => img.src === url));\n Lightbox.show(images, { startIndex, fitToScreen: false });\n return;\n }\n }\n // Non-image renditions (video, audio, anything else) open in a new tab\n // — browsers have native players for these and the lightbox plugin\n // only handles images.\n window.open(url, '_blank', 'noopener');\n }\n\n async onActionCopyRenditionUrl(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const url = element.dataset.url;\n if (!url) return;\n try {\n if (navigator.clipboard && window.isSecureContext) {\n await navigator.clipboard.writeText(url);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = url;\n document.body.appendChild(textarea);\n textarea.select();\n document.execCommand('copy');\n document.body.removeChild(textarea);\n }\n // Short-lived visual confirmation on the clicked button\n const icon = element.querySelector('i');\n if (icon) {\n const orig = icon.className;\n icon.className = 'bi bi-check-lg text-success';\n setTimeout(() => { icon.className = orig; }, 1200);\n }\n this.getApp()?.toast?.success?.('URL copied to clipboard');\n } catch (err) {\n console.error('Failed to copy rendition URL:', err);\n this.getApp()?.toast?.error?.('Failed to copy URL');\n }\n }\n\n // Delegates up the parent chain to FileView.onActionRegenerateRenditions\n // so the section button behaves identically to the ContextMenu item.\n async onActionRegenerateFromSection() {\n let node = this.parent;\n while (node) {\n if (typeof node.onActionRegenerateRenditions === 'function') {\n return node.onActionRegenerateRenditions();\n }\n node = node.parent;\n }\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Shared action helpers (also used by FileView's ContextMenu-triggered actions)\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction openFileInPreview(model, categoryConfig) {\n const url = model.get('url');\n if (!url) return;\n const type = categoryConfig.previewType;\n\n if (type === 'image') {\n const Lightbox = typeof window !== 'undefined' ? window.MOJO?.plugins?.LightboxGallery : null;\n const renditions = model.get('renditions') || {};\n const images = [\n { src: url, alt: 'Original' },\n ...Object.values(renditions)\n .filter(r => r && r.url && typeof r.content_type === 'string' && r.content_type.startsWith('image/'))\n .map(r => ({ src: r.url, alt: r.role || '' }))\n ];\n if (Lightbox && typeof Lightbox.show === 'function') {\n Lightbox.show(images, { fitToScreen: false });\n } else {\n window.open(url, '_blank');\n }\n return;\n }\n\n if (type === 'pdf') {\n const PDFViewer = typeof window !== 'undefined' ? window.MOJO?.plugins?.PDFViewer : null;\n if (PDFViewer && typeof PDFViewer.showDialog === 'function') {\n PDFViewer.showDialog(url, { title: model.get('filename') });\n } else {\n window.open(url, '_blank');\n }\n return;\n }\n\n window.open(url, '_blank');\n}\n\nfunction downloadFile(model) {\n const url = model.get('url');\n if (!url) return;\n const a = document.createElement('a');\n a.href = url;\n a.download = model.get('filename') || '';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileSharesSection — Active shortlink shares for this file\n//\n// Lists ShortLink rows where source=fileman-share and file=<this.id>. Each\n// share is a distinct shortlink minted by `model.share({...})` (see\n// Files.js). Revoking a share flips `is_active=false`; the audit row is\n// preserved per the backend contract — never DELETE the row, that just\n// causes a fresh shortlink to be minted on next read.\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileSharesSection extends View {\n constructor(options = {}) {\n super({\n className: 'file-shares-section p-3',\n ...options\n });\n this.template = `\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"text-muted small\">\n Active and historical shareable links for this file.\n </div>\n <button type=\"button\" class=\"btn btn-sm btn-primary\"\n data-action=\"share-file-from-section\">\n <i class=\"bi bi-link-45deg me-1\"></i>Share new\n </button>\n </div>\n <div data-container=\"file-shares-table\"></div>\n `;\n }\n\n async onInit() {\n const fileId = this.model.get('id');\n if (!fileId) return;\n\n const collection = new ShortLinkList({\n params: {\n source: 'fileman-share',\n file: fileId,\n sort: '-created',\n size: 25,\n },\n });\n this._sharesCollection = collection;\n\n this.sharesTable = new TableView({\n containerId: 'file-shares-table',\n collection,\n hideActivePillNames: ['source', 'file'],\n columns: [\n {\n key: 'code',\n label: 'Short URL',\n template: `\n <div class=\"d-flex align-items-center gap-2\">\n <code>{{model.code}}</code>\n <button class=\"btn btn-sm btn-link p-0 text-muted\"\n data-action=\"copy-share-code\"\n data-code=\"{{model.code}}\"\n title=\"Copy short URL\">\n <i class=\"bi bi-clipboard\"></i>\n </button>\n </div>\n `,\n },\n { key: 'user.display_name', label: 'Shared By', formatter: \"default('—')\" },\n { key: 'hit_count', label: 'Hits', width: '80px', sortable: true },\n { key: 'track_clicks', label: 'Tracked', width: '90px', formatter: 'yesnoicon' },\n { key: 'is_active', label: 'Active', width: '80px', formatter: 'yesnoicon' },\n { key: 'expires_at', label: 'Expires', width: '160px', formatter: \"datetime|default('Never')\", sortable: true },\n { key: 'created', label: 'Created', width: '160px', formatter: 'datetime', sortable: true },\n { key: 'metadata.note', label: 'Note', formatter: \"truncate(40)|default('—')\", visibility: 'lg' },\n ],\n paginated: true,\n sortable: true,\n searchable: false,\n filterable: false,\n contextMenu: [\n { label: 'Copy Short URL', action: 'copy-share-code', icon: 'bi-clipboard' },\n { divider: true },\n { label: 'Revoke', action: 'revoke-share', icon: 'bi-slash-circle', danger: true },\n ],\n tableOptions: {\n hover: true,\n size: 'sm',\n emptyMessage: 'No shares yet — click “Share new” to mint a tracked link.',\n emptyIcon: 'bi-link-45deg',\n actions: [],\n },\n });\n this.addChild(this.sharesTable);\n }\n\n /** Re-fetch the shares list (called after a new share is minted). */\n refreshShares() {\n return this._sharesCollection?.fetch();\n }\n\n // Section bubbles section-scoped actions up to FileView. Mirrors the\n // FileRenditionsSection.onActionRegenerateFromSection pattern.\n async onActionShareFileFromSection() {\n let node = this.parent;\n while (node) {\n if (typeof node.onActionShareFile === 'function') {\n return node.onActionShareFile();\n }\n node = node.parent;\n }\n return null;\n }\n\n async onActionCopyShareCode(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n const code = element?.dataset?.code;\n if (!code) return;\n const url = buildShortUrl(code, this.getApp?.());\n try {\n await navigator.clipboard.writeText(url);\n this.getApp()?.toast?.success?.(`Copied: ${url}`);\n } catch (_e) {\n this.getApp()?.toast?.warning?.('Copy failed — select the URL manually.');\n }\n }\n\n async onActionRevokeShare(event, element) {\n if (event) { event.preventDefault(); event.stopPropagation(); }\n // The contextMenu wires `data-id` on the row; resolve from the table.\n const row = element?.closest?.('[data-row-id]');\n const id = row?.dataset?.rowId || element?.dataset?.id;\n if (!id) return;\n const target = this._sharesCollection?.get?.(id);\n if (!target) return;\n\n const confirmed = await Modal.confirm(\n 'Revoke this share? Anyone with the short URL will get a 404 — the audit row is preserved.',\n 'Revoke Share',\n { confirmText: 'Revoke', confirmClass: 'btn-danger' }\n );\n if (!confirmed) return;\n\n try {\n await target.save({ is_active: false });\n this.getApp()?.toast?.success?.('Share revoked');\n await this.refreshShares();\n } catch (err) {\n console.error('Failed to revoke share:', err);\n this.getApp()?.toast?.error?.('Failed to revoke share');\n }\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// FileView (main component)\n// ──────────────────────────────────────────────────────────────────────────\n\nclass FileView extends View {\n constructor(options = {}) {\n super({\n className: 'file-view',\n ...options\n });\n\n this.model = options.model || new File(options.data || {});\n this.sideNavView = null;\n this.contextMenu = null;\n\n // The SideNavView uses flex + overflow-y: auto on its content panel,\n // which needs a *bounded* parent height to scroll correctly. Without\n // a max-height the nav content stretches the dialog and overflows\n // past its own bounds. `min-height` keeps short content from looking\n // cramped; `max-height: 70vh` keeps tall content scrollable inside\n // the dialog on any viewport.\n this.template = `\n <div class=\"file-view-container d-flex flex-column\" style=\"min-height: 0;\">\n <!-- Header + Context Menu -->\n <div class=\"d-flex justify-content-between align-items-start mb-3 flex-shrink-0\">\n <div data-container=\"file-header\" style=\"flex: 1; min-width: 0;\"></div>\n <div data-container=\"file-context-menu\" class=\"ms-3 flex-shrink-0\"></div>\n </div>\n <!-- Section body -->\n <div data-container=\"file-sidenav\" class=\"flex-grow-1\" style=\"min-height: 400px; max-height: 70vh;\"></div>\n </div>\n `;\n }\n\n _getCategoryConfig() {\n return getCategoryConfig(this.model);\n }\n\n async onInit() {\n const categoryConfig = this._getCategoryConfig();\n\n // ── Header ──────────────────────────────────\n this.header = new View({\n containerId: 'file-header',\n template: this._buildHeaderTemplate(categoryConfig)\n });\n this.header.setModel(this.model);\n this.addChild(this.header);\n\n // ── Section views ───────────────────────────\n const sections = [];\n\n // Preview — always shown, default active\n const previewView = new FilePreviewSection({\n model: this.model,\n categoryConfig\n });\n sections.push({ key: 'preview', label: 'Preview', icon: categoryConfig.icon, view: previewView });\n\n // Details — always shown\n const detailsView = new DataView({\n model: this.model,\n className: 'p-3',\n showEmptyValues: true,\n emptyValueText: '—',\n columns: 2,\n fields: [\n { name: 'id', label: 'ID' },\n { name: 'filename', label: 'Filename' },\n { name: 'storage_filename', label: 'Storage Filename' },\n { name: 'content_type', label: 'Content Type' },\n { name: 'file_size', label: 'File Size', format: 'filesize' },\n { name: 'category', label: 'Category' },\n { name: 'upload_status', label: 'Status', format: 'badge' },\n { name: 'created', label: 'Created', format: 'datetime' },\n { name: 'modified', label: 'Modified', format: 'datetime' },\n { name: 'user.display_name', label: 'Uploaded By' },\n { name: 'file_manager.name', label: 'Storage Backend' },\n { name: 'storage_file_path', label: 'Storage Path' },\n { name: 'url', label: 'Public URL', format: 'url' },\n { name: 'is_public', label: 'Is Public', format: 'boolean' }\n ]\n });\n sections.push({ key: 'details', label: 'Details', icon: 'bi-info-circle', view: detailsView });\n\n // Renditions — always shown; the section itself decides whether to\n // render the table, a \"processing\" placeholder, or a \"upload pending\"\n // placeholder based on current model state. Backend renditions are\n // async, so even completed files may start with an empty map.\n const renditionsView = new FileRenditionsSection({ model: this.model });\n sections.push({ key: 'renditions', label: 'Renditions', icon: 'bi-layers', view: renditionsView });\n\n // Shares — list of shortlinks (source=fileman-share) for this file.\n // Hidden for unsaved models — the section needs an `id` to scope.\n if (this.model.get('id')) {\n this.sharesSection = new FileSharesSection({ model: this.model });\n sections.push({ key: 'shares', label: 'Shares', icon: 'bi-link-45deg', view: this.sharesSection });\n }\n\n // Metadata — only when backend returned a non-empty metadata object\n const metadata = this.model.get('metadata');\n if (metadata && typeof metadata === 'object' && Object.keys(metadata).length) {\n const metadataView = new DataView({\n data: metadata,\n className: 'p-3',\n columns: 2,\n showEmptyValues: false\n });\n sections.push({ key: 'metadata', label: 'Metadata', icon: 'bi-braces', view: metadataView });\n }\n\n // ── SideNavView ─────────────────────────────\n this.sideNavView = new SideNavView({\n containerId: 'file-sidenav',\n activeSection: 'preview',\n navWidth: 200,\n contentPadding: '1.25rem 1.5rem',\n enableResponsive: true,\n minWidth: 500,\n sections\n });\n this.addChild(this.sideNavView);\n\n // ── Context Menu ────────────────────────────\n this.contextMenu = new ContextMenu({\n containerId: 'file-context-menu',\n className: 'context-menu-view header-menu-absolute',\n context: this.model,\n config: {\n icon: 'bi-three-dots-vertical',\n items: [\n { label: 'View', action: 'view-file', icon: 'bi-eye' },\n { label: 'Download', action: 'download-file', icon: 'bi-download' },\n { label: 'Copy URL', action: 'copy-url', icon: 'bi-clipboard' },\n { label: 'Share Link…', action: 'share-file', icon: 'bi-link-45deg' },\n { type: 'divider' },\n { label: 'Edit Details', action: 'edit-file', icon: 'bi-pencil' },\n this.model.get('is_public')\n ? { label: 'Make Private', action: 'make-private', icon: 'bi-lock' }\n : { label: 'Make Public', action: 'make-public', icon: 'bi-unlock' },\n { label: 'Regenerate Previews', action: 'regenerate-renditions', icon: 'bi-arrow-repeat' },\n { type: 'divider' },\n { label: 'Delete File', action: 'delete-file', icon: 'bi-trash', danger: true }\n ]\n }\n });\n this.addChild(this.contextMenu);\n\n // No auto-poll: a `completed` upload means renditions are done. If the\n // user explicitly clicks \"Regenerate\", _maybeStartRenditionsPoll is\n // invoked with { force: true } from that handler so the new renditions\n // appear without a manual refresh.\n }\n\n async onBeforeDestroy() {\n this._stopRenditionsPoll();\n }\n\n _buildHeaderTemplate(categoryConfig) {\n const thumbnailUrl = this.model.getThumbnailUrl && this.model.getThumbnailUrl();\n const thumbHtml = thumbnailUrl\n ? `<img src=\"${escapeAttr(thumbnailUrl)}\" alt=\"thumbnail\" class=\"rounded\" style=\"width: 80px; height: 80px; object-fit: cover;\">`\n : `<div class=\"rounded bg-light d-flex align-items-center justify-content-center\" style=\"width: 80px; height: 80px;\">\n <i class=\"bi ${categoryConfig.icon} text-secondary\" style=\"font-size: 2.25rem;\"></i>\n </div>`;\n\n return `\n <div class=\"d-flex align-items-center gap-3\">\n <div class=\"file-view-thumb flex-shrink-0\">\n ${thumbHtml}\n </div>\n <div class=\"flex-grow-1\" style=\"min-width:0;\">\n <h3 class=\"mb-1 text-break\">{{model.filename|default('Unnamed file')}}</h3>\n <div class=\"text-muted small d-flex flex-wrap align-items-center gap-2\">\n <span><i class=\"bi bi-hdd me-1\"></i>{{model.file_size|filesize}}</span>\n <span class=\"text-muted\">·</span>\n <span>{{model.content_type|default('unknown')}}</span>\n <span class=\"text-muted\">·</span>\n <span class=\"badge ${categoryConfig.badgeClass}\">{{model.category|default('other')|capitalize}}</span>\n {{#model.upload_status|bool}}\n <span class=\"text-muted\">·</span>\n <span class=\"badge {{model.upload_status|badge}}\">{{model.upload_status|capitalize}}</span>\n {{/model.upload_status|bool}}\n {{#model.is_public|bool}}\n <span class=\"text-muted\">·</span>\n <span class=\"badge bg-success\"><i class=\"bi bi-unlock me-1\"></i>Public</span>\n {{/model.is_public|bool}}\n {{^model.is_public|bool}}\n <span class=\"text-muted\">·</span>\n <span class=\"badge bg-secondary\"><i class=\"bi bi-lock me-1\"></i>Private</span>\n {{/model.is_public|bool}}\n </div>\n {{#model.created|bool}}\n <div class=\"text-muted small mt-1\">\n Uploaded {{model.created|epoch|datetime}}\n </div>\n {{/model.created|bool}}\n </div>\n </div>\n `;\n }\n\n // ── Action handlers ─────────────────────────────\n\n async onActionViewFile() {\n openFileInPreview(this.model, this._getCategoryConfig());\n }\n\n async onActionDownloadFile() {\n downloadFile(this.model);\n }\n\n async onActionCopyUrl() {\n const url = this.model.get('url');\n if (!url) return;\n try {\n if (navigator.clipboard && window.isSecureContext) {\n await navigator.clipboard.writeText(url);\n } else {\n const textarea = document.createElement('textarea');\n textarea.value = url;\n document.body.appendChild(textarea);\n textarea.select();\n document.execCommand('copy');\n document.body.removeChild(textarea);\n }\n this.getApp()?.toast?.success?.('URL copied to clipboard');\n } catch (error) {\n console.error('Failed to copy URL:', error);\n this.getApp()?.toast?.error?.('Failed to copy URL');\n }\n }\n\n async onActionEditFile() {\n const resp = await Modal.modelForm({\n title: `Edit File - ${this.model.get('filename')}`,\n model: this.model,\n formConfig: FileForms.edit\n });\n if (resp) {\n this.render();\n }\n }\n\n async onActionMakePublic() {\n await this.model.save({ is_public: true });\n this.render();\n }\n\n async onActionMakePrivate() {\n await this.model.save({ is_public: false });\n this.render();\n }\n\n async onActionShareFile() {\n if (!this.model.get('id')) {\n this.getApp()?.toast?.warning?.('Save the file before sharing it.');\n return;\n }\n\n // Step 1 — collect share options.\n const formResult = await Modal.form({\n title: 'Share Link',\n size: 'sm',\n help: 'Each share creates a distinct, audit-tracked short URL attributed to you.',\n fields: [\n { name: 'expire_days', type: 'number', label: 'Expire after (days)', value: 30, min: 0, cols: 12, help: '0 = never expires. Server max: 3650.' },\n { name: 'track_clicks', type: 'switch', label: 'Track clicks', value: true, cols: 12, help: 'Records per-click history (IP, user-agent, bot/human).' },\n { name: 'note', type: 'textarea', label: 'Note (optional)', rows: 2, cols: 12, maxlength: 512, help: 'Private audit note — not shown to recipients.' },\n ],\n submitText: 'Share',\n });\n if (!formResult) return;\n\n // Build options dict — only include keys with meaningful values.\n const opts = {};\n if (formResult.expire_days !== undefined && formResult.expire_days !== null && formResult.expire_days !== '') {\n opts.expire_days = Number(formResult.expire_days);\n }\n if (formResult.track_clicks !== undefined) {\n opts.track_clicks = !!formResult.track_clicks;\n }\n if (formResult.note) {\n opts.note = String(formResult.note).slice(0, 512);\n }\n\n // Step 2 — mint the share.\n let resp;\n try {\n resp = await this.model.share(Object.keys(opts).length ? opts : true);\n } catch (err) {\n console.error('Share failed:', err);\n Modal.showError(err?.data?.error || err?.message || 'Failed to create share link');\n return;\n }\n\n const data = resp?.data;\n const shortUrl = data?.url;\n if (!resp?.success || !shortUrl) {\n Modal.showError(data?.error || 'Failed to create share link');\n return;\n }\n\n // Best-effort copy to clipboard so the user can paste immediately.\n let copied = false;\n try {\n await navigator.clipboard?.writeText?.(shortUrl);\n copied = true;\n } catch (_e) {\n copied = false;\n }\n\n // Step 3 — show the result with the URL + summary.\n const expiry = data.expires_at\n ? new Date(data.expires_at).toLocaleString()\n : 'Never';\n const tracked = data.track_clicks ? 'Yes' : 'No';\n const copyHint = copied\n ? '<div class=\"form-text text-success mb-2\"><i class=\"bi bi-check-circle me-1\"></i>Copied to clipboard.</div>'\n : '<div class=\"form-text text-muted mb-2\">Select the URL above to copy.</div>';\n const summary = `\n <div class=\"mb-2\">\n <label class=\"form-label small text-muted mb-1\">Share URL</label>\n <input type=\"text\" class=\"form-control font-monospace\" readonly value=\"${escapeAttr(shortUrl)}\">\n ${copyHint}\n </div>\n <dl class=\"row small mb-0\">\n <dt class=\"col-4 text-muted\">Expires</dt><dd class=\"col-8\">${escapeHtml(expiry)}</dd>\n <dt class=\"col-4 text-muted\">Tracked</dt><dd class=\"col-8\">${escapeHtml(tracked)}</dd>\n ${data.shortlink_code ? `<dt class=\"col-4 text-muted\">Code</dt><dd class=\"col-8\"><code>${escapeHtml(data.shortlink_code)}</code></dd>` : ''}\n </dl>\n `;\n await Modal.alert(summary, 'Share link created', { type: 'success' });\n\n // Step 4 — refresh the Shares section if it's mounted.\n try {\n await this.sharesSection?.refreshShares?.();\n } catch (err) {\n console.warn('Failed to refresh shares section:', err);\n }\n }\n\n async onActionRegenerateRenditions() {\n const confirmed = await Modal.confirm(\n 'Rebuild all previews and thumbnails for this file? Existing renditions will be replaced. Generation runs in the background and may take several minutes for video.',\n 'Regenerate Previews',\n { confirmText: 'Regenerate' }\n );\n if (!confirmed) return;\n\n try {\n await this.model.regenerateRenditions();\n this.getApp()?.toast?.success?.('Regenerating previews in the background…');\n } catch (err) {\n console.error('Failed to trigger regenerate_renditions:', err);\n this.getApp()?.toast?.error?.('Failed to start preview regeneration');\n return;\n }\n // Start polling so the new renditions appear automatically as the\n // worker finishes.\n this._maybeStartRenditionsPoll({ force: true });\n }\n\n // ── Renditions polling ──────────────────────────\n // Mirrors the IncidentView analysis-progress polling shape: recursive\n // setTimeout, attempt counter, no overlap with a previous fetch.\n\n _maybeStartRenditionsPoll(options = {}) {\n if (this._renditionsPollTimer) return; // already polling\n // Only the explicit `force: true` path remains — this is invoked from\n // onActionRegenerateRenditions to catch newly built renditions as the\n // worker finishes.\n if (!options.force) return;\n const maxAttempts = 60; // 5 minutes at 5s\n const intervalMs = 5000;\n let attempts = 0;\n\n const tick = () => {\n this._renditionsPollTimer = null;\n if (!this.model) return;\n if (this.model.hasRenditions && this.model.hasRenditions()) return;\n if (++attempts > maxAttempts) return;\n\n this._renditionsPollTimer = setTimeout(async () => {\n try {\n await this.model.fetch();\n } catch (err) {\n console.warn('FileView: renditions poll fetch failed:', err);\n }\n tick();\n }, intervalMs);\n };\n\n tick();\n }\n\n _stopRenditionsPoll() {\n if (this._renditionsPollTimer) {\n clearTimeout(this._renditionsPollTimer);\n this._renditionsPollTimer = null;\n }\n }\n\n async onActionDeleteFile() {\n const confirmed = await Modal.confirm(\n `Are you sure you want to delete the file \"${this.model.get('filename')}\"? This action cannot be undone.`,\n 'Confirm Deletion',\n { confirmClass: 'btn-danger', confirmText: 'Delete' }\n );\n if (!confirmed) return;\n\n const resp = await this.model.destroy();\n if (resp && resp.success) {\n this.emit('file:deleted', { model: this.model });\n }\n }\n\n // Prevent model changes from triggering a full re-render.\n // Section views manage their own reactivity. Same pattern as UserView.\n _onModelChange() {\n // no-op\n }\n\n async showSection(name) {\n if (this.sideNavView) {\n await this.sideNavView.showSection(name);\n }\n }\n\n getActiveSection() {\n return this.sideNavView ? this.sideNavView.getActiveSection() : null;\n }\n\n static create(options = {}) {\n return new FileView(options);\n }\n}\n\n// ──────────────────────────────────────────────────────────────────────────\n// Small HTML helpers — kept private to this file\n// ──────────────────────────────────────────────────────────────────────────\n\nfunction escapeHtml(str) {\n if (str == null) return '';\n return String(str)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Build a full short URL from a shortlink code. Prefers\n * `app.config.shortlink_base_url`, falls back to `window.location.origin`.\n * Mirrors the helper in src/extensions/admin/shortlinks/ShortLinkView.js\n * but lives here so this core view doesn't depend on the admin extension.\n */\nfunction buildShortUrl(code, app) {\n if (!code) return '';\n const base =\n app?.config?.shortlink_base_url ||\n (typeof window !== 'undefined' ? window.location.origin : '');\n return `${String(base).replace(/\\/+$/, '')}/s/${code}`;\n}\n\nfunction escapeAttr(str) {\n return escapeHtml(str);\n}\n\nfunction formatBytes(bytes) {\n if (bytes == null || isNaN(bytes)) return '';\n const n = Number(bytes);\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;\n return `${(n / 1024 / 1024 / 1024).toFixed(1)} GB`;\n}\n\nFile.VIEW_CLASS = FileView;\nFile.MODEL_REF = 'fileman.File';\n\nexport default FileView;\n","import View from '@core/View.js';\nimport FilePreviewView from '@core/views/data/FilePreviewView.js';\n\n/**\n * ChatMessageView - Individual message display with theme support\n * \n * Supports two themes:\n * - 'compact': List-based admin/activity feed style\n * - 'bubbles': Chat bubble style with left/right positioning\n */\nclass ChatMessageView extends View {\n constructor(options = {}) {\n // Compute full className before super() so _syncAttrs picks it up\n const message = options.message || {};\n const theme = options.theme || 'compact';\n const isCurrentUser = options.isCurrentUser || false;\n const role = message.role || (isCurrentUser ? 'user' : null);\n\n let cls = 'chat-message';\n if (theme === 'bubbles') {\n cls += isCurrentUser ? ' message-right' : ' message-left';\n }\n if (role === 'assistant') cls += ' message-assistant';\n else if (role === 'user') cls += ' message-user';\n\n super({\n className: cls,\n ...options\n });\n\n this.message = message;\n this.theme = theme;\n this.isCurrentUser = isCurrentUser;\n this.role = role;\n }\n\n getTemplate() {\n // System event messages (same for both themes). Content may contain\n // pre-rendered HTML (e.g. status-change badges), so use triple-brace.\n if (this.message.type === 'system_event') {\n return `\n <div class=\"chat-message-system text-center text-muted small py-2\">\n <i class=\"bi bi-info-circle me-1\"></i>\n {{{message.content}}}\n </div>\n `;\n }\n\n // Theme-specific templates\n if (this.theme === 'bubbles') {\n return this.getBubblesTemplate();\n } else {\n return this.getCompactTemplate();\n }\n }\n\n /**\n * Get compact theme template (Option 4 - Admin/Activity Feed Style)\n */\n getCompactTemplate() {\n const userClass = this.isCurrentUser ? 'bg-primary' : 'bg-secondary';\n const isAssistant = this.role === 'assistant';\n\n return `\n <div class=\"message-item\">\n <div class=\"message-avatar ${isAssistant ? 'bg-dark' : userClass}\">\n ${isAssistant ? '<img src=\"https://mojo-verify.s3.amazonaws.com/signatures/14e7aab75c2749cb846f7d57298691ac/mojo_ai_7c0322e9.png\" class=\"mojo-avatar-icon\" alt=\"\">' : `\n {{#message.author.avatarUrl}}\n <img src=\"{{message.author.avatarUrl}}\" alt=\"{{message.author.name}}\" class=\"w-100 h-100 rounded-circle\">\n {{/message.author.avatarUrl}}\n {{^message.author.avatarUrl}}\n {{message.author.name|initials}}\n {{/message.author.avatarUrl}}\n `}\n </div>\n <div class=\"message-content\">\n <div class=\"message-header\">\n <div class=\"message-author\">\n ${isAssistant ? 'Mojo' : '{{message.author.name}}'}\n {{#isCurrentUser}}\n <span class=\"badge bg-primary badge-sm ms-1\">You</span>\n {{/isCurrentUser}}\n </div>\n <div class=\"message-time text-muted\">{{message.timestamp|relative}}</div>\n </div>\n <div class=\"message-text\">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container=\"blocks-${this.message.id || this.id}\"></div>\n <div data-container=\"attachments\"></div>\n </div>\n </div>\n `;\n }\n\n /**\n * Get bubbles theme template (Option 1 - Modern Chat Bubbles)\n */\n getBubblesTemplate() {\n const isAssistant = this.role === 'assistant';\n\n return `\n <div class=\"message-bubble-wrapper\">\n <div class=\"message-meta\">\n <strong>${isAssistant ? '<img src=\"https://mojo-verify.s3.amazonaws.com/signatures/14e7aab75c2749cb846f7d57298691ac/mojo_ai_7c0322e9.png\" class=\"mojo-avatar-icon me-1\" alt=\"\">Mojo' : '{{message.author.name}}'}</strong>\n <span class=\"text-muted\">· {{message.timestamp|relative}}</span>\n </div>\n <div class=\"message-bubble\">\n <div class=\"message-text\">{{{message.content}}}</div>\n ${this._getToolCallsTemplate()}\n <div data-container=\"blocks-${this.message.id || this.id}\"></div>\n <div data-container=\"attachments\"></div>\n </div>\n </div>\n `;\n }\n\n /**\n * Get tool calls display template (collapsible section showing tool usage)\n * @private\n */\n _getToolCallsTemplate() {\n if (!this.message.tool_calls || this.message.tool_calls.length === 0) {\n return '';\n }\n const esc = (str) => {\n const div = document.createElement('div');\n div.textContent = str;\n return div.innerHTML;\n };\n const toolBadges = this.message.tool_calls.map(tc => {\n const name = esc(tc.name || tc.function?.name || 'tool');\n const statusClass = tc.status === 'error' ? 'bg-danger' : 'bg-info';\n return `<span class=\"badge ${statusClass} me-1\">${name}</span>`;\n }).join('');\n\n const collapseId = `tools-${this.message.id || this.id}`;\n return `\n <div class=\"message-tool-calls mt-1\">\n <a class=\"text-muted small\" data-bs-toggle=\"collapse\" href=\"#${collapseId}\" role=\"button\" aria-expanded=\"false\">\n <i class=\"bi bi-tools me-1\"></i>${this.message.tool_calls.length} tool call${this.message.tool_calls.length > 1 ? 's' : ''}\n </a>\n <div class=\"collapse\" id=\"${collapseId}\">\n <div class=\"mt-1\">${toolBadges}</div>\n </div>\n </div>\n `;\n }\n\n async onAfterRender() {\n // Render attachments if any. Clean up any prior FilePreview children first\n // so a re-render doesn't duplicate them.\n if (this.message.attachments && this.message.attachments.length > 0) {\n const attachmentsContainer = this.element.querySelector('[data-container=\"attachments\"]');\n if (attachmentsContainer) {\n attachmentsContainer.innerHTML = '';\n for (const id in this.children) {\n const child = this.children[id];\n if (child instanceof FilePreviewView) this.removeChild(child);\n }\n this.message.attachments.forEach(file => {\n const filePreview = new FilePreviewView({ file });\n this.addChild(filePreview);\n filePreview.render(true, attachmentsContainer);\n });\n }\n }\n }\n}\n\nexport default ChatMessageView;\n","import View from '@core/View.js';\nimport applyFileDropMixin from '@core/mixins/FileDropMixin.js';\nimport { File } from '@core/models/Files.js';\n\n/**\n * ChatInputView - Input area with file drop support and attachment preview\n */\nclass ChatInputView extends View {\n constructor(options = {}) {\n super({\n className: 'chat-input-view',\n ...options\n });\n\n this.placeholder = options.placeholder || 'Type a message...';\n this.buttonText = options.buttonText || 'Send';\n this.showFileInput = options.showFileInput !== false; // default true\n this.attachments = []; // Array of uploaded file data\n this.pendingUploads = new Map(); // Track in-progress uploads\n }\n\n getTemplate() {\n return `\n <div class=\"chat-input-container\">\n <div class=\"chat-input-attachments\" data-container=\"attachments\"></div>\n <div class=\"chat-input-wrapper\">\n <textarea\n class=\"chat-input form-control\"\n placeholder=\"${this.placeholder}\"\n rows=\"2\"></textarea>\n <button class=\"chat-send-btn btn btn-primary\" data-action=\"send-message\" type=\"button\">\n <i class=\"bi bi-send-fill\"></i>\n <span class=\"spinner-border spinner-border-sm d-none\" role=\"status\" aria-hidden=\"true\"></span>\n </button>\n </div>\n ${this.showFileInput ? `\n <div class=\"chat-input-footer\">\n <small class=\"text-muted\">\n <i class=\"bi bi-paperclip\"></i>\n Drag & drop files to attach\n </small>\n </div>\n ` : ''}\n </div>\n `;\n }\n\n async onAfterRender() {\n // Enable file drop on the entire input container (if file input is shown)\n if (this.showFileInput) {\n this.enableFileDrop({\n dropZoneSelector: '.chat-input-container',\n multiple: true,\n acceptedTypes: ['*/*'], // Accept all file types\n visualFeedback: true,\n dragOverClass: 'drag-over',\n dragActiveClass: 'drag-active'\n });\n }\n\n // Auto-resize textarea as user types and handle Enter key\n const textarea = this.element.querySelector('.chat-input');\n if (textarea) {\n textarea.addEventListener('input', () => this.autoResizeTextarea(textarea));\n textarea.addEventListener('keydown', (e) => this.handleKeydown(e));\n }\n }\n\n /**\n * Handle textarea keydown (send on Enter without Shift)\n */\n handleKeydown(event) {\n if (event.key === 'Enter' && !event.shiftKey) {\n event.preventDefault();\n this.onActionSendMessage(event, event.target);\n }\n }\n\n /**\n * Handle file drop\n * @param {File[]} files - Dropped files\n */\n async onFileDrop(files) {\n for (const file of files) {\n await this.uploadFile(file);\n }\n }\n\n /**\n * Upload a file\n * @param {File} file - File to upload\n */\n async uploadFile(file) {\n const fileModel = new File();\n const uploadId = Date.now() + Math.random();\n\n // Add preview immediately\n this.addFilePreview(uploadId, file, 0);\n this.pendingUploads.set(uploadId, { file, fileModel });\n\n try {\n const result = await fileModel.upload({\n file: file,\n onProgress: (progress) => {\n this.updateFileProgress(uploadId, progress);\n },\n onComplete: (uploadResult) => {\n this.handleUploadComplete(uploadId, fileModel);\n }\n });\n\n } catch (error) {\n console.error('File upload failed:', error);\n this.handleUploadError(uploadId, error);\n }\n }\n\n /**\n * Add file preview to UI\n * @param {string} uploadId - Unique upload ID\n * @param {File} file - File object\n * @param {number} progress - Upload progress (0-100)\n */\n addFilePreview(uploadId, file, progress) {\n const container = this.element.querySelector('[data-container=\"attachments\"]');\n if (!container) return;\n\n const preview = document.createElement('div');\n preview.className = 'attachment-preview';\n preview.dataset.uploadId = uploadId;\n preview.innerHTML = `\n <div class=\"attachment-info\">\n <i class=\"bi bi-file-earmark\"></i>\n <span class=\"attachment-name\">${this.escapeHtml(file.name)}</span>\n <span class=\"attachment-size\">(${this.formatFileSize(file.size)})</span>\n </div>\n <div class=\"attachment-progress\">\n <div class=\"progress\" style=\"height: 4px;\">\n <div class=\"progress-bar\" role=\"progressbar\" style=\"width: ${progress}%\"></div>\n </div>\n </div>\n <button class=\"attachment-remove btn btn-sm btn-link text-danger\" data-action=\"remove-attachment\" data-upload-id=\"${uploadId}\" type=\"button\">\n <i class=\"bi bi-x\"></i>\n </button>\n `;\n\n container.appendChild(preview);\n }\n\n /**\n * Update file upload progress\n * @param {string} uploadId - Upload ID\n * @param {number} progress - Progress (0-100)\n */\n updateFileProgress(uploadId, progress) {\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n const progressBar = preview.querySelector('.progress-bar');\n if (progressBar) {\n progressBar.style.width = `${progress}%`;\n }\n }\n }\n\n /**\n * Handle upload completion\n * @param {string} uploadId - Upload ID\n * @param {Object} result - Upload result data (contains file.id)\n */\n handleUploadComplete(uploadId, fileModel) {\n // Store the file data with its ID\n this.attachments.push({\n id: fileModel.id,\n name: fileModel.get(\"name\"),\n uploadId: uploadId\n });\n this.pendingUploads.delete(uploadId);\n\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n preview.classList.add('upload-complete');\n const progressContainer = preview.querySelector('.attachment-progress');\n if (progressContainer) {\n progressContainer.remove();\n }\n }\n }\n\n /**\n * Handle upload error\n * @param {string} uploadId - Upload ID\n * @param {Error} error - Error object\n */\n handleUploadError(uploadId, error) {\n this.pendingUploads.delete(uploadId);\n\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n preview.classList.add('upload-error');\n preview.querySelector('.attachment-info').innerHTML +=\n `<span class=\"text-danger ms-2\">Upload failed</span>`;\n }\n }\n\n /**\n * Remove attachment\n */\n async onActionRemoveAttachment(event, element) {\n const uploadId = element.dataset.uploadId;\n\n // Remove from pending uploads\n this.pendingUploads.delete(uploadId);\n\n // Remove from completed attachments\n const preview = this.element.querySelector(`[data-upload-id=\"${uploadId}\"]`);\n if (preview) {\n // TODO: Get the file ID from the preview and remove from attachments array\n preview.remove();\n }\n }\n\n\n\n /**\n * Send message\n */\n async onActionSendMessage(event, element) {\n const textarea = this.element.querySelector('.chat-input');\n const text = textarea.value.trim();\n\n // Don't send if empty and no attachments\n if (!text && this.attachments.length === 0) {\n return;\n }\n\n // Don't send if uploads are pending\n if (this.pendingUploads.size > 0) {\n // TODO: Show message that uploads are in progress\n return;\n }\n\n // Show busy state\n this.setBusy(true);\n\n // Emit event with message data\n this.emit('message:send', {\n text: text,\n files: this.attachments\n });\n\n // Note: Don't clear here - let the parent ChatView call clearInput() after successful send\n }\n\n /**\n * Enable or disable the entire input area\n * @param {boolean} enabled - Whether the input should be enabled\n */\n setEnabled(enabled) {\n const textarea = this.element?.querySelector('.chat-input');\n const button = this.element?.querySelector('.chat-send-btn');\n if (textarea) textarea.disabled = !enabled;\n if (button) button.disabled = !enabled;\n }\n\n /**\n * Set busy state (show/hide spinner)\n * @param {boolean} busy - Whether to show busy state\n */\n setBusy(busy) {\n const button = this.element.querySelector('.chat-send-btn');\n const icon = button.querySelector('.bi-send-fill');\n const spinner = button.querySelector('.spinner-border');\n\n if (busy) {\n button.disabled = true;\n icon.classList.add('d-none');\n spinner.classList.remove('d-none');\n } else {\n button.disabled = false;\n icon.classList.remove('d-none');\n spinner.classList.add('d-none');\n }\n }\n\n /**\n * Clear input and attachments\n */\n clearInput() {\n const textarea = this.element.querySelector('.chat-input');\n if (textarea) {\n textarea.value = '';\n textarea.style.height = 'auto';\n }\n\n const container = this.element.querySelector('[data-container=\"attachments\"]');\n if (container) {\n container.innerHTML = '';\n }\n\n this.attachments = [];\n this.pendingUploads.clear();\n \n // Reset busy state\n this.setBusy(false);\n }\n\n /**\n * Auto-resize textarea based on content\n * @param {HTMLTextAreaElement} textarea\n */\n autoResizeTextarea(textarea) {\n textarea.style.height = 'auto';\n textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';\n }\n\n /**\n * Format file size for display\n * @param {number} bytes\n * @returns {string}\n */\n formatFileSize(bytes) {\n if (bytes === 0) return '0 B';\n const k = 1024;\n const sizes = ['B', 'KB', 'MB', 'GB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];\n }\n\n /**\n * Escape HTML to prevent XSS\n * @param {string} text\n * @returns {string}\n */\n escapeHtml(text) {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n}\n\n// Apply FileDropMixin\napplyFileDropMixin(ChatInputView);\n\nexport default ChatInputView;\n","import View from '@core/View.js';\nimport ChatMessageView from './ChatMessageView.js';\nimport ChatInputView from './ChatInputView.js';\n\n/**\n * ChatView - Modern chat interface with theme support\n * \n * Themes:\n * - 'compact' (default): Admin/activity feed style, list-based layout\n * - 'bubbles': Modern chat bubbles with left/right positioning\n * \n * Usage:\n * const chat = new ChatView({\n * adapter: myAdapter,\n * theme: 'compact', // or 'bubbles'\n * currentUserId: 123,\n * inputPlaceholder: 'Add a comment...',\n * inputButtonText: 'Send'\n * });\n */\nclass ChatView extends View {\n constructor(options = {}) {\n super({\n className: 'chat-view',\n ...options\n });\n\n this.adapter = options.adapter;\n this.theme = options.theme || 'compact'; // 'compact' or 'bubbles'\n this.currentUserId = options.currentUserId;\n this.inputPlaceholder = options.inputPlaceholder || 'Type a message...';\n this.inputButtonText = options.inputButtonText || 'Send';\n this.showFileInput = options.showFileInput !== false; // default true\n this.showInput = options.showInput !== false; // default true\n this.MessageViewClass = options.messageViewClass || ChatMessageView;\n this.messages = [];\n this.messageViews = new Map(); // Track message views by ID\n this._thinkingEl = null;\n }\n\n getTemplate() {\n return `\n <div class=\"chat-container chat-theme-${this.theme}\">\n <div class=\"chat-messages\" data-container=\"messages\"></div>\n ${this.showInput ? '<div class=\"chat-input-wrapper\" data-container=\"input\"></div>' : ''}\n </div>\n `;\n }\n\n async onInit() {\n // Initial fetch of messages\n this.messages = await this.adapter.fetch();\n\n // Create input view (if not hidden)\n if (this.showInput) {\n this.inputView = new ChatInputView({\n containerId: 'input',\n placeholder: this.inputPlaceholder,\n buttonText: this.inputButtonText,\n showFileInput: this.showFileInput\n });\n this.addChild(this.inputView);\n\n // Listen for new messages\n this.inputView.on('message:send', async (data) => {\n await this.handleSendMessage(data);\n });\n }\n }\n\n async onAfterRender() {\n // Build message views\n this._buildMessageViews();\n \n // Render children (like ListView does)\n await this._renderChildren();\n \n // Scroll to bottom\n this.scrollToBottom();\n }\n\n /**\n * Render child message views (similar to ListView._renderChildren)\n * @private\n */\n async _renderChildren() {\n // Collect message view IDs so we can skip them in the standard path\n const messageViewIds = new Set();\n this.messageViews.forEach(v => messageViewIds.add(v.id));\n\n // Render non-message children (e.g. input view) via the standard path\n for (const id in this.children) {\n const child = this.children[id];\n if (!child || messageViewIds.has(id)) continue;\n child.parent = this;\n await Promise.resolve(child.render()).catch(err =>\n console.warn(`ChatView child render error (${id})`, err)\n );\n }\n\n // Then place message views into the messages container\n const messagesContainer = this.element.querySelector('[data-container=\"messages\"]');\n if (!messagesContainer) return;\n\n this.messageViews.forEach((messageView) => {\n messagesContainer.appendChild(messageView.element);\n messageView.render(false);\n });\n }\n\n /**\n * Build message views for all messages (similar to ListView._buildItems)\n * @private\n */\n _buildMessageViews() {\n if (!this.messages || this.messages.length === 0) return;\n \n this.messages.forEach(message => {\n if (!this.messageViews.has(message.id)) {\n this._createMessageView(message);\n }\n });\n }\n\n /**\n * Create a message view (similar to ListView._createItemView)\n * @private\n */\n _createMessageView(message) {\n if (this.messageViews.has(message.id)) return;\n \n const isCurrentUser = message.author && message.author.id === this.currentUserId;\n \n const messageView = new this.MessageViewClass({\n message: message,\n theme: this.theme,\n isCurrentUser: isCurrentUser\n });\n \n this.addChild(messageView);\n this.messageViews.set(message.id, messageView);\n \n return messageView;\n }\n\n /**\n * Add a new message to the chat (for real-time updates)\n * @param {Object} message - Message data\n * @param {boolean} scroll - Whether to scroll to bottom after adding\n */\n addMessage(message, scroll = true) {\n if (this.messageViews.has(message.id)) return;\n \n const messageView = this._createMessageView(message);\n \n // If already rendered, append to DOM immediately\n if (this.isMounted()) {\n const messagesContainer = this.element.querySelector('[data-container=\"messages\"]');\n if (messagesContainer) {\n messagesContainer.appendChild(messageView.element);\n messageView.render(false);\n }\n }\n \n if (scroll) {\n this.scrollToBottom();\n }\n }\n\n /**\n * Handle sending a new message\n * @param {Object} data - Message data {text, files}\n * @private\n */\n async handleSendMessage(data) {\n try {\n // If there's text, send it as a note\n if (data.text && data.text.trim()) {\n const result = await this.adapter.addNote({\n text: data.text,\n files: data.files && data.files.length > 0 ? [data.files[0]] : []\n });\n \n if (!result.success) {\n throw new Error('Failed to send message');\n }\n }\n \n // If there are multiple files, or files without text, create a note for each\n const startIndex = (data.text && data.text.trim() && data.files?.length > 0) ? 1 : 0;\n \n for (let i = startIndex; i < (data.files?.length || 0); i++) {\n const file = data.files[i];\n const result = await this.adapter.addNote({\n text: '', // Empty text, just the file\n files: [file]\n });\n \n if (!result.success) {\n console.error('Failed to upload file:', file);\n }\n }\n \n // Fetch updated messages\n this.messages = await this.adapter.fetch();\n \n // Find the new message(s) and add them\n this.messages.forEach(message => {\n if (!this.messageViews.has(message.id)) {\n this.addMessage(message, true);\n }\n });\n \n // Clear input (this also resets busy state)\n this.inputView.clearInput();\n \n } catch (error) {\n console.error('Failed to send message:', error);\n // Reset busy state on error\n this.inputView.setBusy(false);\n try {\n this.getApp().toast.error('Failed to send message');\n } catch (e) {\n // Toast not available — fail silently\n }\n }\n }\n\n /**\n * Show an animated thinking indicator at the bottom of the messages area.\n * Only one indicator is shown at a time — subsequent calls update the text.\n * @param {string} [text='Thinking...'] - Status text to display\n */\n showThinking(text = 'Thinking...') {\n const container = this.element?.querySelector('[data-container=\"messages\"]');\n if (!container) return;\n\n if (!this._thinkingEl) {\n this._thinkingEl = document.createElement('div');\n this._thinkingEl.className = 'chat-thinking';\n this._thinkingEl.innerHTML = `\n <div class=\"chat-thinking-content\">\n <span class=\"chat-thinking-dots\">\n <span></span><span></span><span></span>\n </span>\n <span class=\"chat-thinking-text\"></span>\n <span class=\"chat-thinking-timer text-muted\"></span>\n </div>\n `;\n container.appendChild(this._thinkingEl);\n\n this._thinkingStart = Date.now();\n this._thinkingInterval = setInterval(() => {\n const elapsed = Math.floor((Date.now() - this._thinkingStart) / 1000);\n const mins = Math.floor(elapsed / 60);\n const secs = elapsed % 60;\n const timerEl = this._thinkingEl?.querySelector('.chat-thinking-timer');\n if (timerEl) {\n timerEl.textContent = mins > 0\n ? `${mins}m ${String(secs).padStart(2, '0')}s`\n : `${secs}s`;\n }\n }, 1000);\n }\n\n this._thinkingEl.querySelector('.chat-thinking-text').textContent = text;\n this.scrollToBottom();\n }\n\n /**\n * Remove the thinking indicator\n */\n hideThinking() {\n if (this._thinkingInterval) {\n clearInterval(this._thinkingInterval);\n this._thinkingInterval = null;\n }\n if (this._thinkingEl) {\n this._thinkingEl.remove();\n this._thinkingEl = null;\n }\n }\n\n /**\n * Enable or disable the chat input\n * @param {boolean} enabled - Whether the input should be enabled\n */\n setInputEnabled(enabled) {\n if (this.inputView?.setEnabled) {\n this.inputView.setEnabled(enabled);\n }\n }\n\n /**\n * Scroll chat to bottom\n */\n scrollToBottom() {\n const container = this.element.querySelector('.chat-messages');\n if (container) {\n requestAnimationFrame(() => {\n container.scrollTop = container.scrollHeight;\n });\n }\n }\n\n /**\n * Clear all messages\n */\n clearMessages() {\n this.messageViews.forEach((view) => {\n // Remove from parent's children hash so _renderChildren won't re-render them\n delete this.children[view.id];\n view.destroy();\n });\n this.messageViews.clear();\n this.messages = [];\n\n const container = this.element?.querySelector('[data-container=\"messages\"]');\n if (container) {\n container.innerHTML = '';\n }\n }\n\n /**\n * Refresh messages from adapter\n */\n async refresh() {\n this.clearMessages();\n this.messages = await this.adapter.fetch();\n this._buildMessageViews();\n \n if (this.isMounted()) {\n await this._renderChildren();\n this.scrollToBottom();\n }\n }\n}\n\nexport default ChatView;\n"],"names":["MetricsPermission","Model","constructor","data","super","endpoint","id_key","MetricsPermissionList","Collection","options","ModelClass","MetricsForms","edit","title","fields","name","type","label","columns","help","GeoIPForms","size","required","readonly","cols","step","GeoLocatedIP","lookup","ip","model","resp","rest","GET","success","EDIT_FORM","EDIT_LOCATION_FORM","EDIT_SECURITY_FORM","value","EDIT_NETWORK_FORM","GeoLocatedIPList","TablePage","Page","pageName","this","description","collection","defaultQuery","groupField","tableViewConfig","actions","contextMenu","batchActions","batchBarLocation","clickAction","addForm","formFields","formCreate","editForm","formEdit","itemView","itemViewClass","deleteTemplate","formDialogConfig","viewDialogOptions","searchable","sortable","filterable","paginated","selectionMode","selectable","filters","additionalFilters","hideActivePills","hideActivePillNames","searchPlacement","tableOptions","striped","bordered","hover","responsive","emptyMessage","searchPlaceholder","showAdd","showExport","onItemView","onItemEdit","onItemDelete","onAdd","onExport","tableViewOptions","urlSyncEnabled","lastUpdated","isLoading","template","buildTemplate","onInit","applyQueryToCollection","tableView","TableView","containerId","fetchOnMount","async","event","showItemDialog","addChild","setupEventListeners","on","Date","toLocaleTimeString","updateStatusDisplay","syncUrl","key","handleFilterEdit","params","query","Object","keys","length","start","parseInt","sort","search","reservedParams","entries","forEach","includes","startsWith","JSON","parse","e","field","parseFilterKey","hasOwnProperty","setParams","force","getApp","router","currentUrl","URL","window","location","currentParams","searchParams","desiredParams","collectionParams","stringify","hasChanges","some","String","_item","updateBrowserUrl","element","updatedElement","querySelector","textContent","countElement","count","meta","onEnter","requiresGroup","activeGroup","id","setTimeout","updateFilterPills","updateSortIcons","_openDeepLinkedItem","itemId","fetchOne","_clearItemParam","_setItemParam","fetchOnView","Modal","loading","fetch","error","hideLoading","showError","message","ViewClass","getItemViewClass","viewInstance","dialog","header","body","centered","getFormDialogConfig","getModelClass","getModelName","refresh","getSelectedItems","clearSelection","filterKey","filterConfig","getAllAvailableFilters","find","f","currentValue","config","result","form","filter_value","setFilter","restEnabled","render","clearAllFilters","onGroupChange","group","onBeforeDestroy","off","showStatus","create","TabView","View","tabs","activeTab","tabsClass","contentClass","minWidth","enableResponsive","tabPadding","dropdownStyle","enableTransitions","transitionDuration","viewOptions","tagName","className","tabLabels","currentMode","tabWidthCache","Map","lastContainerWidth","resizeObserver","_measurementSpan","_tabComputedStyle","isMobileMode","hasOverflow","view","addTab","handleResize","bind","renderTemplate","buildTabNavigation","buildTabContent","buildDropdownNavigation","buildTabsNavigation","tabItems","map","isActive","tabId","getTabId","escapeHtml","join","activeLabel","dropdownItems","buttonHtml","buildMobileDropdownNavigation","shouldUseMobileDropdown","enableMobileDropdown","viewportWidth","innerWidth","mobileBreakpoint","tabPanes","toLowerCase","replace","showTab","tabLabel","console","warn","activeView","isMounted","contains","previousTab","updateTabNavigation","updateTabContent","emit","activeTabLabel","previousTabLabel","reRenderNavigation","prevTabButton","classList","remove","setAttribute","activeTabButton","add","activeTabId","previousTabId","activePane","prevPane","Promise","resolve","duration","_getTransitionDuration","container","offsetHeight","onTabActivated","getComputedStyle","matches","match","parseFloat","onActionShowTab","getAttribute","_initializeMeasurementStyles","tabButton","style","font","letterSpacing","paddingLeft","paddingRight","onAfterRender","setupResponsiveHandling","onAfterMount","disconnect","removeEventListener","parentElement","removeChild","destroy","onSectionActivated","getActiveTab","getTabLabels","getTab","makeActive","permissions","activeUser","hasPerm","parent","removeTab","calculateTabWidth","has","get","document","estimatedWidth","set","createElement","visibility","position","whiteSpace","span","fontSize","fontFamily","appendChild","width","offsetWidth","getTotalTabWidth","reduce","total","getContainerWidth","shouldUseDropdown","containerWidth","totalTabWidth","Math","max","updateNavigationMode","ResizeObserver","observe","addEventListener","abs","newMode","mode","navigationContainer","newNavigation","outerHTML","getNavigationMode","setNavigationMode","clearWidthCache","clear","SideNavView","sections","activeSection","navWidth","contentPadding","sectionConfigs","sectionViews","sectionKeys","_addSectionConfig","_hasPermission","push","perm","nav","_buildDropdownNav","_buildSidebarNav","icon","activeConfig","c","items","filter","_mountSection","_setupResponsive","values","showSection","previousSection","_unmountSection","_updateNavState","_showContentLoading","_hideContentLoading","spinner","innerHTML","cssText","prepend","unmount","activeKey","querySelectorAll","link","section","dataset","toggle","selectBtn","onActionNavigate","el","preventDefault","_updateMode","_getContainerWidth","getActiveSection","getSectionKeys","getSection","addSection","removeSection","k","_onModelChange","FilePreviewView","file","isImage","content_type","isPdf","getTemplate","thumbnailUrl","url","onActionViewFile","LightboxGallery","MOJO","plugins","show","src","alt","filename","open","PDFViewer","showDialog","SHORTLINK_SOURCE_OPTIONS","TWITTER_CARD_OPTIONS","flattenShortLinkMetadata","metadata","m","og_title","og_description","og_image","twitter_card","twitter_title","twitter_description","twitter_image","buildShortLinkMetadata","formData","flatKey","targetKey","v","extractShortLinkPayload","flatKeys","payload","ShortLink","ShortLinkList","ShortLinkClick","ShortLinkClickList","_shortLinkSharedFields","placeholder","min","rows","ShortLinkForms","CATEGORY_CONFIG","image","previewType","badgeClass","video","audio","pdf","spreadsheet","presentation","archive","other","FilePreviewSection","categoryConfig","escapeAttr","getThumbnailUrl","poster","preview","getBestImageRendition","openFileInPreview","onActionDownloadFile","downloadFile","FileRenditionsSection","hasRenditions","_buildGalleryTemplate","isUploadPending","_buildWaitingTemplate","_buildEmptyTemplate","renditions","getRenditions","cards","r","_buildCard","ct","role","file_size","bytes","isNaN","n","Number","toFixed","formatBytes","dimensions","height","viewData","footer","onActionRefreshRenditions","err","onActionViewRendition","stopPropagation","Lightbox","images","startIndex","findIndex","img","fitToScreen","onActionCopyRenditionUrl","navigator","clipboard","isSecureContext","writeText","textarea","select","execCommand","orig","toast","onActionRegenerateFromSection","node","onActionRegenerateRenditions","a","href","download","click","FileSharesSection","fileId","source","_sharesCollection","sharesTable","formatter","action","divider","danger","emptyIcon","refreshShares","onActionShareFileFromSection","onActionShareFile","onActionCopyShareCode","code","app","base","shortlink_base_url","origin","buildShortUrl","_e","warning","onActionRevokeShare","row","closest","rowId","target","confirm","confirmText","confirmClass","save","is_active","FileView","File","sideNavView","_getCategoryConfig","cat","getCategory","getCategoryConfig","_buildHeaderTemplate","setModel","previewView","detailsView","DataView","showEmptyValues","emptyValueText","format","renditionsView","sharesSection","metadataView","ContextMenu","context","_stopRenditionsPoll","onActionCopyUrl","onActionEditFile","modelForm","formConfig","FileForms","onActionMakePublic","is_public","onActionMakePrivate","formResult","maxlength","submitText","opts","expire_days","track_clicks","note","slice","share","shortUrl","copied","expiry","expires_at","toLocaleString","tracked","copyHint","summary","shortlink_code","alert","regenerateRenditions","_maybeStartRenditionsPoll","_renditionsPollTimer","attempts","tick","clearTimeout","onActionDeleteFile","str","VIEW_CLASS","MODEL_REF","ChatMessageView","theme","isCurrentUser","cls","getBubblesTemplate","getCompactTemplate","userClass","isAssistant","_getToolCallsTemplate","tool_calls","toolBadges","tc","div","esc","function","status","collapseId","attachments","attachmentsContainer","children","child","filePreview","ChatInputView","buttonText","showFileInput","pendingUploads","enableFileDrop","dropZoneSelector","multiple","acceptedTypes","visualFeedback","dragOverClass","dragActiveClass","autoResizeTextarea","handleKeydown","shiftKey","onActionSendMessage","onFileDrop","files","uploadFile","fileModel","uploadId","now","random","addFilePreview","upload","onProgress","progress","updateFileProgress","onComplete","uploadResult","handleUploadComplete","handleUploadError","formatFileSize","progressBar","delete","progressContainer","onActionRemoveAttachment","text","trim","setBusy","setEnabled","enabled","button","disabled","busy","clearInput","scrollHeight","i","floor","log","pow","applyFileDropMixin","ChatView","adapter","currentUserId","inputPlaceholder","inputButtonText","showInput","MessageViewClass","messageViewClass","messages","messageViews","_thinkingEl","inputView","handleSendMessage","_buildMessageViews","_renderChildren","scrollToBottom","messageViewIds","Set","catch","messagesContainer","messageView","_createMessageView","author","addMessage","scroll","addNote","Error","showThinking","_thinkingStart","_thinkingInterval","setInterval","elapsed","mins","secs","timerEl","padStart","hideThinking","clearInterval","setInputEnabled","requestAnimationFrame","scrollTop","clearMessages"],"mappings":"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"}
|