web-mojo 2.2.100 → 2.2.102

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.
Files changed (195) hide show
  1. package/CHANGELOG.md +598 -0
  2. package/README.md +2 -2
  3. package/dist/admin-models.cjs.js +2 -0
  4. package/dist/admin-models.cjs.js.map +1 -0
  5. package/dist/admin-models.es.js +2 -0
  6. package/dist/admin-models.es.js.map +1 -0
  7. package/dist/admin.cjs.js +1 -1
  8. package/dist/admin.css +42 -0
  9. package/dist/admin.es.js +1 -1
  10. package/dist/auth.cjs.js +1 -1
  11. package/dist/auth.es.js +1 -1
  12. package/dist/charts.cjs.js +1 -1
  13. package/dist/charts.cjs.js.map +1 -1
  14. package/dist/charts.css +897 -1
  15. package/dist/charts.es.js +1 -1
  16. package/dist/charts.es.js.map +1 -1
  17. package/dist/chat.css +96 -0
  18. package/dist/chunks/AssistantPanelView-CMRTtoqS.js +2 -0
  19. package/dist/chunks/AssistantPanelView-CMRTtoqS.js.map +1 -0
  20. package/dist/chunks/AssistantPanelView-CaVkWhVD.js +2 -0
  21. package/dist/chunks/AssistantPanelView-CaVkWhVD.js.map +1 -0
  22. package/dist/chunks/ChatView-B73uox2v.js +2 -0
  23. package/dist/chunks/ChatView-B73uox2v.js.map +1 -0
  24. package/dist/chunks/ChatView-W8daOwIo.js +2 -0
  25. package/dist/chunks/ChatView-W8daOwIo.js.map +1 -0
  26. package/dist/chunks/Collection-BZlmtcuL.js +2 -0
  27. package/dist/chunks/Collection-BZlmtcuL.js.map +1 -0
  28. package/dist/chunks/Collection-Bwoq6muu.js +2 -0
  29. package/dist/chunks/Collection-Bwoq6muu.js.map +1 -0
  30. package/dist/chunks/ContextMenu-BPPtuqKk.js +2 -0
  31. package/dist/chunks/ContextMenu-BPPtuqKk.js.map +1 -0
  32. package/dist/chunks/ContextMenu-q76hjQb6.js +2 -0
  33. package/dist/chunks/ContextMenu-q76hjQb6.js.map +1 -0
  34. package/dist/chunks/DataView-BbrwHMV4.js +2 -0
  35. package/dist/chunks/{DataView-6lhf855r.js.map → DataView-BbrwHMV4.js.map} +1 -1
  36. package/dist/chunks/DataView-k-7wmk5_.js +2 -0
  37. package/dist/chunks/{DataView-Bb83ww5U.js.map → DataView-k-7wmk5_.js.map} +1 -1
  38. package/dist/chunks/FormView-DPSuwWMq.js +3 -0
  39. package/dist/chunks/{FormView-DEROOklJ.js.map → FormView-DPSuwWMq.js.map} +1 -1
  40. package/dist/chunks/FormView-Dcy7XOtC.js +3 -0
  41. package/dist/chunks/{FormView-UmvPskWT.js.map → FormView-Dcy7XOtC.js.map} +1 -1
  42. package/dist/chunks/ListView-DHC-yBIw.js +2 -0
  43. package/dist/chunks/ListView-DHC-yBIw.js.map +1 -0
  44. package/dist/chunks/ListView-iGBsD4a7.js +2 -0
  45. package/dist/chunks/ListView-iGBsD4a7.js.map +1 -0
  46. package/dist/chunks/MetricsCountryMapView-CAD9wR_T.js +2 -0
  47. package/dist/chunks/MetricsCountryMapView-CAD9wR_T.js.map +1 -0
  48. package/dist/chunks/MetricsCountryMapView-Dzk3Yrzx.js +2 -0
  49. package/dist/chunks/MetricsCountryMapView-Dzk3Yrzx.js.map +1 -0
  50. package/dist/chunks/Modal-DBJU16cc.js +3 -0
  51. package/dist/chunks/Modal-DBJU16cc.js.map +1 -0
  52. package/dist/chunks/Modal-DuULCMFZ.js +3 -0
  53. package/dist/chunks/Modal-DuULCMFZ.js.map +1 -0
  54. package/dist/chunks/Passkeys-CGRZ8ZMv.js +2 -0
  55. package/dist/chunks/{Passkeys-CIRjqi7n.js.map → Passkeys-CGRZ8ZMv.js.map} +1 -1
  56. package/dist/chunks/Passkeys-Dr8-oSm9.js +2 -0
  57. package/dist/chunks/{Passkeys-BPV1gNkR.js.map → Passkeys-Dr8-oSm9.js.map} +1 -1
  58. package/dist/chunks/TokenManager-CFsr1qUV.js +2 -0
  59. package/dist/chunks/TokenManager-CFsr1qUV.js.map +1 -0
  60. package/dist/chunks/TokenManager-CHQxK_e5.js +2 -0
  61. package/dist/chunks/TokenManager-CHQxK_e5.js.map +1 -0
  62. package/dist/chunks/User-DNQhdBtI.js +2 -0
  63. package/dist/chunks/User-DNQhdBtI.js.map +1 -0
  64. package/dist/chunks/User-Dg7xpYEI.js +2 -0
  65. package/dist/chunks/User-Dg7xpYEI.js.map +1 -0
  66. package/dist/chunks/UserProfileView-B5nczdfw.js +2 -0
  67. package/dist/chunks/UserProfileView-B5nczdfw.js.map +1 -0
  68. package/dist/chunks/UserProfileView-Bpz3VZmP.js +2 -0
  69. package/dist/chunks/UserProfileView-Bpz3VZmP.js.map +1 -0
  70. package/dist/chunks/View-C5n3sIFi.js +2 -0
  71. package/dist/chunks/View-C5n3sIFi.js.map +1 -0
  72. package/dist/chunks/View-Yazho7OL.js +2 -0
  73. package/dist/chunks/View-Yazho7OL.js.map +1 -0
  74. package/dist/chunks/WebApp-CeiDNV6L.js +2 -0
  75. package/dist/chunks/WebApp-CeiDNV6L.js.map +1 -0
  76. package/dist/chunks/WebApp-irKlhuFX.js +2 -0
  77. package/dist/chunks/WebApp-irKlhuFX.js.map +1 -0
  78. package/dist/chunks/admin-BkxeK68u.js +2 -0
  79. package/dist/chunks/admin-BkxeK68u.js.map +1 -0
  80. package/dist/chunks/admin-vjoNbv_1.js +2 -0
  81. package/dist/chunks/admin-vjoNbv_1.js.map +1 -0
  82. package/dist/chunks/exportChart-DbsHDCxw.js +2 -0
  83. package/dist/chunks/exportChart-DbsHDCxw.js.map +1 -0
  84. package/dist/chunks/exportChart-Dk8D_du5.js +2 -0
  85. package/dist/chunks/exportChart-Dk8D_du5.js.map +1 -0
  86. package/dist/chunks/index-BNjCQA7q.js +2 -0
  87. package/dist/chunks/{index-CSmG_P2r.js.map → index-BNjCQA7q.js.map} +1 -1
  88. package/dist/chunks/index-DBsIDOAa.js +2 -0
  89. package/dist/chunks/{index-uo77m1wt.js.map → index-DBsIDOAa.js.map} +1 -1
  90. package/dist/chunks/{version-CRfGMZSI.js → version-B0cBv8MN.js} +2 -2
  91. package/dist/chunks/{version-CRfGMZSI.js.map → version-B0cBv8MN.js.map} +1 -1
  92. package/dist/chunks/{version-D-5X69D6.js → version-DtqCY0ZY.js} +2 -2
  93. package/dist/chunks/{version-D-5X69D6.js.map → version-DtqCY0ZY.js.map} +1 -1
  94. package/dist/core.css +306 -0
  95. package/dist/css/web-mojo.css +1 -1
  96. package/dist/docit.cjs.js +1 -1
  97. package/dist/docit.cjs.js.map +1 -1
  98. package/dist/docit.es.js +1 -1
  99. package/dist/docit.es.js.map +1 -1
  100. package/dist/index.cjs.js +1 -1
  101. package/dist/index.cjs.js.map +1 -1
  102. package/dist/index.es.js +1 -1
  103. package/dist/index.es.js.map +1 -1
  104. package/dist/lightbox.cjs.js +1 -1
  105. package/dist/lightbox.cjs.js.map +1 -1
  106. package/dist/lightbox.es.js +1 -1
  107. package/dist/lightbox.es.js.map +1 -1
  108. package/dist/map.cjs.js +1 -1
  109. package/dist/map.cjs.js.map +1 -1
  110. package/dist/map.es.js +1 -1
  111. package/dist/map.es.js.map +1 -1
  112. package/dist/portal.css +86 -0
  113. package/dist/timeline.cjs.js +1 -1
  114. package/dist/timeline.cjs.js.map +1 -1
  115. package/dist/timeline.es.js +1 -1
  116. package/dist/timeline.es.js.map +1 -1
  117. package/dist/user-profile.cjs.js +1 -1
  118. package/dist/user-profile.es.js +1 -1
  119. package/dist/web-mojo.lite.iife.js +4185 -3872
  120. package/dist/web-mojo.lite.iife.js.map +1 -1
  121. package/dist/web-mojo.lite.iife.min.js +301 -323
  122. package/dist/web-mojo.lite.iife.min.js.map +1 -1
  123. package/package.json +7 -2
  124. package/dist/chunks/AssistantPanelView-CZhZmVSG.js +0 -2
  125. package/dist/chunks/AssistantPanelView-CZhZmVSG.js.map +0 -1
  126. package/dist/chunks/AssistantPanelView-WFHYQjq7.js +0 -2
  127. package/dist/chunks/AssistantPanelView-WFHYQjq7.js.map +0 -1
  128. package/dist/chunks/ChatView-Bj3Tvx7a.js +0 -2
  129. package/dist/chunks/ChatView-Bj3Tvx7a.js.map +0 -1
  130. package/dist/chunks/ChatView-Uoqp80PU.js +0 -2
  131. package/dist/chunks/ChatView-Uoqp80PU.js.map +0 -1
  132. package/dist/chunks/Collection-C39Oy2q0.js +0 -2
  133. package/dist/chunks/Collection-C39Oy2q0.js.map +0 -1
  134. package/dist/chunks/Collection-CyK0u557.js +0 -2
  135. package/dist/chunks/Collection-CyK0u557.js.map +0 -1
  136. package/dist/chunks/ContextMenu-D-C7V6J6.js +0 -2
  137. package/dist/chunks/ContextMenu-D-C7V6J6.js.map +0 -1
  138. package/dist/chunks/ContextMenu-hjqR1pGW.js +0 -2
  139. package/dist/chunks/ContextMenu-hjqR1pGW.js.map +0 -1
  140. package/dist/chunks/DataView-6lhf855r.js +0 -2
  141. package/dist/chunks/DataView-Bb83ww5U.js +0 -2
  142. package/dist/chunks/Dialog-B0r9puaZ.js +0 -3
  143. package/dist/chunks/Dialog-B0r9puaZ.js.map +0 -1
  144. package/dist/chunks/Dialog-GzCkpMad.js +0 -3
  145. package/dist/chunks/Dialog-GzCkpMad.js.map +0 -1
  146. package/dist/chunks/FormView-DEROOklJ.js +0 -3
  147. package/dist/chunks/FormView-UmvPskWT.js +0 -3
  148. package/dist/chunks/ListView-D_hOtfWZ.js +0 -2
  149. package/dist/chunks/ListView-D_hOtfWZ.js.map +0 -1
  150. package/dist/chunks/ListView-Jkke6pU1.js +0 -2
  151. package/dist/chunks/ListView-Jkke6pU1.js.map +0 -1
  152. package/dist/chunks/MetricsCountryMapView-UdvJWArx.js +0 -2
  153. package/dist/chunks/MetricsCountryMapView-UdvJWArx.js.map +0 -1
  154. package/dist/chunks/MetricsCountryMapView-jLWCL6Hm.js +0 -2
  155. package/dist/chunks/MetricsCountryMapView-jLWCL6Hm.js.map +0 -1
  156. package/dist/chunks/MetricsMiniChartWidget-CV-McxOl.js +0 -2
  157. package/dist/chunks/MetricsMiniChartWidget-CV-McxOl.js.map +0 -1
  158. package/dist/chunks/MetricsMiniChartWidget-DG0DeDuR.js +0 -2
  159. package/dist/chunks/MetricsMiniChartWidget-DG0DeDuR.js.map +0 -1
  160. package/dist/chunks/MiniPieChart-B3sM4Bjv.js +0 -2
  161. package/dist/chunks/MiniPieChart-B3sM4Bjv.js.map +0 -1
  162. package/dist/chunks/MiniPieChart-DL5Rynvm.js +0 -2
  163. package/dist/chunks/MiniPieChart-DL5Rynvm.js.map +0 -1
  164. package/dist/chunks/MiniSeriesChart-C4DPVbU_.js +0 -2
  165. package/dist/chunks/MiniSeriesChart-C4DPVbU_.js.map +0 -1
  166. package/dist/chunks/MiniSeriesChart-DdNMLwfh.js +0 -2
  167. package/dist/chunks/MiniSeriesChart-DdNMLwfh.js.map +0 -1
  168. package/dist/chunks/Modal-jWZGSSOo.js +0 -2
  169. package/dist/chunks/Modal-jWZGSSOo.js.map +0 -1
  170. package/dist/chunks/Modal-xSaurlfZ.js +0 -2
  171. package/dist/chunks/Modal-xSaurlfZ.js.map +0 -1
  172. package/dist/chunks/Passkeys-BPV1gNkR.js +0 -2
  173. package/dist/chunks/Passkeys-CIRjqi7n.js +0 -2
  174. package/dist/chunks/TokenManager-CKgK0MIz.js +0 -2
  175. package/dist/chunks/TokenManager-CKgK0MIz.js.map +0 -1
  176. package/dist/chunks/TokenManager-Cu0YmxAx.js +0 -2
  177. package/dist/chunks/TokenManager-Cu0YmxAx.js.map +0 -1
  178. package/dist/chunks/UserProfileView-BXT8COCF.js +0 -2
  179. package/dist/chunks/UserProfileView-BXT8COCF.js.map +0 -1
  180. package/dist/chunks/UserProfileView-Slj-9C__.js +0 -2
  181. package/dist/chunks/UserProfileView-Slj-9C__.js.map +0 -1
  182. package/dist/chunks/WebApp-CXkNqtZX.js +0 -2
  183. package/dist/chunks/WebApp-CXkNqtZX.js.map +0 -1
  184. package/dist/chunks/WebApp-m6YeJFWY.js +0 -2
  185. package/dist/chunks/WebApp-m6YeJFWY.js.map +0 -1
  186. package/dist/chunks/WebSocketClient-BQAZr8C4.js +0 -2
  187. package/dist/chunks/WebSocketClient-BQAZr8C4.js.map +0 -1
  188. package/dist/chunks/WebSocketClient-YR5d82h8.js +0 -2
  189. package/dist/chunks/WebSocketClient-YR5d82h8.js.map +0 -1
  190. package/dist/chunks/admin-CpLR06pX.js +0 -2
  191. package/dist/chunks/admin-CpLR06pX.js.map +0 -1
  192. package/dist/chunks/admin-JtLeoEHZ.js +0 -2
  193. package/dist/chunks/admin-JtLeoEHZ.js.map +0 -1
  194. package/dist/chunks/index-CSmG_P2r.js +0 -2
  195. package/dist/chunks/index-uo77m1wt.js +0 -2
