Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 112 additions & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));

:root {
/* Semantic tokens: Green Pixel overrides these without changing classic Print Stream. */
--bg: #eceef0;
--bg-2: #f6f7f8;
--panel: #fbfbfc;
Expand Down Expand Up @@ -30,7 +31,32 @@
--sar: env(safe-area-inset-right, 0px);
}

:root[data-theme="dark"] {
:root[data-visual-theme="green-pixel"] {
--bg: #020503;
--bg-2: #061009;
--panel: #07150b;
--panel-2: #0b2111;
--ink: #c7ffd0;
--ink-2: #83d892;
--muted: #4d9d5b;
--line: #174f25;
--line-2: #28783a;
--cyan: #65ff8a;
--cyan-soft: rgba(43, 255, 92, .10);
--brand: #39ff68;
--brand-ink: #011b07;
--brand-soft: rgba(57, 255, 104, .14);
--pos: #39ff68;
--neg: #ff6d6d;
--ok: #39ff68;
--head-bg: rgba(2, 10, 4, .9);
--shadow: 0 0 0 1px rgba(57,255,104,.08), 0 0 34px rgba(30,255,77,.10);
--stripe: radial-gradient(circle, rgba(57,255,104,.35) 1px, transparent 1.5px);
--page-glow: radial-gradient(700px 420px at 75% -10%, rgba(27,255,75,.15), transparent 62%);
}

:root[data-theme="dark"] {

--bg: #0a0b0d;
--bg-2: #0f1114;
--panel: #14161a;
Expand All @@ -54,6 +80,30 @@
--page-glow: radial-gradient(900px 400px at 85% -10%, rgba(34,211,238,.08), transparent 60%);
}

:root[data-theme="dark"][data-visual-theme="green-pixel"] {
--bg: #020503;
--bg-2: #061009;
--panel: #07150b;
--panel-2: #0b2111;
--ink: #c7ffd0;
--ink-2: #83d892;
--muted: #4d9d5b;
--line: #174f25;
--line-2: #28783a;
--cyan: #65ff8a;
--cyan-soft: rgba(43, 255, 92, .10);
--brand: #39ff68;
--brand-ink: #011b07;
--brand-soft: rgba(57, 255, 104, .14);
--pos: #39ff68;
--neg: #ff6d6d;
--ok: #39ff68;
--head-bg: rgba(2, 10, 4, .9);
--shadow: 0 0 0 1px rgba(57,255,104,.08), 0 0 34px rgba(30,255,77,.10);
--stripe: radial-gradient(circle, rgba(57,255,104,.35) 1px, transparent 1.5px);
--page-glow: radial-gradient(700px 420px at 75% -10%, rgba(27,255,75,.15), transparent 62%);
}

@theme inline {
--color-bg: var(--bg);
--color-bg-2: var(--bg-2);
Expand Down Expand Up @@ -96,6 +146,67 @@
@layer utilities {
.katex { max-width: 100%; overflow-x: auto; overflow-y: hidden; padding: 2px 8px; }

:root[data-visual-theme="green-pixel"] .ps-strip {
border-left-width: 1px;
box-shadow: var(--shadow);
background-image: none;
}

:root[data-visual-theme="green-pixel"] .ps-hero::after,
:root[data-visual-theme="green-pixel"] .ps-panel::before,
:root[data-visual-theme="green-pixel"] .ps-panel::after,
:root[data-visual-theme="green-pixel"] .ps-side-panel::before,
:root[data-visual-theme="green-pixel"] .ps-side-panel::after {
display: none;
}

:root[data-visual-theme="green-pixel"] body,
:root[data-visual-theme="green-pixel"] button,
:root[data-visual-theme="green-pixel"] input,
:root[data-visual-theme="green-pixel"] select {
font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
}

:root[data-visual-theme="green-pixel"] body {
background-image: var(--page-glow), radial-gradient(rgba(57,255,104,.13) 1px, transparent 1px);
background-size: auto, 5px 5px;
}

:root[data-visual-theme="green-pixel"] .ps-strip,
:root[data-visual-theme="green-pixel"] .ps-map,
:root[data-visual-theme="green-pixel"] .ps-panel,
:root[data-visual-theme="green-pixel"] .ps-side-panel {
border-color: var(--line);
border-radius: 2px;
box-shadow: var(--shadow);
background-image: radial-gradient(rgba(57,255,104,.12) .8px, transparent .8px);
background-size: 4px 4px;
}

:root[data-visual-theme="green-pixel"] .ps-panel::before {
display: block;
width: 32px;
height: 32px;
background: var(--brand);
opacity: .8;
}

:root[data-visual-theme="green-pixel"] .ps-hero {
border-bottom-color: var(--brand);
text-shadow: 0 0 18px rgba(57,255,104,.18);
}

:root[data-visual-theme="green-pixel"] .ps-hero h1,
:root[data-visual-theme="green-pixel"] .ps-panel h2 {
letter-spacing: .02em;
text-transform: uppercase;
}

:root[data-visual-theme="green-pixel"] .ps-barcode {
background: repeating-linear-gradient(90deg, var(--brand) 0 2px, transparent 2px 4px, var(--brand) 4px 5px, transparent 5px 8px);
box-shadow: 0 0 8px rgba(57,255,104,.7);
}

/* Print Stream Base Components */
.ps-panel {
position: relative;
Expand Down
102 changes: 59 additions & 43 deletions components/theme-toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
'use client';

import { Moon, Sun } from 'lucide-react';
import { useSyncExternalStore } from 'react';
import { Moon, Palette, Sun } from 'lucide-react';
import { useEffect, useSyncExternalStore } from 'react';

type Theme = 'light' | 'dark';
export type VisualTheme = 'print' | 'green-pixel';
const themeEvent = 'numeric-theme-change';
const visualThemeEvent = 'numeric-visual-theme-change';

function getVisualTheme(): VisualTheme {
if (typeof window === 'undefined') return 'print';
return window.localStorage.getItem('numeric-visual-theme') === 'green-pixel'
? 'green-pixel'
: 'print';
}

function getTheme(): Theme {
if (typeof window === 'undefined') return 'light';
Expand All @@ -20,63 +29,70 @@ function subscribe(callback: () => void) {

export function ThemeToggle() {
const theme = useSyncExternalStore(subscribe, getTheme, () => 'light');
const visualTheme = useSyncExternalStore(
(callback) => {
window.addEventListener(visualThemeEvent, callback);
return () => window.removeEventListener(visualThemeEvent, callback);
},
getVisualTheme,
() => 'print',
);
const isDark = theme === 'dark';

function toggleTheme(event: React.MouseEvent<HTMLButtonElement>) {
const next: Theme = isDark ? 'light' : 'dark';
useEffect(() => {
document.documentElement.dataset.visualTheme = visualTheme;
}, [visualTheme]);

// Fallback for browsers that don't support View Transitions
if (!document.startViewTransition) {
function setTheme(next: Theme, event?: React.MouseEvent<HTMLButtonElement>) {
const apply = () => {
document.documentElement.dataset.theme = next;
window.localStorage.setItem('numeric-theme', next);
window.dispatchEvent(new Event(themeEvent));
};
if (!event || !document.startViewTransition) {
apply();
return;
}

const x = event.clientX;
const y = event.clientY;
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
);

const transition = document.startViewTransition(() => {
document.documentElement.dataset.theme = next;
window.localStorage.setItem('numeric-theme', next);
window.dispatchEvent(new Event(themeEvent));
});

const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
const transition = document.startViewTransition(apply);
transition.ready.then(() => {
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
];

// To light: new view expands. To dark: old view shrinks (revealing dark underneath)
document.documentElement.animate(
{
clipPath: isDark ? [...clipPath].reverse() : clipPath,
},
{
duration: 400,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
pseudoElement: isDark
? '::view-transition-old(root)'
: '::view-transition-new(root)',
}
{ clipPath: isDark ? [`circle(${endRadius}px at ${x}px ${y}px)`, `circle(0px at ${x}px ${y}px)`] : [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`] },
{ duration: 400, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)' },
);
});
}

function setVisualTheme(next: VisualTheme) {
document.documentElement.dataset.visualTheme = next;
window.localStorage.setItem('numeric-visual-theme', next);
window.dispatchEvent(new Event(visualThemeEvent));
}

return (
<button
className="inline-flex h-10 items-center gap-2 rounded-lg border border-[var(--line)] bg-[var(--panel)] px-3 font-mono text-xs font-semibold tracking-wide text-[var(--muted)] transition hover:border-[var(--brand)] hover:bg-[var(--brand-soft)] hover:text-[var(--brand)] focus-visible:outline-2 focus-visible:outline-[var(--cyan)]"
onClick={toggleTheme}
aria-label={`${isDark ? 'Light' : 'Dark'} modega o'tish`}
// title="Ko'rinishni almashtirish"
>
{isDark ? <Sun size={16} aria-hidden="true" /> : <Moon size={16} aria-hidden="true" />}
<span className="max-[620px]:hidden">{isDark ? 'Light' : 'Dark'}</span>
</button>
<div className="relative flex items-center gap-1.5">
<button className="inline-flex h-10 items-center gap-2 rounded-lg border border-[var(--line)] bg-[var(--panel)] px-3 font-mono text-xs font-semibold tracking-wide text-[var(--muted)] transition hover:border-[var(--brand)] hover:bg-[var(--brand-soft)] hover:text-[var(--brand)] focus-visible:outline-2 focus-visible:outline-[var(--cyan)]" onClick={(event) => setTheme(isDark ? 'light' : 'dark', event)} aria-label={`${isDark ? 'Light' : 'Dark'} modega o'tish`}>
{isDark ? <Sun size={16} aria-hidden="true" /> : <Moon size={16} aria-hidden="true" />}
<span className="max-[620px]:hidden">{isDark ? 'Light' : 'Dark'}</span>
</button>
<details className="group relative">
<summary className="grid size-10 cursor-pointer list-none place-items-center rounded-lg border border-[var(--line)] bg-[var(--panel)] text-[var(--muted)] transition hover:border-[var(--brand)] hover:bg-[var(--brand-soft)] hover:text-[var(--brand)] focus-visible:outline-2 focus-visible:outline-[var(--cyan)]" aria-label="Theme sozlamalari">
<Palette size={16} aria-hidden="true" />
</summary>
<div className="absolute right-0 top-12 z-40 w-64 rounded-xl border border-[var(--line)] bg-[var(--panel)] p-4 shadow-[var(--shadow)]">
<p className="mb-3 font-mono text-[11px] font-bold uppercase tracking-[.14em] text-[var(--muted)]">Theme sozlamalari</p>
<label className="grid gap-2 text-sm font-semibold text-[var(--ink)]">
Design
<select value={visualTheme} onChange={(event) => setVisualTheme(event.target.value as VisualTheme)} className="h-10 rounded-lg border border-[var(--line)] bg-[var(--bg-2)] px-3 text-sm text-[var(--ink)] outline-none focus:border-[var(--brand)]">
<option value="print">Print Stream — Retro</option>
<option value="green-pixel">Green Pixel — Matrix</option>
</select>
</label>
<p className="mt-3 text-xs leading-relaxed text-[var(--muted)]">Print Stream yoki Green Pixel tanlovingiz shu qurilmada saqlanadi.</p>
</div>
</details>
</div>
);
}
6 changes: 3 additions & 3 deletions components/ui/print-stream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
// Barcode strip component
export function PsStrip({ eyebrow, id, name }: { eyebrow: string; id: string; name: string }) {
return (
<div className="flex items-center gap-3.5 font-mono text-xs font-semibold tracking-[.14em] uppercase text-muted py-2.5 px-3.5 border border-line rounded-lg bg-panel shadow-[var(--shadow)]">
<div className="ps-strip flex items-center gap-3.5 font-mono text-xs font-semibold tracking-[.14em] uppercase text-muted py-2.5 px-3.5 border border-line bg-panel shadow-[var(--shadow)]">
<span>{eyebrow}</span>
<i className="ps-barcode flex-1 h-[14px] opacity-55" aria-hidden />
<span>{id} · {name}</span>
Expand All @@ -28,7 +28,7 @@ export function PsHero({
conditionSub?: React.ReactNode;
}) {
return (
<section className="flex flex-wrap items-end justify-between gap-8 my-8">
<section className="ps-hero flex flex-wrap items-end justify-between gap-8 my-8">
<div>
<span className="block font-mono text-xs font-semibold tracking-[.14em] uppercase text-muted leading-[1.2]">
{eyebrow}
Expand Down Expand Up @@ -59,7 +59,7 @@ export function PsHero({
// Step Map
export function PsMap({ steps, title = "3 ta oddiy qadam", eyebrow = "Method map" }: { steps: {n: string, t: React.ReactNode, s: React.ReactNode}[], title?: string, eyebrow?: string }) {
return (
<section className="flex flex-col lg:flex-row items-stretch gap-4 my-6 p-4 bg-panel border border-line rounded-[10px] shadow-[var(--shadow)]">
<section className="ps-map flex flex-col lg:flex-row items-stretch gap-4 my-6 p-4 bg-panel border border-line shadow-[var(--shadow)]">
<div className="grid gap-1.5 content-center pb-3 lg:pb-0 border-b lg:border-b-0 lg:border-r border-dashed border-line-2 shrink-0 lg:pr-5 w-full lg:w-auto min-w-[170px]">
<span className="block font-mono text-xs font-semibold tracking-[.14em] uppercase text-muted leading-[1.2]">
{eyebrow}
Expand Down
Loading