projects.astro 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. ---
  2. import scriptIcon from "../assets/script.png";
  3. import Win98Window from "./win98window.astro"
  4. let projects = Astro.props.projects;
  5. ---
  6. <Win98Window
  7. title="Projects "
  8. layout={{
  9. mobile: { left: '3%', top: '10%', width: '94%', height: '78vh' },
  10. desktop: { left: '10%', top: '10%', width: '80%', height: '75vh' }
  11. }}
  12. >
  13. <div id="inner">
  14. <div class="explorer-toolbar" aria-hidden="true">
  15. <span class="explorer-path">C:\Users\simon\Projects</span>
  16. </div>
  17. <div class="explorer-files">
  18. <div id="projects-container">
  19. <div class="projects-grid" >
  20. {projects.map((item) => (
  21. <button
  22. class="project"
  23. id={`${item.id}item`}
  24. type="button"
  25. data-project={item.id}
  26. aria-label={`Open ${item.title}`}
  27. >
  28. <span class="project-icon" aria-hidden="true"><img src={scriptIcon.src} class="w-auto h-8" alt="" /></span>
  29. <span class="project-title">{item.title}</span>
  30. <span class="project-meta" aria-hidden="true">Application</span>
  31. </button>
  32. ))}
  33. </div>
  34. <div class="projects-details" aria-live="polite">
  35. <div class="details-header">
  36. <div class="details-title" id="project-details-title">Select a project</div>
  37. <div class="details-actions">
  38. <button class="details-btn" id="project-open" type="button" disabled>Open</button>
  39. <button class="details-btn" id="project-repo" type="button" disabled>Repo</button>
  40. </div>
  41. </div>
  42. <div class="details-body">
  43. <div class="details-blurb" id="project-details-blurb">Click once to preview. Double-click still opens.</div>
  44. <div class="details-framewrap" id="project-details-framewrap" hidden>
  45. <iframe id="project-details-frame" title="Project preview" loading="lazy" referrerpolicy="no-referrer" sandbox="allow-scripts allow-same-origin allow-forms allow-popups"></iframe>
  46. </div>
  47. </div>
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52. </Win98Window>
  53. <script>
  54. (function () {
  55. const root = document.currentScript?.closest('.window') || document;
  56. const container = root.querySelector('#projects-container');
  57. if (!container) return;
  58. const grid = container.querySelector('.projects-grid');
  59. if (!grid) return;
  60. const titleEl = container.querySelector('#project-details-title');
  61. const blurbEl = container.querySelector('#project-details-blurb');
  62. const frameWrap = container.querySelector('#project-details-framewrap');
  63. const frame = container.querySelector('#project-details-frame');
  64. const openBtn = container.querySelector('#project-open');
  65. const repoBtn = container.querySelector('#project-repo');
  66. const projects = {
  67. mikro: {
  68. title: 'Mikro',
  69. previewUrl: 'https://mikro.simo.ng/?embed=1',
  70. repoUrl: 'https://git.simo.ng/simo/Mikro',
  71. blurb: 'Minimal, opinionated RSS reader.',
  72. },
  73. glance: {
  74. title: 'Glance',
  75. previewUrl: 'https://glance.simo.ng',
  76. repoUrl: 'https://git.simo.ng/simo/Glance',
  77. blurb: 'Idle dashboard optimized for E-Ink displays.',
  78. },
  79. mneme: {
  80. title: 'Mneme',
  81. previewUrl: null,
  82. repoUrl: 'https://git.simo.ng/simo/mneme',
  83. blurb: 'Tiny Spotify microservice.',
  84. },
  85. };
  86. function openRedirect(url) {
  87. if (!url) return;
  88. window.open(url, '_blank', 'noopener,noreferrer');
  89. }
  90. function setSelected(projectId) {
  91. const p = projects[projectId];
  92. if (!p) return;
  93. container.querySelectorAll('.project').forEach((el) => {
  94. el.toggleAttribute('data-selected', el.getAttribute('data-project') === projectId);
  95. });
  96. if (titleEl) titleEl.textContent = p.title;
  97. if (blurbEl) blurbEl.textContent = p.blurb || '';
  98. const openUrl = p.previewUrl;
  99. if (openBtn) {
  100. openBtn.disabled = !openUrl;
  101. openBtn.onclick = () => openRedirect(openUrl);
  102. }
  103. if (repoBtn) {
  104. repoBtn.disabled = !p.repoUrl;
  105. repoBtn.onclick = () => openRedirect(p.repoUrl);
  106. }
  107. if (frameWrap && frame) {
  108. if (p.previewUrl) {
  109. frameWrap.hidden = false;
  110. frame.src = p.previewUrl;
  111. } else {
  112. frameWrap.hidden = true;
  113. frame.removeAttribute('src');
  114. }
  115. }
  116. }
  117. // Prevent the homepage window-spawning click handler from running.
  118. container.querySelectorAll('.project').forEach((pbtn) => {
  119. pbtn.addEventListener('click', (e) => {
  120. e.stopImmediatePropagation();
  121. e.stopPropagation();
  122. e.preventDefault();
  123. const id = pbtn.getAttribute('data-project');
  124. if (id) setSelected(id);
  125. });
  126. pbtn.addEventListener('dblclick', (e) => {
  127. e.stopImmediatePropagation();
  128. e.stopPropagation();
  129. e.preventDefault();
  130. const id = pbtn.getAttribute('data-project');
  131. if (!id) return;
  132. const p = projects[id];
  133. if (!p) return;
  134. openRedirect(p.previewUrl || p.repoUrl);
  135. });
  136. });
  137. // Pick first project by default for quick scanning
  138. const first = container.querySelector('.project[data-project]');
  139. if (first) setSelected(first.getAttribute('data-project'));
  140. })();
  141. </script>
  142. <style>
  143. #inner {
  144. height: 100%;
  145. appearance: none;
  146. background-color: #fff;
  147. border-radius: 0;
  148. box-shadow:
  149. inset -1px -1px #fff,
  150. inset 1px 1px grey,
  151. inset -2px -2px #dfdfdf,
  152. inset 2px 2px #0a0a0a;
  153. box-sizing: border-box;
  154. padding: 0;
  155. overflow: hidden;
  156. }
  157. .explorer-toolbar {
  158. position: sticky;
  159. top: 0;
  160. z-index: 1;
  161. display: flex;
  162. align-items: center;
  163. gap: 6px;
  164. padding: 10px 10px;
  165. background: #d4d0c8;
  166. border-bottom: 1px solid #808080;
  167. box-shadow: inset 0 1px #fff;
  168. }
  169. .explorer-path {
  170. display: block;
  171. flex: 1;
  172. padding: 8px 12px;
  173. font-size: 14px;
  174. background: #fff;
  175. box-shadow:
  176. inset -1px -1px #fff,
  177. inset 1px 1px grey,
  178. inset -2px -2px #dfdfdf,
  179. inset 2px 2px #0a0a0a;
  180. white-space: nowrap;
  181. overflow: hidden;
  182. text-overflow: ellipsis;
  183. min-height: 30px;
  184. }
  185. /* File area: add a little inner margin like Explorer */
  186. .explorer-files {
  187. padding: 6px;
  188. height: calc(100% - 44px);
  189. box-sizing: border-box;
  190. }
  191. #projects {
  192. animation: createBox 0.2s;
  193. }
  194. @keyframes createBox {
  195. from {
  196. transform: scale(0);
  197. }
  198. to {
  199. transform: scale(1);
  200. }
  201. }
  202. .project {
  203. user-select: none;
  204. box-sizing: border-box;
  205. width: 100%;
  206. max-width: 100%;
  207. padding: 4px 6px;
  208. gap: 8px;
  209. border: 1px solid transparent;
  210. }
  211. .project h3 {
  212. min-width: 0;
  213. flex: 1;
  214. }
  215. .project:focus {
  216. outline: 1px dotted #000;
  217. outline-offset: -3px;
  218. border-color: #000;
  219. background: #000080;
  220. color: #fff;
  221. }
  222. #projects-container {
  223. display: grid;
  224. grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  225. grid-template-rows: auto 1fr;
  226. grid-template-areas:
  227. "status details"
  228. "grid details";
  229. gap: 8px;
  230. min-height: 0;
  231. height: 100%;
  232. }
  233. .projects-grid {
  234. grid-area: grid;
  235. min-height: 0;
  236. }
  237. .projects-details {
  238. grid-area: details;
  239. min-height: 0;
  240. }
  241. /* Ensure the scroll area never spills past the inner frame */
  242. #projects-container {
  243. padding-right: 6px;
  244. margin-right: 2px;
  245. }
  246. #projects-container .project:hover {
  247. background: #e6e6e6;
  248. border-color: #808080;
  249. }
  250. .projects-status {
  251. display: flex;
  252. align-items: center;
  253. gap: 6px;
  254. padding: 6px 8px;
  255. margin: 6px;
  256. background: #efefef;
  257. border: 1px solid #808080;
  258. box-shadow: inset 0 1px #fff;
  259. font-size: 12px;
  260. }
  261. .projects-grid {
  262. height: 100%;
  263. overflow: auto;
  264. padding: 6px;
  265. margin: 0 6px 6px;
  266. background: #fff;
  267. box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf,
  268. inset 2px 2px #0a0a0a;
  269. }
  270. .projects-grid {
  271. display: flex;
  272. flex-direction: column;
  273. gap: 2px;
  274. }
  275. .project {
  276. text-align: left;
  277. background: transparent;
  278. }
  279. .projects-grid .project {
  280. display: grid;
  281. grid-template-columns: 34px 1fr auto;
  282. align-items: center;
  283. gap: 8px;
  284. padding: 6px 8px;
  285. border: 1px solid transparent;
  286. }
  287. .project-icon {
  288. display: inline-flex;
  289. align-items: center;
  290. justify-content: center;
  291. }
  292. .project-title {
  293. font-weight: 700;
  294. white-space: nowrap;
  295. overflow: hidden;
  296. text-overflow: ellipsis;
  297. }
  298. .project-meta {
  299. font-size: 11px;
  300. color: #222;
  301. padding: 2px 6px;
  302. border: 1px solid #808080;
  303. background: #efefef;
  304. box-shadow: inset 0 1px #fff;
  305. white-space: nowrap;
  306. }
  307. .projects-grid .project:hover {
  308. background: #e6e6e6;
  309. border-color: #808080;
  310. }
  311. .projects-grid .project:focus {
  312. outline: 1px dotted #000;
  313. outline-offset: -3px;
  314. border-color: #000;
  315. background: #000080;
  316. color: #fff;
  317. }
  318. #projects-container {
  319. display: grid;
  320. grid-template-columns: 1fr 1.25fr;
  321. gap: 8px;
  322. height: calc(100% - 0px);
  323. }
  324. .projects-details {
  325. display: flex;
  326. flex-direction: column;
  327. margin: 6px 6px 6px 0;
  328. background: #fff;
  329. box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf,
  330. inset 2px 2px #0a0a0a;
  331. min-width: 0;
  332. }
  333. .details-header {
  334. display: flex;
  335. align-items: center;
  336. justify-content: space-between;
  337. gap: 8px;
  338. padding: 8px 10px;
  339. background: #d4d0c8;
  340. border-bottom: 1px solid #808080;
  341. box-shadow: inset 0 1px #fff;
  342. }
  343. .details-title {
  344. font-weight: 700;
  345. white-space: nowrap;
  346. overflow: hidden;
  347. text-overflow: ellipsis;
  348. }
  349. .details-actions {
  350. display: flex;
  351. gap: 6px;
  352. flex-shrink: 0;
  353. }
  354. .details-btn {
  355. appearance: none;
  356. font-family: inherit;
  357. font-size: 12px;
  358. padding: 2px 10px;
  359. background: #d4d0c8;
  360. border: 1px solid #000;
  361. box-shadow: inset 1px 1px #fff, inset -1px -1px #808080;
  362. cursor: pointer;
  363. }
  364. .details-btn:disabled {
  365. opacity: 0.6;
  366. cursor: not-allowed;
  367. }
  368. .details-body {
  369. height: 100%;
  370. padding: 10px;
  371. overflow: hidden;
  372. display: flex;
  373. flex-direction: column;
  374. gap: 10px;
  375. min-height: 0;
  376. }
  377. .details-blurb {
  378. font-size: 12px;
  379. line-height: 1.3;
  380. color: #222;
  381. border: 1px solid #000;
  382. background: #ffffe1;
  383. box-shadow: 2px 2px 0 #000;
  384. padding: 8px 10px;
  385. }
  386. .details-framewrap {
  387. flex: 1;
  388. min-height: 0;
  389. border: 1px solid #000;
  390. box-shadow: 2px 2px 0 #000;
  391. background: #fff;
  392. overflow: hidden;
  393. }
  394. #project-details-frame {
  395. width: 100%;
  396. height: 100%;
  397. border: 0;
  398. background: #fff;
  399. }
  400. .projects-grid {
  401. margin: 6px 0 6px 6px;
  402. }
  403. .projects-grid .project[data-selected] {
  404. outline: 1px dotted #000;
  405. outline-offset: -3px;
  406. border-color: #000;
  407. background: #000080;
  408. color: #fff;
  409. }
  410. @media (max-width: 510px) {
  411. #projects-container {
  412. grid-template-columns: 1fr;
  413. grid-template-rows: auto 1fr;
  414. grid-template-areas:
  415. "grid"
  416. "details";
  417. height: 100%;
  418. }
  419. .projects-grid {
  420. height: auto;
  421. margin: 6px;
  422. padding: 8px;
  423. overflow-x: auto;
  424. overflow-y: hidden;
  425. display: flex;
  426. flex-direction: row;
  427. gap: 8px;
  428. scroll-snap-type: x mandatory;
  429. -webkit-overflow-scrolling: touch;
  430. }
  431. .projects-grid .project {
  432. flex: 0 0 auto;
  433. min-width: 170px;
  434. max-width: 220px;
  435. grid-template-columns: 34px 1fr;
  436. grid-template-rows: auto auto;
  437. align-items: center;
  438. }
  439. .projects-grid .project-meta {
  440. display: none;
  441. }
  442. .projects-grid .project {
  443. scroll-snap-align: start;
  444. border: 1px solid #000;
  445. box-shadow: 2px 2px 0 #000;
  446. background: linear-gradient(180deg, #fff, #f3f3f3);
  447. }
  448. .projects-details {
  449. margin: 0 6px 6px;
  450. }
  451. }
  452. </style>