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 ---
const checkMode = () => {
// Match CSS breakpoint
const isMobile = window.innerWidth <= 1024;
const isMobile = window.innerWidth <= 1600;
if (isMobile && !isMobileMode) {
// Entering Mobile
@@ -891,7 +891,7 @@ import {
}
/* --- MOBILE STYLES --- */
@media (max-width: 1024px) {
@media (max-width: 1600px) {
.control-panel {
padding: 0;
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">
<div class="mobile-header">
<span class="mobile-brand">SYNTAXBULLET</span>
<ChevronDown class="mobile-toggle-icon" size={24} />
</div>
<div class="sidebar-content">
<div class="brand-group">
<a href="/" class="brand-link">
@@ -25,7 +21,7 @@ import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro";
</p>
<div class="sidebar-actions">
<a href="/" class="sidebar-link">
<a href="/" class="sidebar-link desktop-only">
<span class="icon"><Zap size={20} /></span> GENERATE
</a>
<a href="/blog" class="sidebar-link">
@@ -206,59 +202,31 @@ import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro";
}
/* Responsive */
@media (max-width: 1024px) {
@media (max-width: 1600px) {
.sidebar {
width: 100%;
height: 100%;
max-width: none;
min-width: 0;
border-right: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding: 0; /* Remove padding from container, move to toggle */
border-bottom: none;
padding: 0;
align-items: center; /* Center horizontally */
}
.mobile-header {
display: flex; /* Show on mobile */
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);
display: none;
}
.sidebar-content {
padding: 0 3rem 3rem 3rem; /* Adjust padding */
padding: 2rem;
align-items: center;
text-align: center;
overflow: hidden;
transition:
max-height 0.4s ease,
opacity 0.4s ease,
padding 0.4s ease;
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;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
.brand-subtitle,
@@ -267,28 +235,12 @@ import { ChevronDown, Zap, FileText, Mail } from "@lucide/astro";
align-items: center;
}
/* Hide the large title in the content on mobile since we have the header,
or keep it? The user might want the full bio etc.
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. */
.desktop-only {
display: none !important;
}
}
</style>
<script>
const sidebar = document.querySelector(".sidebar");
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");
});
}
// No script needed for sidebar anymore as it is always visible and static
</script>

View File

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

View File

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

View File

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