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
2,555 changes: 2,555 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-icons": "5.0.1",
"react-rnd": "10.5.2",
"react-youtube": "10.1.0",
"simplex-noise": "4.0.3",
"zustand": "5.0.3"
},
"devDependencies": {
Expand Down
160 changes: 67 additions & 93 deletions src/components/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,90 @@
import { useState } from 'react';
import { useMemo } from 'react';

import { Toolbox } from '../toolbox';
import { Notepad } from '../tools/notepad';
import { Todo } from '../tools/todo';
import { SnackbarProvider } from '@/contexts/snackbar';
import { WindowsProvider } from '@/contexts/windows';
import type { AsciiPattern } from '@/lib/ascii/types';
import { FirePattern } from '@/lib/ascii/patterns/fire';
import { RainPattern } from '@/lib/ascii/patterns/rain';
import { BonsaiPattern } from '@/lib/ascii/patterns/bonsai';
import { SnowPattern } from '@/lib/ascii/patterns/snow';
import { WavePattern } from '@/lib/ascii/patterns/waves';
import { AuroraPattern } from '@/lib/ascii/patterns/aurora';
import { WeatherPattern } from '@/lib/ascii/patterns/weather/weather-pattern';
import { useSettings } from '@/stores/settings';
import type { PatternId, Location } from '@/stores/settings';
import { AsciiBackground } from '../ascii-background/ascii-background';
import { Settings } from '../settings';
import { StoreConsumer } from '../store-consumer';

import { useLocalStorage } from '@/hooks/use-local-storage';
import { Pomodoro } from '../tools/pomodoro';
import { Breathing } from '../tools/breathing';
import { Toolbox } from '../toolbox';
import { Ambient } from '../tools/ambient/ambient';
import { Timers } from '../tools/timers';
import { Breathing } from '../tools/breathing';
import { Lofi } from '../tools/lofi';
import { Background } from '../background';
import { Settings } from '../settings';
import { SnackbarProvider } from '@/contexts/snackbar';

import { Notepad } from '../tools/notepad';
import { Pomodoro } from '../tools/pomodoro';
import { Timers } from '../tools/timers';
import { Todo } from '../tools/todo';
import styles from './app.module.css';
import { SomaFM } from '../tools/somafm';

export function App() {
const [openApps, setOpenApps] = useLocalStorage<Array<string>>(
'haus:open-windows',
[],
const createPattern = (id: PatternId, location: Location | null): AsciiPattern => {
switch (id) {
case 'fire': return new FirePattern({});
case 'rain': return new RainPattern();
case 'bonsai': return new BonsaiPattern({});
case 'snow': return new SnowPattern({});
case 'waves': return new WavePattern({});
case 'aurora': return new AuroraPattern({});
case 'weather': return new WeatherPattern(location);
default: return new FirePattern({});
}
};

function AppContent() {
const patternId = useSettings(s => s.backgroundPattern);
const location = useSettings(s => s.location);

const pattern = useMemo(
() => createPattern(patternId, location),
[patternId, location],
);
const [minimizedApps, setMinimizedApps] = useState<Array<string>>([]);

const isAppOpen = (app: string) => {
return openApps.some(a => a === app);
};

const isAppMinimized = (app: string) => {
return minimizedApps.some(a => a === app);
};

const openApp = (app: string) => {
if (isAppOpen(app) && !isAppMinimized(app)) return;

setOpenApps(prev => [...prev, app]);
setMinimizedApps(prev => prev.filter(a => a !== app).filter(Boolean));
};

const closeApp = (app: string) => {
setOpenApps(prev => prev.filter(a => a !== app).filter(Boolean));
};

const minimizeApp = (app: string) => {
setMinimizedApps(prev => [...prev, app]);
};

return (
<StoreConsumer>
<SnackbarProvider>
<WindowsProvider>
<div className={styles.app}>
<Background />

<Toolbox
minimizedApps={minimizedApps}
openApp={openApp}
openApps={openApps}
/>
<div className={styles.app}>
<AsciiBackground pattern={pattern} />
<Toolbox />

<Notepad
isOpen={isAppOpen('notepad')}
onClose={() => closeApp('notepad')}
/>
<Notepad />

<Todo isOpen={isAppOpen('todo')} onClose={() => closeApp('todo')} />
<Todo />

<Pomodoro
isMinimized={isAppMinimized('pomodoro')}
isOpen={isAppOpen('pomodoro')}
onClose={() => closeApp('pomodoro')}
onMinimize={() => minimizeApp('pomodoro')}
/>
<Pomodoro />

<Breathing
isOpen={isAppOpen('breathing')}
onClose={() => closeApp('breathing')}
/>
<Breathing />

<Ambient
isMinimized={isAppMinimized('ambient')}
isOpen={isAppOpen('ambient')}
onClose={() => closeApp('ambient')}
onMinimize={() => minimizeApp('ambient')}
/>
<Ambient />

<Timers
isMinimized={isAppMinimized('timers')}
isOpen={isAppOpen('timers')}
onClose={() => closeApp('timers')}
onMinimize={() => minimizeApp('timers')}
/>
<Timers />

<Lofi
isMinimized={isAppMinimized('lofi')}
isOpen={isAppOpen('lofi')}
onClose={() => closeApp('lofi')}
onMinimize={() => minimizeApp('lofi')}
/>
<Lofi />

<SomaFM
isMinimized={isAppMinimized('somafm')}
isOpen={isAppOpen('somafm')}
onClose={() => closeApp('somafm')}
onMinimize={() => minimizeApp('somafm')}
/>

<SomaFM
isMinimized={isAppMinimized('somafm')}
isOpen={isAppOpen('somafm')}
onClose={() => closeApp('somafm')}
onMinimize={() => minimizeApp('somafm')}
/>
<Settings />
</div>
);
}

<Settings />
</div>
export function App() {
return (
<StoreConsumer>
<SnackbarProvider>
<WindowsProvider>
<AppContent />
</WindowsProvider>
</SnackbarProvider>
</StoreConsumer>
Expand Down
8 changes: 8 additions & 0 deletions src/components/ascii-background/ascii-background.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.canvas {
width: 100%;
height: 100%;
inset: 0;
pointer-events: none;
position: fixed;
z-index: -1;
}
45 changes: 45 additions & 0 deletions src/components/ascii-background/ascii-background.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect, useRef } from 'react';

import { CanvasEngine } from '@/lib/ascii/engine';
import type { AsciiPattern } from '@/lib/ascii/types';
import { useSettings } from '@/stores/settings';

import styles from './ascii-background.module.css';

interface Props {
pattern: AsciiPattern;
}

export function AsciiBackground({ pattern }: Props) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const engineRef = useRef<CanvasEngine | null>(null);
const backgroundOpacity = useSettings(s => s.backgroundOpacity);

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;

const engine = new CanvasEngine(canvas);
engineRef.current = engine;
engine.start();

const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
engine.resize(width, height);
}
});
observer.observe(canvas);

return () => {
observer.disconnect();
engine.stop();
};
}, []);

