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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"> ← 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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user