@@ -0,0 +1,2 @@
1
+ import{M as e,C as t}from"./Collection-Bwoq6muu.js";import{P as i,C as s}from"./ContextMenu-q76hjQb6.js";import{a as n,F as a,b as o}from"./Modal-DuULCMFZ.js";import{T as l,p as r}from"./Passkeys-CGRZ8ZMv.js";import{V as c}from"./View-C5n3sIFi.js";import d from"./DataView-k-7wmk5_.js";import{a as h}from"./FormView-DPSuwWMq.js";class MetricsPermission extends e{constructor(e={}){super(e,{endpoint:"/api/metrics/permissions",id_key:"account"})}}class MetricsPermissionList extends t{constructor(e={}){super({ModelClass:MetricsPermission,endpoint:"/api/metrics/permissions",...e})}}const m={edit:{title:"Edit Metrics Permissions",fields:[{name:"account",type:"text",label:"Account",columns:12},{name:"view_permissions",type:"tags",label:"View Permissions",help:'Enter permissions or "public"',columns:12},{name:"write_permissions",type:"tags",label:"Write Permissions",help:"Enter permissions",columns:12}]}},p={title:"Edit Location",size:"lg",fields:[{name:"ip_address",label:"IP Address",type:"text",required:!0,readonly:!0,cols:6},{name:"subnet",label:"Subnet",type:"text",cols:6},{name:"country_name",label:"Country",type:"text",cols:6},{name:"country_code",label:"Country Code",type:"text",cols:6},{name:"region",label:"Region",type:"text",cols:6},{name:"city",label:"City",type:"text",cols:6},{name:"postal_code",label:"Postal Code",type:"text",cols:6},{name:"timezone",label:"Timezone",type:"text",cols:6},{name:"latitude",label:"Latitude",type:"number",step:"any",cols:6},{name:"longitude",label:"Longitude",type:"number",step:"any",cols:6}]};class GeoLocatedIP extends e{constructor(e={}){super(e,{endpoint:"/api/system/geoip"})}static async lookup(e){const t=new GeoLocatedIP,i=await t.rest.GET("/api/system/geoip/lookup",{ip:e});return i.success&&i.data&&i.data.data?new GeoLocatedIP(i.data.data):null}}GeoLocatedIP.EDIT_FORM=p,GeoLocatedIP.EDIT_LOCATION_FORM=p,GeoLocatedIP.EDIT_SECURITY_FORM={title:"Edit Security",size:"md",fields:[{name:"threat_level",label:"Threat Level",type:"select",cols:12,options:[{value:"",label:"None"},{value:"low",label:"Low"},{value:"medium",label:"Medium"},{value:"high",label:"High"},{value:"critical",label:"Critical"}]},{name:"is_threat",label:"Threat",type:"switch",cols:6},{name:"is_suspicious",label:"Suspicious",type:"switch",cols:6},{name:"is_known_attacker",label:"Known Attacker",type:"switch",cols:6},{name:"is_known_abuser",label:"Known Abuser",type:"switch",cols:6},{name:"risk_score",label:"Risk Score",type:"number",cols:6},{name:"is_tor",label:"TOR Exit Node",type:"switch",cols:6},{name:"is_vpn",label:"VPN",type:"switch",cols:6},{name:"is_proxy",label:"Proxy",type:"switch",cols:6},{name:"is_cloud",label:"Cloud Provider",type:"switch",cols:6},{name:"is_datacenter",label:"Datacenter",type:"switch",cols:6}]},GeoLocatedIP.EDIT_NETWORK_FORM={title:"Edit Network",size:"md",fields:[{name:"asn",label:"ASN",type:"text",cols:6},{name:"asn_org",label:"ASN Organization",type:"text",cols:6},{name:"isp",label:"ISP",type:"text",cols:12},{name:"connection_type",label:"Connection Type",type:"text",cols:6},{name:"provider",label:"Provider",type:"text",cols:6},{name:"is_mobile",label:"Mobile Connection",type:"switch",cols:6},{name:"mobile_carrier",label:"Mobile Carrier",type:"text",cols:6},{name:"last_seen",label:"Last Seen",type:"datetime",cols:12}]};class GeoLocatedIPList extends t{constructor(e={}){super({ModelClass:GeoLocatedIP,endpoint:"/api/system/geoip",...e})}}class TablePage extends i{constructor(e={}){super({...e,pageName:e.pageName||e.name||"table"}),this.title=e.title||this.pageName,this.description=e.description||"",this.Collection=e.Collection||null,this.collection=e.collection||null,this.defaultQuery=e.defaultQuery||{},this.groupField=e.groupField||"group",this.tableViewConfig={columns:e.columns||[],actions:e.actions||null,contextMenu:e.contextMenu||null,batchActions:e.batchActions||null,batchBarLocation:e.batchBarLocation||"top",clickAction:e.clickAction||"view",addForm:e.addForm||e.formFields||e.formCreate,editForm:e.editForm||e.formEdit||e.formFields,itemView:e.itemView||e.itemViewClass,deleteTemplate:e.deleteTemplate,formDialogConfig:e.formDialogConfig,viewDialogOptions:e.viewDialogOptions,searchable:!1!==e.searchable,sortable:!1!==e.sortable,filterable:!1!==e.filterable,paginated:!1!==e.paginated,selectionMode:e.selectionMode||(e.selectable?"multiple":"none"),filters:e.filters||e.additionalFilters||[],hideActivePills:e.hideActivePills||!1,hideActivePillNames:e.hideActivePillNames||[],searchPlacement:e.searchPlacement||"toolbar",tableOptions:{striped:!0,bordered:!1,hover:!0,responsive:!1,...e.tableOptions},emptyMessage:e.emptyMessage||"No data available",searchPlaceholder:e.searchPlaceholder||"Search...",showAdd:!1!==e.showAdd,showExport:!1!==e.showExport,onItemView:e.onItemView,onItemEdit:e.onItemEdit,onItemDelete:e.onItemDelete,onAdd:e.onAdd,onExport:e.onExport,...e.tableViewOptions},this.urlSyncEnabled=!1!==e.urlSyncEnabled,this.lastUpdated=null,this.isLoading=!1,this.template=e.template||this.buildTemplate()}buildTemplate(){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 '}async onInit(){await super.onInit(),this.collection||(this.Collection?this.collection=new this.Collection:this.collection=new t),this.applyQueryToCollection(),this.tableView=new l({collection:this.collection,containerId:"table",fetchOnMount:!0,...this.tableViewConfig,onItemView:async(e,t)=>{if(this.tableViewConfig.onItemView)return this.tableViewConfig.onItemView(e,t);await this.showItemDialog(e)}}),this.addChild(this.tableView),this.setupEventListeners()}setupEventListeners(){this.urlSyncEnabled&&this.collection&&(this.collection.on("fetch:start",()=>{this.isLoading=!0}),this.collection.on("fetch:end",()=>{this.isLoading=!1,this.lastUpdated=/* @__PURE__ */(new Date).toLocaleTimeString(),this.updateStatusDisplay()})),this.tableView.on("params-changed",()=>{this.urlSyncEnabled&&this.syncUrl()}),this.tableView.on("filter:edit",async({key:e})=>{await this.handleFilterEdit(e)}),this.tableView.on("row:view",async({model:e})=>{this.onItemView&&await this.onItemView(e)}),this.tableView.on("row:edit",async({model:e})=>{this.onItemEdit&&await this.onItemEdit(e)}),this.tableView.on("row:delete",async({model:e})=>{this.onItemDelete&&await this.onItemDelete(e)}),this.tableView.on("table:add",async({event:e})=>{}),this.tableView.on("table:export",async({data:e})=>{this.tableViewConfig.onExport&&await this.tableViewConfig.onExport(e)})}applyQueryToCollection(){const e={},t={...this.defaultQuery,...this.query};if(!t||0===Object.keys(t).length)return;void 0!==t.start&&(e.start=parseInt(t.start)||0),void 0!==t.size&&(e.size=parseInt(t.size)||10),void 0!==t.sort&&(e.sort=t.sort),void 0!==t.search&&(e.search=t.search);const i=["start","size","sort","search","page","_item"];Object.entries(t).forEach(([t,s])=>{if(!i.includes(t)&&void 0!==s&&""!==s)if("string"==typeof s&&(s.startsWith("{")||s.startsWith("[")))try{e[t]=JSON.parse(s)}catch(n){e[t]=s}else e[t]=s}),Object.keys(e).length>0&&(Object.keys(e).forEach(t=>{const{field:i,lookup:s}=r(t);"in"!==s&&"not_in"!==s||!e.hasOwnProperty(i)||delete e[i]}),this.collection.setParams({...this.collection.params,...e}))}syncUrl(e=!0){if(!this.urlSyncEnabled||!this.collection||!this.getApp()?.router)return;const t=new URL(window.location),i={};for(const[o,l]of t.searchParams)"page"!==o&&(i[o]=l);const s={},n=this.collection.params||{};n.start&&(s.start=n.start),n.size&&(s.size=n.size),n.sort&&(s.sort=n.sort),n.search&&(s.search=n.search),Object.entries(n).forEach(([e,t])=>{["start","size","sort","search"].includes(e)||void 0===t||""===t||(s[e]="object"==typeof t?JSON.stringify(t):t)});const a=Object.keys(s).some(e=>String(i[e]||"")!==String(s[e]||""))||Object.keys(i).some(e=>!(e in s));this.query._item&&(s._item=this.query._item),this.query=s,(a||e)&&this.updateBrowserUrl(s,!0,!1)}updateStatusDisplay(){if(!this.element)return;const e=this.element.querySelector('[data-status="last-updated"]');e&&(e.textContent=this.lastUpdated||"Never");const t=this.element.querySelector('[data-status="record-count"]');if(t&&this.collection){const e=this.collection.meta?.count||this.collection.length();t.textContent=e}}async onEnter(){await super.onEnter(),this.options.requiresGroup&&!this.query[this.groupField]&&this.getApp().activeGroup&&(this.query[this.groupField]=this.getApp().activeGroup.id),this.applyQueryToCollection(),this.tableView&&this.tableView.element&&setTimeout(()=>{this.tableView.updateFilterPills(),this.tableView.updateSortIcons()},100),this.query._item&&this._openDeepLinkedItem(this.query._item)}async _openDeepLinkedItem(e){try{const t=await this.collection.fetchOne(e);t&&await this.showItemDialog(t)}catch(t){this._clearItemParam()}}async showItemDialog(e){this._setItemParam(e.id);const t=this.tableView.getItemViewClass(e);if(t){const i=new t({model:e,collection:this.collection});await n.dialog({header:!1,body:i,size:"lg",centered:!1,...this.tableView.getFormDialogConfig(this.tableView.getModelClass(e)),...this.tableView.viewDialogOptions})}else await n.data({title:`View ${this.tableView.getModelName(e)} #${e.id}`,model:e});this._clearItemParam()}_setItemParam(e){const t={...this.query,_item:e};this.query=t,this.updateBrowserUrl(t,!0,!1)}_clearItemParam(){delete this.query._item,this.updateBrowserUrl(this.query,!0,!1)}async refresh(){await this.tableView.refresh()}getSelectedItems(){return this.tableView.getSelectedItems()}clearSelection(){this.tableView.clearSelection()}async handleFilterEdit(e){const t=this.tableView.getAllAvailableFilters().find(t=>t.key===e),i=this.collection.params[e];if(!t)return;const s={name:"filter_value",label:t.label||e,value:i,...t.config},a=await n.form({title:`Edit ${s.label} Filter`,size:"md",fields:[s]});a&&void 0!==a.filter_value&&(this.tableView.setFilter(e,a.filter_value),this.collection.restEnabled&&this.collection.fetch(),await this.tableView.render(),this.syncUrl())}clearAllFilters(){if(!this.collection)return;const{start:e,size:t,sort:i}=this.collection.params;this.collection.params={start:e,size:t},i&&(this.collection.params.sort=i),this.syncUrl(),this.collection.restEnabled?this.collection.fetch():this.tableView.render()}async onGroupChange(e){e&&this.collection&&this.options.requiresGroup&&(this.query[this.groupField]=e.id,this.applyQueryToCollection(),this.collection&&this.collection.restEnabled&&this.collection.fetch())}async onBeforeDestroy(){this.collection&&(this.collection.off("fetch:start"),this.collection.off("fetch:end")),this.tableView&&(this.tableView.off("params-changed"),this.tableView.off("table:search"),this.tableView.off("table:sort"),this.tableView.off("table:page"),this.tableView.off("filter:edit"),this.tableView.off("row:view"),this.tableView.off("row:edit"),this.tableView.off("row:delete"),this.tableView.off("table:add"),this.tableView.off("table:export")),await super.onBeforeDestroy()}get showStatus(){return!0===this.options.showStatus}static create(e={}){return new this(e)}}class TabView extends c{constructor(e={}){const{tabs:t,activeTab:i,tabsClass:s,contentClass:n,minWidth:a,enableResponsive:o,tabPadding:l,dropdownStyle:r,enableTransitions:c,transitionDuration:d,...h}=e;super({tagName:"div",className:"tab-view",...h}),this.tabs={},this.tabLabels=Object.keys(this.tabs),this.activeTab=i||this.tabLabels[0]||null,this.tabsClass=s||"nav nav-tabs mb-3",this.contentClass=n||"tab-content",this.enableTransitions=!1!==c,this.transitionDuration=d||150,this.dropdownStyle=r||"select",this.minWidth=a||300,this.enableResponsive=!1!==o,this.tabPadding=l||80,this.currentMode="tabs",this.tabWidthCache=/* @__PURE__ */new Map,this.lastContainerWidth=0,this.resizeObserver=null,this._measurementSpan=null,this._tabComputedStyle=null,this.isMobileMode=!1,this.hasOverflow=!1;for(const[m,p]of Object.entries(t))this.addTab(m,p);this.handleResize=this.handleResize.bind(this)}async renderTemplate(){return`\n <div class="tab-view-container">\n ${this.buildTabNavigation()}\n ${this.buildTabContent()}\n </div>\n `}buildTabNavigation(){return 0===this.tabLabels.length?"":"dropdown"===this.currentMode?this.buildDropdownNavigation():this.buildTabsNavigation()}buildTabsNavigation(){const e=this.tabLabels.map(e=>{const t=e===this.activeTab,i=this.getTabId(e);return`\n <li class="nav-item" role="presentation">\n <button class="nav-link ${t?"active":""}"\n id="${i}-tab"\n data-action="show-tab"\n data-tab-label="${this.escapeHtml(e)}"\n type="button"\n role="tab"\n aria-controls="${i}"\n aria-selected="${t}">\n ${this.escapeHtml(e)}\n </button>\n </li>\n `}).join("");return`\n <ul class="${this.tabsClass}" role="tablist" data-tab-mode="tabs">\n ${e}\n </ul>\n `}buildDropdownNavigation(){const e=this.activeTab||this.tabLabels[0],t=this.tabLabels.map(e=>{const t=e===this.activeTab;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="show-tab"\n data-tab-label="${this.escapeHtml(e)}"\n type="button">\n ${this.escapeHtml(e)}\n ${t?'<i class="bi bi-check-lg ms-2"></i>':""}\n </button>\n </li>\n `}).join("");let i;return i="select"===this.dropdownStyle?`\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(e)}</span>\n </button>\n `:`\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(e)}\n </button>\n `,`\n <div class="dropdown mb-3" data-tab-mode="dropdown">\n ${i}\n <ul class="dropdown-menu" aria-labelledby="tab-dropdown-${this.id}">\n ${t}\n </ul>\n </div>\n `}buildMobileDropdownNavigation(){const e=this.activeTab||this.tabLabels[0],t=this.tabLabels.map(e=>{const t=e===this.activeTab;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="show-tab"\n data-tab-label="${this.escapeHtml(e)}"\n type="button">\n ${this.escapeHtml(e)}\n ${t?'<i class="bi bi-check ms-2"></i>':""}\n </button>\n </li>\n `}).join("");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(e)}\n </button>\n <ul class="dropdown-menu w-100">\n ${t}\n </ul>\n </div>\n `}shouldUseMobileDropdown(){if(!this.enableMobileDropdown)return!1;const e=window.innerWidth;return e<this.mobileBreakpoint||this.hasOverflow&&e<992}buildTabContent(){if(0===this.tabLabels.length)return'<div class="alert alert-info">No tabs to display</div>';const e=this.tabLabels.map(e=>{const t=e===this.activeTab,i=this.getTabId(e);return`\n <div class="tab-pane fade ${t?"show active":""}"\n id="${i}"\n role="tabpanel"\n aria-labelledby="${i}-tab"\n data-tab-label="${this.escapeHtml(e)}">\n <div data-container="${i}-content"></div>\n </div>\n `}).join("");return`\n <div class="${this.contentClass}">\n ${e}\n </div>\n `}getTabId(e){return`tab-${e.toLowerCase().replace(/[^a-z0-9]/g,"-")}-${this.id}`}async showTab(e,t={}){const{force:i=!1}=t;if(!this.tabs[e])return console.warn(`TabView: Tab "${e}" does not exist`),!1;if(this.activeTab===e&&!i){const t=this.tabs[e];if(t&&t.isMounted()&&this.element.contains(t.element))return!0}const s=this.activeTab;this.activeTab=e;try{return await this.updateTabNavigation(e,s),await this.updateTabContent(e,s),this.emit("tab:changed",{activeTab:e,previousTab:s}),!0}catch(n){return console.error("TabView: Error showing tab:",n),this.activeTab=s,!1}}async updateTabNavigation(e,t){if(!this.element)return;if("dropdown"===this.currentMode)return void(await this.reRenderNavigation());if(t){const e=this.element.querySelector(`[data-tab-label="${t}"]`);e&&(e.classList.remove("active"),e.setAttribute("aria-selected","false"))}const i=this.element.querySelector(`[data-tab-label="${e}"]`);i&&(i.classList.add("active"),i.setAttribute("aria-selected","true"))}async updateTabContent(e,t){if(!this.element)return;const i=this.getTabId(e),s=t?this.getTabId(t):null,n=this.element.querySelector(`#${i}`),a=s?this.element.querySelector(`#${s}`):null,o=this.tabs[e];if(this.enableTransitions){if(a&&a.classList.contains("show")&&(a.classList.remove("show"),await new Promise(e=>{const t=this._getTransitionDuration(a)||this.transitionDuration;setTimeout(e,t)}),a.classList.remove("active")),o){const e=this.element.querySelector(`[data-container="${i}-content"]`);e&&!o.isMounted()&&await o.render(!0,e)}n&&(n.classList.add("active"),n.offsetHeight,n.classList.add("show"))}else{if(a&&a.classList.remove("show","active"),o){const e=this.element.querySelector(`[data-container="${i}-content"]`);e&&!o.isMounted()&&await o.render(!0,e)}n&&n.classList.add("show","active")}o&&o.onTabActivated&&await o.onTabActivated()}_getTransitionDuration(e){if(!e||"undefined"==typeof window||!window.getComputedStyle)return 0;const t=(window.getComputedStyle(e).transitionDuration||"0s").match(/^([0-9.]+)(m?s)$/);if(!t)return 0;const i=parseFloat(t[1]);return"s"===t[2]?1e3*i:i}async onActionShowTab(e,t){const i=t.getAttribute("data-tab-label");i&&await this.showTab(i)}_initializeMeasurementStyles(){if(!this.element||this._tabComputedStyle)return;const e=this.element.querySelector(".nav-link");if(e&&"function"==typeof window.getComputedStyle){const t=window.getComputedStyle(e);this._tabComputedStyle={font:t.font,letterSpacing:t.letterSpacing};const i=parseFloat(t.paddingLeft)||0,s=parseFloat(t.paddingRight)||0;this.tabPadding=i+s+12}}async onAfterRender(){await super.onAfterRender(),this._initializeMeasurementStyles(),this.enableResponsive&&this.setupResponsiveHandling(),this.activeTab&&this.tabs[this.activeTab]&&await this.showTab(this.activeTab,{force:!0})}async onAfterMount(){await super.onAfterMount()}async onBeforeDestroy(){await super.onBeforeDestroy(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),"undefined"!=typeof window&&window.removeEventListener("resize",this.handleResize),this._measurementSpan&&this._measurementSpan.parentElement&&this._measurementSpan.parentElement.removeChild(this._measurementSpan),this._measurementSpan=null;for(const[e,t]of Object.entries(this.tabs))t&&"function"==typeof t.destroy&&await t.destroy()}async onSectionActivated(){const e=this.activeTab?this.tabs[this.activeTab]:null;e?.onTabActivated&&await e.onTabActivated()}getActiveTab(){return this.activeTab}getTabLabels(){return[...this.tabLabels]}getTab(e){return this.tabs[e]||null}async addTab(e,t,i=!1){return this.tabs[e]?(console.warn(`TabView: Tab "${e}" already exists`),!1):!(t.options.permissions&&!this.getApp().activeUser.hasPerm(t.options.permissions)||(this.tabs[e]=t,t.containerId=this.getTabId(e),t.parent=this,this.tabLabels=Object.keys(this.tabs),this.activeTab&&!i||(this.activeTab=e),this.isMounted()&&(await this.render(),(i||this.activeTab===e)&&await this.showTab(e)),this.emit("tab:added",{label:e,view:t}),0))}async removeTab(e){if(!this.tabs[e])return console.warn(`TabView: Tab "${e}" does not exist`),!1;const t=this.tabs[e];return t&&"function"==typeof t.destroy&&await t.destroy(),delete this.tabs[e],this.tabLabels=Object.keys(this.tabs),this.activeTab===e&&(this.activeTab=this.tabLabels[0]||null),this.isMounted()&&(await this.render(),this.activeTab&&await this.showTab(this.activeTab)),this.emit("tab:removed",{label:e,view:t}),!0}calculateTabWidth(e){if(this.tabWidthCache.has(e))return this.tabWidthCache.get(e);if("undefined"==typeof document){const t=8*e.length+this.tabPadding;return this.tabWidthCache.set(e,t),t}this._measurementSpan||(this._measurementSpan=document.createElement("span"),this._measurementSpan.style.visibility="hidden",this._measurementSpan.style.position="absolute",this._measurementSpan.style.whiteSpace="nowrap");const t=this._measurementSpan;this._tabComputedStyle?(t.style.font=this._tabComputedStyle.font,t.style.letterSpacing=this._tabComputedStyle.letterSpacing):(t.style.fontSize="14px",t.style.fontFamily="system-ui, -apple-system, sans-serif"),t.textContent=e,document.body.appendChild(t);const i=t.offsetWidth+this.tabPadding;return document.body.removeChild(t),this.tabWidthCache.set(e,i),i}getTotalTabWidth(){return this.tabLabels.reduce((e,t)=>e+this.calculateTabWidth(t),0)}getContainerWidth(){return this.element&&(this.element.parentElement||this.element).offsetWidth||this.minWidth}shouldUseDropdown(){if(!this.enableResponsive)return!1;const e=this.getContainerWidth(),t=this.getTotalTabWidth();return e<Math.max(t,this.minWidth)}setupResponsiveHandling(){if(this.element&&this.enableResponsive)if(this.updateNavigationMode(),"undefined"!=typeof ResizeObserver){this.resizeObserver=new ResizeObserver(()=>{this.handleResize()});const e=this.element.parentElement||this.element;this.resizeObserver.observe(e)}else window.addEventListener("resize",this.handleResize)}async handleResize(){const e=this.getContainerWidth();Math.abs(e-this.lastContainerWidth)>50&&(this.lastContainerWidth=e,await this.updateNavigationMode())}async updateNavigationMode(){const e=this.shouldUseDropdown()?"dropdown":"tabs";e!==this.currentMode&&(this.currentMode=e,this.isMounted()&&await this.reRenderNavigation(),this.emit("navigation:modeChanged",{mode:this.currentMode,containerWidth:this.getContainerWidth(),totalTabWidth:this.getTotalTabWidth()}))}async reRenderNavigation(){if(!this.element)return;const e=this.element.querySelector("[data-tab-mode]");if(e){const t=this.buildTabNavigation();e.outerHTML=t}}getNavigationMode(){return this.currentMode}async setNavigationMode(e){"tabs"===e||"dropdown"===e?(this.currentMode=e,this.isMounted()&&await this.reRenderNavigation()):console.warn('TabView: Invalid navigation mode. Use "tabs" or "dropdown"')}clearWidthCache(){this.tabWidthCache.clear()}static create(e={}){return new TabView(e)}}class SideNavView extends c{constructor(e={}){const{sections:t=[],activeSection:i,navWidth:s,contentPadding:n,enableResponsive:a,minWidth:o,...l}=e;super({tagName:"div",className:"side-nav-view",...l}),this.navWidth=s||200,this.contentPadding=n||"1.5rem 2.5rem",this.enableResponsive=!1!==a,this.minWidth=o||500,this.sectionConfigs=[],this.sectionViews={},this.sectionKeys=[],this.activeSection=null,this.currentMode="sidebar",this.resizeObserver=null,this.lastContainerWidth=0;for(const r of t)this._addSectionConfig(r);this.activeSection=i||this.sectionKeys[0]||null,this.handleResize=this.handleResize.bind(this)}_addSectionConfig(e){"divider"!==e.type?e.permissions&&!this._hasPermission(e.permissions)||(this.sectionConfigs.push(e),this.sectionKeys.push(e.key),e.view&&(this.sectionViews[e.key]=e.view,e.view.parent=this)):this.sectionConfigs.push({type:"divider",label:e.label})}_hasPermission(e){try{return this.getApp().activeUser.hasPerm(e)}catch{return!0}}async renderTemplate(){const e="dropdown"===this.currentMode?this._buildDropdownNav():this._buildSidebarNav();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 ${"dropdown"===this.currentMode?`\n <div class="snv-dropdown">${e}</div>\n <div class="snv-content" data-container="snv-content"></div>\n `:`\n <div class="snv-layout">\n <nav class="snv-nav">${e}</nav>\n <div class="snv-content" data-container="snv-content"></div>\n </div>\n `}\n `}_buildSidebarNav(){return this.sectionConfigs.map(e=>{if("divider"===e.type)return`<div class="snv-nav-label">${this.escapeHtml(e.label)}</div>`;const t=e.key===this.activeSection,i=e.icon?`<i class="bi ${e.icon}"></i>`:"";return`<a role="button" class="${t?"active":""}" data-action="navigate" data-section="${e.key}">${i} ${this.escapeHtml(e.label)}</a>`}).join("")}_buildDropdownNav(){const e=this.sectionConfigs.find(e=>e.key===this.activeSection),t=e?e.label:this.sectionKeys[0],i=this.sectionConfigs.filter(e=>"divider"!==e.type).map(e=>{const t=e.key===this.activeSection;return`\n <li>\n <button class="dropdown-item ${t?"active":""}"\n data-action="navigate"\n data-section="${e.key}"\n type="button">\n ${e.icon?`<i class="bi ${e.icon} me-2"></i>`:""}\n ${this.escapeHtml(e.label)}\n ${t?'<i class="bi bi-check-lg ms-2"></i>':""}\n </button>\n </li>\n `}).join("");return`\n <div class="dropdown">\n <button class="snv-select-btn" type="button"\n data-bs-toggle="dropdown" aria-expanded="false">\n ${e?.icon?`<i class="bi ${e.icon}"></i>`:""}\n <span>${this.escapeHtml(t)}</span>\n </button>\n <ul class="dropdown-menu w-100">${i}</ul>\n </div>\n `}async onAfterRender(){await super.onAfterRender(),this.activeSection&&await this._mountSection(this.activeSection),this.enableResponsive&&this._setupResponsive()}async onBeforeDestroy(){await super.onBeforeDestroy(),this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),"undefined"!=typeof window&&window.removeEventListener("resize",this.handleResize);for(const e of Object.values(this.sectionViews))e&&"function"==typeof e.destroy&&await e.destroy()}async showSection(e){if(!this.sectionViews[e])return console.warn(`SideNavView: Section "${e}" does not exist`),!1;if(e===this.activeSection){const t=this.sectionViews[e];if(t&&t.isMounted()&&this.element?.contains(t.element))return!0}const t=this.activeSection;this.activeSection=e,t&&t!==e&&await this._unmountSection(t),await this._mountSection(e);const i=this.sectionViews[e];return i?.onSectionActivated&&await i.onSectionActivated(),this._updateNavState(e),this.emit("section:changed",{activeSection:e,previousSection:t}),!0}async _mountSection(e){const t=this.sectionViews[e];if(!t)return;const i=this.element?.querySelector('[data-container="snv-content"]');if(i&&!t.isMounted()){this._showContentLoading(i);try{await t.render(!0,i)}finally{this._hideContentLoading(i)}}}_showContentLoading(e){if(!e)return;let t=e.querySelector(".snv-loading");t||(t=document.createElement("div"),t.className="snv-loading",t.innerHTML='<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>',t.style.cssText="display:flex;align-items:center;justify-content:center;padding:3rem;",e.prepend(t))}_hideContentLoading(e){if(!e)return;const t=e.querySelector(".snv-loading");t&&t.remove()}async _unmountSection(e){const t=this.sectionViews[e];t&&t.isMounted()&&await t.unmount()}_updateNavState(e){if(!this.element)return;this.element.querySelectorAll(".snv-nav a, .dropdown-item").forEach(t=>{const i=t.dataset.section;i&&t.classList.toggle("active",i===e)});const t=this.element.querySelector(".snv-select-btn span");if(t){const i=this.sectionConfigs.find(t=>t.key===e);i&&(t.textContent=i.label)}}async onActionNavigate(e,t){e.preventDefault();const i=t.dataset.section;return i&&await this.showSection(i),!0}_setupResponsive(){if(this.element&&this.enableResponsive)if(this._updateMode(),"undefined"!=typeof ResizeObserver){this.resizeObserver=new ResizeObserver(()=>{this.handleResize()});const e=this.element.parentElement||this.element;this.resizeObserver.observe(e)}else window.addEventListener("resize",this.handleResize)}async handleResize(){const e=this._getContainerWidth();Math.abs(e-this.lastContainerWidth)>50&&(this.lastContainerWidth=e,await this._updateMode())}_getContainerWidth(){return this.element&&(this.element.parentElement||this.element).offsetWidth||this.minWidth}async _updateMode(){const e=this._getContainerWidth(),t=e<this.minWidth?"dropdown":"sidebar";t!==this.currentMode&&(this.currentMode=t,this.isMounted()&&await this.render(),this.emit("navigation:modeChanged",{mode:this.currentMode,containerWidth:e}))}getActiveSection(){return this.activeSection}getSectionKeys(){return[...this.sectionKeys]}getSection(e){return this.sectionViews[e]||null}async addSection(e,t=!1){return e.key&&this.sectionViews[e.key]?(console.warn(`SideNavView: Section "${e.key}" already exists`),!1):(this._addSectionConfig(e),this.isMounted()&&(await this.render(),t&&e.key&&await this.showSection(e.key)),this.emit("section:added",{config:e}),!0)}async removeSection(e){const t=this.sectionViews[e];return t?("function"==typeof t.destroy&&await t.destroy(),delete this.sectionViews[e],this.sectionKeys=this.sectionKeys.filter(t=>t!==e),this.sectionConfigs=this.sectionConfigs.filter(t=>t.key!==e),this.activeSection===e&&(this.activeSection=this.sectionKeys[0]||null),this.isMounted()&&await this.render(),this.emit("section:removed",{key:e}),!0):(console.warn(`SideNavView: Section "${e}" does not exist`),!1)}_onModelChange(){}static create(e={}){return new SideNavView(e)}}class FilePreviewView extends c{constructor(e={}){super({className:"file-preview",...e}),this.file=e.file||{},this.isImage=this.file.content_type?.startsWith("image/"),this.isPdf="application/pdf"===this.file.content_type}getTemplate(){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 `}async onActionViewFile(){if(this.isImage){const e=window.MOJO?.plugins?.LightboxGallery;e?e.show({src:this.file.url,alt:this.file.filename}):window.open(this.file.url,"_blank")}else if(this.isPdf){const e=window.MOJO?.plugins?.PDFViewer;e?e.showDialog(this.file.url,{title:this.file.filename}):window.open(this.file.url,"_blank")}else window.open(this.file.url,"_blank")}}const u=[{value:"admin",label:"Admin"},{value:"email",label:"Email"},{value:"sms",label:"SMS"},{value:"push",label:"Push"},{value:"fileman",label:"File Manager"},{value:"api",label:"API"},{value:"other",label:"Other"}],b=[{value:"",label:"— None —"},{value:"summary",label:"summary"},{value:"summary_large_image",label:"summary_large_image"}];function g(e={}){const t=e||{};return{og_title:t["og:title"]||"",og_description:t["og:description"]||"",og_image:t["og:image"]||"",twitter_card:t["twitter:card"]||"",twitter_title:t["twitter:title"]||"",twitter_description:t["twitter:description"]||"",twitter_image:t["twitter:image"]||""}}function v(e={}){const t={og_title:"og:title",og_description:"og:description",og_image:"og:image",twitter_card:"twitter:card",twitter_title:"twitter:title",twitter_description:"twitter:description",twitter_image:"twitter:image"},i={};for(const[s,n]of Object.entries(t)){const t=e[s];null!=t&&""!==t&&(i[n]=t)}return i}function w(e={}){const t=["og_title","og_description","og_image","twitter_card","twitter_title","twitter_description","twitter_image"],i={...e};for(const n of t)delete i[n];const s=v(e);return Object.keys(s).length>0&&(i.metadata=s),i}class ShortLink extends e{constructor(e={}){super(e,{endpoint:"/api/shortlink/link"})}}class ShortLinkList extends t{constructor(e={}){super({ModelClass:ShortLink,endpoint:"/api/shortlink/link",...e})}}class ShortLinkClick extends e{constructor(e={}){super(e,{endpoint:"/api/shortlink/history"})}}class ShortLinkClickList extends t{constructor(e={}){super({ModelClass:ShortLinkClick,endpoint:"/api/shortlink/history",...e})}}const f=[{name:"url",type:"url",label:"Destination URL",required:!0,placeholder:"https://example.com/page",cols:12},{name:"source",type:"select",label:"Source",options:u,value:"admin",cols:6},{name:"expire_days",type:"number",label:"Expire (days)",value:3,min:0,cols:3,help:"0 = never"},{name:"expire_hours",type:"number",label:"Expire (hours)",value:0,min:0,cols:3},{name:"track_clicks",type:"switch",label:"Track clicks",value:!1,cols:4,help:"Record per-click history and per-link metrics."},{name:"bot_passthrough",type:"switch",label:"Bypass bot preview",value:!1,cols:4,help:"Bots receive a plain redirect (use for transactional links)."},{name:"is_protected",type:"switch",label:"Protected",value:!1,cols:4,help:"Prevents accidental deletion."},{type:"heading",label:"OpenGraph Preview (optional)",cols:12},{name:"og_title",type:"text",label:"og:title",placeholder:"Shown in Slack/iMessage preview",cols:12},{name:"og_description",type:"textarea",label:"og:description",rows:2,cols:12},{name:"og_image",type:"url",label:"og:image",placeholder:"https://example.com/preview.jpg",cols:12}],y={create:{title:"Create Shortlink",size:"md",fields:[...f],help:"Leave OG fields blank to let the server scrape the destination automatically."},edit:{title:"Edit Shortlink",size:"md",fields:[{name:"is_active",type:"switch",label:"Active",cols:4},...f,{name:"twitter_card",type:"select",label:"twitter:card",options:b,cols:6},{name:"twitter_title",type:"text",label:"twitter:title",cols:6},{name:"twitter_description",type:"textarea",label:"twitter:description",rows:2,cols:12},{name:"twitter_image",type:"url",label:"twitter:image",cols:12}]}};ShortLink.EDIT_FORM=y.edit;const x={image:{icon:"bi-image",previewType:"image",badgeClass:"bg-info"},video:{icon:"bi-camera-video",previewType:"video",badgeClass:"bg-primary"},audio:{icon:"bi-music-note-beamed",previewType:"audio",badgeClass:"bg-primary"},pdf:{icon:"bi-file-earmark-pdf",previewType:"pdf",badgeClass:"bg-danger"},document:{icon:"bi-file-earmark-text",previewType:"document",badgeClass:"bg-secondary"},spreadsheet:{icon:"bi-file-earmark-spreadsheet",previewType:"document",badgeClass:"bg-success"},presentation:{icon:"bi-file-earmark-slides",previewType:"document",badgeClass:"bg-warning"},archive:{icon:"bi-file-earmark-zip",previewType:"download",badgeClass:"bg-dark"},other:{icon:"bi-file-earmark",previewType:"download",badgeClass:"bg-secondary"}};class FilePreviewSection extends c{constructor(e={}){super({className:"file-preview-section p-3",...e}),this.categoryConfig=e.categoryConfig||x.other}_onModelChange(){const e=this.categoryConfig?.previewType;"video"!==e&&"audio"!==e&&this.isMounted()&&this.render()}getTemplate(){const e=this.categoryConfig.previewType,t=this.model.get("url")||"",i=this.model.get("filename")||"";if("image"===e)return`\n <div class="text-center">\n <img src="${C(this.model.getThumbnailUrl&&this.model.getThumbnailUrl()||t)}"\n alt="${C(i)}"\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 `;if("video"===e){const e=this.model.getThumbnailUrl&&this.model.getThumbnailUrl();return`\n <div class="text-center">\n <video controls preload="metadata"\n src="${C(t)}"\n ${e?`poster="${C(e)}"`:""}\n style="width: 100%; max-height: 70vh; background:#000;"></video>\n </div>\n `}if("audio"===e)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">${k(i)}</h5>\n <audio controls class="w-100" src="${C(t)}"></audio>\n </div>\n `;if("pdf"===e)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">${k(i)}</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 `;if("document"===e){const e=this.model.getBestImageRendition&&this.model.getBestImageRendition();return`\n <div class="text-center p-4 bg-light rounded">\n ${e?`<img src="${C(e.url)}" alt="${C(i)} preview" class="img-fluid rounded mb-3" style="max-height: 50vh;">`:`<i class="bi ${this.categoryConfig.icon} text-secondary" style="font-size: 5rem;"></i>`}\n <h5 class="mt-3 text-break">${k(i)}</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 `}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">${k(i)}</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 `}async onActionViewFile(){_(this.model,this.categoryConfig)}async onActionDownloadFile(){S(this.model)}}class FileRenditionsSection extends c{constructor(e={}){super({className:"file-renditions-section p-3",...e})}getTemplate(){return this.model.hasRenditions&&this.model.hasRenditions()?this._buildGalleryTemplate():this.model.isUploadPending&&this.model.isUploadPending()?this._buildWaitingTemplate():this._buildEmptyTemplate()}_buildGalleryTemplate(){const e=this.model.getRenditions(),t=e.map(e=>this._buildCard(e)).join(""),i=e.length;return`\n <div class="d-flex justify-content-between align-items-center mb-3">\n <div class="text-muted small">\n ${i} rendition${1===i?"":"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">${t}</div>\n `}_buildCard(e){const t=e&&e.url?e.url:"",i=e&&"string"==typeof e.content_type?e.content_type:"",s=e&&e.role?e.role:"rendition",n=e&&e.filename?e.filename:s,a=e&&e.file_size?function(e){if(null==e||isNaN(e))return"";const t=Number(e);return t<1024?`${t} B`:t<1048576?`${(t/1024).toFixed(1)} KB`:t<1073741824?`${(t/1024/1024).toFixed(1)} MB`:`${(t/1024/1024/1024).toFixed(1)} GB`}(e.file_size):"",o=e&&e.width&&e.height?`${e.width} × ${e.height}`:"",l=['data-action="view-rendition"',`data-url="${C(t)}"`,`data-ct="${C(i)}"`,`data-filename="${C(n)}"`,`data-role="${C(s)}"`].join(" ");let r;r=i.startsWith("image/")&&t?`<img src="${C(t)}" alt="${C(s)}"\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);">`:i.startsWith("video/")?`\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 ${k(i)}\n </span>\n </div>`:i.startsWith("audio/")?'\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 <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>';const c=t?`\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 ${l}\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="${C(t)}"\n title="Copy URL">\n <i class="bi bi-clipboard"></i>\n </button>\n <a href="${C(t)}" download="${C(n)}"\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 `:"";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 ${t?l:""} ${t?'role="button" style="cursor: pointer;"':""}>\n ${r}\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="${C(s)}">${k(s)}</span>\n ${a?`<small class="text-muted flex-shrink-0" style="font-variant-numeric: tabular-nums;">${k(a)}</small>`:""}\n </div>\n ${o?`<div class="small text-muted" style="font-variant-numeric: tabular-nums;">${k(o)}</div>`:""}\n </div>\n ${c}\n </div>\n </div>\n `}_buildEmptyTemplate(){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 '}_buildWaitingTemplate(){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 '}async onActionRefreshRenditions(){try{await this.model.fetch()}catch(e){console.warn("FileView: refresh-renditions fetch failed:",e)}}async onActionViewRendition(e,t){e&&(e.preventDefault(),e.stopPropagation());const i=t.dataset.url,s=t.dataset.ct||"";if(i){if(s.startsWith("image/")){const e="undefined"!=typeof window?window.MOJO?.plugins?.LightboxGallery:null;if(e&&"function"==typeof e.show){const t=this.model.getRenditions().filter(e=>e&&e.url&&"string"==typeof e.content_type&&e.content_type.startsWith("image/")).map(e=>({src:e.url,alt:e.role||""})),s=Math.max(0,t.findIndex(e=>e.src===i));return void e.show(t,{startIndex:s,fitToScreen:!1})}}window.open(i,"_blank","noopener")}}async onActionCopyRenditionUrl(e,t){e&&(e.preventDefault(),e.stopPropagation());const i=t.dataset.url;if(i)try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(i);else{const e=document.createElement("textarea");e.value=i,document.body.appendChild(e),e.select(),document.execCommand("copy"),document.body.removeChild(e)}const e=t.querySelector("i");if(e){const t=e.className;e.className="bi bi-check-lg text-success",setTimeout(()=>{e.className=t},1200)}this.getApp()?.toast?.success?.("URL copied to clipboard")}catch(s){console.error("Failed to copy rendition URL:",s),this.getApp()?.toast?.error?.("Failed to copy URL")}}async onActionRegenerateFromSection(){let e=this.parent;for(;e;){if("function"==typeof e.onActionRegenerateRenditions)return e.onActionRegenerateRenditions();e=e.parent}}}function _(e,t){const i=e.get("url");if(!i)return;const s=t.previewType;if("image"===s){const t="undefined"!=typeof window?window.MOJO?.plugins?.LightboxGallery:null,s=e.get("renditions")||{},n=[{src:i,alt:"Original"},...Object.values(s).filter(e=>e&&e.url&&"string"==typeof e.content_type&&e.content_type.startsWith("image/")).map(e=>({src:e.url,alt:e.role||""}))];return void(t&&"function"==typeof t.show?t.show(n,{fitToScreen:!1}):window.open(i,"_blank"))}if("pdf"===s){const t="undefined"!=typeof window?window.MOJO?.plugins?.PDFViewer:null;return void(t&&"function"==typeof t.showDialog?t.showDialog(i,{title:e.get("filename")}):window.open(i,"_blank"))}window.open(i,"_blank")}function S(e){const t=e.get("url");if(!t)return;const i=document.createElement("a");i.href=t,i.download=e.get("filename")||"",document.body.appendChild(i),i.click(),document.body.removeChild(i)}class FileSharesSection extends c{constructor(e={}){super({className:"file-shares-section p-3",...e}),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 '}async onInit(){const e=this.model.get("id");if(!e)return;const t=new ShortLinkList({params:{source:"fileman-share",file:e,sort:"-created",size:25}});this._sharesCollection=t,this.sharesTable=new l({containerId:"file-shares-table",collection:t,hideActivePillNames:["source","file"],columns:[{key:"code",label:"Short URL",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 '},{key:"user.display_name",label:"Shared By",formatter:"default('—')"},{key:"hit_count",label:"Hits",width:"80px",sortable:!0},{key:"track_clicks",label:"Tracked",width:"90px",formatter:"yesnoicon"},{key:"is_active",label:"Active",width:"80px",formatter:"yesnoicon"},{key:"expires_at",label:"Expires",width:"160px",formatter:"datetime|default('Never')",sortable:!0},{key:"created",label:"Created",width:"160px",formatter:"datetime",sortable:!0},{key:"metadata.note",label:"Note",formatter:"truncate(40)|default('—')",visibility:"lg"}],paginated:!0,sortable:!0,searchable:!1,filterable:!1,contextMenu:[{label:"Copy Short URL",action:"copy-share-code",icon:"bi-clipboard"},{divider:!0},{label:"Revoke",action:"revoke-share",icon:"bi-slash-circle",danger:!0}],tableOptions:{hover:!0,size:"sm",emptyMessage:"No shares yet — click “Share new” to mint a tracked link.",emptyIcon:"bi-link-45deg",actions:[]}}),this.addChild(this.sharesTable)}refreshShares(){return this._sharesCollection?.fetch()}async onActionShareFileFromSection(){let e=this.parent;for(;e;){if("function"==typeof e.onActionShareFile)return e.onActionShareFile();e=e.parent}return null}async onActionCopyShareCode(e,t){e&&(e.preventDefault(),e.stopPropagation());const i=t?.dataset?.code;if(!i)return;const s=function(e,t){if(!e)return"";const i=t?.config?.shortlink_base_url||("undefined"!=typeof window?window.location.origin:"");return`${String(i).replace(/\/+$/,"")}/s/${e}`}(i,this.getApp?.());try{await navigator.clipboard.writeText(s),this.getApp()?.toast?.success?.(`Copied: ${s}`)}catch(n){this.getApp()?.toast?.warning?.("Copy failed — select the URL manually.")}}async onActionRevokeShare(e,t){e&&(e.preventDefault(),e.stopPropagation());const i=t?.closest?.("[data-row-id]"),s=i?.dataset?.rowId||t?.dataset?.id;if(!s)return;const a=this._sharesCollection?.get?.(s);if(a&&await n.confirm("Revoke this share? Anyone with the short URL will get a 404 — the audit row is preserved.","Revoke Share",{confirmText:"Revoke",confirmClass:"btn-danger"}))try{await a.save({is_active:!1}),this.getApp()?.toast?.success?.("Share revoked"),await this.refreshShares()}catch(o){console.error("Failed to revoke share:",o),this.getApp()?.toast?.error?.("Failed to revoke share")}}}class FileView extends c{constructor(e={}){super({className:"file-view",...e}),this.model=e.model||new a(e.data||{}),this.sideNavView=null,this.contextMenu=null,this.template='\n <div class="file-view-container d-flex flex-column" style="min-height: 0;">\n \x3c!-- Header + Context Menu --\x3e\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 \x3c!-- Section body --\x3e\n <div data-container="file-sidenav" class="flex-grow-1" style="min-height: 400px; max-height: 70vh;"></div>\n </div>\n '}_getCategoryConfig(){return function(e){const t=e&&"function"==typeof e.getCategory?e.getCategory():e?.get?.("category")||"other";return x[t]||x.other}(this.model)}async onInit(){const e=this._getCategoryConfig();this.header=new c({containerId:"file-header",template:this._buildHeaderTemplate(e)}),this.header.setModel(this.model),this.addChild(this.header);const t=[],i=new FilePreviewSection({model:this.model,categoryConfig:e});t.push({key:"preview",label:"Preview",icon:e.icon,view:i});const n=new d({model:this.model,className:"p-3",showEmptyValues:!0,emptyValueText:"—",columns:2,fields:[{name:"id",label:"ID"},{name:"filename",label:"Filename"},{name:"storage_filename",label:"Storage Filename"},{name:"content_type",label:"Content Type"},{name:"file_size",label:"File Size",format:"filesize"},{name:"category",label:"Category"},{name:"upload_status",label:"Status",format:"badge"},{name:"created",label:"Created",format:"datetime"},{name:"modified",label:"Modified",format:"datetime"},{name:"user.display_name",label:"Uploaded By"},{name:"file_manager.name",label:"Storage Backend"},{name:"storage_file_path",label:"Storage Path"},{name:"url",label:"Public URL",format:"url"},{name:"is_public",label:"Is Public",format:"boolean"}]});t.push({key:"details",label:"Details",icon:"bi-info-circle",view:n});const a=new FileRenditionsSection({model:this.model});t.push({key:"renditions",label:"Renditions",icon:"bi-layers",view:a}),this.model.get("id")&&(this.sharesSection=new FileSharesSection({model:this.model}),t.push({key:"shares",label:"Shares",icon:"bi-link-45deg",view:this.sharesSection}));const o=this.model.get("metadata");if(o&&"object"==typeof o&&Object.keys(o).length){const e=new d({data:o,className:"p-3",columns:2,showEmptyValues:!1});t.push({key:"metadata",label:"Metadata",icon:"bi-braces",view:e})}this.sideNavView=new SideNavView({containerId:"file-sidenav",activeSection:"preview",navWidth:200,contentPadding:"1.25rem 1.5rem",enableResponsive:!0,minWidth:500,sections:t}),this.addChild(this.sideNavView),this.contextMenu=new s({containerId:"file-context-menu",className:"context-menu-view header-menu-absolute",context:this.model,config:{icon:"bi-three-dots-vertical",items:[{label:"View",action:"view-file",icon:"bi-eye"},{label:"Download",action:"download-file",icon:"bi-download"},{label:"Copy URL",action:"copy-url",icon:"bi-clipboard"},{label:"Share Link…",action:"share-file",icon:"bi-link-45deg"},{type:"divider"},{label:"Edit Details",action:"edit-file",icon:"bi-pencil"},this.model.get("is_public")?{label:"Make Private",action:"make-private",icon:"bi-lock"}:{label:"Make Public",action:"make-public",icon:"bi-unlock"},{label:"Regenerate Previews",action:"regenerate-renditions",icon:"bi-arrow-repeat"},{type:"divider"},{label:"Delete File",action:"delete-file",icon:"bi-trash",danger:!0}]}}),this.addChild(this.contextMenu)}async onBeforeDestroy(){this._stopRenditionsPoll()}_buildHeaderTemplate(e){const t=this.model.getThumbnailUrl&&this.model.getThumbnailUrl();return`\n <div class="d-flex align-items-center gap-3">\n <div class="file-view-thumb flex-shrink-0">\n ${t?`<img src="${C(t)}" alt="thumbnail" class="rounded" style="width: 80px; height: 80px; object-fit: cover;">`:`<div class="rounded bg-light d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">\n <i class="bi ${e.icon} text-secondary" style="font-size: 2.25rem;"></i>\n </div>`}\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 ${e.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 `}async onActionViewFile(){_(this.model,this._getCategoryConfig())}async onActionDownloadFile(){S(this.model)}async onActionCopyUrl(){const e=this.model.get("url");if(e)try{if(navigator.clipboard&&window.isSecureContext)await navigator.clipboard.writeText(e);else{const t=document.createElement("textarea");t.value=e,document.body.appendChild(t),t.select(),document.execCommand("copy"),document.body.removeChild(t)}this.getApp()?.toast?.success?.("URL copied to clipboard")}catch(t){console.error("Failed to copy URL:",t),this.getApp()?.toast?.error?.("Failed to copy URL")}}async onActionEditFile(){await n.modelForm({title:`Edit File - ${this.model.get("filename")}`,model:this.model,formConfig:o.edit})&&this.render()}async onActionMakePublic(){await this.model.save({is_public:!0}),this.render()}async onActionMakePrivate(){await this.model.save({is_public:!1}),this.render()}async onActionShareFile(){if(!this.model.get("id"))return void this.getApp()?.toast?.warning?.("Save the file before sharing it.");const e=await n.form({title:"Share Link",size:"sm",help:"Each share creates a distinct, audit-tracked short URL attributed to you.",fields:[{name:"expire_days",type:"number",label:"Expire after (days)",value:30,min:0,cols:12,help:"0 = never expires. Server max: 3650."},{name:"track_clicks",type:"switch",label:"Track clicks",value:!0,cols:12,help:"Records per-click history (IP, user-agent, bot/human)."},{name:"note",type:"textarea",label:"Note (optional)",rows:2,cols:12,maxlength:512,help:"Private audit note — not shown to recipients."}],submitText:"Share"});if(!e)return;const t={};let i;void 0!==e.expire_days&&null!==e.expire_days&&""!==e.expire_days&&(t.expire_days=Number(e.expire_days)),void 0!==e.track_clicks&&(t.track_clicks=!!e.track_clicks),e.note&&(t.note=String(e.note).slice(0,512));try{i=await this.model.share(!Object.keys(t).length||t)}catch(h){return console.error("Share failed:",h),void n.showError(h?.data?.error||h?.message||"Failed to create share link")}const s=i?.data,a=s?.url;if(!i?.success||!a)return void n.showError(s?.error||"Failed to create share link");let o=!1;try{await(navigator.clipboard?.writeText?.(a)),o=!0}catch(m){o=!1}const l=s.expires_at?new Date(s.expires_at).toLocaleString():"Never",r=s.track_clicks?"Yes":"No",c=o?'<div class="form-text text-success mb-2"><i class="bi bi-check-circle me-1"></i>Copied to clipboard.</div>':'<div class="form-text text-muted mb-2">Select the URL above to copy.</div>',d=`\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="${C(a)}">\n ${c}\n </div>\n <dl class="row small mb-0">\n <dt class="col-4 text-muted">Expires</dt><dd class="col-8">${k(l)}</dd>\n <dt class="col-4 text-muted">Tracked</dt><dd class="col-8">${k(r)}</dd>\n ${s.shortlink_code?`<dt class="col-4 text-muted">Code</dt><dd class="col-8"><code>${k(s.shortlink_code)}</code></dd>`:""}\n </dl>\n `;await n.alert(d,"Share link created",{type:"success"});try{await(this.sharesSection?.refreshShares?.())}catch(h){console.warn("Failed to refresh shares section:",h)}}async onActionRegenerateRenditions(){if(await n.confirm("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.","Regenerate Previews",{confirmText:"Regenerate"})){try{await this.model.regenerateRenditions(),this.getApp()?.toast?.success?.("Regenerating previews in the background…")}catch(e){return console.error("Failed to trigger regenerate_renditions:",e),void this.getApp()?.toast?.error?.("Failed to start preview regeneration")}this._maybeStartRenditionsPoll({force:!0})}}_maybeStartRenditionsPoll(e={}){if(this._renditionsPollTimer)return;if(!e.force)return;let t=0;const i=()=>{this._renditionsPollTimer=null,this.model&&(this.model.hasRenditions&&this.model.hasRenditions()||++t>60||(this._renditionsPollTimer=setTimeout(async()=>{try{await this.model.fetch()}catch(e){console.warn("FileView: renditions poll fetch failed:",e)}i()},5e3)))};i()}_stopRenditionsPoll(){this._renditionsPollTimer&&(clearTimeout(this._renditionsPollTimer),this._renditionsPollTimer=null)}async onActionDeleteFile(){if(!(await n.confirm(`Are you sure you want to delete the file "${this.model.get("filename")}"? This action cannot be undone.`,"Confirm Deletion",{confirmClass:"btn-danger",confirmText:"Delete"})))return;const e=await this.model.destroy();e&&e.success&&this.emit("file:deleted",{model:this.model})}_onModelChange(){}async showSection(e){this.sideNavView&&await this.sideNavView.showSection(e)}getActiveSection(){return this.sideNavView?this.sideNavView.getActiveSection():null}static create(e={}){return new FileView(e)}}function k(e){return null==e?"":String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function C(e){return k(e)}a.VIEW_CLASS=FileView;class ChatMessageView extends c{constructor(e={}){const t=e.message||{},i=e.theme||"compact",s=e.isCurrentUser||!1,n=t.role||(s?"user":null);let a="chat-message";"bubbles"===i&&(a+=s?" message-right":" message-left"),"assistant"===n?a+=" message-assistant":"user"===n&&(a+=" message-user"),super({className:a,...e}),this.message=t,this.theme=i,this.isCurrentUser=s,this.role=n}getTemplate(){return"system_event"===this.message.type?'\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 ':"bubbles"===this.theme?this.getBubblesTemplate():this.getCompactTemplate()}getCompactTemplate(){const e=this.isCurrentUser?"bg-primary":"bg-secondary",t="assistant"===this.role;return`\n <div class="message-item">\n <div class="message-avatar ${t?"bg-dark":e}">\n ${t?'<i class="bi bi-robot"></i>':'\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 ${t?"Assistant":"{{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 `}getBubblesTemplate(){return`\n <div class="message-bubble-wrapper">\n <div class="message-meta">\n <strong>${"assistant"===this.role?'<i class="bi bi-robot me-1"></i>Assistant':"{{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 `}_getToolCallsTemplate(){if(!this.message.tool_calls||0===this.message.tool_calls.length)return"";const e=this.message.tool_calls.map(e=>{const t=(e=>{const t=document.createElement("div");return t.textContent=e,t.innerHTML})(e.name||e.function?.name||"tool");return`<span class="badge ${"error"===e.status?"bg-danger":"bg-info"} me-1">${t}</span>`}).join(""),t=`tools-${this.message.id||this.id}`;return`\n <div class="message-tool-calls mt-1">\n <a class="text-muted small" data-bs-toggle="collapse" href="#${t}" 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="${t}">\n <div class="mt-1">${e}</div>\n </div>\n </div>\n `}async onAfterRender(){if(this.message.attachments&&this.message.attachments.length>0){const e=this.element.querySelector('[data-container="attachments"]');e&&this.message.attachments.forEach(t=>{const i=new FilePreviewView({file:t});this.addChild(i),i.render(!0,e)})}}}class ChatInputView extends c{constructor(e={}){super({className:"chat-input-view",...e}),this.placeholder=e.placeholder||"Type a message...",this.buttonText=e.buttonText||"Send",this.showFileInput=!1!==e.showFileInput,this.attachments=[],this.pendingUploads=/* @__PURE__ */new Map}getTemplate(){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 `}async onAfterRender(){this.showFileInput&&this.enableFileDrop({dropZoneSelector:".chat-input-container",multiple:!0,acceptedTypes:["*/*"],visualFeedback:!0,dragOverClass:"drag-over",dragActiveClass:"drag-active"});const e=this.element.querySelector(".chat-input");e&&(e.addEventListener("input",()=>this.autoResizeTextarea(e)),e.addEventListener("keydown",e=>this.handleKeydown(e)))}handleKeydown(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),this.onActionSendMessage(e,e.target))}async onFileDrop(e){for(const t of e)await this.uploadFile(t)}async uploadFile(e){const t=new a,i=Date.now()+Math.random();this.addFilePreview(i,e,0),this.pendingUploads.set(i,{file:e,fileModel:t});try{await t.upload({file:e,onProgress:e=>{this.updateFileProgress(i,e)},onComplete:e=>{this.handleUploadComplete(i,t)}})}catch(s){console.error("File upload failed:",s),this.handleUploadError(i,s)}}addFilePreview(e,t,i){const s=this.element.querySelector('[data-container="attachments"]');if(!s)return;const n=document.createElement("div");n.className="attachment-preview",n.dataset.uploadId=e,n.innerHTML=`\n <div class="attachment-info">\n <i class="bi bi-file-earmark"></i>\n <span class="attachment-name">${this.escapeHtml(t.name)}</span>\n <span class="attachment-size">(${this.formatFileSize(t.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: ${i}%"></div>\n </div>\n </div>\n <button class="attachment-remove btn btn-sm btn-link text-danger" data-action="remove-attachment" data-upload-id="${e}" type="button">\n <i class="bi bi-x"></i>\n </button>\n `,s.appendChild(n)}updateFileProgress(e,t){const i=this.element.querySelector(`[data-upload-id="${e}"]`);if(i){const e=i.querySelector(".progress-bar");e&&(e.style.width=`${t}%`)}}handleUploadComplete(e,t){this.attachments.push({id:t.id,name:t.get("name"),uploadId:e}),this.pendingUploads.delete(e);const i=this.element.querySelector(`[data-upload-id="${e}"]`);if(i){i.classList.add("upload-complete");const e=i.querySelector(".attachment-progress");e&&e.remove()}}handleUploadError(e,t){this.pendingUploads.delete(e);const i=this.element.querySelector(`[data-upload-id="${e}"]`);i&&(i.classList.add("upload-error"),i.querySelector(".attachment-info").innerHTML+='<span class="text-danger ms-2">Upload failed</span>')}async onActionRemoveAttachment(e,t){const i=t.dataset.uploadId;this.pendingUploads.delete(i);const s=this.element.querySelector(`[data-upload-id="${i}"]`);s&&s.remove()}async onActionSendMessage(e,t){const i=this.element.querySelector(".chat-input").value.trim();(i||0!==this.attachments.length)&&(this.pendingUploads.size>0||(this.setBusy(!0),this.emit("message:send",{text:i,files:this.attachments})))}setEnabled(e){const t=this.element?.querySelector(".chat-input"),i=this.element?.querySelector(".chat-send-btn");t&&(t.disabled=!e),i&&(i.disabled=!e)}setBusy(e){const t=this.element.querySelector(".chat-send-btn"),i=t.querySelector(".bi-send-fill"),s=t.querySelector(".spinner-border");e?(t.disabled=!0,i.classList.add("d-none"),s.classList.remove("d-none")):(t.disabled=!1,i.classList.remove("d-none"),s.classList.add("d-none"))}clearInput(){const e=this.element.querySelector(".chat-input");e&&(e.value="",e.style.height="auto");const t=this.element.querySelector('[data-container="attachments"]');t&&(t.innerHTML=""),this.attachments=[],this.pendingUploads.clear(),this.setBusy(!1)}autoResizeTextarea(e){e.style.height="auto",e.style.height=Math.min(e.scrollHeight,150)+"px"}formatFileSize(e){if(0===e)return"0 B";const t=Math.floor(Math.log(e)/Math.log(1024));return parseFloat((e/Math.pow(1024,t)).toFixed(1))+" "+["B","KB","MB","GB"][t]}escapeHtml(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}}h(ChatInputView);class ChatView extends c{constructor(e={}){super({className:"chat-view",...e}),this.adapter=e.adapter,this.theme=e.theme||"compact",this.currentUserId=e.currentUserId,this.inputPlaceholder=e.inputPlaceholder||"Type a message...",this.inputButtonText=e.inputButtonText||"Send",this.showFileInput=!1!==e.showFileInput,this.showInput=!1!==e.showInput,this.MessageViewClass=e.messageViewClass||ChatMessageView,this.messages=[],this.messageViews=/* @__PURE__ */new Map,this._thinkingEl=null}getTemplate(){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 `}async onInit(){this.messages=await this.adapter.fetch(),this.showInput&&(this.inputView=new ChatInputView({containerId:"input",placeholder:this.inputPlaceholder,buttonText:this.inputButtonText,showFileInput:this.showFileInput}),this.addChild(this.inputView),this.inputView.on("message:send",async e=>{await this.handleSendMessage(e)}))}async onAfterRender(){this._buildMessageViews(),await this._renderChildren(),this.scrollToBottom()}async _renderChildren(){const e=/* @__PURE__ */new Set;this.messageViews.forEach(t=>e.add(t.id));for(const i in this.children){const t=this.children[i];t&&!e.has(i)&&(t.parent=this,await Promise.resolve(t.render()).catch(e=>console.warn(`ChatView child render error (${i})`,e)))}const t=this.element.querySelector('[data-container="messages"]');t&&this.messageViews.forEach(e=>{t.appendChild(e.element),e.render(!1)})}_buildMessageViews(){this.messages&&0!==this.messages.length&&this.messages.forEach(e=>{this.messageViews.has(e.id)||this._createMessageView(e)})}_createMessageView(e){if(this.messageViews.has(e.id))return;const t=e.author&&e.author.id===this.currentUserId,i=new this.MessageViewClass({message:e,theme:this.theme,isCurrentUser:t});return this.addChild(i),this.messageViews.set(e.id,i),i}addMessage(e,t=!0){if(this.messageViews.has(e.id))return;const i=this._createMessageView(e);if(this.isMounted()){const e=this.element.querySelector('[data-container="messages"]');e&&(e.appendChild(i.element),i.render(!1))}t&&this.scrollToBottom()}async handleSendMessage(e){try{if(e.text&&e.text.trim()&&!(await this.adapter.addNote({text:e.text,files:e.files&&e.files.length>0?[e.files[0]]:[]})).success)throw new Error("Failed to send message");for(let t=e.text&&e.text.trim()&&e.files?.length>0?1:0;t<(e.files?.length||0);t++){const i=e.files[t];(await this.adapter.addNote({text:"",files:[i]})).success||console.error("Failed to upload file:",i)}this.messages=await this.adapter.fetch(),this.messages.forEach(e=>{this.messageViews.has(e.id)||this.addMessage(e,!0)}),this.inputView.clearInput()}catch(t){console.error("Failed to send message:",t),this.inputView.setBusy(!1);try{this.getApp().toast.error("Failed to send message")}catch(i){}}}showThinking(e="Thinking..."){const t=this.element?.querySelector('[data-container="messages"]');t&&(this._thinkingEl||(this._thinkingEl=document.createElement("div"),this._thinkingEl.className="chat-thinking",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 ',t.appendChild(this._thinkingEl),this._thinkingStart=Date.now(),this._thinkingInterval=setInterval(()=>{const e=Math.floor((Date.now()-this._thinkingStart)/1e3),t=Math.floor(e/60),i=e%60,s=this._thinkingEl?.querySelector(".chat-thinking-timer");s&&(s.textContent=t>0?`${t}m ${String(i).padStart(2,"0")}s`:`${i}s`)},1e3)),this._thinkingEl.querySelector(".chat-thinking-text").textContent=e,this.scrollToBottom())}hideThinking(){this._thinkingInterval&&(clearInterval(this._thinkingInterval),this._thinkingInterval=null),this._thinkingEl&&(this._thinkingEl.remove(),this._thinkingEl=null)}setInputEnabled(e){this.inputView?.setEnabled&&this.inputView.setEnabled(e)}scrollToBottom(){const e=this.element.querySelector(".chat-messages");e&&requestAnimationFrame(()=>{e.scrollTop=e.scrollHeight})}clearMessages(){this.messageViews.forEach(e=>{delete this.children[e.id],e.destroy()}),this.messageViews.clear(),this.messages=[];const e=this.element?.querySelector('[data-container="messages"]');e&&(e.innerHTML="")}async refresh(){this.clearMessages(),this.messages=await this.adapter.fetch(),this._buildMessageViews(),this.isMounted()&&(await this._renderChildren(),this.scrollToBottom())}}export{ChatView as C,FilePreviewView as F,GeoLocatedIP as G,m as M,SideNavView as S,TabView as T,ChatInputView as a,ChatMessageView as b,FileView as c,GeoLocatedIPList as d,MetricsPermission as e,MetricsPermissionList as f,TablePage as g,ShortLink as h,u as i,ShortLinkClickList as j,g as k,y as l,w as m,b as n,v as o,ShortLinkList as p};
2
+ //# sourceMappingURL=ChatView-W8daOwIo.js.map