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