feat: Implement desktop-only mode for the ASCII art generator and sidebar, update breakpoints to 1600px, and simplify the blog page layout with a new back link.

This commit is contained in:
syntaxbullet
2026-02-11 14:41:47 +01:00
parent f4a0e2a82b
commit a79f05c043
5 changed files with 129 additions and 135 deletions

View File

@@ -586,7 +586,7 @@ import {
// --- State Management --- // --- State Management ---
const checkMode = () => { const checkMode = () => {
// Match CSS breakpoint // Match CSS breakpoint
const isMobile = window.innerWidth <= 1024; const isMobile = window.innerWidth <= 1600;
if (isMobile && !isMobileMode) { if (isMobile && !isMobileMode) {
// Entering Mobile // Entering Mobile
@@ -891,7 +891,7 @@ import {
} }
/* --- MOBILE STYLES --- */ /* --- MOBILE STYLES --- */
@media (max-width: 1024px) { @media (max-width: 1600px) {
.control-panel { .control-panel {
padding: 0; padding: 0;
position: fixed; position: fixed;

View File

@@ -1,12 +1,8 @@
--- ---
import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro"; import { Zap, FileText, Mail } from "@lucide/astro";
--- ---
<aside class="sidebar"> <aside class="sidebar">
<div class="mobile-header">
<span class="mobile-brand">SYNTAXBULLET</span>
<ChevronDown class="mobile-toggle-icon" size={24} />
</div>
<div class="sidebar-content"> <div class="sidebar-content">
<div class="brand-group"> <div class="brand-group">
<a href="/" class="brand-link"> <a href="/" class="brand-link">
@@ -25,7 +21,7 @@ import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro";
</p> </p>
<div class="sidebar-actions"> <div class="sidebar-actions">
<a href="/" class="sidebar-link"> <a href="/" class="sidebar-link desktop-only">
<span class="icon"><Zap size={20} /></span> GENERATE <span class="icon"><Zap size={20} /></span> GENERATE
</a> </a>
<a href="/blog" class="sidebar-link"> <a href="/blog" class="sidebar-link">
@@ -206,59 +202,31 @@ import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro";
} }
/* Responsive */ /* Responsive */
@media (max-width: 1024px) { @media (max-width: 1600px) {
.sidebar { .sidebar {
width: 100%; width: 100%;
height: 100%;
max-width: none; max-width: none;
min-width: 0; min-width: 0;
border-right: none; border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: none;
padding: 0; /* Remove padding from container, move to toggle */ padding: 0;
align-items: center; /* Center horizontally */
} }
.mobile-header { .mobile-header {
display: flex; /* Show on mobile */ display: none;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
cursor: pointer;
background: #000;
}
.mobile-brand {
font-size: 1.25rem;
font-weight: 900;
color: #fff;
letter-spacing: -0.5px;
}
.mobile-toggle-icon {
color: rgba(255, 255, 255, 0.5);
transition: transform 0.3s ease;
}
.sidebar.collapsed .mobile-toggle-icon {
transform: rotate(-90deg);
} }
.sidebar-content { .sidebar-content {
padding: 0 3rem 3rem 3rem; /* Adjust padding */ padding: 2rem;
align-items: center; align-items: center;
text-align: center; text-align: center;
overflow: hidden; width: 100%;
transition: display: flex;
max-height 0.4s ease, flex-direction: column;
opacity 0.4s ease, justify-content: center;
padding 0.4s ease; height: 100%;
max-height: 1000px; /* Arbitrary large height */
opacity: 1;
}
.sidebar.collapsed .sidebar-content {
max-height: 0;
opacity: 0;
padding: 0 3rem; /* Collapse padding */
pointer-events: none;
} }
.brand-subtitle, .brand-subtitle,
@@ -267,28 +235,12 @@ import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro";
align-items: center; align-items: center;
} }
/* Hide the large title in the content on mobile since we have the header, .desktop-only {
or keep it? The user might want the full bio etc. display: none !important;
Let's keep the content as is, but maybe hide the "SYNTAXBULLET" big text in content if it's redundant? }
Actually, the design seems to desire the big brand text. I'll leave it. */
} }
</style> </style>
<script> <script>
const sidebar = document.querySelector(".sidebar"); // No script needed for sidebar anymore as it is always visible and static
const toggleBtn = document.querySelector(".mobile-header");
if (sidebar && toggleBtn) {
// Default to collapsed on mobile initially?
// User didn't specify, but usually "collapsible" implies starting collapsed or having the ability.
// Let's start expanded or collapsed? "Make... collapsible".
// I'll default to collapsed to save space as requested "mobile friendly".
if (window.innerWidth <= 1024) {
sidebar.classList.add("collapsed");
}
toggleBtn.addEventListener("click", () => {
sidebar.classList.toggle("collapsed");
});
}
</script> </script>

View File

@@ -1,7 +1,6 @@
--- ---
import { getEntry } from "astro:content"; import { getEntry } from "astro:content";
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import Sidebar from "../../components/Sidebar.astro";
const { slug } = Astro.params; const { slug } = Astro.params;
if (!slug) { if (!slug) {
@@ -19,8 +18,6 @@ const { Content } = await entry.render();
<Layout title={entry.data.title}> <Layout title={entry.data.title}>
<div class="split-layout"> <div class="split-layout">
<Sidebar />
<main class="content-workspace"> <main class="content-workspace">
<div class="content-container"> <div class="content-container">
<article class="h-entry"> <article class="h-entry">
@@ -76,7 +73,6 @@ const { Content } = await entry.render();
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #050505;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
} }

View File

@@ -1,6 +1,5 @@
--- ---
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import Sidebar from "../../components/Sidebar.astro";
import { getCollection, type CollectionEntry } from "astro:content"; import { getCollection, type CollectionEntry } from "astro:content";
const posts = (await getCollection("blog")).sort( const posts = (await getCollection("blog")).sort(
@@ -11,11 +10,10 @@ const posts = (await getCollection("blog")).sort(
<Layout title="System Logs"> <Layout title="System Logs">
<div class="split-layout"> <div class="split-layout">
<Sidebar />
<main class="content-workspace"> <main class="content-workspace">
<div class="content-container"> <div class="content-container">
<header class="page-header"> <header class="page-header">
<a href="/" class="back-link"> &larr; Back to Home </a>
<h1>Blog Articles</h1> <h1>Blog Articles</h1>
<div class="divider"></div> <div class="divider"></div>
</header> </header>
@@ -74,7 +72,6 @@ const posts = (await getCollection("blog")).sort(
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #050505;
overflow-y: auto; /* Enable scrolling for content */ overflow-y: auto; /* Enable scrolling for content */
overflow-x: hidden; overflow-x: hidden;
} }
@@ -85,6 +82,9 @@ const posts = (await getCollection("blog")).sort(
margin: 0 auto; margin: 0 auto;
padding: 4rem 2rem; padding: 4rem 2rem;
box-sizing: border-box; box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100%;
} }
/* Header Styling */ /* Header Styling */
@@ -92,6 +92,23 @@ const posts = (await getCollection("blog")).sort(
margin-bottom: 3rem; margin-bottom: 3rem;
} }
.back-link {
display: inline-block;
margin-bottom: 1.5rem;
font-family:
system-ui,
-apple-system,
sans-serif;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.6);
text-decoration: none;
transition: color 0.2s;
}
.back-link:hover {
color: #fff;
}
h1 { h1 {
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 700; font-weight: 700;
@@ -114,6 +131,7 @@ const posts = (await getCollection("blog")).sort(
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
flex-grow: 1;
} }
.post-link { .post-link {

View File

@@ -16,15 +16,18 @@ import ControlPanel from "../components/ControlPanel.astro";
<div id="loading">Loading...</div> <div id="loading">Loading...</div>
<pre id="ascii-result"></pre> <pre id="ascii-result"></pre>
<canvas id="ascii-canvas"></canvas> <canvas id="ascii-canvas"></canvas>
</div>
<ControlPanel />
<!-- Landing Screen --> <!-- Landing Screen -->
<div id="landing-screen" class="landing-overlay"> <div id="landing-screen" class="landing-overlay">
<div class="landing-content"> <div class="landing-content">
<h1>ASCII Art Generator</h1> <h1>ASCII Art Generator</h1>
<p> <p>
Generate stunning ASCII art from images. Pull a Generate stunning ASCII art from images. Pull a random
random image from an anime API or upload your own to image from an anime API or upload your own to get
get started. started.
</p> </p>
<div class="landing-buttons"> <div class="landing-buttons">
<button id="btn-start-api" class="landing-btn" <button id="btn-start-api" class="landing-btn"
@@ -35,15 +38,12 @@ import ControlPanel from "../components/ControlPanel.astro";
> >
</div> </div>
<p class="disclaimer"> <p class="disclaimer">
<b>Disclaimer:</b> Images loaded via the API are not my <b>Disclaimer:</b> Images loaded via the API are not my own
own and are not filtered or curated. In rare cases, they and are not filtered or curated. In rare cases, they might
might contain sensitive material. contain sensitive material.
</p> </p>
</div> </div>
</div> </div>
</div>
<ControlPanel />
</main> </main>
</div> </div>
@@ -79,35 +79,47 @@ import ControlPanel from "../components/ControlPanel.astro";
} }
// ============= Initialize ============= // ============= Initialize =============
const controller = new AsciiController(
// Feature Flag: Desktop Only
const isDesktop = window.matchMedia("(min-width: 1601px)").matches;
// Use 'let' so we can conditionally assign
let controller: AsciiController | undefined;
let queue: ImageQueue | undefined;
let ui: UIBindings | undefined;
if (isDesktop) {
controller = new AsciiController(
canvas, canvas,
asciiResult, asciiResult,
loadingIndicator, loadingIndicator,
); );
const queue = new ImageQueue(2); queue = new ImageQueue(2);
const ui = new UIBindings(controller, queue, loadNewImage); ui = new UIBindings(controller, queue, loadNewImage);
// Store instances globally for cleanup // Store instances globally for cleanup
window.__ASCII_APP__ = { window.__ASCII_APP__ = {
controller, controller: controller!,
queue, queue: queue!,
ui, ui: ui!,
dispose: () => { dispose: () => {
controller.dispose(); controller?.dispose();
ui.dispose(); ui?.dispose();
queue.dispose(); queue?.dispose();
window.__ASCII_APP__ = undefined; window.__ASCII_APP__ = undefined;
}, },
}; };
// Link settings updates to UI sync // Link settings updates to UI sync
controller.onSettingsChanged(() => ui.updateUI()); controller.onSettingsChanged(() => ui!.updateUI());
}
let retryCount = 0; let retryCount = 0;
const MAX_RETRIES = 3; const MAX_RETRIES = 3;
// ============= Image Loading =============
async function loadNewImage(): Promise<void> { async function loadNewImage(): Promise<void> {
if (!isDesktop || !controller || !queue || !ui) return;
try { try {
let item; let item;
@@ -140,7 +152,9 @@ import ControlPanel from "../components/ControlPanel.astro";
} }
// ============= Initialize UI ============= // ============= Initialize UI =============
if (isDesktop && ui) {
ui.init(); ui.init();
}
// ============= Landing Screen Logic ============= // ============= Landing Screen Logic =============
const landingScreen = document.getElementById("landing-screen"); const landingScreen = document.getElementById("landing-screen");
@@ -150,14 +164,19 @@ import ControlPanel from "../components/ControlPanel.astro";
"file-upload", "file-upload",
) as HTMLInputElement; ) as HTMLInputElement;
const controlPanel = document.querySelector(".control-panel");
const hideLanding = () => { const hideLanding = () => {
landingScreen?.classList.add("hidden"); landingScreen?.classList.add("hidden");
if (isDesktop && controlPanel) {
controlPanel.classList.add("visible");
}
}; };
btnStartApi?.addEventListener("click", () => { btnStartApi?.addEventListener("click", () => {
hideLanding(); hideLanding();
loadNewImage().then(() => { loadNewImage().then(() => {
queue.ensureFilled(); queue?.ensureFilled();
}); });
}); });
@@ -348,25 +367,34 @@ import ControlPanel from "../components/ControlPanel.astro";
} }
/* Responsive */ /* Responsive */
@media (max-width: 1024px) { @media (max-width: 1600px) {
.split-layout { .split-layout {
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
height: 100dvh; height: 100dvh;
} }
/* Hide the entire ASCII workspace on mobile/tablet */
.ascii-workspace { .ascii-workspace {
height: 0; /* Important for flex-grow to work reliably on all browsers */ display: none !important;
flex-grow: 1; }
display: flex;
flex-direction: column;
min-height: 0; /* Allow shrinking */
} }
.canvas-layer { /* Animation States for Control Panel (Desktop) */
flex-grow: 1; @media (min-width: 1601px) {
min-height: 0; :global(.control-panel) {
height: 0; /* Force flex-grow to determine height */ /* Participates in flex flow, reserving space so canvas resizes correctly */
width: 100%;
transform: translateY(150%);
opacity: 0;
transition:
transform 0.8s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.6s ease;
}
:global(.control-panel.visible) {
transform: translateY(0);
opacity: 1;
} }
} }
</style> </style>