You are browsing as a guest. Sign up (or log in) to start making projects!

PrismaOS

  • 3 Devlogs
  • 16 Total hours

This is my very own OS that can be run in the web!

Ship #1

I built PrismaOS, a portfolio that boots and runs like a desktop OS inside a browser tab. Instead of a normal scrolling page, you get a boot sequence, a wallpaper, a menu bar with a live clock, and a dock. Every app opens as a real window you can drag around, focus, resize, and minimize, and the layout you leave behind is remembered.

I didn’t want it to look like the guide, so I built my own visual language around a prism theme and shipped four switchable themes: aqua, graphite, midnight, and aurora. The dock has macOS-style cursor magnification, and the whole shell has its own glass look rather than the default one from the walkthrough.

The feature the guide didn’t ask for is a working terminal. It’s not a prop. It runs help, about, projects, ls, open, theme, and fastfetch, it keeps command history, and it talks to the OS through a small syscall module so open terminal and theme aurora actually reach into the window manager and theme state. There are also two easter eggs in there for anyone who pokes around.

My goal is to further develop it later into a portfolio site worthy of being featured on my GitHub profile.

Try project → See source code →
Open comments for this post

7h 0m 32s logged

Devlog #3

Devlog #2 ended with a shell that read as a desktop but a dock where every icon
opened a placeholder. This time I filled it in: the real apps, a terminal that
can actually drive the OS, and the boot screen I said I’d redo.

About, Projects, and Music are real windows now, all registered in one place and
opened through the window manager. An app is just an entry — id, title, icon,
component — so the dock, the terminal, and the logo menu all launch through the
same path. Projects is the one with real structure: a card grid and a detail
view off a single useState. I made the cards <button> instead of <div>
after realizing I couldn’t tab to a project; that brought focus and Enter/Space
back for free.

The terminal was the part I cared about most. help, about, projects, ls,
open, theme, fastfetch, clear, plus two easter eggs. Parsing was
nothing — a split and a switch. The annoying part was that open terminal needs
the window manager and theme aurora needs the theme state, both living way
above the terminal. I didn’t want to prop-drill the whole WM into a leaf, so the
shell talks to src/os.ts instead:

import { launchApp, listApps, setTheme, THEMES } from "../os";

Small module, handlers registered at mount. open returns false for a missing
app so I can print no such app instead of failing silently. Command history
took two tries — the first walked off the end of the array and left undefined
in the input; fix was making -1 mean “not browsing”.

Boot screen: the old one was a pulsing logo and a 2400ms bar that measured
nothing, opening into a shell with glass and a live clock. Now it prints staged
log lines as the bar crosses them (mount display, load tokens, start WM,
register apps, start prism-sh), the logo sits on a slow refraction sweep, and
any click or key skips it. It only plays once per tab via sessionStorage, and
prefers-reduced-motion takes the same fast path.

Two bugs turned up prepping the build. npm run build died instantly:

src/hooks/useWindows.ts(1,10): error TS6133: 'use' is declared but never read.

An autocomplete import { use, ... } I never called. dev doesn’t typecheck,
but build is tsc --noEmit && vite build with noUnusedLocals, so the first
real build was the first warning. Six characters. The other: About’s only link
was href="https//github.com/..." — missing colon, so it read as a relative
path and 404’d inside my own site. Clicked past it a dozen times.

0
0
15
Open comments for this post

2h 52m 43s logged

Devlog #2

Building my portfolio as an operating system that runs in a browser tab.

Picking up from Devlog #1, where the desktop finally had bones — windows you
could drag, focus, resize, and minimize — but nothing around them. An empty
wallpaper with floating rectangles isn’t an OS; it’s a screensaver. This
milestone is about the two pieces of chrome that make it read as a real desktop
the instant it loads: the menu bar across the top and the dock along the
bottom.

A menu bar

The menu bar is mostly static — a logo that drops open into an “About / Restart”
menu, and the usual File / Edit / View / Help labels that don’t do much yet. The
one part that has to feel alive is the clock in the top-right corner. A frozen
clock immediately breaks the illusion, so it ticks once a second

Nothing fancy — a setInterval that recomputes the formatted string every
second and clears itself on unmount. The formatting is where I tripped.

The crash that ate the whole screen

