components.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. 'use strict';
  2. import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js';
  3. // Helper function that returns a promise that resolves after delay
  4. const Delay = (ms, val) => new Promise(resolve => setTimeout(resolve, ms, val));
  5. export const Icons = {
  6. heart: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>`,
  7. downArrowBox: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15M9 12l3 3m0 0l3-3m-3 3V2.25" /> </svg>`,
  8. upArrowBox: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15m0-3l-3-3m0 0l-3 3m3-3V15" /> </svg>`,
  9. cog: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>`,
  10. settingsH: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75" /> </svg>`,
  11. settingsV: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 13.5V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m12-3V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m-6-9V3.75m0 3.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 9.75V10.5" /> </svg>`,
  12. scan: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118 20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg> `,
  13. desktop: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" /> </svg>`,
  14. alert: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5" /> </svg>`,
  15. bell: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5" /> </svg>`,
  16. refresh: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /> </svg> `,
  17. bars4: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5" /> </svg>`,
  18. bars3: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> </svg>`,
  19. logout: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
  20. save: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>`,
  21. email: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /> </svg>`,
  22. expand: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" /> </svg>`,
  23. shrink: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" /> </svg>`,
  24. ok: props => html`<svg class=${props.class} fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
  25. fail: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
  26. upload: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> </svg> `,
  27. download: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /> </svg> `,
  28. bolt: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>`,
  29. home: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> </svg> `,
  30. link: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /> </svg> `,
  31. shield: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /> </svg> `,
  32. barsdown: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0l-3.75-3.75M17.25 21L21 17.25" /> </svg> `,
  33. arrowdown: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75" /> </svg> `,
  34. arrowup: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" /> </svg>`,
  35. warn: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> </svg>`,
  36. info: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /> </svg>`,
  37. exclamationTriangle: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> </svg>`,
  38. thumbUp: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M6.633 10.5c.806 0 1.533-.446 2.031-1.08a9.041 9.041 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75A2.25 2.25 0 0116.5 4.5c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H13.48c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23H5.904M14.25 9h2.25M5.904 18.75c.083.205.173.405.27.602.197.4-.078.898-.523.898h-.908c-.889 0-1.713-.518-1.972-1.368a12 12 0 01-.521-3.507c0-1.553.295-3.036.831-4.398C3.387 10.203 4.167 9.75 5 9.75h1.053c.472 0 .745.556.5.96a8.958 8.958 0 00-1.302 4.665c0 1.194.232 2.333.654 3.375z" /> </svg>`,
  39. backward: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M21 16.811c0 .864-.933 1.405-1.683.977l-7.108-4.062a1.125 1.125 0 010-1.953l7.108-4.062A1.125 1.125 0 0121 8.688v8.123zM11.25 16.811c0 .864-.933 1.405-1.683.977l-7.108-4.062a1.125 1.125 0 010-1.953L9.567 7.71a1.125 1.125 0 011.683.977v8.123z" /> </svg>`,
  40. chip: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25H6.75A2.25 2.25 0 004.5 6.75v10.5a2.25 2.25 0 002.25 2.25zm.75-12h9v9h-9v-9z" /> </svg>`,
  41. camera: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zM18.75 10.5h.008v.008h-.008V10.5z" /> </svg>`,
  42. arrows: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" /> </svg>`,
  43. doc: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /></svg>`,
  44. };
  45. export const tipColors = {
  46. green: 'bg-green-100 text-green-900 ring-green-300',
  47. yellow: 'bg-yellow-100 text-yellow-900 ring-yellow-300',
  48. red: 'bg-red-100 text-red-900 ring-red-300',
  49. };
  50. export function Button({title, onclick, disabled, cls, icon, ref, colors, hovercolor, disabledcolor}) {
  51. const [spin, setSpin] = useState(false);
  52. const cb = function(ev) {
  53. const res = onclick ? onclick() : null;
  54. if (res && typeof (res.catch) === 'function') {
  55. setSpin(true);
  56. res.catch(() => false).then(() => setSpin(false));
  57. }
  58. };
  59. if (!colors) colors = 'bg-blue-600 hover:bg-blue-500 disabled:bg-blue-400';
  60. return html`
  61. <button type="button" class="inline-flex justify-center items-center gap-2 rounded px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm ${colors} ${cls}"
  62. ref=${ref} onclick=${cb} disabled=${disabled || spin} >
  63. ${title}
  64. <${spin ? Icons.refresh : icon} class="w-4 ${spin ? 'animate-spin' : ''}" />
  65. <//>`
  66. };
  67. export function Notification({ok, text, close}) {
  68. const closebtn = useRef(null);
  69. const from = 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2';
  70. const to = 'translate-y-0 opacity-100 sm:translate-x-0';
  71. const [tr, setTr] = useState(from);
  72. useEffect(function() {
  73. setTr(to);
  74. setTimeout(ev => closebtn && closebtn.current.click && closebtn.current.click(), 1500);
  75. }, []);
  76. const onclose = ev => { setTr(from); setTimeout(close, 300); };
  77. return html`
  78. <div aria-live="assertive" class="z-10 pointer-events-none absolute inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
  79. <div class="flex w-full flex-col items-center space-y-4 sm:items-end">
  80. <div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transform ease-out duration-300 transition ${tr}">
  81. <div class="p-4">
  82. <div class="flex items-start">
  83. <div class="flex-shrink-0">
  84. <${ok ? Icons.ok : Icons.fail} class="h-6 w-6 ${ok ? 'text-green-400' : 'text-red-400'}" />
  85. <//>
  86. <div class="ml-3 w-0 flex-1 pt-0.5">
  87. <p class="text-sm font-medium text-gray-900">${text}</p>
  88. <p class="hidden mt-1 text-sm text-gray-500">Anyone with a link can now view this file.</p>
  89. <//>
  90. <div class="ml-4 flex flex-shrink-0">
  91. <button type="button" ref=${closebtn} onclick=${onclose} class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none">
  92. <span class="sr-only">Close</span>
  93. <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
  94. <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
  95. <//>
  96. <//>
  97. <//>
  98. <//>
  99. <//>
  100. <//>
  101. <//>
  102. <//>`;
  103. };
  104. export function Login({loginFn, logoIcon, title, tipText}) {
  105. const [user, setUser] = useState('');
  106. const [pass, setPass] = useState('');
  107. const onsubmit = function(ev) {
  108. const authhdr = 'Basic ' + btoa(user + ':' + pass);
  109. const headers = {Authorization: authhdr};
  110. return fetch('api/login', {headers}).then(loginFn).finally(r => setPass(''));
  111. };
  112. return html`
  113. <div class="h-full flex items-center justify-center bg-slate-200">
  114. <div class="border rounded bg-white w-96 p-5">
  115. <div class="my-5 py-2 flex items-center justify-center gap-x-4">
  116. <${logoIcon} class="h-12 stroke-cyan-600 stroke-1" />
  117. <h1 class="font-bold text-xl">${title || 'Login'}<//>
  118. <//>
  119. <div class="my-3">
  120. <label class="block text-sm mb-1 dark:text-white">Username</label>
  121. <input type="text" autocomplete="current-user" required
  122. class="font-normal bg-white rounded border border-gray-300 w-full
  123. flex-1 py-0.5 px-2 text-gray-900 placeholder:text-gray-400
  124. focus:outline-none sm:text-sm sm:leading-6 disabled:cursor-not-allowed
  125. disabled:bg-gray-100 disabled:text-gray-500"
  126. oninput=${ev => setUser(ev.target.value)} value=${user} />
  127. <//>
  128. <div class="my-3">
  129. <label class="block text-sm mb-1 dark:text-white">Password</label>
  130. <input type="password" autocomplete="current-password" required
  131. class="font-normal bg-white rounded border border-gray-300 w-full flex-1 py-0.5 px-2 text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500"
  132. oninput=${ev => setPass(ev.target.value)}
  133. value=${pass} onchange=${onsubmit} />
  134. <//>
  135. <div class="mt-7">
  136. <${Button} title="Sign In" icon=${Icons.logout} onclick=${onsubmit} cls="flex w-full justify-center" />
  137. <//>
  138. <div class="mt-5 text-slate-400 text-xs">${tipText}<//>
  139. <//>
  140. <//>`;
  141. };
  142. export function Colored({icon, text, colors}) {
  143. colors ||= 'bg-slate-100 text-slate-900';
  144. return html`
  145. <span class="inline-flex items-center gap-1.5 py-0.5">
  146. ${icon && html`<${icon} class="w-5 h-5" />`}
  147. <span class="inline-block font-medium rounded-md px-2 py-1 text-xs ring-1 ring-inset ${colors}">${text}<//>
  148. <//>`;
  149. };
  150. export function Stat({title, text, tipText, tipIcon, tipColors, colors}) {
  151. return html`
  152. <div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-slate-900 dark:border-gray-800">
  153. <div class="overflow-auto rounded-lg bg-white px-4 py-2 ">
  154. <div class="flex items-center gap-x-2">
  155. <p class="text-sm truncate text-gray-500 font-medium"> ${title} </p>
  156. <//>
  157. <div class="mt-1 flex items-center gap-x-2">
  158. <h3 class="text-xl truncate font-semibold tracking-tight ${colors || 'text-gray-800 dark:text-gray-200'}">
  159. ${text}
  160. <//>
  161. <span class="flex items-center ${tipText || 'hidden'}">
  162. <${Colored} text=${tipText} icon=${tipIcon} colors=${tipColors} />
  163. <//>
  164. <//>
  165. <//>
  166. <//>`;
  167. };
  168. export function TextValue({value, setfn, disabled, placeholder, type, addonRight, addonLeft, attr, min, max, step, mult}) {
  169. const [bg, setBg] = useState('bg-white');
  170. useEffect(() => { if (type == 'number') checkval(+min, +max, +value); }, []);
  171. step ||= '1', mult ||= 1;
  172. const checkval = function(min, max, v) {
  173. setBg('bg-white');
  174. if (min && v < min) setBg('bg-red-100 border-red-200');
  175. if (max && v > max) setBg('bg-red-100 border-red-200');
  176. };
  177. const m = step.match(/^.+\.(.+)/);
  178. const digits = m ? m[1].length : 0;
  179. const onchange = ev => {
  180. let v = ev.target.value;
  181. if (type == 'number') {
  182. checkval(+min, +max, +v);
  183. v = +(parseFloat(v) / mult).toFixed(digits);
  184. }
  185. setfn(v);
  186. };
  187. if (type == 'number') value = +(value * mult).toFixed(digits);
  188. return html`
  189. <div class="flex w-full items-center rounded border shadow-sm ${bg}">
  190. ${addonLeft && html`<span class="inline-flex font-normal truncate py-1 border-r bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs">${addonLeft}<//>` }
  191. <input type=${type || 'text'} disabled=${disabled} value=${value}
  192. step=${step} min=${min} max=${max}
  193. onchange=${onchange} ...${attr}
  194. class="${bg} font-normal text-sm rounded w-full flex-1 py-0.5 px-2 text-gray-700 placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500" placeholder=${placeholder} />
  195. ${addonRight && html`<span class="inline-flex font-normal truncate py-1 border-l bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs overflow-scroll" style="min-width: 50%;">${addonRight}<//>` }
  196. <//>`;
  197. };
  198. export function SelectValue({value, setfn, options, disabled}) {
  199. const toInt = x => x == parseInt(x) ? parseInt(x) : x;
  200. const onchange = ev => setfn(toInt(ev.target.value));
  201. return html`
  202. <select onchange=${onchange} class="w-full rounded font-normal border py-0.5 px-1 text-gray-600 focus:outline-none text-sm disabled:cursor-not-allowed" disabled=${disabled}>
  203. ${options.map(v => html`<option value=${v[0]} selected=${v[0] == value}>${v[1]}<//>`) }
  204. <//>`;
  205. };
  206. export function SwitchValue({value, setfn}) {
  207. const onclick = ev => setfn(!value);
  208. const bg = !!value ? 'bg-blue-600' : 'bg-gray-200';
  209. const tr = !!value ? 'translate-x-5' : 'translate-x-0';
  210. return html`
  211. <button type="button" onclick=${onclick} class="${bg} inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-0 ring-0" role="switch" aria-checked=${!!value}>
  212. <span aria-hidden="true" class="${tr} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 focus:ring-0 transition duration-200 ease-in-out"></span>
  213. </button>`;
  214. };
  215. export function Setting(props) {
  216. let input = TextValue;
  217. if (props.type == 'switch') input = SwitchValue;
  218. if (props.type == 'select') input = SelectValue;
  219. return html`
  220. <div class=${props.cls || 'grid grid-cols-2 gap-2 my-1'}>
  221. <label class="flex items-center text-sm text-gray-700 mr-2 font-medium ${props.title || 'hidden'}">${props.title}<//>
  222. <div class="flex items-center">${h(input, props)}<//>
  223. <//>`;
  224. };
  225. export function Pagination({ totalItems, itemsPerPage, currentPage, setPageFn, colors }) {
  226. const totalPages = Math.ceil(totalItems / itemsPerPage);
  227. const maxPageRange = 2;
  228. const lessThanSymbol = "<";
  229. const greaterThanSymbol = ">";
  230. const whiteSpace = " ";
  231. const itemcls = 'relative inline-flex items-center px-3 py-1 text-sm focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-blue-600';
  232. colors ||= 'bg-blue-600';
  233. const PageItem = ({ page, isActive }) => (
  234. html`<a
  235. onClick=${() => setPageFn(page)}
  236. class="${itemcls} ${isActive ? `${colors} text-white` : 'cursor-pointer text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50'}"
  237. >
  238. ${page}
  239. </a>`
  240. );
  241. return html`
  242. <div class="flex items-center justify-between bg-white px-3 py-2">
  243. <div class="sm:flex sm:flex-1 sm:items-center sm:justify-between space-x-4 whitespace-nowrap select-none">
  244. <p class="text-sm text-slate-500 font-medium">
  245. showing <span class="font-bold text-slate-700">${(currentPage - 1) * itemsPerPage + 1}</span> - <span class="font-medium">${Math.min(currentPage * itemsPerPage, totalItems)}</span> of ${whiteSpace}
  246. <span class="font-bold text-slate-700">${totalItems}</span> results
  247. </p>
  248. <div>
  249. <nav class="isolate inline-flex -space-x-px rounded-md" aria-label="Pagination">
  250. <a
  251. onClick=${() => setPageFn(Math.max(currentPage - 1, 1))}
  252. class="relative inline-flex px-3 items-center text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${currentPage != 1 ? 'cursor-pointer' : ''} focus:z-20 focus:outline-offset-0">
  253. ${lessThanSymbol}
  254. </a>
  255. <${PageItem} page=${1} isActive=${currentPage === 1} />
  256. ${currentPage > maxPageRange + 2 ? html`<span class="${itemcls} ring-1 ring-inset ring-gray-300 text-slate-300">...</span>` : ''}
  257. ${Array.from({length: Math.min(totalPages, maxPageRange * 2 + 1)}, (_, i) => Math.max(2, currentPage - maxPageRange) + i).map(page => page > 1 && page < totalPages && html`<${PageItem} page=${page} isActive=${currentPage === page} />`)}
  258. ${currentPage < totalPages - (maxPageRange + 1) ? html`<span class="${itemcls} ring-1 ring-inset ring-gray-300 text-slate-300">...</span>` : ''}
  259. ${totalPages > 1 ? html`<${PageItem} page=${totalPages} isActive=${currentPage === totalPages} />` : ''}
  260. <a
  261. onClick=${() => setPageFn(Math.min(currentPage + 1, totalPages))}
  262. class="relative inline-flex px-3 items-center text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${currentPage != totalPages ? 'cursor-pointer' : ''} focus:z-20 focus:outline-offset-0">
  263. ${greaterThanSymbol}
  264. </a>
  265. </nav>
  266. </div>
  267. </div>
  268. </div>`;
  269. };
  270. export function UploadFileButton(props) {
  271. const [upload, setUpload] = useState(null); // Upload promise
  272. const [status, setStatus] = useState(''); // Current upload status
  273. const btn = useRef(null);
  274. const input = useRef(null);
  275. // Send a large file chunk by chunk
  276. const sendFileData = function(url, fileName, fileData, chunkSize) {
  277. return new Promise(function(resolve, reject) {
  278. const finish = ok => {
  279. setUpload(null);
  280. const res = props.onupload ? props.onupload(ok, fileName, fileData.length) : null;
  281. if (res && typeof (res.catch) === 'function') {
  282. res.catch(() => false).then(() => ok ? resolve() : reject());
  283. } else {
  284. ok ? resolve() : reject();
  285. }
  286. };
  287. const sendChunk = function(offset) {
  288. var chunk = fileData.subarray(offset, offset + chunkSize) || '';
  289. var opts = {method: 'POST', body: chunk};
  290. var fullUrl = url + '?offset=' + offset +
  291. '&total=' + fileData.length +
  292. '&name=' + encodeURIComponent(fileName);
  293. var ok;
  294. setStatus('Uploading ' + fileName + ', bytes ' + offset + '..' +
  295. (offset + chunk.length) + ' of ' + fileData.length);
  296. fetch(fullUrl, opts)
  297. .then(function(res) {
  298. if (res.ok && chunk.length > 0) sendChunk(offset + chunk.length);
  299. ok = res.ok;
  300. return res.text();
  301. })
  302. .then(function(text) {
  303. if (!ok) setStatus('Error: ' + text), finish(ok); // Fail
  304. if (chunk.length > 0) return; // More chunks to send
  305. setStatus(x => x + '. Done, resetting device...');
  306. finish(ok); // All chunks sent
  307. });
  308. };
  309. sendChunk(0);
  310. });
  311. };
  312. const onchange = function(ev) {
  313. if (!ev.target.files[0]) return;
  314. let r = new FileReader(), f = ev.target.files[0];
  315. r.readAsArrayBuffer(f);
  316. r.onload = function() {
  317. setUpload(sendFileData(props.url, f.name, new Uint8Array(r.result), 2048));
  318. ev.target.value = '';
  319. ev.preventDefault();
  320. btn && btn.current.base.click();
  321. };
  322. };
  323. const onclick = function(ev) {
  324. let fn; setUpload(x => fn = x);
  325. if (!fn) input.current.click(); // No upload in progress, show file dialog
  326. return fn;
  327. };
  328. return html`
  329. <div class="inline-flex flex-col ${props.class}">
  330. <input class="hidden" type="file" ref=${input} onchange=${onchange} accept=${props.accept} />
  331. <${Button} title=${props.title} icon=${Icons.download} onclick=${onclick} ref=${btn} colors=${props.colors} />
  332. <div class="pt-2 text-sm text-slate-400 ${status || 'hidden'}">${status}<//>
  333. <//>`;
  334. };