From fae7efcd5d5ff7415fa39a2f2a7ad7edc8386b13 Mon Sep 17 00:00:00 2001 From: multipleof4 Date: Wed, 23 Sep 2026 12:39:56 -0700 Subject: [PATCH] Build reusable octopus file browser --- .gitignore | 3 + THIRD_PARTY_NOTICES | 80 +++ demo/assets/index-DSvdNgV5.js | 1 + demo/assets/index-rQJBLWWV.css | 1 + demo/index.html | 17 + index.html | 16 + package-lock.json | 853 ++++++++++++++++++++++++++++++++ package.json | 40 ++ src/demo.css | 17 + src/demo.js | 6 + src/file-browser-utils.js | 54 ++ src/icon-resolver.js | 29 ++ src/octopus-file-browser.css | 91 ++++ src/octopus-file-browser.js | 118 +++++ test/file-browser-utils.test.js | 38 ++ vite.config.js | 28 ++ vite.lib.config.js | 14 + 17 files changed, 1406 insertions(+) create mode 100644 .gitignore create mode 100644 THIRD_PARTY_NOTICES create mode 100644 demo/assets/index-DSvdNgV5.js create mode 100644 demo/assets/index-rQJBLWWV.css create mode 100644 demo/index.html create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/demo.css create mode 100644 src/demo.js create mode 100644 src/file-browser-utils.js create mode 100644 src/icon-resolver.js create mode 100644 src/octopus-file-browser.css create mode 100644 src/octopus-file-browser.js create mode 100644 test/file-browser-utils.test.js create mode 100644 vite.config.js create mode 100644 vite.lib.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..746087d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +*.log diff --git a/THIRD_PARTY_NOTICES b/THIRD_PARTY_NOTICES new file mode 100644 index 0000000..8aaf36c --- /dev/null +++ b/THIRD_PARTY_NOTICES @@ -0,0 +1,80 @@ +octopus-file-browser +Third-Party Software Notices + +This project incorporates or was informed by the following third-party works. + +------------------------------------------------------------------------------- +Gitea +https://github.com/go-gitea/gitea + +The file-list layout was studied as a design reference. No Gitea source code is +included in this package. + +Copyright (c) 2016 The Gitea Authors +Copyright (c) 2015 The Gogs Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------- +Material Icon Theme 5.38.1 +https://github.com/material-extensions/vscode-material-icon-theme + +Copyright (c) 2025 Material Extensions + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------- +Vite 8.3.0 +https://github.com/vitejs/vite + +Copyright (c) 2019-present, VoidZero Inc. and Vite contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/demo/assets/index-DSvdNgV5.js b/demo/assets/index-DSvdNgV5.js new file mode 100644 index 0000000..679a27d --- /dev/null +++ b/demo/assets/index-DSvdNgV5.js @@ -0,0 +1 @@ +(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var e=`:host{--ofb-border:#d0d7de;--ofb-text:#181c21;--ofb-muted:#57606a;--ofb-link:#1f2328;--ofb-folder:#54aeff;--ofb-hover:#f6f8fa;--ofb-focus:#0969da;--ofb-recent:#d15700;color:var(--ofb-text);min-width:0;font:14px/20px -apple-system,BlinkMacSystemFont,Segoe UI,system-ui,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif;display:block}*{box-sizing:border-box}.list{border:1px solid var(--ofb-border);border-radius:5px;margin:0;padding:0;list-style:none;overflow:hidden}.entry{appearance:none;border:0;border-top:1px solid var(--ofb-border);color:inherit;cursor:pointer;font:inherit;text-align:left;background:#fff;grid-template-columns:minmax(0,1fr) 120px;width:100%;min-height:37px;margin:0;padding:0;display:grid}li:first-child .entry{border-top:0}.entry:hover{background:var(--ofb-hover)}.entry:active{background:#eef1f4}.entry:focus-visible{box-shadow:inset 0 0 0 2px var(--ofb-focus);outline:0}.cell{align-items:center;min-width:0;padding:8px 10px;display:flex}.name{gap:8px}.icon{object-fit:contain;flex:none;width:18px;height:18px}.label,.modified{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.label{color:var(--ofb-link);min-width:0}.size{color:var(--ofb-muted);white-space:nowrap;flex:none;margin-left:8px;font-size:12px}.modified{color:var(--ofb-muted)}.modified.recent{color:var(--ofb-recent)}.modified{justify-content:flex-end}.empty{color:var(--ofb-muted);text-align:center;margin:0;padding:24px}@media (width<=640px){.entry{grid-template-columns:minmax(0,1fr) 104px;min-height:44px}.cell{padding:10px 8px}.name{gap:7px}.icon{width:17px;height:17px}}@media (prefers-reduced-motion:no-preference){.entry{transition:background-color 80ms}}`,t=[`B`,`KB`,`MB`,`GB`,`TB`];function n(e=[]){return[...e].sort((e,t)=>Number(t.type===`directory`)-Number(e.type===`directory`)||e.name.localeCompare(t.name,void 0,{numeric:!0,sensitivity:`base`}))}function r(e){if(!Number.isFinite(e)||e<0)return`—`;if(!e)return`0 B`;let n=Math.min(Math.floor(Math.log(e)/Math.log(1024)),t.length-1),r=e/1024**n;return`${r.toFixed(n&&r<10?1:0)} ${t[n]}`}function i(e,t=Date.now()){let n=new Date(e);if(Number.isNaN(n.getTime()))return`—`;let r=n.getTime()-t,i=Math.abs(r),a=i<36e5?[`minute`,6e4]:i<864e5?[`hour`,36e5]:i<6048e5?[`day`,864e5]:i<2592e6?[`week`,6048e5]:i<31536e6?[`month`,2592e6]:[`year`,31536e6];return new Intl.RelativeTimeFormat(void 0,{numeric:`auto`}).format(Math.round(r/a[1]),a[0])}function a(e,t=Date.now()){let n=new Date(e).getTime(),r=t-n;return Number.isFinite(n)&&r>=0&&r<2592e6}function o(e=``){let t=e.toLowerCase();if(t===`license`||t.startsWith(`license.`))return`License`;if(t===`third_party_notices`)return`Notices`;if(t.startsWith(`.git`))return`Git`;let n=t.includes(`.`)?t.split(`.`).pop():``;return{css:`CSS`,html:`HTML`,js:`JavaScript`,json:`JSON`,md:`Markdown`,png:`Image`,svg:`SVG`,yml:`YAML`,yaml:`YAML`,ts:`TypeScript`}[n]||(n?n.toUpperCase():`File`)}var s=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3e%3cpath%20fill='%237e57c2'%20d='M20%2018h-2v-2h-2v2c0%20.193%200%20.703%201.254%201.033A3.345%203.345%200%200%201%2020%2022h2v2h2v-2c0-.388-.562-.851-1.254-1.034C20.356%2020.34%2020%2018.84%2020%2018m-3.254%202.966C14.356%2020.34%2014%2018.84%2014%2018h-2v-2h-2v8h2v-2h4v2h2v-2c0-.388-.562-.851-1.254-1.034'/%3e%3cpath%20fill='%237e57c2'%20d='M24%204H4v20a4%204%200%200%200%204%204h16.16A3.84%203.84%200%200%200%2028%2024.16V8a4%204%200%200%200-4-4m2%2014h-2v-2h-2v2c0%20.193%200%20.703%201.254%201.033A3.345%203.345%200%200%201%2026%2022v2a2%202%200%200%201-2%202h-2a2%202%200%200%201-2-2%202%202%200%200%201-2%202h-2a2%202%200%200%201-2-2%202%202%200%200%201-2%202h-2a2%202%200%200%201-2-2v-8a2%202%200%200%201%202-2h2a2%202%200%200%201%202%202%202%202%200%200%201%202-2h2a2%202%200%200%201%202%202%202%202%200%200%201%202-2h2a2%202%200%200%201%202%202Z'/%3e%3c/svg%3e`,c=`data:image/svg+xml,%3csvg%20viewBox='0%200%2016%2016'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='m6.922%203.768-.644-.536A1%201%200%200%200%205.638%203H2a1%201%200%200%200-1%201v8a1%201%200%200%200%201%201h12a1%201%200%200%200%201-1V5a1%201%200%200%200-1-1H7.562a1%201%200%200%201-.64-.232'%20fill='%2390a4ae'%20/%3e%3c/svg%3e`,l=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3e%3cpath%20fill='%23e64a19'%20d='M13.172%202.828%2011.78%204.22l1.91%201.91%202%202A2.986%202.986%200%200%201%2020%2010.81a3.25%203.25%200%200%201-.31%201.31l2.06%202a2.68%202.68%200%200%201%203.37.57%202.86%202.86%200%200%201%20.88%202.117%203.02%203.02%200%200%201-.856%202.109A2.9%202.9%200%200%201%2023%2019.81a2.93%202.93%200%200%201-2.13-.87%202.694%202.694%200%200%201-.56-3.38l-2-2.06a3%203%200%200%201-.31.12V20a3%203%200%200%201%201.44%201.09%202.92%202.92%200%200%201%20.56%201.72%202.88%202.88%200%200%201-.878%202.128%202.98%202.98%200%200%201-2.048.871%202.981%202.981%200%200%201-2.514-4.719A3%203%200%200%201%2016%2020v-6.38a2.96%202.96%200%200%201-1.44-1.09%202.9%202.9%200%200%201-.56-1.72%202.9%202.9%200%200%201%20.31-1.31l-3.9-3.9-7.579%207.572a4%204%200%200%200-.001%205.658l10.342%2010.342a4%204%200%200%200%205.656%200l10.344-10.344a4%204%200%200%200%200-5.656L18.828%202.828a4%204%200%200%200-5.656%200'/%3e%3c/svg%3e`,u=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3e%3cpath%20fill='%23e65100'%20d='m4%204%202%2022%2010%202%2010-2%202-22Zm19.72%207H11.28l.29%203h11.86l-.802%209.335L15.99%2025l-6.635-1.646L8.93%2019h3.02l.19%202%203.86.77%203.84-.77.29-4H8.84L8%208h16Z'/%3e%3c/svg%3e`,d=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3e%3cpath%20fill='%2326a69a'%20d='M8.5%206h4l-4-4zM3.875%201H9.5l4%204v8.6c0%20.773-.616%201.4-1.375%201.4h-8.25c-.76%200-1.375-.627-1.375-1.4V2.4c0-.777.612-1.4%201.375-1.4M4%2013.6h8V8l-2.625%202.8L8%209.4zm1.25-7.7c-.76%200-1.375.627-1.375%201.4s.616%201.4%201.375%201.4c.76%200%201.375-.627%201.375-1.4S6.009%205.9%205.25%205.9'/%3e%3c/svg%3e`,f=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3e%3cpath%20fill='%23ffca28'%20d='M2%202v12h12V2zm6%206h1v4a1.003%201.003%200%200%201-1%201H7a1.003%201.003%200%200%201-1-1v-1h1v1h1zm3%200h2v1h-2v1h1a1.003%201.003%200%200%201%201%201v1a1.003%201.003%200%200%201-1%201h-2v-1h2v-1h-1a1.003%201.003%200%200%201-1-1V9a1.003%201.003%200%200%201%201-1'/%3e%3c/svg%3e`,p=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%20-960%20960%20960'%3e%3cpath%20fill='%23f9a825'%20d='M560-160v-80h120q17%200%2028.5-11.5T720-280v-80q0-38%2022-69t58-44v-14q-36-13-58-44t-22-69v-80q0-17-11.5-28.5T680-720H560v-80h120q50%200%2085%2035t35%2085v80q0%2017%2011.5%2028.5T840-560h40v160h-40q-17%200-28.5%2011.5T800-360v80q0%2050-35%2085t-85%2035zm-280%200q-50%200-85-35t-35-85v-80q0-17-11.5-28.5T120-400H80v-160h40q17%200%2028.5-11.5T160-600v-80q0-50%2035-85t85-35h120v80H280q-17%200-28.5%2011.5T240-680v80q0%2038-22%2069t-58%2044v14q36%2013%2058%2044t22%2069v80q0%2017%2011.5%2028.5T280-240h120v80z'/%3e%3c/svg%3e`,m=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'%3e%3cpath%20fill='%23ff5722'%20d='M8%201a5.5%205.5%200%200%200-4%209.26V15l4-1.5%204%201.5v-4.74A5.49%205.49%200%200%200%208%201m0%201.5a4%204%200%201%201%200%208%204%204%200%200%201%200-8m0%202a2%202%200%201%200%200%204%202%202%200%200%200%200-4'/%3e%3c/svg%3e`,h=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3e%3cpath%20fill='%2342a5f5'%20d='m14%2010-4%203.5L6%2010H4v12h4v-6l2%202%202-2v6h4V10zm12%206v-6h-4v6h-4l6%208%206-8z'/%3e%3c/svg%3e`,g=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3e%3cpath%20fill='%23e53935'%20d='M4%204v24h24V4Zm20%2020h-4V12h-4v12H8V8h16Z'/%3e%3c/svg%3e`,_=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3e%3cpath%20fill='%23ffb300'%20d='M29.168%2014.03a2.7%202.7%200%200%200-1.968-.83%202.51%202.51%200%200%200-1.929.8h-4.443l3.078-3.078a2.835%202.835%200%200%200%202.857-2.842%202.6%202.6%200%200%200-.831-1.969%202.82%202.82%200%200%200-2.014-.788%202.67%202.67%200%200%200-1.968.788%202.36%202.36%200%200%200-.812%201.922L18%2011.17V6.726a2.51%202.51%200%200%200%20.8-1.929%202.7%202.7%200%200%200-.832-1.968%202.745%202.745%200%200%200-3.936%200%202.7%202.7%200%200%200-.832%201.968%202.51%202.51%200%200%200%20.8%201.93v4.443l-3.138-3.138a2.36%202.36%200%200%200-.812-1.922%202.66%202.66%200%200%200-1.968-.788%202.83%202.83%200%200%200-2.014.788%202.6%202.6%200%200%200-.831%201.969%202.74%202.74%200%200%200%20.831%202.013%202.8%202.8%200%200%200%202.026.829l3.078%203.078H6.729a2.51%202.51%200%200%200-1.929-.8%202.7%202.7%200%200%200-1.968.831%202.745%202.745%200%200%200%200%203.937%202.7%202.7%200%200%200%201.968.832%202.51%202.51%200%200%200%201.929-.8h4.443l-3.078%203.077a2.835%202.835%200%200%200-2.857%202.842%202.6%202.6%200%200%200%20.831%201.969%202.82%202.82%200%200%200%202.014.788%202.67%202.67%200%200%200%201.968-.788%202.36%202.36%200%200%200%20.812-1.922L14%2020.827v4.444a2.51%202.51%200%200%200-.8%201.929%202.784%202.784%200%200%200%204.768%201.968A2.7%202.7%200%200%200%2018.8%2027.2a2.51%202.51%200%200%200-.8-1.929v-4.444l3.138%203.138a2.36%202.36%200%200%200%20.812%201.922%202.66%202.66%200%200%200%201.968.788%202.83%202.83%200%200%200%202.014-.788%202.6%202.6%200%200%200%20.831-1.969%202.74%202.74%200%200%200-.831-2.013%202.8%202.8%200%200%200-2.026-.829L20.828%2018h4.443a2.51%202.51%200%200%200%201.93.8%202.784%202.784%200%200%200%201.967-4.769Z'/%3e%3c/svg%3e`,v=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20xml:space='preserve'%20viewBox='0%200%2016%2016'%3e%3cpath%20fill='%230288d1'%20d='M2%202v12h12V2zm4%206h3v1H8v4H7V9H6zm5%200h2v1h-2v1h1a1.003%201.003%200%200%201%201%201v1a1.003%201.003%200%200%201-1%201h-2v-1h2v-1h-1a1.003%201.003%200%200%201-1-1V9a1.003%201.003%200%200%201%201-1'/%3e%3c/svg%3e`,y=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3e%3cpath%20fill='%23a0f'%20d='M29.313%2012h-6.664a1.427%201.427%200%200%201-1.1-2.24l4.398-6.676A.703.703%200%200%200%2025.397%202H8.428a.62.62%200%200%200-.55.289l-5.77%208.627A.703.703%200%200%200%202.658%2012h8.175a1.427%201.427%200%200%201%201.099%202.24l-4.48%206.676A.702.702%200%200%200%208%2022l6.695.002A1.34%201.34%200%200%201%2016%2023.375v5.934a.652.652%200%200%200%201.168.433l12.694-16.586a.725.725%200%200%200-.55-1.156'/%3e%3c/svg%3e`,b=`data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cpath%20fill='%23ff5252'%20d='M13%209h5.5L13%203.5zM6%202h8l6%206v12c0%201.1-.9%202-2%202H6c-1.1%200-2-.9-2-2V4c0-1.1.9-2%202-2m12%2016v-2H9v2zm-4-4v-2H6v2z'/%3e%3c/svg%3e`,x={css:s,htm:u,html:u,jpeg:d,jpg:d,js:f,json:p,md:h,mjs:f,png:d,svg:_,ts:v,yaml:b,yml:b};function S(e){if(e.icon)return e.icon;if(e.type===`directory`)return c;let t=e.name.toLowerCase();return t===`license`||t.startsWith(`license.`)?m:t===`package.json`||t===`package-lock.json`?g:t.startsWith(`vite.config.`)?y:t.startsWith(`.git`)?l:x[t.split(`.`).pop()]||`data:image/svg+xml,%3csvg%20viewBox='0%200%2016%2016'%20xmlns='http://www.w3.org/2000/svg'%3e%3cpath%20d='m8.668%206h3.6641l-3.6641-3.668v3.668m-4.668-4.668h5.332l4%204v8c0%200.73828-0.59375%201.3359-1.332%201.3359h-8c-0.73828%200-1.332-0.59766-1.332-1.3359v-10.664c0-0.74219%200.59375-1.3359%201.332-1.3359m3.332%201.3359h-3.332v10.664h8v-6h-4.668z'%20fill='%2390a4ae'%20/%3e%3c/svg%3e`}var C=globalThis.HTMLElement||class{};function w(e,t,n){let r=document.createElement(e);return t&&(r.className=t),n!==void 0&&(r.textContent=n),r}var T=class extends C{#e=[];#t=[];static get observedAttributes(){return[`empty-label`]}constructor(){super(),this.attachShadow({mode:`open`})}connectedCallback(){this.#o()}attributeChangedCallback(){this.isConnected&&this.#o()}set entries(e){this.#e=Array.isArray(e)?e:[],this.#t=[],this.#o()}get entries(){return this.#e}get path(){return[...this.#t]}navigate(e=[]){let t=this.#e,n=[];for(let r of e){let e=t.find(e=>e.type===`directory`&&e.name===r);if(!e)break;n.push(r),t=e.children||[]}this.#t=n,this.#o(),this.#r(`octopus:navigate`,{path:this.path,entries:this.#n()})}#n(){return this.#t.reduce((e,t)=>e.find(e=>e.type===`directory`&&e.name===t)?.children||[],this.#e)}#r(e,t){this.dispatchEvent(new CustomEvent(e,{bubbles:!0,composed:!0,detail:t}))}#i(e){if(e.parent)return this.navigate(this.#t.slice(0,-1));if(e.type===`directory`)return this.navigate([...this.#t,e.name]);this.#r(`octopus:open`,{entry:e,path:[...this.#t,e.name]})}#a(e){let t=w(`li`),n=w(`button`,`entry`);n.type=`button`,n.title=e.parent?`Parent folder`:e.name,n.setAttribute(`aria-label`,e.parent?`Parent folder`:`${e.name}, ${e.type===`directory`?`folder`:o(e.name)}`),n.addEventListener(`click`,()=>this.#i(e));let s=w(`span`,`cell name`),c=w(`img`,`icon`);c.alt=``,c.src=S(e),s.append(c,w(`span`,`label`,e.name)),e.type!==`directory`&&s.append(w(`span`,`size`,r(e.size)));let l=w(`span`,`cell modified`,e.modified?i(e.modified):``);return e.modified&&(l.title=new Date(e.modified).toLocaleString(),a(e.modified)&&l.classList.add(`recent`)),n.append(s,l),t.append(n),t}#o(){if(!this.shadowRoot||typeof document>`u`)return;let t=w(`style`);t.textContent=e;let r=w(`ul`,`list`);r.setAttribute(`aria-label`,`Files`);let i=n(this.#n());this.#t.length&&r.append(this.#a({name:`..`,type:`directory`,parent:!0})),i.length?i.forEach(e=>r.append(this.#a(e))):r.append(w(`li`,`empty`,this.getAttribute(`empty-label`)||`This folder is empty.`)),this.shadowRoot.replaceChildren(t,r)}};globalThis.customElements&&!customElements.get(`octopus-file-browser`)&&customElements.define(`octopus-file-browser`,T);var E=[{name:`.gitignore`,type:`file`,modified:`2026-09-23T17:05:01.924Z`,size:26},{name:`AGENTS.md`,type:`file`,modified:`2026-09-23T18:32:40.331Z`,size:105},{name:`docs`,type:`directory`,modified:`2026-09-23T16:30:08.419Z`,children:[{name:`ofb.png`,type:`file`,modified:`2026-09-23T16:30:08.419Z`,size:907351},{name:`README.md`,type:`file`,modified:`2026-09-23T16:30:08.416Z`,size:14}]},{name:`index.html`,type:`file`,modified:`2026-09-23T17:30:19.971Z`,size:446},{name:`package-lock.json`,type:`file`,modified:`2026-09-23T17:02:24.555Z`,size:26336},{name:`package.json`,type:`file`,modified:`2026-09-23T17:30:19.966Z`,size:986},{name:`src`,type:`directory`,modified:`2026-09-23T17:01:59.151Z`,children:[{name:`demo.css`,type:`file`,modified:`2026-09-23T17:30:19.989Z`,size:298},{name:`demo.js`,type:`file`,modified:`2026-09-23T17:30:19.993Z`,size:198},{name:`file-browser-utils.js`,type:`file`,modified:`2026-09-23T17:30:19.978Z`,size:2101},{name:`icon-resolver.js`,type:`file`,modified:`2026-09-23T17:01:59.141Z`,size:1499},{name:`octopus-file-browser.css`,type:`file`,modified:`2026-09-23T17:30:35.003Z`,size:2076},{name:`octopus-file-browser.js`,type:`file`,modified:`2026-09-23T17:30:35.010Z`,size:4324}]},{name:`test`,type:`directory`,modified:`2026-09-23T17:01:59.162Z`,children:[{name:`file-browser-utils.test.js`,type:`file`,modified:`2026-09-23T17:30:20.005Z`,size:1558}]},{name:`THIRD_PARTY_NOTICES`,type:`file`,modified:`2026-09-23T17:05:01.941Z`,size:3905},{name:`vite.config.js`,type:`file`,modified:`2026-09-23T17:30:19.997Z`,size:1069},{name:`vite.lib.config.js`,type:`file`,modified:`2026-09-23T17:30:20.000Z`,size:407}],D=document.querySelector(`octopus-file-browser`);D.entries=E; \ No newline at end of file diff --git a/demo/assets/index-rQJBLWWV.css b/demo/assets/index-rQJBLWWV.css new file mode 100644 index 0000000..b529516 --- /dev/null +++ b/demo/assets/index-rQJBLWWV.css @@ -0,0 +1 @@ +:root{color:#181c21;font-synthesis:none;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,system-ui,sans-serif}body{min-width:280px;margin:0}main{max-width:1220px;margin:0 auto;padding:20px}@media (width<=640px){main{padding:8px}} diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..0c0d03c --- /dev/null +++ b/demo/index.html @@ -0,0 +1,17 @@ + + + + + + + + octopus-file-browser + + + + +
+ +
+ + diff --git a/index.html b/index.html new file mode 100644 index 0000000..ff77235 --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + + octopus-file-browser + + +
+ +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d1de391 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,853 @@ +{ + "name": "octopus-file-browser", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "octopus-file-browser", + "version": "0.1.0", + "license": "UNLICENSED", + "dependencies": { + "material-icon-theme": "5.38.1" + }, + "devDependencies": { + "vite": "8.3.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.10.tgz", + "integrity": "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.10.tgz", + "integrity": "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.10.tgz", + "integrity": "sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.10.tgz", + "integrity": "sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.10.tgz", + "integrity": "sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.10.tgz", + "integrity": "sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.10.tgz", + "integrity": "sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.10.tgz", + "integrity": "sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.10.tgz", + "integrity": "sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.10.tgz", + "integrity": "sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.10.tgz", + "integrity": "sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.10.tgz", + "integrity": "sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.10.tgz", + "integrity": "sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.10.tgz", + "integrity": "sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.10.tgz", + "integrity": "sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/chroma-js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", + "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/material-icon-theme": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/material-icon-theme/-/material-icon-theme-5.38.1.tgz", + "integrity": "sha512-14cFM4NJGdbuo68rIZTq9TSX0f5BtA6VF+eNX3zq23Z5NoEeVtM4Zn4PpZ0ERl++50jCBrywKFWXmOjbxf0xTA==", + "license": "MIT", + "dependencies": { + "chroma-js": "^3.2.0" + }, + "engines": { + "vscode": "^1.55.0" + }, + "funding": { + "url": "https://github.com/sponsors/material-extensions" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.10.tgz", + "integrity": "sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.10", + "@rolldown/binding-android-arm64": "1.2.10", + "@rolldown/binding-darwin-arm64": "1.2.10", + "@rolldown/binding-darwin-x64": "1.2.10", + "@rolldown/binding-freebsd-x64": "1.2.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.10", + "@rolldown/binding-linux-arm64-gnu": "1.2.10", + "@rolldown/binding-linux-arm64-musl": "1.2.10", + "@rolldown/binding-linux-ppc64-gnu": "1.2.10", + "@rolldown/binding-linux-s390x-gnu": "1.2.10", + "@rolldown/binding-linux-x64-gnu": "1.2.10", + "@rolldown/binding-linux-x64-musl": "1.2.10", + "@rolldown/binding-openharmony-arm64": "1.2.10", + "@rolldown/binding-win32-arm64-msvc": "1.2.10", + "@rolldown/binding-win32-x64-msvc": "1.2.10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f019529 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "octopus-file-browser", + "version": "0.1.0", + "description": "A compact, framework-free file browser inspired by Gitea's repository view.", + "type": "module", + "main": "./dist/octopus-file-browser.umd.cjs", + "module": "./dist/octopus-file-browser.js", + "exports": { + ".": { + "import": "./dist/octopus-file-browser.js", + "require": "./dist/octopus-file-browser.umd.cjs" + } + }, + "files": [ + "dist", + "THIRD_PARTY_NOTICES" + ], + "scripts": { + "dev": "vite", + "test": "node --test", + "demo": "vite build", + "build:demo": "npm run demo", + "build:library": "vite build --config vite.lib.config.js", + "build": "npm run build:demo && npm run build:library", + "check": "npm test && npm run build" + }, + "keywords": [ + "file-browser", + "web-component", + "javascript", + "gitea" + ], + "license": "UNLICENSED", + "dependencies": { + "material-icon-theme": "5.38.1" + }, + "devDependencies": { + "vite": "8.3.0" + } +} diff --git a/src/demo.css b/src/demo.css new file mode 100644 index 0000000..a1a360d --- /dev/null +++ b/src/demo.css @@ -0,0 +1,17 @@ +:root { + color: #181c21; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; + font-synthesis: none; +} + +body { margin: 0; min-width: 280px; } + +main { + margin: 0 auto; + max-width: 1220px; + padding: 20px; +} + +@media (max-width: 640px) { + main { padding: 8px; } +} diff --git a/src/demo.js b/src/demo.js new file mode 100644 index 0000000..b7256d4 --- /dev/null +++ b/src/demo.js @@ -0,0 +1,6 @@ +import './demo.css'; +import './octopus-file-browser.js'; +import entries from 'virtual:octopus-repo-tree'; + +const browser = document.querySelector('octopus-file-browser'); +browser.entries = entries; diff --git a/src/file-browser-utils.js b/src/file-browser-utils.js new file mode 100644 index 0000000..eb19eef --- /dev/null +++ b/src/file-browser-utils.js @@ -0,0 +1,54 @@ +const UNITS = ['B', 'KB', 'MB', 'GB', 'TB']; + +export function sortEntries(entries = []) { + return [...entries].sort((a, b) => + Number(b.type === 'directory') - Number(a.type === 'directory') || + a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }), + ); +} + +export function formatFileSize(bytes) { + if (!Number.isFinite(bytes) || bytes < 0) return '—'; + if (!bytes) return '0 B'; + const unit = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), UNITS.length - 1); + const value = bytes / 1024 ** unit; + return `${value.toFixed(unit && value < 10 ? 1 : 0)} ${UNITS[unit]}`; +} + +export function formatRelativeDate(value, now = Date.now()) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return '—'; + const delta = date.getTime() - now; + const abs = Math.abs(delta); + const units = abs < 60 * 60e3 + ? ['minute', 60e3] + : abs < 24 * 60 * 60e3 + ? ['hour', 60 * 60e3] + : abs < 7 * 24 * 60 * 60e3 + ? ['day', 24 * 60 * 60e3] + : abs < 30 * 24 * 60 * 60e3 + ? ['week', 7 * 24 * 60 * 60e3] + : abs < 365 * 24 * 60 * 60e3 + ? ['month', 30 * 24 * 60 * 60e3] + : ['year', 365 * 24 * 60 * 60e3]; + return new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }) + .format(Math.round(delta / units[1]), units[0]); +} + +export function isRecent(value, now = Date.now()) { + const time = new Date(value).getTime(); + const age = now - time; + return Number.isFinite(time) && age >= 0 && age < 30 * 24 * 60 * 60e3; +} + +export function fileKindLabel(name = '') { + const lower = name.toLowerCase(); + if (lower === 'license' || lower.startsWith('license.')) return 'License'; + if (lower === 'third_party_notices') return 'Notices'; + if (lower.startsWith('.git')) return 'Git'; + const extension = lower.includes('.') ? lower.split('.').pop() : ''; + return ({ + css: 'CSS', html: 'HTML', js: 'JavaScript', json: 'JSON', md: 'Markdown', + png: 'Image', svg: 'SVG', yml: 'YAML', yaml: 'YAML', ts: 'TypeScript', + })[extension] || (extension ? extension.toUpperCase() : 'File'); +} diff --git a/src/icon-resolver.js b/src/icon-resolver.js new file mode 100644 index 0000000..e074443 --- /dev/null +++ b/src/icon-resolver.js @@ -0,0 +1,29 @@ +import css from 'material-icon-theme/icons/css.svg'; +import file from 'material-icon-theme/icons/file.svg'; +import folder from 'material-icon-theme/icons/folder.svg'; +import git from 'material-icon-theme/icons/git.svg'; +import html from 'material-icon-theme/icons/html.svg'; +import image from 'material-icon-theme/icons/image.svg'; +import javascript from 'material-icon-theme/icons/javascript.svg'; +import json from 'material-icon-theme/icons/json.svg'; +import license from 'material-icon-theme/icons/license.svg'; +import markdown from 'material-icon-theme/icons/markdown.svg'; +import npm from 'material-icon-theme/icons/npm.svg'; +import svg from 'material-icon-theme/icons/svg.svg'; +import typescript from 'material-icon-theme/icons/typescript.svg'; +import vite from 'material-icon-theme/icons/vite.svg'; +import yaml from 'material-icon-theme/icons/yaml.svg'; + +const EXTENSIONS = { css, htm: html, html, jpeg: image, jpg: image, js: javascript, + json, md: markdown, mjs: javascript, png: image, svg, ts: typescript, yaml, yml: yaml }; + +export function resolveIcon(entry) { + if (entry.icon) return entry.icon; + if (entry.type === 'directory') return folder; + const name = entry.name.toLowerCase(); + if (name === 'license' || name.startsWith('license.')) return license; + if (name === 'package.json' || name === 'package-lock.json') return npm; + if (name.startsWith('vite.config.')) return vite; + if (name.startsWith('.git')) return git; + return EXTENSIONS[name.split('.').pop()] || file; +} diff --git a/src/octopus-file-browser.css b/src/octopus-file-browser.css new file mode 100644 index 0000000..154fd63 --- /dev/null +++ b/src/octopus-file-browser.css @@ -0,0 +1,91 @@ +:host { + --ofb-border: #d0d7de; + --ofb-text: #181c21; + --ofb-muted: #57606a; + --ofb-link: #1f2328; + --ofb-folder: #54aeff; + --ofb-hover: #f6f8fa; + --ofb-focus: #0969da; + --ofb-recent: #d15700; + color: var(--ofb-text); + display: block; + font: 14px/20px -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, Roboto, + "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; + min-width: 0; +} + +* { box-sizing: border-box; } + +.list { + border: 1px solid var(--ofb-border); + border-radius: 5px; + list-style: none; + margin: 0; + overflow: hidden; + padding: 0; +} + +.entry { + appearance: none; + background: #fff; + border: 0; + border-top: 1px solid var(--ofb-border); + color: inherit; + cursor: pointer; + display: grid; + font: inherit; + grid-template-columns: minmax(0, 1fr) 120px; + margin: 0; + min-height: 37px; + padding: 0; + text-align: left; + width: 100%; +} + +li:first-child .entry { border-top: 0; } +.entry:hover { background: var(--ofb-hover); } +.entry:active { background: #eef1f4; } +.entry:focus-visible { box-shadow: inset 0 0 0 2px var(--ofb-focus); outline: 0; } + +.cell { + align-items: center; + display: flex; + min-width: 0; + padding: 8px 10px; +} + +.name { gap: 8px; } +.icon { flex: 0 0 auto; height: 18px; object-fit: contain; width: 18px; } +.label, .modified { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.label { color: var(--ofb-link); min-width: 0; } +.size { + color: var(--ofb-muted); + flex: 0 0 auto; + font-size: 12px; + margin-left: 8px; + white-space: nowrap; +} +.modified { color: var(--ofb-muted); } +.modified.recent { color: var(--ofb-recent); } +.modified { justify-content: flex-end; } + +.empty { + color: var(--ofb-muted); + margin: 0; + padding: 24px; + text-align: center; +} + +@media (max-width: 640px) { + .entry { + grid-template-columns: minmax(0, 1fr) 104px; + min-height: 44px; + } + .cell { padding: 10px 8px; } + .name { gap: 7px; } + .icon { height: 17px; width: 17px; } +} + +@media (prefers-reduced-motion: no-preference) { + .entry { transition: background-color 80ms ease; } +} diff --git a/src/octopus-file-browser.js b/src/octopus-file-browser.js new file mode 100644 index 0000000..1fe4620 --- /dev/null +++ b/src/octopus-file-browser.js @@ -0,0 +1,118 @@ +import styles from './octopus-file-browser.css?inline'; +import { fileKindLabel, formatFileSize, formatRelativeDate, isRecent, sortEntries } from './file-browser-utils.js'; +import { resolveIcon } from './icon-resolver.js'; + +const HTMLElementBase = globalThis.HTMLElement || class {}; + +function element(tag, className, text) { + const node = document.createElement(tag); + if (className) node.className = className; + if (text !== undefined) node.textContent = text; + return node; +} + +export class OctopusFileBrowser extends HTMLElementBase { + #entries = []; + #path = []; + + static get observedAttributes() { return ['empty-label']; } + + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + connectedCallback() { this.#render(); } + attributeChangedCallback() { if (this.isConnected) this.#render(); } + + set entries(value) { + this.#entries = Array.isArray(value) ? value : []; + this.#path = []; + this.#render(); + } + + get entries() { return this.#entries; } + get path() { return [...this.#path]; } + + navigate(path = []) { + let entries = this.#entries; + const valid = []; + for (const part of path) { + const directory = entries.find((entry) => entry.type === 'directory' && entry.name === part); + if (!directory) break; + valid.push(part); + entries = directory.children || []; + } + this.#path = valid; + this.#render(); + this.#emit('octopus:navigate', { path: this.path, entries: this.#currentEntries() }); + } + + #currentEntries() { + return this.#path.reduce((entries, part) => + entries.find((entry) => entry.type === 'directory' && entry.name === part)?.children || [], + this.#entries); + } + + #emit(name, detail) { + this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail })); + } + + #open(entry) { + if (entry.parent) return this.navigate(this.#path.slice(0, -1)); + if (entry.type === 'directory') return this.navigate([...this.#path, entry.name]); + this.#emit('octopus:open', { entry, path: [...this.#path, entry.name] }); + } + + #renderEntry(entry) { + const item = element('li'); + const row = element('button', 'entry'); + row.type = 'button'; + row.title = entry.parent ? 'Parent folder' : entry.name; + row.setAttribute('aria-label', entry.parent + ? 'Parent folder' + : `${entry.name}, ${entry.type === 'directory' ? 'folder' : fileKindLabel(entry.name)}`); + row.addEventListener('click', () => this.#open(entry)); + + const name = element('span', 'cell name'); + const icon = element('img', 'icon'); + icon.alt = ''; + icon.src = resolveIcon(entry); + name.append(icon, element('span', 'label', entry.name)); + if (entry.type !== 'directory') name.append(element('span', 'size', formatFileSize(entry.size))); + const modified = element('span', 'cell modified', entry.modified ? formatRelativeDate(entry.modified) : ''); + if (entry.modified) { + modified.title = new Date(entry.modified).toLocaleString(); + if (isRecent(entry.modified)) modified.classList.add('recent'); + } + row.append(name, modified); + item.append(row); + return item; + } + + #render() { + if (!this.shadowRoot || typeof document === 'undefined') return; + const style = element('style'); + style.textContent = styles; + const list = element('ul', 'list'); + list.setAttribute('aria-label', 'Files'); + const entries = sortEntries(this.#currentEntries()); + if (this.#path.length) list.append(this.#renderEntry({ name: '..', type: 'directory', parent: true })); + if (entries.length) entries.forEach((entry) => list.append(this.#renderEntry(entry))); + else list.append(element('li', 'empty', this.getAttribute('empty-label') || 'This folder is empty.')); + this.shadowRoot.replaceChildren(style, list); + } +} + +if (globalThis.customElements && !customElements.get('octopus-file-browser')) + customElements.define('octopus-file-browser', OctopusFileBrowser); + +export function createOctopusFileBrowser(target, options = {}) { + const browser = document.createElement('octopus-file-browser'); + if (options.emptyLabel) browser.setAttribute('empty-label', options.emptyLabel); + browser.entries = options.entries || []; + target.append(browser); + return browser; +} + +export { fileKindLabel, formatFileSize, formatRelativeDate, isRecent, sortEntries }; diff --git a/test/file-browser-utils.test.js b/test/file-browser-utils.test.js new file mode 100644 index 0000000..f8361f6 --- /dev/null +++ b/test/file-browser-utils.test.js @@ -0,0 +1,38 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { fileKindLabel, formatFileSize, formatRelativeDate, isRecent, sortEntries } from '../src/file-browser-utils.js'; + +test('sortEntries keeps folders first and names natural', () => { + const sorted = sortEntries([ + { name: 'file10.js', type: 'file' }, + { name: 'zeta', type: 'directory' }, + { name: 'file2.js', type: 'file' }, + { name: 'alpha', type: 'directory' }, + ]); + assert.deepEqual(sorted.map(({ name }) => name), ['alpha', 'zeta', 'file2.js', 'file10.js']); +}); + +test('formatFileSize uses compact binary units', () => { + assert.equal(formatFileSize(0), '0 B'); + assert.equal(formatFileSize(1536), '1.5 KB'); + assert.equal(formatFileSize(undefined), '—'); +}); + +test('formatRelativeDate reports useful relative values', () => { + const now = Date.parse('2026-09-23T12:00:00Z'); + assert.equal(formatRelativeDate('2026-09-22T12:00:00Z', now), 'yesterday'); + assert.equal(formatRelativeDate('2026-09-09T12:00:00Z', now), '2 weeks ago'); +}); + +test('isRecent marks dates younger than 30 days', () => { + const now = Date.parse('2026-09-23T12:00:00Z'); + assert.equal(isRecent('2026-09-01T12:00:00Z', now), true); + assert.equal(isRecent('2026-08-01T12:00:00Z', now), false); + assert.equal(isRecent('invalid', now), false); +}); + +test('fileKindLabel identifies special and common files', () => { + assert.equal(fileKindLabel('LICENSE'), 'License'); + assert.equal(fileKindLabel('app.js'), 'JavaScript'); + assert.equal(fileKindLabel('Dockerfile'), 'File'); +}); diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..12f26d8 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,28 @@ +import { readdirSync, statSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +const VIRTUAL_ID = 'virtual:octopus-repo-tree'; +const RESOLVED_ID = `\0${VIRTUAL_ID}`; +const ignored = new Set(['.git', 'demo', 'dist', 'node_modules']); + +function scan(directory) { + return readdirSync(directory, { withFileTypes: true }) + .filter((entry) => !ignored.has(entry.name)) + .map((entry) => { + const path = resolve(directory, entry.name); + const stats = statSync(path); + return entry.isDirectory() + ? { name: entry.name, type: 'directory', modified: stats.mtime.toISOString(), children: scan(path) } + : { name: entry.name, type: 'file', modified: stats.mtime.toISOString(), size: stats.size }; + }); +} + +export default defineConfig({ + build: { outDir: 'demo' }, + plugins: [{ + name: 'octopus-demo-tree', + resolveId(id) { return id === VIRTUAL_ID ? RESOLVED_ID : null; }, + load(id) { return id === RESOLVED_ID ? `export default ${JSON.stringify(scan(process.cwd()))}` : null; }, + }], +}); diff --git a/vite.lib.config.js b/vite.lib.config.js new file mode 100644 index 0000000..2b14c0e --- /dev/null +++ b/vite.lib.config.js @@ -0,0 +1,14 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + lib: { + entry: resolve(import.meta.dirname, 'src/octopus-file-browser.js'), + fileName: (format) => format === 'es' ? 'octopus-file-browser.js' : 'octopus-file-browser.umd.cjs', + formats: ['es', 'umd'], + name: 'OctopusFileBrowser', + }, + outDir: 'dist', + }, +});