I wanted the clock in Korean — “7월 26일 (일) 오후 11:24” — so I reached for the
browser’s built-in localization instead of hand-formatting dates:

const date = d.toLocaleDateString("ko-KR", { month: "short", day: "numeric", weekday: "short" })
const time = d.toLocaleTimeString("ko_KR", { hour: "2-digit", minute: "2-digit" })

And the app went white. Not a subtly-wrong date — the entire MenuBar component
threw and took the tree down with it:

Uncaught RangeError: invalid language tag: "ko_KR"
    formatNow  MenuBar.tsx:74

It took me an embarrassing minute because the two lines look identical at a
glance. Look again: the date says ko-KR, the time says ko_KR. A hyphen on one
line, an underscore on the next.

The fix was one keystroke — underscore to hyphen — and the shell came back.

Lesson filed away: locale tags are hyphenated, always, and Intl validates them
strictly enough to crash. Two things to fix properly later — normalize the locale
in one constant so I can’t typo it twice, and wrap the shell in an error boundary
so a single throw dims one widget instead of nuking the screen.

The dock

The dock is the fun part. I wanted the macOS-style effect where icons swell as
the cursor passes over them and the neighbors ease up too, so it feels like the
row is being pushed under a magnifying glass rather than icons popping one at a
time.

On a trackpad-less touch screen there’s no hover state, so a finger tap would
otherwise make the whole dock lurch to wherever you touched. Gating on
pointerType === "mouse" keeps touch calm and leaves the magnification as a
mouse-only flourish, which is exactly where it belongs.

Where stands

  • Menu bar with a working logo menu and a live, once-a-second clock.
  • Clock is localized through Intl — after learning the hard way that the tag
    must be ko-KR, not ko_KR.
  • Dock with smooth Gaussian cursor magnification and a subtle lift, mouse-only.
  • The desktop finally reads as a desktop at first glance, even before any real
    apps exist.

Next up

The apps themselves — About and Projects as real windows wired into the window
manager, replacing the placeholder stand-ins the dock currently launches. And,
not incidentally, the boot sequence needs a proper redesign; the current one is
placeholder-grade and doesn’t match the polish of the shell it opens into.

1
0
22
Open comments for this post

6h 30m 16s logged

Devlog #1

Building my portfolio as an operating system that runs in a browser tab.

Why this exists

When I first saw the brief, I honestly just planned to knock something out — the
minimum that clears the requirements and move on. But somewhere between the first
commit and the second, I changed my mind. If I’m going to build a website anyway,
why not make it the website — my actual portfolio. So I decided to treat
PrismaOS as the real thing and pour everything I have into it: a portfolio site
that is itself the project. Every window, every pixel of the shell, is something
I want to be proud to show. That reframing is why this repo suddenly has design
tokens, four themes, and a genie animation nobody asked for.

The concept: instead of a scrolling landing page, the visitor boots into a tiny
web desktop — a menu bar, a dock, draggable windows — and explores my work the
way you’d poke around someone’s actual computer.

This milestone: the window manager

Devlog #1 covers the skeleton — the part that makes it feel like an OS rather

One source of truth

Every window’s position, size, focus order (z-index), and minimized state lives
in a single hook, useWindows. Components never track their own coordinates.
Opening, closing, focusing, moving, and resizing all route through that one
place.

Dragging without dropping frames

The naive approach — call setState on every pointer move — makes windows stutter
because React re-renders on every pixel. So during a drag I don’t touch state at
all. I mutate the DOM node directly (el.style.left/top) for a smooth 60fps, and
only commit the final position to state when the pointer is released.

The other trick is setPointerCapture. Once the pointer is captured, the drag
keeps working even if the cursor flies outside the window — or outside the
browser entirely. Touch gets the same behavior for free, since Pointer Events
unify mouse and touch.

handle.setPointerCapture(e.pointerId)
// ...move: write to el.style directly (no re-render)
// ...up:   commit once via onDragEnd()

Resizing from the bottom-right corner uses the exact same pattern: capture,
mutate the DOM live, commit on release.

Bug I caught

Clicking any window brings it to the front by bumping a z-counter.

0
0
9

Delete project?

Are you sure you want to permanently delete this project? This action cannot be undone.

All devlogs, followers, and associated data will be removed.

Followers

Loading…