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,34 +16,34 @@ 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>
<!-- 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> </div>
<ControlPanel /> <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> </main>
</div> </div>
@@ -79,35 +79,47 @@ import ControlPanel from "../components/ControlPanel.astro";
} }
// ============= Initialize ============= // ============= 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 // Feature Flag: Desktop Only
window.__ASCII_APP__ = { const isDesktop = window.matchMedia("(min-width: 1601px)").matches;
controller,
queue,
ui,
dispose: () => {
controller.dispose();
ui.dispose();
queue.dispose();
window.__ASCII_APP__ = undefined;
},
};
// Link settings updates to UI sync // Use 'let' so we can conditionally assign
controller.onSettingsChanged(() => ui.updateUI()); 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; 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 =============
ui.init(); if (isDesktop && ui) {
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 */ /* 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 { :global(.control-panel.visible) {
flex-grow: 1; transform: translateY(0);
min-height: 0; opacity: 1;
height: 0; /* Force flex-grow to determine height */
} }
} }
</style> </style>