useEffect(() => {
engineRef.current?.setPattern(pattern);
}, [pattern]);

return <canvas className={styles.canvas} ref={canvasRef} style={{ opacity: backgroundOpacity }} />;
}
6 changes: 0 additions & 6 deletions src/components/background/background.module.css

This file was deleted.

38 changes: 0 additions & 38 deletions src/components/background/background.tsx

This file was deleted.

1 change: 0 additions & 1 deletion src/components/background/index.ts

This file was deleted.

73 changes: 72 additions & 1 deletion src/components/settings/settings.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
right: 20px;
bottom: 68px;
z-index: 9997;
width: 260px;
width: 280px;
padding: 16px;
background: var(--color-neutral-100);
border: 1px solid var(--color-neutral-200);
Expand Down Expand Up @@ -62,7 +62,78 @@
border-radius: 4px;
}

& input[type='text'] {
width: 100%;
height: 32px;
padding: 0 8px;
font-size: var(--font-xsm);
color: var(--color-foreground);
background: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
}

& input[type='range'] {
width: 100%;
}
}

.locationRow {
display: flex;
gap: 6px;

& input {
flex: 1;
min-width: 0;
}

& button {
height: 32px;
padding: 0 10px;
font-size: var(--font-xsm);
cursor: pointer;
color: var(--color-foreground);
background: var(--color-neutral-50);
border: 1px solid var(--color-neutral-200);
border-radius: 4px;
white-space: nowrap;

&:disabled {
opacity: 0.5;
cursor: default;
}
}
}

.useLocation {
margin-top: 6px;
padding: 0;
font-size: var(--font-xsm);
cursor: pointer;
background: none;
border: none;
color: var(--color-foreground-subtle);
text-decoration: underline;

&:hover {
color: var(--color-foreground);
}
}

.locationInfo {
display: block;
margin-top: 4px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
}

.credit {
margin-top: 14px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);

& a {
color: var(--color-foreground);
text-decoration: underline;
}
}
Loading