@hienlh/ppm 0.17.57 → 0.18.0
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/.release-run.sh +5 -0
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +4 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +16 -2
- package/bun.lock +3 -0
- package/dist/web/assets/{ai-resource-editor-CXWQcx3_.js → ai-resource-editor-BLYgg033.js} +1 -1
- package/dist/web/assets/{angular-html-D4YJUMLP.js → angular-html-jfMqUuRd.js} +1 -1
- package/dist/web/assets/{angular-ts-CUvq2h3X.js → angular-ts-wytn9PMm.js} +1 -1
- package/dist/web/assets/{api-client-COQtP-dB.js → api-client-B9OFv3nY.js} +1 -1
- package/dist/web/assets/api-client-CKo60P6G.js +1 -0
- package/dist/web/assets/{api-settings-Do0Z0IRN.js → api-settings-DGhshDRg.js} +1 -1
- package/dist/web/assets/api-settings-fdkhqLSF.js +1 -0
- package/dist/web/assets/{api-themes-DXi8sz-9.js → api-themes-okiyhhp9.js} +1 -1
- package/dist/web/assets/{apl-DSANFLx0.js → apl-i-ahKx6O.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-B4J2-O8Q.js +1 -0
- package/dist/web/assets/{astro-CXIotiNS.js → astro-DVBCJfG6.js} +1 -1
- package/dist/web/assets/{audio-preview-D7jNc8mi.js → audio-preview-y8XpCNrh.js} +1 -1
- package/dist/web/assets/{blade-BvrP2gPd.js → blade-BHlL8SqU.js} +1 -1
- package/dist/web/assets/{bsl-CGf6Xuyu.js → bsl-D-UOfR1F.js} +1 -1
- package/dist/web/assets/{cairo-BboZ9qIf.js → cairo-Bn6rucli.js} +1 -1
- package/dist/web/assets/can-open-in-ppm-BzVXoV-d.js +1 -0
- package/dist/web/assets/chat-tab-BcR-3_6R.js +13 -0
- package/dist/web/assets/clock-B1CPBp2g.js +1 -0
- package/dist/web/assets/{cobol-D39V6ukp.js → cobol-Byn9FYKu.js} +1 -1
- package/dist/web/assets/code-editor-CJ8g7dqm.js +10 -0
- package/dist/web/assets/{coffee-DTtK7kKi.js → coffee-DjBl4gm-.js} +1 -1
- package/dist/web/assets/{conflict-editor-CpGcSopK.js → conflict-editor-BP7JFZzG.js} +3 -3
- package/dist/web/assets/context-menu-BSE5yuMG.js +1 -0
- package/dist/web/assets/copy-Su9m2utd.js +1 -0
- package/dist/web/assets/{cpp-CnU9hfBq.js → cpp-CKzic-3E.js} +1 -1
- package/dist/web/assets/{crystal-DMjCL--0.js → crystal-Bkg-Ly_g.js} +1 -1
- package/dist/web/assets/csv-preview-xAwUDNat.js +2 -0
- package/dist/web/assets/{data-grid-overlay-editor-Bdc80TJ1.js → data-grid-overlay-editor-DMTCHIG9.js} +1 -1
- package/dist/web/assets/{database-viewer-DTMmic31.js → database-viewer-BVLWYPa4.js} +1 -1
- package/dist/web/assets/dialog-D__LRQTn.js +1 -0
- package/dist/web/assets/diff-viewer-BheSOHfl.js +4 -0
- package/dist/web/assets/{docx-preview-CR2ZtvNP.js → docx-preview-CGlMqltP.js} +1 -1
- package/dist/web/assets/{edge-CTom35R_.js → edge-DVbZnd3Y.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-D1TEK7Jz.js → edit-diff-preview-ClJAJwlL.js} +1 -1
- package/dist/web/assets/{elixir-BAX9HSOu.js → elixir-Dwi8Wu7W.js} +1 -1
- package/dist/web/assets/{elm-CpD28xsW.js → elm-DtIv-HXJ.js} +1 -1
- package/dist/web/assets/{erb-DHX2IIEs.js → erb-D3HEDz1x.js} +1 -1
- package/dist/web/assets/{erlang-B_etLcPG.js → erlang-2MjTCzGz.js} +1 -1
- package/dist/web/assets/{esm-PmlN1RTs.js → esm-BfqDE09P.js} +1 -1
- package/dist/web/assets/explorer-body-DE5339g9.js +2 -0
- package/dist/web/assets/explorer-window-content-BrTHAoXM.js +1 -0
- package/dist/web/assets/{extension-webview-BTCFWY2A.js → extension-webview-DRq1tWC1.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-CpR0cN7O.js → fortran-fixed-form-BLyAtyPM.js} +1 -1
- package/dist/web/assets/{fsharp-DxITxZW5.js → fsharp-Bxbn_gbr.js} +1 -1
- package/dist/web/assets/{gdresource-DJ7IvROZ.js → gdresource-sFMY4D2Q.js} +1 -1
- package/dist/web/assets/{git-commit-sQznNP43.js → git-commit-BzB9Lio5.js} +1 -1
- package/dist/web/assets/git-log-panel-pjyOjchU.js +1 -0
- package/dist/web/assets/{git-rebase-DkHvejAR.js → git-rebase-CKSVd848.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CtQ3yXN-.js +1 -0
- package/dist/web/assets/{glide-data-grid-LinTYl0_.js → glide-data-grid-LX3N4SsJ.js} +9 -9
- package/dist/web/assets/{glimmer-js-BwJnDfL-.js → glimmer-js-BY8aExAJ.js} +1 -1
- package/dist/web/assets/{glimmer-ts-Coz155XK.js → glimmer-ts-Bak54dBX.js} +1 -1
- package/dist/web/assets/{glsl-BED7ZmUZ.js → glsl-nR2eaJQe.js} +1 -1
- package/dist/web/assets/{graphql-B_KyE6oz.js → graphql-CGPPFAU6.js} +1 -1
- package/dist/web/assets/group-chat-tab-CVQfptkT.js +1 -0
- package/dist/web/assets/{hack-sxywdIAQ.js → hack-CXIMX-Ol.js} +1 -1
- package/dist/web/assets/{haml-Dts9JlPU.js → haml-tPY-9Pfm.js} +1 -1
- package/dist/web/assets/{handlebars-C9HXIMtj.js → handlebars-BuW1LT7T.js} +1 -1
- package/dist/web/assets/{html-2p0RvXi4.js → html-bgVcwZ-t.js} +1 -1
- package/dist/web/assets/{html-derivative-BiTPuOba.js → html-derivative-D6-IxTgn.js} +1 -1
- package/dist/web/assets/{http-CHd2gYPf.js → http-CJ1zyB-h.js} +1 -1
- package/dist/web/assets/{hurl-boMPgDm9.js → hurl-CmoT7l3T.js} +1 -1
- package/dist/web/assets/{hxml-BQvjiuBt.js → hxml-CztOsL8o.js} +1 -1
- package/dist/web/assets/{image-preview-0SGb7_tH.js → image-preview-BDySmiim.js} +1 -1
- package/dist/web/assets/index-XoDY9bc0.js +52 -0
- package/dist/web/assets/index-YCTraslE.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-Cg0mIJn9.js +1 -0
- package/dist/web/assets/{jinja-B-zKiwlp.js → jinja-B1pq3jJq.js} +1 -1
- package/dist/web/assets/{jison-C2vJYjbJ.js → jison-D8B5pCwA.js} +1 -1
- package/dist/web/assets/{julia-DJ8MrQPA.js → julia-D0K3bwpU.js} +1 -1
- package/dist/web/assets/{just-BvQHq7VE.js → just-B9Jlddu-.js} +1 -1
- package/dist/web/assets/keybindings-store-BE1jVUZW.js +1 -0
- package/dist/web/assets/{latex-ChNeshUC.js → latex-BPJSSE6j.js} +1 -1
- package/dist/web/assets/{liquid-KNJEBOwW.js → liquid-lv10fo4U.js} +1 -1
- package/dist/web/assets/{lua-D8NNsLbC.js → lua-PNuaIBEF.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DSxSupDq.js → markdown-renderer-B8Kz5rvP.js} +3 -3
- package/dist/web/assets/markdown-renderer-Btync7yg.js +1 -0
- package/dist/web/assets/{marko-HGo5Qtz4.js → marko-Du3frD6X.js} +1 -1
- package/dist/web/assets/{mdc-omkodQB5.js → mdc-BeDVcFRl.js} +1 -1
- package/dist/web/assets/mobile-bottom-sheet-CzRXE6mY.js +1 -0
- package/dist/web/assets/mobile-explorer-sheet-CDU8xriw.js +1 -0
- package/dist/web/assets/{nextflow-Adq02pv-.js → nextflow-DkcXP71s.js} +1 -1
- package/dist/web/assets/{nginx-ByvWruGD.js → nginx-C5n8N_bt.js} +1 -1
- package/dist/web/assets/{nim-BrMojX4O.js → nim-CIhyfs-A.js} +1 -1
- package/dist/web/assets/notification-store-Bi7VHAy4.js +1 -0
- package/dist/web/assets/{number-overlay-editor-CunvWJnN.js → number-overlay-editor-B8P26qGC.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-D79vAFR_.js +1 -0
- package/dist/web/assets/{panel-store-CpKvXNnm.js → panel-store-gbNJvZB_.js} +1 -1
- package/dist/web/assets/{pdf-preview-DEn8amrO.js → pdf-preview-CtZWvujY.js} +1 -1
- package/dist/web/assets/{perl-Bd_0l-b4.js → perl-Cf31sZYE.js} +1 -1
- package/dist/web/assets/{php-D-DLTnWJ.js → php-Dzl_MDIW.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-DYUOH68U.js +1 -0
- package/dist/web/assets/{postgres-viewer-CjrM15gz.js → postgres-viewer-fqMUi0xS.js} +3 -3
- package/dist/web/assets/{project-store-D0ALup9Z.js → project-store-D1D1MN03.js} +1 -1
- package/dist/web/assets/{pug-D2aO-l2T.js → pug-CxSYHrB3.js} +1 -1
- package/dist/web/assets/{qml-CyQVzNXS.js → qml-DlEjGhwN.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-B1eznNtd.js +1 -0
- package/dist/web/assets/{razor-8tvJXsO3.js → razor-KVt5uW14.js} +1 -1
- package/dist/web/assets/{rst-C4AZyQQl.js → rst-D5BwNTnw.js} +1 -1
- package/dist/web/assets/{ruby-O_z2NTLY.js → ruby-D5fejQwd.js} +1 -1
- package/dist/web/assets/{sas-BtnF4zaV.js → sas-Bj7B1CB2.js} +1 -1
- package/dist/web/assets/{scss-GAyp5euH.js → scss-BB0CUoCL.js} +1 -1
- package/dist/web/assets/settings-store-Bm7uL8VA.js +2 -0
- package/dist/web/assets/settings-tab-DxtBG0DW.js +1 -0
- package/dist/web/assets/{shaderlab-nM5YZwa5.js → shaderlab-BdjEZUMc.js} +1 -1
- package/dist/web/assets/{shellsession-D2OCI4Gv.js → shellsession-Bp1OA1rR.js} +1 -1
- package/dist/web/assets/{soy-DrLNhqfp.js → soy-DlYz3wsC.js} +1 -1
- package/dist/web/assets/sparkles-BvJ1EGWd.js +1 -0
- package/dist/web/assets/{sparql-CsM6F2Yv.js → sparql-CdfII5Jd.js} +1 -1
- package/dist/web/assets/{sql-query-editor-CT5M9PKQ.js → sql-query-editor-D2UxRvG7.js} +1 -1
- package/dist/web/assets/sqlite-viewer-CTBwzT-r.js +1 -0
- package/dist/web/assets/{stata-Bk3cgfXJ.js → stata-BvUA6GUW.js} +1 -1
- package/dist/web/assets/{surrealql-Dkz0DPK8.js → surrealql--DLNbTyI.js} +1 -1
- package/dist/web/assets/{svelte-23si1KV5.js → svelte-CBfSsG4n.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-De3oz9zD.js → system-monitor-tab-Bscf6tzK.js} +1 -1
- package/dist/web/assets/{tab-store-BQmDVh9c.js → tab-store-DnNnaAcE.js} +1 -1
- package/dist/web/assets/{templ-CPxlP4vF.js → templ-Dv8zpKZW.js} +1 -1
- package/dist/web/assets/{terminal-tab-CciNqtMc.js → terminal-tab-CTKU5gaF.js} +3 -3
- package/dist/web/assets/{tex-ugJpgYPc.js → tex-CsrOiqh1.js} +1 -1
- package/dist/web/assets/tool-cards-xstpayaW.js +7 -0
- package/dist/web/assets/treemap-KZPCXAKY-SeI2TxLs.js +1 -0
- package/dist/web/assets/{ts-tags-BovP9TMu.js → ts-tags-3T98k4EJ.js} +1 -1
- package/dist/web/assets/{twig-B7VUm2Ca.js → twig-u6QZ45FH.js} +1 -1
- package/dist/web/assets/{use-blob-url-B3YovXF6.js → use-blob-url-BLRmhlZI.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-k-17kQqf.js → use-monaco-theme-DxYstgYa.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-CYOBtZtD.js → vendor-mermaid-C977xNeE.js} +1 -1
- package/dist/web/assets/{vendor-ui-DMRGWRuc.js → vendor-ui-CXJtBULr.js} +1 -1
- package/dist/web/assets/{video-preview-D-S_YnG6.js → video-preview-cfaoRxG3.js} +1 -1
- package/dist/web/assets/{vue-m75pWowk.js → vue-CxIsxtwn.js} +1 -1
- package/dist/web/assets/{vue-html-odHs7IMn.js → vue-html-BGL5qnuH.js} +1 -1
- package/dist/web/assets/{vue-vine-BbBB3V_G.js → vue-vine-DHPh_gMr.js} +1 -1
- package/dist/web/assets/{xml-BHzIcDWI.js → xml-C-vM2m7J.js} +1 -1
- package/dist/web/assets/{xsl-DTLhN7uu.js → xsl-DvTXNwuO.js} +1 -1
- package/dist/web/index.html +26 -23
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +10 -1
- package/docs/design-guidelines.md +31 -0
- package/docs/project-roadmap.md +10 -0
- package/docs/system-architecture.md +59 -0
- package/package.json +2 -1
- package/src/server/index.ts +8 -0
- package/src/server/routes/files.ts +2 -2
- package/src/server/routes/fs-browse.ts +99 -74
- package/src/server/routes/fs-ops.ts +118 -0
- package/src/server/routes/fs-sqlite.ts +129 -0
- package/src/server/routes/host-info.ts +17 -0
- package/src/server/routes/settings.ts +2 -0
- package/src/services/download-token.service.ts +21 -5
- package/src/services/file.service.ts +8 -12
- package/src/services/fs-browse.service.ts +104 -161
- package/src/services/fs-ops/fs-content-disposition.ts +17 -0
- package/src/services/fs-ops/fs-core-ops.ts +149 -0
- package/src/services/fs-ops/fs-error-response.ts +24 -0
- package/src/services/fs-ops/fs-git-init.service.ts +23 -0
- package/src/services/fs-ops/fs-hidden-names.ts +17 -0
- package/src/services/fs-ops/fs-list-files.service.ts +68 -0
- package/src/services/fs-ops/fs-ops-copy-move.service.ts +60 -0
- package/src/services/fs-ops/fs-ops-mutate.service.ts +59 -0
- package/src/services/fs-ops/fs-ops-read-write.service.ts +70 -0
- package/src/services/fs-ops/fs-ops-stat.service.ts +109 -0
- package/src/services/fs-ops/fs-ops-trash.service.ts +111 -0
- package/src/services/fs-ops/fs-real-path.ts +44 -0
- package/src/services/fs-ops/sql-row-reader.ts +26 -0
- package/src/services/fs-ops/sql-statement-guard.ts +63 -0
- package/src/services/fs-path-guard.service.ts +196 -0
- package/src/services/host-info/apple-bookmark-decoder.ts +97 -0
- package/src/services/host-info/apple-plist-xml-parser.ts +107 -0
- package/src/services/host-info/concurrency-limit.ts +20 -0
- package/src/services/host-info/host-drives.ts +190 -0
- package/src/services/host-info/host-info.service.ts +171 -0
- package/src/services/host-info/host-known-folders.ts +180 -0
- package/src/services/host-info/pinned-linux-bookmarks.ts +89 -0
- package/src/services/host-info/pinned-macos-finder-favorites.ts +175 -0
- package/src/services/host-info/pinned-windows-quick-access.ts +87 -0
- package/src/services/host-info/spawn-runner.ts +41 -0
- package/src/services/sqlite-types.ts +26 -0
- package/src/services/sqlite.service.ts +38 -29
- package/src/services/subagent-transcript-merger.ts +1 -1
- package/src/types/system.ts +50 -0
- package/src/web/app.tsx +19 -3
- package/src/web/components/editor/code-editor.tsx +3 -9
- package/src/web/components/explorer/file-tree.tsx +103 -21
- package/src/web/components/explorer/tree-node-context-menu.tsx +6 -1
- package/src/web/components/explorer/tree-node.tsx +34 -93
- package/src/web/components/explorer/use-file-upload-drag.ts +14 -9
- package/src/web/components/explorer/use-tree-row-dnd.ts +206 -0
- package/src/web/components/floating-window/default-window-chrome.tsx +71 -0
- package/src/web/components/floating-window/floating-window.tsx +167 -0
- package/src/web/components/floating-window/use-window-drag.ts +28 -0
- package/src/web/components/floating-window/use-window-gesture-context.ts +44 -0
- package/src/web/components/floating-window/use-window-keyboard.ts +55 -0
- package/src/web/components/floating-window/use-window-resize.ts +30 -0
- package/src/web/components/floating-window/window-chrome-contract.ts +38 -0
- package/src/web/components/floating-window/window-content-registry.ts +51 -0
- package/src/web/components/floating-window/window-geometry.ts +121 -0
- package/src/web/components/floating-window/window-layer.tsx +92 -0
- package/src/web/components/floating-window/window-persistence.ts +104 -0
- package/src/web/components/floating-window/window-resize-handles.tsx +81 -0
- package/src/web/components/floating-window/window-store-types.ts +20 -0
- package/src/web/components/floating-window/window-store.ts +192 -0
- package/src/web/components/layout/command-palette.tsx +10 -0
- package/src/web/components/layout/mobile-drawer.tsx +11 -2
- package/src/web/components/layout/nav-section-rail.tsx +7 -1
- package/src/web/components/os-explorer/actions/explorer-actions-clipboard.ts +186 -0
- package/src/web/components/os-explorer/actions/explorer-actions-mutate.ts +109 -0
- package/src/web/components/os-explorer/actions/explorer-actions-open.ts +67 -0
- package/src/web/components/os-explorer/actions/explorer-actions-transfer-helpers.ts +35 -0
- package/src/web/components/os-explorer/actions/use-explorer-actions.ts +205 -0
- package/src/web/components/os-explorer/can-open-in-ppm.ts +89 -0
- package/src/web/components/os-explorer/dnd/drag-depth-counter.ts +36 -0
- package/src/web/components/os-explorer/dnd/drag-ghost.ts +44 -0
- package/src/web/components/os-explorer/dnd/drop-target-decision.ts +46 -0
- package/src/web/components/os-explorer/dnd/drop-target-style.ts +7 -0
- package/src/web/components/os-explorer/dnd/entry-drag-payload.ts +115 -0
- package/src/web/components/os-explorer/dnd/entry-drag-state.ts +35 -0
- package/src/web/components/os-explorer/dnd/entry-drop-executor.ts +43 -0
- package/src/web/components/os-explorer/dnd/use-drag-auto-scroll.ts +70 -0
- package/src/web/components/os-explorer/dnd/use-drop-transfer.tsx +82 -0
- package/src/web/components/os-explorer/dnd/use-entry-drag-source.ts +73 -0
- package/src/web/components/os-explorer/dnd/use-entry-drop-target.ts +150 -0
- package/src/web/components/os-explorer/dnd/use-entry-row-dnd.ts +66 -0
- package/src/web/components/os-explorer/dnd/use-path-drop-target.ts +30 -0
- package/src/web/components/os-explorer/explorer-body.tsx +153 -0
- package/src/web/components/os-explorer/explorer-breadcrumb.tsx +108 -0
- package/src/web/components/os-explorer/explorer-context-menu.tsx +131 -0
- package/src/web/components/os-explorer/explorer-dialogs.tsx +103 -0
- package/src/web/components/os-explorer/explorer-modal-shell.tsx +60 -0
- package/src/web/components/os-explorer/explorer-pins-store.ts +63 -0
- package/src/web/components/os-explorer/explorer-sidebar.tsx +198 -0
- package/src/web/components/os-explorer/explorer-sort-menu.tsx +68 -0
- package/src/web/components/os-explorer/explorer-status-bar.tsx +35 -0
- package/src/web/components/os-explorer/explorer-store.ts +179 -0
- package/src/web/components/os-explorer/explorer-toolbar.tsx +139 -0
- package/src/web/components/os-explorer/explorer-window-content.tsx +36 -0
- package/src/web/components/os-explorer/format-file-meta.ts +74 -0
- package/src/web/components/os-explorer/icons/file-type-icon-map.ts +106 -0
- package/src/web/components/os-explorer/icons/file-type-icon.tsx +56 -0
- package/src/web/components/os-explorer/icons/thumbnail-fetch-semaphore.ts +44 -0
- package/src/web/components/os-explorer/icons/thumbnail-image.tsx +124 -0
- package/src/web/components/os-explorer/mobile/mobile-bottom-toolbar.tsx +102 -0
- package/src/web/components/os-explorer/mobile/mobile-explorer-sheet.tsx +61 -0
- package/src/web/components/os-explorer/mobile/mobile-explorer-top-bar.tsx +41 -0
- package/src/web/components/os-explorer/mobile/mobile-more-sheet.tsx +117 -0
- package/src/web/components/os-explorer/mobile/mobile-places-sheet.tsx +120 -0
- package/src/web/components/os-explorer/mobile/mobile-places-strip.tsx +109 -0
- package/src/web/components/os-explorer/mobile/mobile-tap-action.ts +19 -0
- package/src/web/components/os-explorer/mobile/use-mobile-row-tap.ts +50 -0
- package/src/web/components/os-explorer/open-explorer.ts +35 -0
- package/src/web/components/os-explorer/properties-dialog.tsx +99 -0
- package/src/web/components/os-explorer/skins/explorer-skin-chrome.tsx +17 -0
- package/src/web/components/os-explorer/skins/folder-icon-macos.tsx +42 -0
- package/src/web/components/os-explorer/skins/folder-icon-windows.tsx +25 -0
- package/src/web/components/os-explorer/skins/macos-skin.ts +14 -0
- package/src/web/components/os-explorer/skins/macos-window-chrome.tsx +69 -0
- package/src/web/components/os-explorer/skins/skin-types.ts +40 -0
- package/src/web/components/os-explorer/skins/skins.css +25 -0
- package/src/web/components/os-explorer/skins/use-explorer-skin.ts +21 -0
- package/src/web/components/os-explorer/skins/windows-skin.ts +11 -0
- package/src/web/components/os-explorer/skins/windows-window-chrome.tsx +56 -0
- package/src/web/components/os-explorer/sort-and-filter-entries.ts +52 -0
- package/src/web/components/os-explorer/sort-options.ts +29 -0
- package/src/web/components/os-explorer/toolbar-icon-button.ts +18 -0
- package/src/web/components/os-explorer/use-coarse-long-press.ts +97 -0
- package/src/web/components/os-explorer/use-explorer-keyboard.ts +124 -0
- package/src/web/components/os-explorer/use-explorer-navigation.ts +155 -0
- package/src/web/components/os-explorer/use-explorer-open-state.ts +67 -0
- package/src/web/components/os-explorer/use-host-info.ts +85 -0
- package/src/web/components/os-explorer/views/column-row.tsx +126 -0
- package/src/web/components/os-explorer/views/column-view-column.tsx +112 -0
- package/src/web/components/os-explorer/views/column-view-mobile.tsx +98 -0
- package/src/web/components/os-explorer/views/column-view-preview.tsx +70 -0
- package/src/web/components/os-explorer/views/column-view.tsx +204 -0
- package/src/web/components/os-explorer/views/compose-interactive-row-props.ts +77 -0
- package/src/web/components/os-explorer/views/explorer-view-registry.tsx +55 -0
- package/src/web/components/os-explorer/views/icons-view-tile.tsx +158 -0
- package/src/web/components/os-explorer/views/icons-view.tsx +188 -0
- package/src/web/components/os-explorer/views/inline-name-input.tsx +60 -0
- package/src/web/components/os-explorer/views/list-row.tsx +162 -0
- package/src/web/components/os-explorer/views/list-view.tsx +177 -0
- package/src/web/components/os-explorer/views/use-column-view-state.ts +158 -0
- package/src/web/components/os-explorer/views/use-entry-selection.ts +178 -0
- package/src/web/components/settings/settings-tab.tsx +29 -2
- package/src/web/components/sqlite/sqlite-viewer.tsx +5 -2
- package/src/web/components/sqlite/use-sqlite.ts +7 -2
- package/src/web/components/ui/adaptive-context-menu.tsx +57 -1
- package/src/web/components/ui/file-browser-picker.tsx +1 -18
- package/src/web/lib/file-download.ts +6 -4
- package/src/web/lib/fs-api.ts +185 -0
- package/src/web/stores/file-store.ts +38 -1
- package/src/web/stores/settings-store.ts +14 -0
- package/src/web/styles/globals.css +1 -0
- package/dist/web/assets/api-client-RtNXvS0P.js +0 -1
- package/dist/web/assets/api-settings-De--HNHp.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DVRYSsQn.js +0 -1
- package/dist/web/assets/chat-tab-C5U7a_rP.js +0 -13
- package/dist/web/assets/code-editor-CqlzLQ7T.js +0 -10
- package/dist/web/assets/context-menu-DIiZdfbE.js +0 -1
- package/dist/web/assets/csv-preview-DD94T9ff.js +0 -2
- package/dist/web/assets/dialog-HHdNInEH.js +0 -1
- package/dist/web/assets/diff-viewer-DyQLYP7J.js +0 -4
- package/dist/web/assets/git-log-panel-CrUgsLY5.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CutkUNY_.js +0 -1
- package/dist/web/assets/group-chat-tab-BqNJW7y2.js +0 -1
- package/dist/web/assets/index-CVJmlKhp.css +0 -2
- package/dist/web/assets/index-I2ik0ZF7.js +0 -50
- package/dist/web/assets/info-3K5VOQVL-BpNjgbj9.js +0 -1
- package/dist/web/assets/keybindings-store-C46tNxJE.js +0 -1
- package/dist/web/assets/markdown-renderer-xzyhJklS.js +0 -1
- package/dist/web/assets/notification-store-Dek7ClI2.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-D5IUZ66-.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CM29eKDR.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-QtJGaSGt.js +0 -1
- package/dist/web/assets/settings-store-B3LfpCPa.js +0 -2
- package/dist/web/assets/settings-tab-DIuR1VIc.js +0 -1
- package/dist/web/assets/sparkles-B3DmG8PJ.js +0 -1
- package/dist/web/assets/sqlite-viewer-CEObRQ7n.js +0 -1
- package/dist/web/assets/tool-cards-Bdx04yYD.js +0 -7
- package/dist/web/assets/treemap-KZPCXAKY-CjUqpVnS.js +0 -1
- /package/dist/web/assets/{abap-BM5PMK0-.js → abap-C1K0-wzc.js} +0 -0
- /package/dist/web/assets/{actionscript-3-CML-MTNy.js → actionscript-3-CJhQVjLA.js} +0 -0
- /package/dist/web/assets/{ada-DzO1tMqV.js → ada-Bt8r9fME.js} +0 -0
- /package/dist/web/assets/{andromeeda-C3nkvRhj.js → andromeeda-DF1I8qi3.js} +0 -0
- /package/dist/web/assets/{apache-Dg7oWQbs.js → apache-CnlleE6P.js} +0 -0
- /package/dist/web/assets/{apex-3O2170Ju.js → apex-j0C6Xmk6.js} +0 -0
- /package/dist/web/assets/{applescript-5AlzBqQO.js → applescript-DO7AnO8U.js} +0 -0
- /package/dist/web/assets/{apply-theme-BSJDABU3.js → apply-theme-CMQXnUAa.js} +0 -0
- /package/dist/web/assets/{ara-BGtOX5Hn.js → ara-i83AydZj.js} +0 -0
- /package/dist/web/assets/{asciidoc-CIsC4kkq.js → asciidoc-DbBA6-sF.js} +0 -0
- /package/dist/web/assets/{asm-p1-PUkh3.js → asm-B5S47Rgr.js} +0 -0
- /package/dist/web/assets/{aurora-x-BPgGL50S.js → aurora-x-De_IaSKw.js} +0 -0
- /package/dist/web/assets/{awk-Cj93pRL8.js → awk-CfyLHXqS.js} +0 -0
- /package/dist/web/assets/{ayu-dark-C50MYKhw.js → ayu-dark-DTLdZ1Si.js} +0 -0
- /package/dist/web/assets/{ayu-light-DyIp0Idy.js → ayu-light-DTOfkzhu.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-BAdm8pKU.js → ayu-mirage-DsAECCMv.js} +0 -0
- /package/dist/web/assets/{ballerina-DUO9mwyV.js → ballerina-CumGp9A7.js} +0 -0
- /package/dist/web/assets/{bat-Cg8hlSct.js → bat-Ca2SBbYN.js} +0 -0
- /package/dist/web/assets/{beancount-f8f3BBqB.js → beancount-CqyXBwwF.js} +0 -0
- /package/dist/web/assets/{berry-BqVQYZZr.js → berry-yl-kJr_H.js} +0 -0
- /package/dist/web/assets/{bibtex-C_q8ygAM.js → bibtex-DoFFszjV.js} +0 -0
- /package/dist/web/assets/{bicep-DWS-2uWR.js → bicep-CWPi4nfK.js} +0 -0
- /package/dist/web/assets/{bird2-C0pvipi_.js → bird2-DXew-ut0.js} +0 -0
- /package/dist/web/assets/{bot-E9mcVZnr.js → bot-BHXjpQ7x.js} +0 -0
- /package/dist/web/assets/{c-BQFvJ6Ux.js → c-clbySDvd.js} +0 -0
- /package/dist/web/assets/{c3-Da53KeG6.js → c3-B65jvSM1.js} +0 -0
- /package/dist/web/assets/{cadence-B5X2vWm3.js → cadence-Cm8tQTHN.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-Dm3rVY3b.js → catppuccin-frappe-DLeGslh_.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-ZxS-ga_K.js → catppuccin-latte-CR5GeHws.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-B0bAzXh0.js → catppuccin-macchiato-p-XgjxhP.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-BeMC5hqi.js → catppuccin-mocha-B8QOAZLl.js} +0 -0
- /package/dist/web/assets/{chevron-right-DeUue2aK.js → chevron-right-CtcHmZg9.js} +0 -0
- /package/dist/web/assets/{circle-question-mark-PaJgx-3d.js → circle-question-mark-EVgisosn.js} +0 -0
- /package/dist/web/assets/{clarity-BRdirlsw.js → clarity-DPulbvKH.js} +0 -0
- /package/dist/web/assets/{clojure-pSLH-KQp.js → clojure-DFuWS20u.js} +0 -0
- /package/dist/web/assets/{cmake-DsZu4U4F.js → cmake-DejCqcu4.js} +0 -0
- /package/dist/web/assets/{code-TekEnDFM.js → code-Dupqw5Fr.js} +0 -0
- /package/dist/web/assets/{codeowners-CtkKTnsH.js → codeowners-BwAyqWIH.js} +0 -0
- /package/dist/web/assets/{codeql-BpP1PDrj.js → codeql-DgSwFjqI.js} +0 -0
- /package/dist/web/assets/{common-lisp-BX1XgHTt.js → common-lisp-CxIVJCtf.js} +0 -0
- /package/dist/web/assets/{coq-D2bX6-8g.js → coq-DLSjSnmf.js} +0 -0
- /package/dist/web/assets/{csharp-DcR97P0D.js → csharp-C1HHzXBp.js} +0 -0
- /package/dist/web/assets/{css-CIjciIIk.js → css-VXsMY--y.js} +0 -0
- /package/dist/web/assets/{csv-DW48Z5d-.js → csv-BEbcJdHz.js} +0 -0
- /package/dist/web/assets/{csv-parser-DVHBcvuE.js → csv-parser-DHvoTjMF.js} +0 -0
- /package/dist/web/assets/{cue-Byh1xHko.js → cue-BkDnFqUD.js} +0 -0
- /package/dist/web/assets/{cypher-TVlMMWma.js → cypher-1cMqojSQ.js} +0 -0
- /package/dist/web/assets/{d-CxaScvxf.js → d-BnYjRXtH.js} +0 -0
- /package/dist/web/assets/{dark-plus-sn-FFTT1.js → dark-plus-XfbaH7kZ.js} +0 -0
- /package/dist/web/assets/{dart-BmpakMe-.js → dart-Ca7s-8hK.js} +0 -0
- /package/dist/web/assets/{data-grid-types-BuMHLKXV.js → data-grid-types-DP-ZexEN.js} +0 -0
- /package/dist/web/assets/{database-COYAMZaw.js → database-Dj0DxgtX.js} +0 -0
- /package/dist/web/assets/{dax-B1aRUMXT.js → dax-CmuOw-mY.js} +0 -0
- /package/dist/web/assets/{desktop-C6lD62ar.js → desktop-C2gJ6Ovb.js} +0 -0
- /package/dist/web/assets/{diff-CupGmT1a.js → diff-BCEo1Ej7.js} +0 -0
- /package/dist/web/assets/{dist-Bi4gRyhr.js → dist-CipvJy9w.js} +0 -0
- /package/dist/web/assets/{dist-m_oJqIDk.js → dist-D__U4ZJz.js} +0 -0
- /package/dist/web/assets/{docker-DjDr3TJF.js → docker-DN2jlyAh.js} +0 -0
- /package/dist/web/assets/{dotenv-QeT1SLTv.js → dotenv-NExMY2kS.js} +0 -0
- /package/dist/web/assets/{dracula-yI5kpKp5.js → dracula-B9lbE01y.js} +0 -0
- /package/dist/web/assets/{dracula-soft-CYF6cJce.js → dracula-soft-CMBBfKF_.js} +0 -0
- /package/dist/web/assets/{dream-maker-TBDKiHrV.js → dream-maker-DEyDwFsd.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-B7aZTnJ_.js → emacs-lisp-D_qP2zS6.js} +0 -0
- /package/dist/web/assets/{everforest-dark-CN77B1NJ.js → everforest-dark-DbHa2U-p.js} +0 -0
- /package/dist/web/assets/{everforest-light-CyFxGMIs.js → everforest-light-BISkJchP.js} +0 -0
- /package/dist/web/assets/{fennel-C6ktGUzp.js → fennel-6B22SfJR.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-DBEC-Kst.js → file-exclamation-point-D-ZORzYB.js} +0 -0
- /package/dist/web/assets/{fish-Vr62C2vP.js → fish-CubIIvJD.js} +0 -0
- /package/dist/web/assets/{fluent-CH3IJUki.js → fluent-DF55zQ09.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BN7VmmxZ.js → fortran-free-form-DvOu8jAt.js} +0 -0
- /package/dist/web/assets/{gdscript-BYdRNu_P.js → gdscript-DCPxdLpq.js} +0 -0
- /package/dist/web/assets/{gdshader-C2XOq90k.js → gdshader-CTZ381FO.js} +0 -0
- /package/dist/web/assets/{genie-B4hkIXNU.js → genie-C_QDtvgU.js} +0 -0
- /package/dist/web/assets/{gherkin-CW9NH2DW.js → gherkin-BcvK42tF.js} +0 -0
- /package/dist/web/assets/{github-dark-BWCf99kM.js → github-dark-C9afs_4w.js} +0 -0
- /package/dist/web/assets/{github-dark-default-DZ1m_6OP.js → github-dark-default-CKGVrl3Z.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-CUXSQ8bS.js → github-dark-dimmed-C3dQZ_LC.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-B6BvU8H-.js → github-dark-high-contrast-DW78QukY.js} +0 -0
- /package/dist/web/assets/{github-light-Ch-E92cy.js → github-light-BnP2dznS.js} +0 -0
- /package/dist/web/assets/{github-light-default-CKFslV7i.js → github-light-default-C2fPZzPG.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-DiJVeRlZ.js → github-light-high-contrast-BzIuUFR2.js} +0 -0
- /package/dist/web/assets/{gleam-D4dP7V5M.js → gleam-BpeKwEIO.js} +0 -0
- /package/dist/web/assets/{gn-DuO2D2eW.js → gn-nUb_Mit0.js} +0 -0
- /package/dist/web/assets/{gnuplot-Df_8pFbI.js → gnuplot-D7I0v20p.js} +0 -0
- /package/dist/web/assets/{go-BL16cMJO.js → go-D7Qqska7.js} +0 -0
- /package/dist/web/assets/{groovy-rfHeCRqy.js → groovy-DS-pne5-.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-CLbDN-E3.js → gruvbox-dark-hard-C0W01fsq.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-Df0L0qje.js → gruvbox-dark-medium-Bqo6fQbg.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-DV6TAcen.js → gruvbox-dark-soft-D-XSM_fu.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-CWXUzp9b.js → gruvbox-light-hard-IVAwMJRl.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-BOHMe1-7.js → gruvbox-light-medium-38ATiG7i.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-DrrS2sdV.js → gruvbox-light-soft-DnZr3c_0.js} +0 -0
- /package/dist/web/assets/{haskell-DybKb9uW.js → haskell-uQcywkDH.js} +0 -0
- /package/dist/web/assets/{haxe-DLRwZxmt.js → haxe-c6bI4_GT.js} +0 -0
- /package/dist/web/assets/{hcl-CusCzwOk.js → hcl-CQXTiEXq.js} +0 -0
- /package/dist/web/assets/{hjson-BygWIJ-3.js → hjson-ChETf2bz.js} +0 -0
- /package/dist/web/assets/{hlsl-DCKvuEyZ.js → hlsl-BMC4JGoE.js} +0 -0
- /package/dist/web/assets/{horizon-DSOVAzyH.js → horizon-BusFlhLW.js} +0 -0
- /package/dist/web/assets/{horizon-bright-DKfVjGj_.js → horizon-bright-2oD0Mz7W.js} +0 -0
- /package/dist/web/assets/{houston-Ddm8AgpV.js → houston-DdbbjqFg.js} +0 -0
- /package/dist/web/assets/{hy-DzZa6alE.js → hy-D_SOuqw9.js} +0 -0
- /package/dist/web/assets/{image-gallery-CObzTisL.js → image-gallery-BWFpZAAP.js} +0 -0
- /package/dist/web/assets/{imba-aiXalPnK.js → imba-BxLOHO5I.js} +0 -0
- /package/dist/web/assets/{ini-CAvrVa5R.js → ini-ox-CFcg0.js} +0 -0
- /package/dist/web/assets/{java-PRMSQp24.js → java-Bzo8yIal.js} +0 -0
- /package/dist/web/assets/{javascript-DQR6GQKs.js → javascript-DHuz9VXn.js} +0 -0
- /package/dist/web/assets/{json-uWUKqOxq.js → json-0_y80FD5.js} +0 -0
- /package/dist/web/assets/{json5-R_LXJXL0.js → json5-CEQ50pGM.js} +0 -0
- /package/dist/web/assets/{jsonc-tC5zvrPO.js → jsonc-CfrLEAKA.js} +0 -0
- /package/dist/web/assets/{jsonl-BAwnWh7U.js → jsonl-fKbrANC2.js} +0 -0
- /package/dist/web/assets/{jsonnet-CPj4HCRm.js → jsonnet-DKbkPaQ2.js} +0 -0
- /package/dist/web/assets/{jssm-B7HswxOM.js → jssm-Dxxx1Hqu.js} +0 -0
- /package/dist/web/assets/{jsx-z4TGELE1.js → jsx-DlJL-Oj3.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-BpeLlraV.js → kanagawa-dragon-CwN4SV1c.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-CLnIPAGs.js → kanagawa-lotus-BHltayiE.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-BglPniBb.js → kanagawa-wave-D-KXPjw6.js} +0 -0
- /package/dist/web/assets/{kdl-B6blIqLq.js → kdl-DvtoJdFo.js} +0 -0
- /package/dist/web/assets/{kotlin-DVAEYiwY.js → kotlin-CTS4MLcx.js} +0 -0
- /package/dist/web/assets/{kusto-Ba87977j.js → kusto-HtSE_sKm.js} +0 -0
- /package/dist/web/assets/{laserwave-C-NuzoMp.js → laserwave-DaXZG9ge.js} +0 -0
- /package/dist/web/assets/{lean-CJ_KLkrJ.js → lean-BYtGUfWx.js} +0 -0
- /package/dist/web/assets/{less-Dfn9YC_2.js → less-BoxmU_Mk.js} +0 -0
- /package/dist/web/assets/{lib-DTaN9riD.js → lib-Bpbn00xM.js} +0 -0
- /package/dist/web/assets/{libesm-DdtEjhIi.js → libesm-tR4XlhMj.js} +0 -0
- /package/dist/web/assets/{light-plus-B3BLlguG.js → light-plus-Bt5CyUuo.js} +0 -0
- /package/dist/web/assets/{llvm-6UDP3NQb.js → llvm-CSdk9q9V.js} +0 -0
- /package/dist/web/assets/{log-QW3dqr1D.js → log-B-kp_3CL.js} +0 -0
- /package/dist/web/assets/{logo-DUinJheI.js → logo-DdIsXFDO.js} +0 -0
- /package/dist/web/assets/{luau-B1vcBzl8.js → luau-DUBpLlzQ.js} +0 -0
- /package/dist/web/assets/{make-CqNL10gS.js → make-B3JVjoW_.js} +0 -0
- /package/dist/web/assets/{markdown-BQNMNiYS.js → markdown-Seon0vHs.js} +0 -0
- /package/dist/web/assets/{material-theme-rbV8EefR.js → material-theme-Nq-ChX1P.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-BZrskvoo.js → material-theme-darker-CPV4ayXJ.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-B0CBBjBw.js → material-theme-lighter-0kyjYqEd.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-CX_CHnQq.js → material-theme-ocean-DGpAp5co.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-ztiOCPbw.js → material-theme-palenight-B0AUn0NG.js} +0 -0
- /package/dist/web/assets/{matlab-tHQDNC9G.js → matlab-C2waytxs.js} +0 -0
- /package/dist/web/assets/{mdx-D4kZ6c6G.js → mdx-DAis6R6I.js} +0 -0
- /package/dist/web/assets/{mermaid-DlR4U4Pe.js → mermaid-_rnT2YGD.js} +0 -0
- /package/dist/web/assets/{min-dark-BX-xHAD2.js → min-dark-BSSjchUB.js} +0 -0
- /package/dist/web/assets/{min-light-Bxw0_Gqq.js → min-light-DomAFQsa.js} +0 -0
- /package/dist/web/assets/{mipsasm-DYyZAum-.js → mipsasm-bBgtvPyG.js} +0 -0
- /package/dist/web/assets/{mojo-q9wuEYKR.js → mojo-Cp9BayW0.js} +0 -0
- /package/dist/web/assets/{monokai-D9vrJ8JM.js → monokai-CL_9gbmn.js} +0 -0
- /package/dist/web/assets/{moonbit-C9roWxMb.js → moonbit-BS_fcr-P.js} +0 -0
- /package/dist/web/assets/{move-BvhjG2ic.js → move-C79AH45U.js} +0 -0
- /package/dist/web/assets/{narrat-BXGbPwG3.js → narrat-CJ5s3BDf.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-CMy5-Z4k.js → nextflow-groovy-ZHa4JrPJ.js} +0 -0
- /package/dist/web/assets/{night-owl-CBQBcbmw.js → night-owl-Jkf9zFQK.js} +0 -0
- /package/dist/web/assets/{night-owl-light-CC-brflp.js → night-owl-light-DRPeaG5J.js} +0 -0
- /package/dist/web/assets/{nix-B9EAdptu.js → nix-D2Jet9DN.js} +0 -0
- /package/dist/web/assets/{nord-DxASd4qG.js → nord-CREVs22j.js} +0 -0
- /package/dist/web/assets/{nushell-BsY59i7N.js → nushell-CAiDi_mR.js} +0 -0
- /package/dist/web/assets/{objective-c-CoeQAcnP.js → objective-c-CVkmJkC1.js} +0 -0
- /package/dist/web/assets/{objective-cpp-BysdFvND.js → objective-cpp-DIKDO1Ly.js} +0 -0
- /package/dist/web/assets/{ocaml-CrtQYMMK.js → ocaml-R9qtGQhs.js} +0 -0
- /package/dist/web/assets/{odin-B8pz4gIL.js → odin-DTYNQqVr.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-3sSHqoqQ.js → one-dark-pro-BSLWUJn5.js} +0 -0
- /package/dist/web/assets/{one-light-LUZAvT8g.js → one-light-DbkRXG3t.js} +0 -0
- /package/dist/web/assets/{openscad-DcKia-sV.js → openscad-BlPcSnmT.js} +0 -0
- /package/dist/web/assets/{pascal-zM5SlmgF.js → pascal-B1Y46ZZB.js} +0 -0
- /package/dist/web/assets/{pkl-C_r_zLLt.js → pkl-BhJSJbFv.js} +0 -0
- /package/dist/web/assets/{plastic-B3cRtgKD.js → plastic-BzxsCTpi.js} +0 -0
- /package/dist/web/assets/{plsql-BzyjheXU.js → plsql-DBba7dyP.js} +0 -0
- /package/dist/web/assets/{plus-DcuJuDC3.js → plus-Dm36mgLh.js} +0 -0
- /package/dist/web/assets/{po-Dz4OaDNi.js → po-BvgLlnNo.js} +0 -0
- /package/dist/web/assets/{poimandres-BBbtYwmR.js → poimandres-DUaI_1JP.js} +0 -0
- /package/dist/web/assets/{polar-Che1XC2Z.js → polar-C7uILwJn.js} +0 -0
- /package/dist/web/assets/{postcss-DouzwG4F.js → postcss-BgJlpFdw.js} +0 -0
- /package/dist/web/assets/{powerquery-YOdcTZmd.js → powerquery-Dn1MIpZu.js} +0 -0
- /package/dist/web/assets/{powershell-8ojzYH34.js → powershell-Bo-DYzOS.js} +0 -0
- /package/dist/web/assets/{prisma-DCmjceML.js → prisma-CTLs5IdT.js} +0 -0
- /package/dist/web/assets/{prolog-ByNNz2pB.js → prolog-QRGun3eG.js} +0 -0
- /package/dist/web/assets/{proto-DWMt6Qu1.js → proto-B7PI7apn.js} +0 -0
- /package/dist/web/assets/{puppet-osfKyZp4.js → puppet-CBq0tLrP.js} +0 -0
- /package/dist/web/assets/{purescript-ClIblkvh.js → purescript-DFewOQb2.js} +0 -0
- /package/dist/web/assets/{python-Ctyr4faP.js → python-iWuxKXvT.js} +0 -0
- /package/dist/web/assets/{qmldir-CbNgyhnI.js → qmldir-cXiW3AsF.js} +0 -0
- /package/dist/web/assets/{qss-LbRHzb21.js → qss-DASwAbBL.js} +0 -0
- /package/dist/web/assets/{r-Dvfjp0wc.js → r-TU2eaIPw.js} +0 -0
- /package/dist/web/assets/{racket-DI8v8nzD.js → racket-SkRKqrLY.js} +0 -0
- /package/dist/web/assets/{raku-CX1q9GW0.js → raku-D31lthkZ.js} +0 -0
- /package/dist/web/assets/{react-8hrD3-kv.js → react-DjdYfcEe.js} +0 -0
- /package/dist/web/assets/{red-CSz3QEOw.js → red-CTcQZevb.js} +0 -0
- /package/dist/web/assets/{reg-FMjhUQE-.js → reg-D8YAsTGf.js} +0 -0
- /package/dist/web/assets/{regexp-CCSe1Fc4.js → regexp-CD2uvAWx.js} +0 -0
- /package/dist/web/assets/{rel-Ce0UO-OB.js → rel-CAaI-1fg.js} +0 -0
- /package/dist/web/assets/{resolve-theme-B9Mgd5GG.js → resolve-theme-B0eotaPq.js} +0 -0
- /package/dist/web/assets/{riscv-BFZrdZtB.js → riscv-DR-hJVIj.js} +0 -0
- /package/dist/web/assets/{ron-D0t4kvJG.js → ron-CuUCJ0n4.js} +0 -0
- /package/dist/web/assets/{rose-pine-HzwmOzHM.js → rose-pine-aFHxW48i.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-CSp9xAF9.js → rose-pine-dawn-Bqz2sBRY.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-Cnzr2Cw0.js → rose-pine-moon-wqyh0an4.js} +0 -0
- /package/dist/web/assets/{rosmsg-DEJ8Snbv.js → rosmsg-CI1ywimO.js} +0 -0
- /package/dist/web/assets/{rust-2jBzrC0l.js → rust-p7Imk4G_.js} +0 -0
- /package/dist/web/assets/{sass-DD75Izu_.js → sass-C8JfaqCV.js} +0 -0
- /package/dist/web/assets/{save-BM4p9_9n.js → save-ignW_jk2.js} +0 -0
- /package/dist/web/assets/{scala-DpBKROIu.js → scala-DNxCW1By.js} +0 -0
- /package/dist/web/assets/{scheme-DM2jzwUK.js → scheme-BYuUPJoL.js} +0 -0
- /package/dist/web/assets/{sdbl-C34Cg6e7.js → sdbl-DbPeJPUr.js} +0 -0
- /package/dist/web/assets/{shellscript-DxfzKSgU.js → shellscript-DMiHlR3q.js} +0 -0
- /package/dist/web/assets/{shield-check-C3qYSU8a.js → shield-check-BSA9P3nI.js} +0 -0
- /package/dist/web/assets/{shield-off-D5tU7gQT.js → shield-off-Cb9J9x_n.js} +0 -0
- /package/dist/web/assets/{slack-dark-DIIV3bpD.js → slack-dark-C4xGcQXI.js} +0 -0
- /package/dist/web/assets/{slack-ochin-BTkxWjEP.js → slack-ochin-BzgRt_wm.js} +0 -0
- /package/dist/web/assets/{smalltalk-Bm0qg6SA.js → smalltalk-LDeL9PYH.js} +0 -0
- /package/dist/web/assets/{snazzy-light-ClAuuhn4.js → snazzy-light-CXE7a5j_.js} +0 -0
- /package/dist/web/assets/{solarized-dark-93sEWmYT.js → solarized-dark-DgUhY74x.js} +0 -0
- /package/dist/web/assets/{solarized-light-C74FvgcO.js → solarized-light-DD9C-H-9.js} +0 -0
- /package/dist/web/assets/{solidity-H9fSjkQP.js → solidity-CAbDB4D4.js} +0 -0
- /package/dist/web/assets/{splunk-CDTN2iAG.js → splunk-kQREPfIc.js} +0 -0
- /package/dist/web/assets/{sql-CoqObhgo.js → sql-CBzeNAD8.js} +0 -0
- /package/dist/web/assets/{ssh-config-C3Wv6qRp.js → ssh-config-L1euxrsM.js} +0 -0
- /package/dist/web/assets/{stylus-D0uTFXCy.js → stylus-DvBnYniP.js} +0 -0
- /package/dist/web/assets/{swift-DtMMgvRI.js → swift-DF5NDQp6.js} +0 -0
- /package/dist/web/assets/{synthwave-84-DAroyZBu.js → synthwave-84-DbBhB_Hq.js} +0 -0
- /package/dist/web/assets/{system-verilog-Bj7X_lq4.js → system-verilog-D8s2ZW_k.js} +0 -0
- /package/dist/web/assets/{systemd-CH8ApCUs.js → systemd-a5-XZJsG.js} +0 -0
- /package/dist/web/assets/{table-Di8pv2Y_.js → table-4P8kS7sg.js} +0 -0
- /package/dist/web/assets/{talonscript-70lVC3cw.js → talonscript-BLa0URTW.js} +0 -0
- /package/dist/web/assets/{tasl-WQR8VslV.js → tasl-DIoV4XcC.js} +0 -0
- /package/dist/web/assets/{tcl-caTR_frn.js → tcl-ztYAdXfQ.js} +0 -0
- /package/dist/web/assets/{terraform-CVRZ4SZ4.js → terraform-DvpCCiOZ.js} +0 -0
- /package/dist/web/assets/{tokyo-night-CDyT1jCI.js → tokyo-night-BkHlYUv0.js} +0 -0
- /package/dist/web/assets/{toml-B7D2mIDU.js → toml-acIrxos6.js} +0 -0
- /package/dist/web/assets/{tsv-2kdn5dqL.js → tsv-BJB5EVCT.js} +0 -0
- /package/dist/web/assets/{tsx-DJfG3fzg.js → tsx-B1MmXGT3.js} +0 -0
- /package/dist/web/assets/{turtle-DaQac_UT.js → turtle-DFceTiqd.js} +0 -0
- /package/dist/web/assets/{typescript-BLfAIchl.js → typescript-Dz9wU5Y1.js} +0 -0
- /package/dist/web/assets/{typespec-ymBaOi_c.js → typespec-CPuYZijj.js} +0 -0
- /package/dist/web/assets/{typst-DkvUYMT-.js → typst-CfsCIYv8.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-BcuCjH19.js → use-is-mobile-K1a38BwI.js} +0 -0
- /package/dist/web/assets/{utils-E0yyGxXt.js → utils-B9H0d3Fh.js} +0 -0
- /package/dist/web/assets/{v-D4997FGs.js → v-CGo0jcXA.js} +0 -0
- /package/dist/web/assets/{vala-AFg9DmKE.js → vala-AsSR0hrb.js} +0 -0
- /package/dist/web/assets/{vb-DNUJ9fOP.js → vb-C7tvbXv1.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-QDr0jvpJ.js → vendor-xterm-ClRVIJ-k.js} +0 -0
- /package/dist/web/assets/{verilog-BrnAQ3tF.js → verilog--a33ND-p.js} +0 -0
- /package/dist/web/assets/{vesper-DCZnFmY0.js → vesper-B_Z0h4pn.js} +0 -0
- /package/dist/web/assets/{vhdl-DoK4PjhE.js → vhdl-CWL5dZPN.js} +0 -0
- /package/dist/web/assets/{viml-D6EKG66q.js → viml-BglmT14Y.js} +0 -0
- /package/dist/web/assets/{vitesse-black-C6KILWdF.js → vitesse-black-RKFsSqQI.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-BuSr6OT5.js → vitesse-dark-D7zDWNmc.js} +0 -0
- /package/dist/web/assets/{vitesse-light-M-eYyOoe.js → vitesse-light-MAp87W5b.js} +0 -0
- /package/dist/web/assets/{vyper-CwSN64Jn.js → vyper-CgCBYg1G.js} +0 -0
- /package/dist/web/assets/{wasm-BhQEkzil.js → wasm-BOG7ZeTT.js} +0 -0
- /package/dist/web/assets/{wasm-CVlf9Jwb.js → wasm-Bd258nS1.js} +0 -0
- /package/dist/web/assets/{wenyan-BC7irgM3.js → wenyan-DpShPKXZ.js} +0 -0
- /package/dist/web/assets/{wgsl-W-3UHMDO.js → wgsl-BakcCblK.js} +0 -0
- /package/dist/web/assets/{wikitext-T1DoFcm8.js → wikitext-NroRWVAO.js} +0 -0
- /package/dist/web/assets/{wit-DIrx_eCI.js → wit-B5CM2pg6.js} +0 -0
- /package/dist/web/assets/{wolfram-Cza4b443.js → wolfram-7MTFBJpR.js} +0 -0
- /package/dist/web/assets/{x-BsFLg-T8.js → x-BtzqXv1z.js} +0 -0
- /package/dist/web/assets/{yaml-DOZ10aZe.js → yaml-Bwr01G2K.js} +0 -0
- /package/dist/web/assets/{zenscript-DDAXkseY.js → zenscript-Dj8GATvi.js} +0 -0
- /package/dist/web/assets/{zig-CrzwMpQE.js → zig-LAE-fbS-.js} +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual localStorage persistence for floating windows.
|
|
3
|
+
*
|
|
4
|
+
* Hand-rolled instead of zustand's `persist` because the saved rects must be re-clamped
|
|
5
|
+
* against the *current* layer size on load (a rect saved on a 4K screen is off-screen on a
|
|
6
|
+
* laptop), and because only gesture-end commits should hit storage, not every drag frame.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { clampRect, type Bounds, type Rect } from "./window-geometry";
|
|
10
|
+
import type { WindowKind, WindowRuntimeState, WindowVisualState } from "./window-store-types";
|
|
11
|
+
|
|
12
|
+
const STORAGE_KEY = "ppm-windows";
|
|
13
|
+
|
|
14
|
+
/** Only JSON-safe payloads survive a reload — anything else is dropped with the window. */
|
|
15
|
+
export interface PersistedWindow {
|
|
16
|
+
id: string;
|
|
17
|
+
kind: WindowKind;
|
|
18
|
+
rect: Rect;
|
|
19
|
+
state: WindowVisualState;
|
|
20
|
+
payload?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const KINDS: WindowKind[] = ["explorer"];
|
|
24
|
+
const STATES: WindowVisualState[] = ["normal", "maximized", "minimized"];
|
|
25
|
+
|
|
26
|
+
function isRect(v: unknown): v is Rect {
|
|
27
|
+
if (!v || typeof v !== "object") return false;
|
|
28
|
+
const r = v as Record<string, unknown>;
|
|
29
|
+
return (["x", "y", "w", "h"] as const).every((k) => typeof r[k] === "number" && Number.isFinite(r[k]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isSerialisable(payload: unknown): payload is Record<string, unknown> | undefined {
|
|
33
|
+
if (payload === undefined) return true;
|
|
34
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
35
|
+
try {
|
|
36
|
+
JSON.stringify(payload);
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false; // cyclic or non-serialisable payload (e.g. a live handle)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toPersisted(win: WindowRuntimeState): PersistedWindow | null {
|
|
44
|
+
if (!isSerialisable(win.payload)) return null;
|
|
45
|
+
return { id: win.id, kind: win.kind, rect: win.rect, state: win.state, payload: win.payload };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function saveWindowRects(windows: WindowRuntimeState[]): void {
|
|
49
|
+
try {
|
|
50
|
+
// Persisted back-to-front so the restored rank order matches what the user last saw.
|
|
51
|
+
const ordered = [...windows].sort((a, b) => a.rank - b.rank);
|
|
52
|
+
const payload = ordered.map(toPersisted).filter((w): w is PersistedWindow => w !== null);
|
|
53
|
+
if (payload.length === 0) localStorage.removeItem(STORAGE_KEY);
|
|
54
|
+
else localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
|
|
55
|
+
} catch {
|
|
56
|
+
/* quota exceeded or storage disabled — geometry is cosmetic, never block the UI */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read persisted windows, dropping anything malformed, and re-clamp to the current layer. */
|
|
61
|
+
export function loadWindowRects(bounds: Bounds): PersistedWindow[] {
|
|
62
|
+
let raw: string | null = null;
|
|
63
|
+
try {
|
|
64
|
+
raw = localStorage.getItem(STORAGE_KEY);
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
if (!raw) return [];
|
|
69
|
+
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(parsed)) return [];
|
|
77
|
+
|
|
78
|
+
const out: PersistedWindow[] = [];
|
|
79
|
+
for (const item of parsed) {
|
|
80
|
+
if (!item || typeof item !== "object") continue;
|
|
81
|
+
const w = item as Record<string, unknown>;
|
|
82
|
+
if (typeof w.id !== "string" || !KINDS.includes(w.kind as WindowKind)) continue;
|
|
83
|
+
if (!isRect(w.rect) || !isSerialisable(w.payload)) continue;
|
|
84
|
+
const state = STATES.includes(w.state as WindowVisualState)
|
|
85
|
+
? (w.state as WindowVisualState)
|
|
86
|
+
: "normal";
|
|
87
|
+
out.push({
|
|
88
|
+
id: w.id,
|
|
89
|
+
kind: w.kind as WindowKind,
|
|
90
|
+
rect: clampRect(w.rect, bounds),
|
|
91
|
+
state,
|
|
92
|
+
payload: w.payload as Record<string, unknown> | undefined,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function clearWindowRects(): void {
|
|
99
|
+
try {
|
|
100
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
101
|
+
} catch {
|
|
102
|
+
/* storage disabled */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The eight invisible grab strips around a window. Corners sit on top of the edges, matching
|
|
3
|
+
* the hit areas desktop window managers use.
|
|
4
|
+
*
|
|
5
|
+
* A window at this size can be reached with a finger — a tablet past the `md` breakpoint gets
|
|
6
|
+
* the desktop layer — and a 6 px strip is far below any touch target, so the grab areas grow
|
|
7
|
+
* on coarse pointers. They are transparent, so nothing about the window's look changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useMediaQuery } from "@/hooks/use-media-query";
|
|
11
|
+
import { RESIZE_HANDLES, type ResizeHandle } from "./window-geometry";
|
|
12
|
+
|
|
13
|
+
type HandleBinder = (handle: ResizeHandle) => Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
interface HandleSizes {
|
|
16
|
+
edge: number;
|
|
17
|
+
corner: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MOUSE_SIZES: HandleSizes = { edge: 6, corner: 12 };
|
|
21
|
+
const TOUCH_SIZES: HandleSizes = { edge: 12, corner: 20 };
|
|
22
|
+
|
|
23
|
+
const CURSORS: Record<ResizeHandle, string> = {
|
|
24
|
+
n: "cursor-n-resize",
|
|
25
|
+
s: "cursor-s-resize",
|
|
26
|
+
e: "cursor-e-resize",
|
|
27
|
+
w: "cursor-w-resize",
|
|
28
|
+
ne: "cursor-ne-resize",
|
|
29
|
+
nw: "cursor-nw-resize",
|
|
30
|
+
se: "cursor-se-resize",
|
|
31
|
+
sw: "cursor-sw-resize",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Absolute placement per handle, expressed in the same units the frame uses. Each strip is
|
|
36
|
+
* centred on its border, so half of the extra width grows outwards and half inwards — the
|
|
37
|
+
* window's own border never moves.
|
|
38
|
+
*/
|
|
39
|
+
function styleFor(handle: ResizeHandle, sizes: HandleSizes): React.CSSProperties {
|
|
40
|
+
const corner = handle.length === 2;
|
|
41
|
+
const s: React.CSSProperties = { position: "absolute", touchAction: "none", zIndex: 1 };
|
|
42
|
+
if (handle.includes("n")) s.top = -sizes.edge / 2;
|
|
43
|
+
if (handle.includes("s")) s.bottom = -sizes.edge / 2;
|
|
44
|
+
if (handle.includes("w")) s.left = -sizes.edge / 2;
|
|
45
|
+
if (handle.includes("e")) s.right = -sizes.edge / 2;
|
|
46
|
+
if (corner) {
|
|
47
|
+
s.width = sizes.corner;
|
|
48
|
+
s.height = sizes.corner;
|
|
49
|
+
return s;
|
|
50
|
+
}
|
|
51
|
+
// Edge strips stop short of the corners so the corner handles keep their full hit area.
|
|
52
|
+
if (handle === "n" || handle === "s") {
|
|
53
|
+
s.left = sizes.corner;
|
|
54
|
+
s.right = sizes.corner;
|
|
55
|
+
s.height = sizes.edge;
|
|
56
|
+
} else {
|
|
57
|
+
s.top = sizes.corner;
|
|
58
|
+
s.bottom = sizes.corner;
|
|
59
|
+
s.width = sizes.edge;
|
|
60
|
+
}
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function WindowResizeHandles({ bind }: { bind: HandleBinder }) {
|
|
65
|
+
const coarsePointer = useMediaQuery("(pointer: coarse)");
|
|
66
|
+
const sizes = coarsePointer ? TOUCH_SIZES : MOUSE_SIZES;
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
{RESIZE_HANDLES.map((handle) => (
|
|
70
|
+
<div
|
|
71
|
+
key={handle}
|
|
72
|
+
{...bind(handle)}
|
|
73
|
+
data-resize-handle={handle}
|
|
74
|
+
aria-hidden
|
|
75
|
+
className={CURSORS[handle]}
|
|
76
|
+
style={styleFor(handle, sizes)}
|
|
77
|
+
/>
|
|
78
|
+
))}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Shared window types — split out so the store and the persistence layer do not import each other. */
|
|
2
|
+
|
|
3
|
+
import type { Rect } from "./window-geometry";
|
|
4
|
+
|
|
5
|
+
/** Content families the layer can host. The registry maps each kind to its body component. */
|
|
6
|
+
export type WindowKind = "explorer";
|
|
7
|
+
|
|
8
|
+
export type WindowVisualState = "normal" | "maximized" | "minimized";
|
|
9
|
+
|
|
10
|
+
export interface WindowRuntimeState {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: WindowKind;
|
|
13
|
+
/** Geometry in layer coordinates. While maximized this holds the restore rect. */
|
|
14
|
+
rect: Rect;
|
|
15
|
+
/** Dense stacking order, 0 = backmost, renormalised on every focus. */
|
|
16
|
+
rank: number;
|
|
17
|
+
state: WindowVisualState;
|
|
18
|
+
/** Kind-specific, must stay JSON-serialisable to survive a reload. */
|
|
19
|
+
payload?: Record<string, unknown>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating window state: geometry, stacking order and lifecycle for the desktop window layer.
|
|
3
|
+
*
|
|
4
|
+
* Content-agnostic on purpose — the store knows a `kind` and an opaque payload, the registry
|
|
5
|
+
* decides what renders inside. Ranks are kept dense (0..n-1) so the inline z-index never
|
|
6
|
+
* escapes the band reserved below the app's click-away backdrops.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { create } from "zustand";
|
|
10
|
+
import {
|
|
11
|
+
cascadeSpawnRect,
|
|
12
|
+
clampRect,
|
|
13
|
+
MAX_WINDOWS,
|
|
14
|
+
type Bounds,
|
|
15
|
+
type Rect,
|
|
16
|
+
} from "./window-geometry";
|
|
17
|
+
import { loadWindowRects, saveWindowRects } from "./window-persistence";
|
|
18
|
+
import type { WindowKind, WindowRuntimeState, WindowVisualState } from "./window-store-types";
|
|
19
|
+
|
|
20
|
+
export type { WindowKind, WindowRuntimeState, WindowVisualState };
|
|
21
|
+
|
|
22
|
+
/** Fallback layer size used before the container has been measured. */
|
|
23
|
+
const DEFAULT_BOUNDS: Bounds = { w: 1280, h: 800 };
|
|
24
|
+
|
|
25
|
+
interface WindowStore {
|
|
26
|
+
windows: Record<string, WindowRuntimeState>;
|
|
27
|
+
/** Measured size of the layer container; all rects are clamped against it. */
|
|
28
|
+
bounds: Bounds;
|
|
29
|
+
/** True once persisted windows have been read, so restore runs exactly once. */
|
|
30
|
+
restored: boolean;
|
|
31
|
+
|
|
32
|
+
open(kind: WindowKind, payload?: Record<string, unknown>, rect?: Rect): string;
|
|
33
|
+
close(id: string): void;
|
|
34
|
+
focus(id: string): void;
|
|
35
|
+
move(id: string, position: { x: number; y: number }): void;
|
|
36
|
+
resize(id: string, rect: Rect): void;
|
|
37
|
+
setState(id: string, state: WindowVisualState): void;
|
|
38
|
+
/** Merge into a window's payload; persisted so a reload restores the latest state. */
|
|
39
|
+
setPayload(id: string, payload: Record<string, unknown>): void;
|
|
40
|
+
setBounds(bounds: Bounds): void;
|
|
41
|
+
/** Re-hydrate persisted windows into the layer (no-op after the first call). */
|
|
42
|
+
restoreAll(bounds: Bounds): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sortedByRank = (windows: Record<string, WindowRuntimeState>): WindowRuntimeState[] =>
|
|
46
|
+
Object.values(windows).sort((a, b) => a.rank - b.rank);
|
|
47
|
+
|
|
48
|
+
/** Rewrite ranks to 0..n-1 in the given order (last = frontmost). */
|
|
49
|
+
function densify(ordered: WindowRuntimeState[]): Record<string, WindowRuntimeState> {
|
|
50
|
+
const out: Record<string, WindowRuntimeState> = {};
|
|
51
|
+
ordered.forEach((win, rank) => {
|
|
52
|
+
out[win.id] = win.rank === rank ? win : { ...win, rank };
|
|
53
|
+
});
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function persist(windows: Record<string, WindowRuntimeState>): void {
|
|
58
|
+
saveWindowRects(Object.values(windows));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function newId(): string {
|
|
62
|
+
return `win-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const useWindowStore = create<WindowStore>((set, get) => ({
|
|
66
|
+
windows: {},
|
|
67
|
+
bounds: DEFAULT_BOUNDS,
|
|
68
|
+
restored: false,
|
|
69
|
+
|
|
70
|
+
open: (kind, payload, rect) => {
|
|
71
|
+
const { windows, bounds } = get();
|
|
72
|
+
const ordered = sortedByRank(windows);
|
|
73
|
+
|
|
74
|
+
// At the cap the layer would leave its z band, so the oldest window is raised instead
|
|
75
|
+
// of silently dropping the request.
|
|
76
|
+
if (ordered.length >= MAX_WINDOWS) {
|
|
77
|
+
const oldest = ordered[0]!;
|
|
78
|
+
get().focus(oldest.id);
|
|
79
|
+
return oldest.id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const id = newId();
|
|
83
|
+
const spawn = rect ? clampRect(rect, bounds) : cascadeSpawnRect(ordered.map((w) => w.rect), bounds);
|
|
84
|
+
const next = densify([
|
|
85
|
+
...ordered,
|
|
86
|
+
{ id, kind, rect: spawn, rank: ordered.length, state: "normal" as const, payload },
|
|
87
|
+
]);
|
|
88
|
+
set({ windows: next });
|
|
89
|
+
persist(next);
|
|
90
|
+
return id;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
close: (id) => {
|
|
94
|
+
const { windows } = get();
|
|
95
|
+
if (!windows[id]) return;
|
|
96
|
+
const next = densify(sortedByRank(windows).filter((w) => w.id !== id));
|
|
97
|
+
set({ windows: next });
|
|
98
|
+
persist(next);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
focus: (id) => {
|
|
102
|
+
const { windows } = get();
|
|
103
|
+
const target = windows[id];
|
|
104
|
+
if (!target) return;
|
|
105
|
+
const ordered = sortedByRank(windows);
|
|
106
|
+
if (ordered[ordered.length - 1]?.id === id) return; // already frontmost
|
|
107
|
+
const next = densify([...ordered.filter((w) => w.id !== id), target]);
|
|
108
|
+
set({ windows: next });
|
|
109
|
+
persist(next); // stacking order is part of the restored layout
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
move: (id, position) => {
|
|
113
|
+
const { windows, bounds } = get();
|
|
114
|
+
const win = windows[id];
|
|
115
|
+
if (!win) return;
|
|
116
|
+
const rect = clampRect({ ...win.rect, ...position }, bounds);
|
|
117
|
+
const next = { ...windows, [id]: { ...win, rect } };
|
|
118
|
+
set({ windows: next });
|
|
119
|
+
persist(next);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
resize: (id, rect) => {
|
|
123
|
+
const { windows, bounds } = get();
|
|
124
|
+
const win = windows[id];
|
|
125
|
+
if (!win) return;
|
|
126
|
+
const next = { ...windows, [id]: { ...win, rect: clampRect(rect, bounds) } };
|
|
127
|
+
set({ windows: next });
|
|
128
|
+
persist(next);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
setState: (id, state) => {
|
|
132
|
+
const { windows } = get();
|
|
133
|
+
const win = windows[id];
|
|
134
|
+
if (!win || win.state === state) return;
|
|
135
|
+
const next = { ...windows, [id]: { ...win, state } };
|
|
136
|
+
set({ windows: next });
|
|
137
|
+
persist(next);
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
setPayload: (id, payload) => {
|
|
141
|
+
const { windows } = get();
|
|
142
|
+
const win = windows[id];
|
|
143
|
+
if (!win) return;
|
|
144
|
+
const merged = { ...win.payload, ...payload };
|
|
145
|
+
// Content re-renders on every keystroke in some bodies; skip the write when the
|
|
146
|
+
// payload is unchanged so persistence is not hit for nothing.
|
|
147
|
+
const unchanged = Object.entries(merged).every(
|
|
148
|
+
([key, value]) => win.payload?.[key] === value,
|
|
149
|
+
);
|
|
150
|
+
if (unchanged && Object.keys(merged).length === Object.keys(win.payload ?? {}).length) return;
|
|
151
|
+
const next = { ...windows, [id]: { ...win, payload: merged } };
|
|
152
|
+
set({ windows: next });
|
|
153
|
+
persist(next);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
setBounds: (bounds) => {
|
|
157
|
+
const current = get();
|
|
158
|
+
if (current.bounds.w === bounds.w && current.bounds.h === bounds.h) return;
|
|
159
|
+
// A shrinking content area (sidebar opened, browser resized) would otherwise strand
|
|
160
|
+
// windows outside the layer with no way to drag them back.
|
|
161
|
+
const windows: Record<string, WindowRuntimeState> = {};
|
|
162
|
+
for (const win of Object.values(current.windows)) {
|
|
163
|
+
const rect = clampRect(win.rect, bounds);
|
|
164
|
+
windows[win.id] =
|
|
165
|
+
rect.x === win.rect.x && rect.y === win.rect.y && rect.w === win.rect.w && rect.h === win.rect.h
|
|
166
|
+
? win
|
|
167
|
+
: { ...win, rect };
|
|
168
|
+
}
|
|
169
|
+
set({ bounds, windows });
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
restoreAll: (bounds) => {
|
|
173
|
+
if (get().restored) return;
|
|
174
|
+
const saved = loadWindowRects(bounds).slice(0, MAX_WINDOWS);
|
|
175
|
+
const windows = densify(
|
|
176
|
+
saved.map((w, rank) => ({
|
|
177
|
+
id: w.id,
|
|
178
|
+
kind: w.kind,
|
|
179
|
+
rect: w.rect,
|
|
180
|
+
rank,
|
|
181
|
+
state: w.state,
|
|
182
|
+
payload: w.payload,
|
|
183
|
+
})),
|
|
184
|
+
);
|
|
185
|
+
set({ bounds, windows, restored: true });
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
/** Windows in paint order (backmost first) — stable identity per store update. */
|
|
190
|
+
export function windowsInRankOrder(windows: Record<string, WindowRuntimeState>): WindowRuntimeState[] {
|
|
191
|
+
return sortedByRank(windows);
|
|
192
|
+
}
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
Plus,
|
|
19
19
|
Columns2,
|
|
20
20
|
Cloud,
|
|
21
|
+
AppWindow,
|
|
21
22
|
} from "lucide-react";
|
|
23
|
+
import { openExplorer } from "@/components/os-explorer/open-explorer";
|
|
22
24
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
23
25
|
import { useProjectStore } from "@/stores/project-store";
|
|
24
26
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
@@ -232,6 +234,14 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
232
234
|
group: "action",
|
|
233
235
|
shortcut: formatShortcut(getBinding("open-settings")),
|
|
234
236
|
},
|
|
237
|
+
{
|
|
238
|
+
id: "open-file-explorer", label: "Open File Explorer", icon: AppWindow, group: "action",
|
|
239
|
+
keywords: "open file explorer finder browse files folders disk drive window",
|
|
240
|
+
action: () => {
|
|
241
|
+
void openExplorer();
|
|
242
|
+
onClose();
|
|
243
|
+
},
|
|
244
|
+
},
|
|
235
245
|
];
|
|
236
246
|
|
|
237
247
|
// Append extension-contributed commands (with keybinding shortcuts, respecting user overrides)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
2
|
-
import { X, Bug as BugIcon, Cloud } from "lucide-react";
|
|
2
|
+
import { X, Bug as BugIcon, Cloud, FolderTree } from "lucide-react";
|
|
3
3
|
import { useShallow } from "zustand/react/shallow";
|
|
4
4
|
import { useProjectStore } from "@/stores/project-store";
|
|
5
5
|
import { useSettingsStore, type SidebarActiveTab } from "@/stores/settings-store";
|
|
@@ -22,6 +22,7 @@ import { UpgradeButton } from "@/components/layout/upgrade-button";
|
|
|
22
22
|
import { CloudSharePopover } from "@/components/layout/cloud-share-popover";
|
|
23
23
|
import { BottomSheet } from "@/components/ui/mobile-bottom-sheet";
|
|
24
24
|
import { isMobileDevice } from "@/hooks/use-is-mobile";
|
|
25
|
+
import { openExplorer } from "@/components/os-explorer/open-explorer";
|
|
25
26
|
import { cn } from "@/lib/utils";
|
|
26
27
|
|
|
27
28
|
// Tab ids the mobile drawer can render content for. `search` is desktop-only for now;
|
|
@@ -136,10 +137,18 @@ export function MobileDrawer({ isOpen, onClose, initialTab }: MobileDrawerProps)
|
|
|
136
137
|
onReorder={setSidebarTabOrder}
|
|
137
138
|
/>
|
|
138
139
|
|
|
139
|
-
{/* Report Bug + Cloud & Share + Version / Upgrade */}
|
|
140
|
+
{/* Files (OS explorer) + Report Bug + Cloud & Share + Version / Upgrade */}
|
|
140
141
|
<div className="flex items-center justify-between px-4 py-2 border-t border-border text-[11px]">
|
|
141
142
|
<UpgradeButton align="left" />
|
|
142
143
|
<div className="flex items-center gap-3">
|
|
144
|
+
{/* Not a sidebar tab — the explorer opens as its own full-screen sheet. */}
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => { onClose(); void openExplorer(); }}
|
|
147
|
+
className="flex items-center gap-1 text-[10px] text-text-subtle hover:text-text-secondary transition-colors"
|
|
148
|
+
>
|
|
149
|
+
<FolderTree className="size-3" />
|
|
150
|
+
<span>Files</span>
|
|
151
|
+
</button>
|
|
143
152
|
<button
|
|
144
153
|
onClick={() => setCloudOpen(true)}
|
|
145
154
|
className="flex items-center gap-1 text-[10px] text-text-subtle hover:text-text-secondary transition-colors"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useMemo, memo } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
-
import { Settings, Bug, Cloud } from "lucide-react";
|
|
3
|
+
import { Settings, Bug, Cloud, FolderTree } from "lucide-react";
|
|
4
|
+
import { openExplorer } from "@/components/os-explorer/open-explorer";
|
|
4
5
|
import { useSettingsStore, type SidebarActiveTab } from "@/stores/settings-store";
|
|
5
6
|
import { getAvailableTabs } from "@/lib/sidebar-tabs/tab-registry";
|
|
6
7
|
import { resolveTabOrder } from "@/lib/sidebar-tabs/resolve-tab-order";
|
|
@@ -71,6 +72,9 @@ function FooterUtil({ icon: Icon, label, onClick, active }: {
|
|
|
71
72
|
}) {
|
|
72
73
|
return (
|
|
73
74
|
<button
|
|
75
|
+
type="button"
|
|
76
|
+
aria-label={label}
|
|
77
|
+
title={label}
|
|
74
78
|
onClick={onClick}
|
|
75
79
|
className={cn(
|
|
76
80
|
"group relative flex items-center justify-center size-8 rounded-[7px] transition-colors shrink-0",
|
|
@@ -206,6 +210,8 @@ export const NavSectionRail = memo(function NavSectionRail({ className }: { clas
|
|
|
206
210
|
</>,
|
|
207
211
|
document.body,
|
|
208
212
|
)}
|
|
213
|
+
{/* Not a sidebar tab — the explorer lives in its own floating window. */}
|
|
214
|
+
<FooterUtil icon={FolderTree} label="File Explorer" onClick={() => void openExplorer()} />
|
|
209
215
|
<FooterUtil icon={Bug} label="Report Bug" onClick={handleReportBug} />
|
|
210
216
|
<FooterUtil icon={Settings} label="Settings" active={sidebarActiveTab === "settings"} onClick={() => handleTabClick("settings")} />
|
|
211
217
|
</div>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cut / copy / paste against the real filesystem, plus the collision prompt.
|
|
3
|
+
*
|
|
4
|
+
* The clipboard itself lives in `file-store` and holds absolute paths, so a cut in the
|
|
5
|
+
* project tree can be pasted into an explorer window and the other way round. Everything
|
|
6
|
+
* here goes through `/api/fs/*`; the tree keeps its project-scoped routes for the case
|
|
7
|
+
* where both ends are inside the same project.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { toast } from "sonner";
|
|
11
|
+
import { fsApi, FsError } from "@/lib/fs-api";
|
|
12
|
+
import { useFileStore, type ClipboardState } from "@/stores/file-store";
|
|
13
|
+
import { dirnameOf, errorDescription, joinPath } from "../format-file-meta";
|
|
14
|
+
import { fsChanged } from "../explorer-store";
|
|
15
|
+
import { freeName, runOne } from "./explorer-actions-transfer-helpers";
|
|
16
|
+
|
|
17
|
+
export type CollisionChoice = "replace" | "keep-both" | "skip";
|
|
18
|
+
|
|
19
|
+
export interface CollisionRequest {
|
|
20
|
+
name: string;
|
|
21
|
+
destination: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Asks the user what to do about an existing file. Returning "skip" is always safe. */
|
|
25
|
+
export type CollisionResolver = (request: CollisionRequest) => Promise<CollisionChoice>;
|
|
26
|
+
|
|
27
|
+
export interface TransferContext {
|
|
28
|
+
sep: string;
|
|
29
|
+
resolve: CollisionResolver;
|
|
30
|
+
/**
|
|
31
|
+
* Asked only when "Replace" cannot trash the existing entry (`NO_TRASH`) — the dialog copy
|
|
32
|
+
* promises a recoverable trash-first replace, so silently falling back to a permanent
|
|
33
|
+
* delete would contradict what the user just agreed to. Returning false skips the item.
|
|
34
|
+
*/
|
|
35
|
+
confirmPermanentOverwrite(name: string): Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TransferResult {
|
|
39
|
+
succeeded: number;
|
|
40
|
+
skipped: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Copy or move `sources` into `dstDir`.
|
|
46
|
+
*
|
|
47
|
+
* Exported on its own (not just through the clipboard) because drag-and-drop performs the
|
|
48
|
+
* exact same operation with a different trigger.
|
|
49
|
+
*/
|
|
50
|
+
export async function transfer(
|
|
51
|
+
sources: string[],
|
|
52
|
+
dstDir: string,
|
|
53
|
+
op: "copy" | "move",
|
|
54
|
+
ctx: TransferContext,
|
|
55
|
+
): Promise<TransferResult> {
|
|
56
|
+
const result: TransferResult = { succeeded: 0, skipped: 0, failed: 0 };
|
|
57
|
+
const touched = new Set<string>([dstDir]);
|
|
58
|
+
|
|
59
|
+
for (const source of sources) {
|
|
60
|
+
const name = source.split(/[/\\]/).filter(Boolean).pop() ?? source;
|
|
61
|
+
const sourceDir = dirnameOf(source, ctx.sep);
|
|
62
|
+
touched.add(sourceDir);
|
|
63
|
+
// Moving an entry into the folder it already lives in is a no-op, not an error.
|
|
64
|
+
if (op === "move" && sourceDir === dstDir) {
|
|
65
|
+
result.skipped++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let destination = joinPath(dstDir, name, ctx.sep);
|
|
70
|
+
// A copy into the same folder always collides with itself (destination === source),
|
|
71
|
+
// which the server refuses as self-nesting (EINVAL), not EEXIST — so the collision
|
|
72
|
+
// prompt would never open. Every OS instead makes this "name (2)" directly.
|
|
73
|
+
if (op === "copy" && sourceDir === dstDir) {
|
|
74
|
+
const alternative = await freeName(dstDir, name, ctx.sep);
|
|
75
|
+
if (!alternative) {
|
|
76
|
+
result.failed++;
|
|
77
|
+
toast.error(`Copy failed: ${name}`, { description: "No free name available" });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
destination = joinPath(dstDir, alternative, ctx.sep);
|
|
81
|
+
try {
|
|
82
|
+
await runOne(source, destination, op);
|
|
83
|
+
result.succeeded++;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
result.failed++;
|
|
86
|
+
toast.error(`Copy failed: ${name}`, { description: errorDescription(e) });
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await runOne(source, destination, op);
|
|
93
|
+
result.succeeded++;
|
|
94
|
+
continue;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (!(e instanceof FsError) || e.code !== "EEXIST") {
|
|
97
|
+
result.failed++;
|
|
98
|
+
toast.error(`${op === "copy" ? "Copy" : "Move"} failed: ${name}`, { description: errorDescription(e) });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const choice = await ctx.resolve({ name, destination });
|
|
104
|
+
if (choice === "skip") {
|
|
105
|
+
result.skipped++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (choice === "keep-both") {
|
|
109
|
+
try {
|
|
110
|
+
const alternative = await freeName(dstDir, name, ctx.sep);
|
|
111
|
+
if (!alternative) throw new Error("No free name available");
|
|
112
|
+
destination = joinPath(dstDir, alternative, ctx.sep);
|
|
113
|
+
await runOne(source, destination, op);
|
|
114
|
+
result.succeeded++;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
result.failed++;
|
|
117
|
+
toast.error(`${op === "copy" ? "Copy" : "Move"} failed: ${name}`, { description: errorDescription(e) });
|
|
118
|
+
}
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Replace: trash the existing entry first so the overwrite stays recoverable, matching
|
|
123
|
+
// the collision dialog's own copy.
|
|
124
|
+
try {
|
|
125
|
+
await fsApi.remove(destination, false);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
if (!(e instanceof FsError) || e.code !== "NO_TRASH") {
|
|
128
|
+
result.failed++;
|
|
129
|
+
toast.error(`Could not remove existing ${name}`, { description: errorDescription(e) });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// No trash backend on this host — the dialog's "moves to Trash" promise cannot be
|
|
133
|
+
// kept, so ask explicitly before doing something permanent instead of silently
|
|
134
|
+
// downgrading to it.
|
|
135
|
+
const proceed = await ctx.confirmPermanentOverwrite(name);
|
|
136
|
+
if (!proceed) {
|
|
137
|
+
result.skipped++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await fsApi.remove(destination, true);
|
|
142
|
+
} catch (removeErr) {
|
|
143
|
+
result.failed++;
|
|
144
|
+
toast.error(`Could not remove existing ${name}`, { description: errorDescription(removeErr) });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await runOne(source, destination, op);
|
|
150
|
+
result.succeeded++;
|
|
151
|
+
} catch (e) {
|
|
152
|
+
result.failed++;
|
|
153
|
+
// The existing entry is already gone (trashed or deleted above) but the replacement
|
|
154
|
+
// was never written — say so explicitly rather than a generic "Copy/Move failed".
|
|
155
|
+
toast.error(`${name} was removed but the replacement could not be written`, { description: errorDescription(e) });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fsChanged(...touched);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Put absolute paths on the shared clipboard. `origin` lets the tree keep its own routes. */
|
|
164
|
+
export function setClipboardPaths(
|
|
165
|
+
paths: string[],
|
|
166
|
+
operation: "cut" | "copy",
|
|
167
|
+
origin?: ClipboardState["origin"],
|
|
168
|
+
): void {
|
|
169
|
+
if (paths.length === 0) return;
|
|
170
|
+
useFileStore.getState().setClipboard({ paths: [...paths], operation, origin });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Paste the shared clipboard into `dstDir`. A cut clipboard is consumed on success. */
|
|
174
|
+
export async function pasteInto(dstDir: string, ctx: TransferContext): Promise<void> {
|
|
175
|
+
const clipboard = useFileStore.getState().clipboard;
|
|
176
|
+
if (!clipboard || clipboard.paths.length === 0) return;
|
|
177
|
+
|
|
178
|
+
const result = await transfer(clipboard.paths, dstDir, clipboard.operation === "cut" ? "move" : "copy", ctx);
|
|
179
|
+
if (clipboard.operation === "cut" && result.failed === 0) {
|
|
180
|
+
useFileStore.getState().setClipboard(null);
|
|
181
|
+
}
|
|
182
|
+
if (result.succeeded > 0) {
|
|
183
|
+
const verb = clipboard.operation === "cut" ? "Moved" : "Copied";
|
|
184
|
+
toast.success(`${verb} ${result.succeeded} item${result.succeeded === 1 ? "" : "s"}`);
|
|
185
|
+
}
|
|
186
|
+
}
|