|
|
@@ -10,9 +10,8 @@ let projects = Astro.props.projects;
|
|
|
<Win98Window
|
|
|
title="Projects "
|
|
|
layout={{
|
|
|
- mobile: { left: '5%', top: '20%', width: '90%', height: '60vh' },
|
|
|
- // tablet: { left: '30%', top: '20%', width: '50%', height: 'auto' },
|
|
|
- desktop: { left: "25%", top: "20%", width: "50%", height: '60vh' }
|
|
|
+ mobile: { left: '3%', top: '10%', width: '94%', height: '78vh' },
|
|
|
+ desktop: { left: '10%', top: '10%', width: '80%', height: '75vh' }
|
|
|
}}
|
|
|
>
|
|
|
<div id="inner">
|
|
|
@@ -20,31 +19,144 @@ let projects = Astro.props.projects;
|
|
|
<span class="explorer-path">C:\Users\simon\Projects</span>
|
|
|
</div>
|
|
|
<div class="explorer-files">
|
|
|
- <div id="projects-container">
|
|
|
- {
|
|
|
- projects.map((item) => (
|
|
|
- <div
|
|
|
- class="project"
|
|
|
- id={`${item.id}item`}
|
|
|
- tabindex="0"
|
|
|
- style="display: flex; flex-direction: row; align-items: center; justify-content: flex-start; margin: 0px;"
|
|
|
- >
|
|
|
- <img src={scriptIcon.src} class="w-auto h-8" alt="" />
|
|
|
- <h3
|
|
|
- class="text-lg"
|
|
|
- style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left;"
|
|
|
- >
|
|
|
- {item.title}
|
|
|
- </h3>
|
|
|
+ <div id="projects-container">
|
|
|
+ <div class="projects-grid" >
|
|
|
+ {projects.map((item) => (
|
|
|
+ <button
|
|
|
+ class="project"
|
|
|
+ id={`${item.id}item`}
|
|
|
+ type="button"
|
|
|
+ data-project={item.id}
|
|
|
+ aria-label={`Open ${item.title}`}
|
|
|
+ >
|
|
|
+ <span class="project-icon" aria-hidden="true"><img src={scriptIcon.src} class="w-auto h-8" alt="" /></span>
|
|
|
+ <span class="project-title">{item.title}</span>
|
|
|
+ <span class="project-meta" aria-hidden="true">Application</span>
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
</div>
|
|
|
- ))
|
|
|
- }
|
|
|
-
|
|
|
- </div>
|
|
|
+ <div class="projects-details" aria-live="polite">
|
|
|
+ <div class="details-header">
|
|
|
+ <div class="details-title" id="project-details-title">Select a project</div>
|
|
|
+ <div class="details-actions">
|
|
|
+ <button class="details-btn" id="project-open" type="button" disabled>Open</button>
|
|
|
+ <button class="details-btn" id="project-repo" type="button" disabled>Repo</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="details-body">
|
|
|
+ <div class="details-blurb" id="project-details-blurb">Click once to preview. Double-click still opens.</div>
|
|
|
+ <div class="details-framewrap" id="project-details-framewrap" hidden>
|
|
|
+ <iframe id="project-details-frame" title="Project preview" loading="lazy" referrerpolicy="no-referrer" sandbox="allow-scripts allow-same-origin allow-forms allow-popups"></iframe>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</Win98Window>
|
|
|
|
|
|
+
|
|
|
+<script>
|
|
|
+ (function () {
|
|
|
+ const root = document.currentScript?.closest('.window') || document;
|
|
|
+ const container = root.querySelector('#projects-container');
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ const grid = container.querySelector('.projects-grid');
|
|
|
+ if (!grid) return;
|
|
|
+
|
|
|
+ const titleEl = container.querySelector('#project-details-title');
|
|
|
+ const blurbEl = container.querySelector('#project-details-blurb');
|
|
|
+ const frameWrap = container.querySelector('#project-details-framewrap');
|
|
|
+ const frame = container.querySelector('#project-details-frame');
|
|
|
+ const openBtn = container.querySelector('#project-open');
|
|
|
+ const repoBtn = container.querySelector('#project-repo');
|
|
|
+
|
|
|
+ const projects = {
|
|
|
+ mikro: {
|
|
|
+ title: 'Mikro',
|
|
|
+ previewUrl: 'https://mikro.simo.ng',
|
|
|
+ repoUrl: 'https://git.simo.ng/simo/Mikro',
|
|
|
+ blurb: 'Minimal, opinionated RSS reader.',
|
|
|
+ },
|
|
|
+ glance: {
|
|
|
+ title: 'Glance',
|
|
|
+ previewUrl: 'https://glance.simo.ng',
|
|
|
+ repoUrl: 'https://git.simo.ng/simo/Glance',
|
|
|
+ blurb: 'Idle dashboard optimized for E-Ink displays.',
|
|
|
+ },
|
|
|
+ mneme: {
|
|
|
+ title: 'Mneme',
|
|
|
+ previewUrl: null,
|
|
|
+ repoUrl: 'https://git.simo.ng/simo/mneme',
|
|
|
+ blurb: 'Tiny Spotify microservice.',
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ function openRedirect(url) {
|
|
|
+ if (!url) return;
|
|
|
+ window.open(url, '_blank', 'noopener,noreferrer');
|
|
|
+ }
|
|
|
+
|
|
|
+ function setSelected(projectId) {
|
|
|
+ const p = projects[projectId];
|
|
|
+ if (!p) return;
|
|
|
+
|
|
|
+ container.querySelectorAll('.project').forEach((el) => {
|
|
|
+ el.toggleAttribute('data-selected', el.getAttribute('data-project') === projectId);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (titleEl) titleEl.textContent = p.title;
|
|
|
+ if (blurbEl) blurbEl.textContent = p.blurb || '';
|
|
|
+
|
|
|
+ const openUrl = p.previewUrl;
|
|
|
+ if (openBtn) {
|
|
|
+ openBtn.disabled = !openUrl;
|
|
|
+ openBtn.onclick = () => openRedirect(openUrl);
|
|
|
+ }
|
|
|
+ if (repoBtn) {
|
|
|
+ repoBtn.disabled = !p.repoUrl;
|
|
|
+ repoBtn.onclick = () => openRedirect(p.repoUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (frameWrap && frame) {
|
|
|
+ if (p.previewUrl) {
|
|
|
+ frameWrap.hidden = false;
|
|
|
+ frame.src = p.previewUrl;
|
|
|
+ } else {
|
|
|
+ frameWrap.hidden = true;
|
|
|
+ frame.removeAttribute('src');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // Prevent the homepage window-spawning click handler from running.
|
|
|
+ container.querySelectorAll('.project').forEach((pbtn) => {
|
|
|
+ pbtn.addEventListener('click', (e) => {
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ e.stopPropagation();
|
|
|
+ e.preventDefault();
|
|
|
+ const id = pbtn.getAttribute('data-project');
|
|
|
+ if (id) setSelected(id);
|
|
|
+ });
|
|
|
+ pbtn.addEventListener('dblclick', (e) => {
|
|
|
+ e.stopImmediatePropagation();
|
|
|
+ e.stopPropagation();
|
|
|
+ e.preventDefault();
|
|
|
+ const id = pbtn.getAttribute('data-project');
|
|
|
+ if (!id) return;
|
|
|
+ const p = projects[id];
|
|
|
+ if (!p) return;
|
|
|
+ openRedirect(p.previewUrl || p.repoUrl);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // Pick first project by default for quick scanning
|
|
|
+ const first = container.querySelector('.project[data-project]');
|
|
|
+ if (first) setSelected(first.getAttribute('data-project'));
|
|
|
+ })();
|
|
|
+</script>
|
|
|
<style>
|
|
|
#inner {
|
|
|
height: 100%;
|
|
|
@@ -94,6 +206,8 @@ let projects = Astro.props.projects;
|
|
|
/* File area: add a little inner margin like Explorer */
|
|
|
.explorer-files {
|
|
|
padding: 6px;
|
|
|
+ height: calc(100% - 44px);
|
|
|
+ box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -136,17 +250,26 @@ let projects = Astro.props.projects;
|
|
|
}
|
|
|
|
|
|
#projects-container {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 0;
|
|
|
- padding: 0;
|
|
|
- width: 100%;
|
|
|
- max-width: 100%;
|
|
|
- box-sizing: border-box;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
|
|
|
+ grid-template-rows: auto 1fr;
|
|
|
+ grid-template-areas:
|
|
|
+ "status details"
|
|
|
+ "grid details";
|
|
|
+ gap: 8px;
|
|
|
+ min-height: 0;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
|
|
|
- height: calc(100% - 64px);
|
|
|
- overflow-y: auto;
|
|
|
- overflow-x: hidden;
|
|
|
+
|
|
|
+ .projects-grid {
|
|
|
+ grid-area: grid;
|
|
|
+ min-height: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-details {
|
|
|
+ grid-area: details;
|
|
|
+ min-height: 0;
|
|
|
}
|
|
|
|
|
|
/* Ensure the scroll area never spills past the inner frame */
|
|
|
@@ -159,4 +282,242 @@ let projects = Astro.props.projects;
|
|
|
background: #e6e6e6;
|
|
|
border-color: #808080;
|
|
|
}
|
|
|
+
|
|
|
+ .projects-status {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ padding: 6px 8px;
|
|
|
+ margin: 6px;
|
|
|
+ background: #efefef;
|
|
|
+ border: 1px solid #808080;
|
|
|
+ box-shadow: inset 0 1px #fff;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .projects-grid {
|
|
|
+ height: 100%;
|
|
|
+ overflow: auto;
|
|
|
+ padding: 6px;
|
|
|
+ margin: 0 6px 6px;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf,
|
|
|
+ inset 2px 2px #0a0a0a;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 2px;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .project {
|
|
|
+ text-align: left;
|
|
|
+ background: transparent;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid .project {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 34px 1fr auto;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 6px 8px;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .project-icon {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .project-title {
|
|
|
+ font-weight: 700;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .project-meta {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #222;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border: 1px solid #808080;
|
|
|
+ background: #efefef;
|
|
|
+ box-shadow: inset 0 1px #fff;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ .projects-grid .project:hover {
|
|
|
+ background: #e6e6e6;
|
|
|
+ border-color: #808080;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid .project:focus {
|
|
|
+ outline: 1px dotted #000;
|
|
|
+ outline-offset: -3px;
|
|
|
+ border-color: #000;
|
|
|
+ background: #000080;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ #projects-container {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1.25fr;
|
|
|
+ gap: 8px;
|
|
|
+ height: calc(100% - 0px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-details {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ margin: 6px 6px 6px 0;
|
|
|
+ background: #fff;
|
|
|
+ box-shadow: inset -1px -1px #fff, inset 1px 1px grey, inset -2px -2px #dfdfdf,
|
|
|
+ inset 2px 2px #0a0a0a;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .details-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ background: #d4d0c8;
|
|
|
+ border-bottom: 1px solid #808080;
|
|
|
+ box-shadow: inset 0 1px #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .details-title {
|
|
|
+ font-weight: 700;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+
|
|
|
+ .details-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .details-btn {
|
|
|
+ appearance: none;
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 2px 10px;
|
|
|
+ background: #d4d0c8;
|
|
|
+ border: 1px solid #000;
|
|
|
+ box-shadow: inset 1px 1px #fff, inset -1px -1px #808080;
|
|
|
+ cursor: pointer;
|
|
|
+ }
|
|
|
+ .details-btn:disabled {
|
|
|
+ opacity: 0.6;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+
|
|
|
+ .details-body {
|
|
|
+ height: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+ min-height: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .details-blurb {
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.3;
|
|
|
+ color: #222;
|
|
|
+ border: 1px solid #000;
|
|
|
+ background: #ffffe1;
|
|
|
+ box-shadow: 2px 2px 0 #000;
|
|
|
+ padding: 8px 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .details-framewrap {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ border: 1px solid #000;
|
|
|
+ box-shadow: 2px 2px 0 #000;
|
|
|
+ background: #fff;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ #project-details-frame {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border: 0;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid {
|
|
|
+ margin: 6px 0 6px 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid .project[data-selected] {
|
|
|
+ outline: 1px dotted #000;
|
|
|
+ outline-offset: -3px;
|
|
|
+ border-color: #000;
|
|
|
+ background: #000080;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ @media (max-width: 510px) {
|
|
|
+ #projects-container {
|
|
|
+ grid-template-columns: 1fr;
|
|
|
+ grid-template-rows: auto 1fr;
|
|
|
+ grid-template-areas:
|
|
|
+ "grid"
|
|
|
+ "details";
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid {
|
|
|
+ height: auto;
|
|
|
+ margin: 6px;
|
|
|
+ padding: 8px;
|
|
|
+ overflow-x: auto;
|
|
|
+ overflow-y: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ gap: 8px;
|
|
|
+ scroll-snap-type: x mandatory;
|
|
|
+ -webkit-overflow-scrolling: touch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid .project {
|
|
|
+ flex: 0 0 auto;
|
|
|
+ min-width: 170px;
|
|
|
+ max-width: 220px;
|
|
|
+ grid-template-columns: 34px 1fr;
|
|
|
+ grid-template-rows: auto auto;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid .project-meta {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-grid .project {
|
|
|
+ scroll-snap-align: start;
|
|
|
+ border: 1px solid #000;
|
|
|
+ box-shadow: 2px 2px 0 #000;
|
|
|
+ background: linear-gradient(180deg, #fff, #f3f3f3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .projects-details {
|
|
|
+ margin: 0 6px 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
</style>
|