From 531ad261452427b976c09139c6d13bfdd86bdd6b Mon Sep 17 00:00:00 2001 From: Maxwell Young Date: Fri, 4 Sep 2026 13:31:37 +0800 Subject: [PATCH] make Maxwell OS a working computer full of the real portfolio Maxwell OS was a parody shell: windows could not be dragged or resized, Snake was a picture, Minesweeper had a fixed board, My Computer showed the app list, and nothing persisted. It said nothing about the actual work. The desktop is now a working engine over the real portfolio: - Windows drag, resize, maximise on double-click, cycle with Alt/Ctrl+Tab, and the layout plus Display Properties survive reloads (client-only render so the saved session is there on first paint). - A real file system is generated from projects, case studies, essays, resume, open-source proof and the Canon feed. My Computer browses it with a folder tree, address bar and status bar; Notepad opens files with a link back to the live page; MS-DOS Prompt has ls, cd, cat, tree, open, history and the old jokes, all against the same tree. - Minesweeper reuses the Lab's board engine with a real counter, timer, flags and keyboard play. Snake is a real game with food, score, a saved high score, pause, and a d-pad on touch screens. - Display Properties changes wallpaper and colour scheme live, with OK, Cancel and Apply behaving like the real thing. - About shows the real bio; Internet links to the real pages. The model stays pure and unit-tested (22 tests covering the window manager, file system, path resolution, terminal, snake and settings). --- src/app/os/page.tsx | 8 +- .../maxwell-os/MaxwellOS.module.css | 69 ++++- src/components/maxwell-os/MaxwellOS.tsx | 201 ++++++++++++-- src/components/maxwell-os/MaxwellOSLoader.tsx | 8 + src/components/maxwell-os/apps.tsx | 193 +++++++++++++ src/components/maxwell-os/games.tsx | 90 +++++++ src/lib/maxwellOS.test.ts | 43 ++- src/lib/maxwellOS.ts | 253 ++++++++++++++++-- src/lib/maxwellOSFiles.ts | 228 ++++++++++++++++ 9 files changed, 1035 insertions(+), 58 deletions(-) create mode 100644 src/components/maxwell-os/MaxwellOSLoader.tsx create mode 100644 src/components/maxwell-os/apps.tsx create mode 100644 src/components/maxwell-os/games.tsx create mode 100644 src/lib/maxwellOSFiles.ts diff --git a/src/app/os/page.tsx b/src/app/os/page.tsx index caf56cbf..41e9fb49 100644 --- a/src/app/os/page.tsx +++ b/src/app/os/page.tsx @@ -1,4 +1,4 @@ -import type {Metadata} from "next"; -import MaxwellOS from "@/components/maxwell-os/MaxwellOS"; -export const metadata:Metadata={title:"Maxwell OS",description:"A fictional desktop system by Maxwell Young."}; -export default function OSPage(){return } +import type { Metadata } from "next"; +import MaxwellOSLoader from "@/components/maxwell-os/MaxwellOSLoader"; +export const metadata: Metadata = { title: "Maxwell OS", description: "A fictional desktop system by Maxwell Young. The files are real." }; +export default function OSPage() { return ; } diff --git a/src/components/maxwell-os/MaxwellOS.module.css b/src/components/maxwell-os/MaxwellOS.module.css index d17a1945..8f295c8d 100644 --- a/src/components/maxwell-os/MaxwellOS.module.css +++ b/src/components/maxwell-os/MaxwellOS.module.css @@ -1,2 +1,67 @@ -.os{position:fixed;inset:0;overflow:hidden;background:#087f80;color:#000;font:14px "MS Sans Serif",Tahoma,sans-serif;user-select:none}.wallmark{position:absolute;right:5%;bottom:14%;color:#ffffff20;font:bold 82px Arial;line-height:.72;text-align:right}.wallmark span{font-size:22px;letter-spacing:10px}.icons{display:grid;width:112px;padding:18px;gap:17px}.icons button{border:1px solid transparent;background:none;color:#fff;text-shadow:1px 1px #000;padding:4px;font:12px Tahoma}.icons button:focus{background:#00008080;border:1px dotted #fff}.icons span{display:block}.pixelIcon{position:relative;display:block;width:42px;height:38px;margin:0 auto 4px;background:#d5d5d5;border:3px solid #fff;border-right-color:#303030;border-bottom-color:#303030;box-shadow:inset 0 0 0 3px #777;color:#000080;text-shadow:none;font:bold 9px monospace;line-height:34px;text-align:center;image-rendering:pixelated}.pixelIcon:after{content:attr(data-icon)}.pixelIcon i{position:absolute;left:5px;right:5px;bottom:-7px;height:4px;background:#222}.pixelIconSmall{display:inline-block;width:17px;height:15px;margin:0 5px 0 0;border-width:1px;box-shadow:inset 0 0 0 1px #777;font-size:5px;line-height:13px}.pixelIconSmall i{display:none}.window{position:absolute;background:#c0c0c0;border:3px outset #fff;box-shadow:3px 3px #111;min-width:360px;min-height:260px;display:flex;flex-direction:column}.max{inset:0 0 36px!important;width:auto!important;height:auto!important}.window>header{height:25px;background:linear-gradient(90deg,#000080,#1084d0);color:#fff;display:flex;align-items:center;padding:2px 3px;font-weight:bold}.window>header strong{flex:1}.window>header button{width:22px;height:20px;padding:0;border:2px outset #fff;background:#c0c0c0;font:bold 12px Arial}.window>header button:active,.start:active,.choices button:active,.officeChoices button:active{border-style:inset;transform:translate(1px,1px)}.menuBar{height:25px;padding:5px 8px;border-bottom:1px solid #777}.content{flex:1;margin:3px;border:2px inset #eee;overflow:auto;background:#fff}.taskbar{height:36px;position:absolute;bottom:0;left:0;right:0;background:#c0c0c0;border-top:2px outset #fff;display:flex;padding:3px;gap:4px;z-index:9999}.taskbar button{border:2px outset #fff;background:#c0c0c0}.start{font-weight:bold;font-size:14px}.start i{font-size:18px;color:#124fbc}.task{width:175px;overflow:hidden;text-align:left}.tray{margin-left:auto;border:2px inset #fff;padding:6px 10px}.trayAlert{background:#ffef8a;color:#700;font-weight:bold}.startMenu{position:absolute;z-index:10000;left:2px;bottom:36px;width:270px;background:#c0c0c0;border:3px outset #fff;display:flex}.startMenu aside{width:30px;background:#777b88;color:#fff;writing-mode:vertical-rl;transform:rotate(180deg);font:bold 19px Arial;padding:6px}.startMenu>div{flex:1;padding:4px}.startMenu button,.startMenu a{display:flex;align-items:center;gap:8px;width:100%;min-height:34px;border:0;background:none;color:#000;text-decoration:none;text-align:left}.startMenu button:hover{background:#000080;color:#fff}.startMenu button span:last-child{margin-left:auto}.context{position:absolute;z-index:10001;width:150px;background:#c0c0c0;border:2px outset #fff;padding:3px}.context button{display:block;width:100%;border:0;text-align:left;background:none}.context button:hover{background:#000080;color:#fff}.notes{width:100%;height:100%;border:0;resize:none;padding:12px;font:16px monospace}.terminal{height:100%;box-sizing:border-box;background:#000;color:#ddd;padding:10px;font:15px monospace}.terminal input{background:#000;border:0;outline:0;color:#fff;font:inherit}.browser>div{padding:7px;background:#c0c0c0}.browser input{width:65%}.browser article{text-align:center;padding:9%}.browser h1{color:#000080;font-size:32px}.browser a{color:#00e;text-decoration:underline;cursor:pointer}.explorer{display:grid;grid-template-columns:180px 1fr;height:100%}.explorer aside{padding:12px;background:#eee;line-height:2}.explorer>div{display:grid;grid-template-columns:repeat(4,110px);align-content:start;padding:12px}.explorer button{border:0;background:none}.explorer button b{display:block;font:700 16px monospace;color:#000080}.empty,.panel,.about{text-align:center;padding:9%}.empty>b{font-size:55px}.empty button{display:block;margin:15px auto;padding:9px;border:3px outset #fff;background:#c0c0c0;color:#900;font-weight:bold}.empty small{display:inline-block;background:#800;color:#fff;padding:5px 9px;font:bold 13px monospace}.office{box-sizing:border-box;min-height:100%;padding:7% 10%;background:#f5f0df;color:#242019;border:12px solid #ded5b9;box-shadow:inset 0 0 0 2px #777;font-family:Georgia,serif}.officeNo{font:12px monospace;letter-spacing:3px;border-bottom:1px solid #999;padding-bottom:12px}.office h1{font-size:30px;max-width:650px}.office p{font-size:19px;line-height:1.65;max-width:680px}.officeChoices{display:grid;gap:10px;max-width:660px;margin:28px 0}.officeChoices button{display:flex;align-items:center;gap:10px;text-align:left;padding:14px;background:#c0c0c0;border:3px outset #fff;font:bold 15px Tahoma}.officeChoices button:hover,.officeChoices button:focus{background:#000080;color:#fff;outline:1px dotted #fff;outline-offset:-5px}.officeChoices u{display:grid;place-items:center;width:24px;height:24px;background:#fff;color:#000;border:1px solid #000;text-decoration:none}.officeChoices span{margin-left:auto;font-size:10px;letter-spacing:1px}.office small{font-family:monospace}.adventure{height:100%;min-height:520px;background:#090d2a;color:#fff;display:grid;grid-template-columns:minmax(0,2fr) minmax(250px,1fr)}.scene{position:relative;overflow:hidden;background:linear-gradient(#07112d 0 55%,#cf6d46 56%,#151a3f 58%);min-height:520px;image-rendering:pixelated}.skyPixels{position:absolute;inset:8% 8% 46%;background:radial-gradient(circle,#fff 0 1px,transparent 2px);background-size:43px 31px;opacity:.7}.moon{position:absolute;right:17%;top:9%;color:#ffe9a8;font:70px Georgia;text-shadow:8px 3px #de7d52}.beam{position:absolute;left:35%;top:18%;width:42%;height:10%;background:#fff4a533;clip-path:polygon(0 40%,100% 0,100% 100%,0 60%)}.sea{position:absolute;inset:58% 0 0;background:repeating-linear-gradient(0deg,#101942 0 10px,#244574 11px 15px,#172753 16px 25px)}.cliffs{position:absolute;left:0;bottom:28%;width:58%;height:27%;background:#26353d;clip-path:polygon(0 25%,20% 8%,35% 40%,55% 15%,75% 55%,100% 38%,100% 100%,0 100%)}.lighthouse{position:absolute;left:29%;bottom:39%;width:54px;height:150px;background:repeating-linear-gradient(0deg,#eee 0 25px,#b54238 26px 48px);clip-path:polygon(17% 0,83% 0,100% 100%,0 100%);filter:drop-shadow(5px 5px #091020)}.lighthouse i{position:absolute;left:4px;top:-22px;width:46px;height:25px;background:#222;border:5px solid #eee}.lighthouse b{position:absolute;left:22px;bottom:0;width:14px;height:26px;background:#151a3f}.oracle{position:absolute;right:8%;bottom:22%;width:88px;height:150px;background:#b02f50;border:7px solid #421c3d;box-shadow:7px 7px #080b1d;text-align:center;font:bold 9px monospace}.oracle em{display:block;padding:5px;background:#f1b65a;color:#35142c;font-style:normal}.oracle i{display:block;height:54px;margin:8px;background:#1b1938;border:4px inset #df8090}.oracle b{color:#fff1a0}.dock{position:absolute;left:0;right:18%;bottom:6%;height:20px;background:#704832;transform:skewY(-4deg);box-shadow:0 10px #38271f}.scene p{position:absolute;left:4%;right:4%;bottom:1%;z-index:2;background:#050817e8;border:3px solid #fff;padding:12px;font:15px/1.4 monospace}.commandArea{padding:22px;background:#d2c4a1;color:#17152d;border-left:7px solid #080b1d}.commandArea>strong{display:block;margin:4px 0 9px;font:11px monospace;letter-spacing:2px}.verbs,.choices{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:22px}.verbs button,.choices button{min-height:42px;border:3px outset #fff;background:#c0c0c0;font:bold 12px monospace;text-align:left;padding:8px}.verbs button:hover,.choices button:hover,.verbs button:focus,.choices button:focus{background:#fff3a1;outline:2px solid #201b48}.verbs button.selected{background:#201b48;color:#fff;border-style:inset}.choices{grid-template-columns:1fr}.choices span{float:right}.infestation{position:absolute;inset:0 0 36px;z-index:9000;pointer-events:none}.ant{--turn:0deg;position:absolute;width:15px;height:7px;pointer-events:none;animation:crawl 5s steps(12) infinite;filter:drop-shadow(1px 1px #fff8)}.ant b,.ant span,.ant em{position:absolute;top:2px;width:5px;height:5px;border-radius:50%;background:#160c08}.ant b{left:0}.ant span{left:5px}.ant em{left:10px}.ant:before,.ant:after{content:"";position:absolute;left:3px;top:-3px;width:9px;height:12px;border-top:1px solid #160c08;border-bottom:1px solid #160c08;transform:skewX(35deg)}.ant:after{transform:skewX(-35deg)}.ant.flee{animation-duration:1.2s}.file,.bin{animation-name:cluster}.antDialog{position:absolute;right:18px;bottom:18px;width:390px;background:#c0c0c0;border:3px outset #fff;box-shadow:4px 4px #111;pointer-events:auto}.antDialog header{background:#000080;color:#fff;padding:5px;font-weight:bold}.antDialog>div{display:flex;gap:15px;padding:18px}.antDialog>div>strong{font-size:34px;color:#d88a00}.antDialog p{margin:0;line-height:1.6}.antDialog small{background:#700;color:#fff;padding:3px;font-family:monospace}.antDialog footer{text-align:right;padding:10px;border-top:1px solid #777}.antDialog button{padding:7px 14px;border:3px outset #fff;background:#c0c0c0}@keyframes crawl{50%{transform:translate(110px,-30px) rotate(var(--turn))}100%{transform:translate(0,-80px) rotate(var(--turn))}}@keyframes cluster{50%{transform:translate(22px,15px) rotate(var(--turn))}100%{transform:translate(-15px,-18px) rotate(var(--turn))}}.mines,.snake{text-align:center;padding:30px}.counter{font:24px monospace;background:#c0c0c0;padding:10px}.grid{display:grid;grid-template-columns:repeat(9,30px);justify-content:center}.grid button{height:30px;border:3px outset #fff;background:#c0c0c0}.grid button.open{border:1px solid #888}.snakeBoard{padding:80px;background:#102510;color:#9f9;font:22px monospace}.snakeBoard span{color:#f55} -@media(max-width:650px){.os{font-size:13px}.wallmark{font-size:40px}.icons{grid-template-columns:repeat(3,88px);width:auto;padding:10px;gap:8px}.window{left:4px!important;top:4px!important;width:calc(100% - 8px)!important;height:calc(100% - 44px)!important}.task{max-width:70px}.office{padding:10% 6%;border-width:5px}.office h1{font-size:23px}.office p{font-size:16px}.adventure{grid-template-columns:1fr;min-height:100%}.scene{min-height:58vh}.commandArea{border-left:0;border-top:6px solid #080b1d;padding:12px}.verbs{grid-template-columns:repeat(4,1fr)}.verbs button{font-size:9px;padding:4px}.antDialog{left:8px;right:8px;bottom:8px;width:auto}.tray{padding:5px}.startMenu{width:280px}}@media(prefers-reduced-motion:reduce){.ant{animation:none}.beam{display:none}} \ No newline at end of file +/* Maxwell OS. Scheme colours arrive as CSS variables from Display Properties. */ +.os{--os-desktop:#087f80;--os-face:#c0c0c0;--os-title-a:#000080;--os-title-b:#1084d0;--os-text:#000;--os-link:#000080;position:fixed;inset:0;overflow:hidden;background-color:var(--os-desktop);background-size:cover;color:var(--os-text);font:14px "MS Sans Serif",Tahoma,sans-serif;user-select:none;touch-action:none} +.boot{position:fixed;inset:0;display:grid;place-items:center;background:#087f80;color:#fff;font:14px Tahoma,sans-serif} +.wallmark{position:absolute;right:5%;bottom:14%;color:#ffffff20;font:bold 82px Arial;line-height:.72;text-align:right;pointer-events:none}.wallmark span{font-size:22px;letter-spacing:10px} +.icons{display:grid;width:112px;padding:18px;gap:17px}.icons button{border:1px solid transparent;background:none;color:#fff;text-shadow:1px 1px #000;padding:4px;font:12px Tahoma;cursor:default}.icons button:focus-visible{background:#00008080;border:1px dotted #fff;outline:0}.icons span{display:block} +.pixelIcon{position:relative;display:block;width:42px;height:38px;margin:0 auto 4px;background:#d5d5d5;border:3px solid #fff;border-right-color:#303030;border-bottom-color:#303030;box-shadow:inset 0 0 0 3px #777;color:#000080;text-shadow:none;font:bold 9px monospace;line-height:34px;text-align:center;image-rendering:pixelated}.pixelIcon:after{content:attr(data-icon)}.pixelIcon i{position:absolute;left:5px;right:5px;bottom:-7px;height:4px;background:#222} +.pixelIconSmall{display:inline-block;width:17px;height:15px;margin:0 5px 0 0;border-width:1px;box-shadow:inset 0 0 0 1px #777;font-size:5px;line-height:13px;vertical-align:middle}.pixelIconSmall i{display:none} +/* windows */ +.window{position:absolute;background:var(--os-face);border:3px outset #fff;box-shadow:3px 3px #111;min-width:320px;min-height:220px;display:flex;flex-direction:column;color:var(--os-text)} +.max{inset:0 0 36px!important;width:auto!important;height:auto!important} +.window>header{height:25px;background:linear-gradient(90deg,var(--os-title-a),var(--os-title-b));color:#fff;display:flex;align-items:center;padding:2px 3px;font-weight:bold;cursor:default;touch-action:none}.inactive>header{background:linear-gradient(90deg,#808080,#b5b5b5)} +.window>header strong{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.window>header button{width:22px;height:20px;padding:0;border:2px outset #fff;background:var(--os-face);color:#000;font:bold 12px Arial} +.window>header button:active,.start:active,.choices button:active,.officeChoices button:active,.dialogButtons button:active{border-style:inset;transform:translate(1px,1px)} +.content{flex:1;margin:3px;border:2px inset #eee;overflow:auto;background:#fff;min-height:0;position:relative} +.resizer{position:absolute;right:0;bottom:0;width:18px;height:18px;cursor:nwse-resize;background:repeating-linear-gradient(135deg,transparent 0 3px,#777 3px 4px,#fff 4px 5px);clip-path:polygon(100% 0,100% 100%,0 100%);touch-action:none} +.menuBar{display:flex;align-items:center;gap:12px;height:25px;padding:0 8px;background:var(--os-face);border-bottom:1px solid #777;font-size:13px}.menuBar button,.menuBar a{border:0;background:none;color:inherit;font:inherit;cursor:default;text-decoration:none}.menuBar a{color:var(--os-link);text-decoration:underline}.menuBar span{margin-left:auto;color:#555;font-size:12px} +/* taskbar and menus */ +.taskbar{height:36px;position:absolute;bottom:0;left:0;right:0;background:var(--os-face);border-top:2px outset #fff;display:flex;padding:3px;gap:4px;z-index:9999;overflow:hidden}.taskbar button{border:2px outset #fff;background:var(--os-face);color:#000;white-space:nowrap} +.start{font-weight:bold;font-size:14px}.start i{font-size:18px;color:#124fbc;font-style:normal}.startOpen{border-style:inset;background:#dcdcdc} +.task{width:175px;overflow:hidden;text-align:left;text-overflow:ellipsis}.taskActive{border-style:inset;background:#dcdcdc;font-weight:bold;background-image:repeating-conic-gradient(#fff 0 25%,#0000 0 50%);background-size:2px 2px} +.tray{margin-left:auto;border:2px inset #fff;padding:6px 10px;white-space:nowrap}.trayAlert{background:#ffef8a;color:#700;font-weight:bold} +.startMenu{position:absolute;z-index:10000;left:2px;bottom:36px;width:270px;background:var(--os-face);border:3px outset #fff;display:flex;box-shadow:3px 3px #111}.startMenu aside{width:30px;background:#777b88;color:#fff;writing-mode:vertical-rl;transform:rotate(180deg);font:bold 19px Arial;padding:6px}.startMenu>div{flex:1;padding:4px} +.startMenu button,.startMenu a{display:flex;align-items:center;gap:8px;width:100%;min-height:34px;border:0;background:none;color:#000;text-decoration:none;text-align:left;font:inherit;padding:0 4px}.startMenu button:hover,.startMenu a:hover,.startMenu button:focus-visible{background:var(--os-title-a);color:#fff;outline:0}.startMenu button span:last-child{margin-left:auto}.startMenu hr{border:0;border-top:1px solid #777;border-bottom:1px solid #fff;margin:3px 0} +.context{position:absolute;z-index:10001;width:150px;background:var(--os-face);border:2px outset #fff;padding:3px;box-shadow:2px 2px #111}.context button{display:block;width:100%;border:0;text-align:left;background:none;color:#000;padding:4px 6px;font:inherit}.context button:hover{background:var(--os-title-a);color:#fff}.context hr{border:0;border-top:1px solid #777;border-bottom:1px solid #fff} +/* My Computer */ +.explorer{display:flex;flex-direction:column;height:100%;background:#fff}.addressBar{display:flex;align-items:center;gap:6px;padding:4px 6px;background:var(--os-face);border-bottom:1px solid #777;font-size:12px}.addressBar button{border:2px outset #fff;background:var(--os-face);color:#000;padding:2px 6px;font:12px Tahoma}.addressBar button:disabled{color:#888}.addressBar input{flex:1;border:2px inset #eee;background:#fff;font:13px monospace;padding:2px 6px;min-width:0} +.explorerBody{display:grid;grid-template-columns:190px 1fr;flex:1;min-height:0}.tree{border-right:2px inset #eee;background:#f4f4f4;overflow:auto;padding:6px 0;display:flex;flex-direction:column}.treeItem{display:flex;align-items:center;gap:6px;border:0;background:none;text-align:left;font:13px Tahoma;padding:3px 8px;color:#000;white-space:nowrap}.treeItem:hover{background:#e6e6e6}.treeActive{background:var(--os-title-a)!important;color:#fff} +.fileGrid{display:grid;grid-template-columns:repeat(auto-fill,104px);align-content:start;gap:6px;padding:12px;overflow:auto}.fileGrid button{display:flex;flex-direction:column;align-items:center;gap:4px;border:1px solid transparent;background:none;padding:6px 2px;font:12px Tahoma;color:#000;word-break:break-word;text-align:center}.fileGrid button:focus-visible{outline:1px dotted #000}.fileSelected{background:var(--os-title-a)!important;color:#fff!important} +.emptyFolder{grid-column:1/-1;color:#666;padding:20px} +.folderIcon,.fileIcon,.exeIcon{display:block;width:34px;height:28px;position:relative;flex:none}.treeItem .folderIcon{width:16px;height:12px} +.folderIcon{background:#f0c14b;border:1px solid #7a5a00;border-radius:2px 2px 1px 1px;box-shadow:inset 0 -3px #d9a520}.folderIcon:before{content:"";position:absolute;left:-1px;top:-5px;width:45%;height:5px;background:#f0c14b;border:1px solid #7a5a00;border-bottom:0;border-radius:2px 2px 0 0} +.fileIcon{width:24px;height:30px;background:#fff;border:1px solid #444;background-image:repeating-linear-gradient(#0000 0 5px,#888 5px 6px);background-size:60% 100%;background-position:4px 8px;background-repeat:no-repeat}.fileIcon:before{content:"";position:absolute;right:-1px;top:-1px;border:5px solid;border-color:#c0c0c0 #c0c0c0 #444 #444} +.exeIcon{width:30px;height:30px;background:#c0c0c0;border:2px outset #fff}.exeIcon:before{content:"";position:absolute;inset:6px 6px 8px;background:var(--os-title-a)}.exeIcon:after{content:"";position:absolute;left:8px;right:8px;bottom:3px;height:2px;background:#333} +.statusBar{display:flex;justify-content:space-between;gap:12px;padding:3px 8px;background:var(--os-face);border-top:1px solid #777;font-size:12px}.statusBar span{border:1px inset #ddd;padding:1px 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.statusBar span:last-child{flex:1} +/* Notepad and terminal */ +.notepad{display:flex;flex-direction:column;height:100%}.notes{flex:1;width:100%;border:0;resize:none;padding:12px;font:15px/1.5 monospace;outline:0;color:#000;background:#fff} +.terminal{height:100%;box-sizing:border-box;background:#000;color:#ddd;padding:10px;font:15px monospace;overflow:auto;white-space:pre-wrap;word-break:break-word;cursor:text}.terminal input{background:#000;border:0;outline:0;color:#fff;font:inherit;width:70%} +/* Display Properties */ +.settings{display:flex;flex-direction:column;height:100%;background:var(--os-face);padding:10px}.tabs{display:flex;gap:2px}.tabs button{border:2px outset #fff;border-bottom:0;background:var(--os-face);color:#000;padding:4px 12px;font:13px Tahoma}.tabActive{position:relative;top:2px;background:#d8d8d8!important} +.tabPanel{flex:1;border:2px outset #fff;padding:14px;overflow:auto;display:flex;flex-direction:column;gap:14px} +.monitor{width:190px;height:150px;margin:0 auto;background:#ddd;border:6px solid #bbb;border-bottom-width:18px;border-radius:6px;padding:6px;box-shadow:2px 2px #555}.monitorScreen{height:100%;background-size:cover;position:relative;border:2px inset #999}.monitorScreen i{position:absolute;left:18%;top:22%;width:55%;height:8px;box-shadow:0 8px 0 0 #c0c0c0,0 16px 0 0 #c0c0c0,0 24px 0 0 #c0c0c0} +.settings fieldset{border:2px groove #fff;padding:8px 12px;display:grid;gap:6px}.settings legend{padding:0 4px}.settings label{display:flex;align-items:center;gap:8px;font-size:13px}.settings label i{width:16px;height:12px;border:1px solid #333} +.dialogButtons{display:flex;justify-content:flex-end;gap:6px;padding-top:10px}.dialogButtons button{min-width:76px;padding:5px 10px;border:2px outset #fff;background:var(--os-face);color:#000;font:13px Tahoma} +/* Internet, About, Help, Recycle Bin */ +.browser{display:flex;flex-direction:column;height:100%}.browser article{padding:24px 9%;line-height:1.6}.browser h1{color:var(--os-link);font-size:30px;margin:0 0 8px}.browser ul{padding-left:20px}.browser a{color:#00e;text-decoration:underline;cursor:pointer} +.empty,.panel,.about{text-align:center;padding:8%}.empty>b{font-size:55px}.empty button{display:block;margin:15px auto;padding:9px;border:3px outset #fff;background:var(--os-face);color:#900;font-weight:bold}.empty small{display:inline-block;background:#800;color:#fff;padding:5px 9px;font:bold 13px monospace} +.about{text-align:left;padding:28px 34px;line-height:1.55}.about>b{display:block;width:42px;height:38px;background:#d5d5d5;border:3px solid #fff;border-right-color:#303030;border-bottom-color:#303030;box-shadow:inset 0 0 0 3px #777;color:#000080;font:bold 9px monospace;line-height:32px;text-align:center}.about h1{margin:10px 0 0;font-size:26px}.aboutTitle{margin:0 0 12px;color:#555}.about a{color:var(--os-link)} +.panel{text-align:left;padding:24px 30px;line-height:1.55}.shortcuts{display:grid;grid-template-columns:auto 1fr;gap:6px 16px}.shortcuts dt{font-family:monospace;background:#e8e8e8;padding:1px 6px;border:1px solid #bbb;white-space:nowrap}.shortcuts dd{margin:0} +/* Minesweeper and Snake */ +.mines,.snake{display:flex;flex-direction:column;align-items:center;gap:10px;padding:16px;height:100%;box-sizing:border-box;background:var(--os-face)} +.counter{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;max-width:290px;padding:6px 8px;border:3px inset #fff;font:bold 13px monospace}.counter button{width:32px;height:32px;border:3px outset #fff;background:var(--os-face);font-size:18px}.led{background:#000;color:#f22;font:bold 22px/1 monospace;padding:3px 6px;letter-spacing:1px} +.grid{display:grid;grid-template-columns:repeat(9,30px);border:3px inset #fff}.grid button{height:30px;border:3px outset #fff;background:var(--os-face);font:bold 14px Tahoma;padding:0;color:#000}.grid button.open{border:1px solid #888;background:#c8c8c8}.grid .boom{background:#f33} +.grid button[data-n="1"]{color:#00f}.grid button[data-n="2"]{color:#080}.grid button[data-n="3"]{color:#f00}.grid button[data-n="4"]{color:#008}.grid button[data-n="5"]{color:#800}.grid button[data-n="6"]{color:#088}.grid button[data-n="7"]{color:#000}.grid button[data-n="8"]{color:#888} +.gameHint{margin:0;font-size:12px;color:#333;text-align:center} +.snakeBoard{position:relative;display:grid;grid-template-columns:repeat(16,1fr);width:min(100%,320px);aspect-ratio:1;background:#102510;border:3px inset #fff;outline:0}.snakeBoard:focus-visible{box-shadow:0 0 0 2px #9f9}.snakeBoard i{display:block;border:1px solid #0a1a0a;box-sizing:border-box}.snakeHead{background:#dfffd0}.snakeBody{background:#7fdb5f}.snakeFood{background:#f55;border-radius:50%} +.snakeOverlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:#00000099;color:#dfd;text-align:center;font:13px monospace;padding:16px}.snakeOverlay b{font-size:22px;letter-spacing:3px}.snakeOverlay button{margin-top:6px;padding:6px 14px;border:3px outset #fff;background:var(--os-face);color:#000;font:bold 12px Tahoma} +.dpad{display:none;grid-template-columns:repeat(4,1fr);gap:6px;width:100%;max-width:320px}.dpad button{height:44px;border:3px outset #fff;background:var(--os-face);font-size:16px} +/* stories */ +.office{box-sizing:border-box;min-height:100%;padding:7% 10%;background:#f5f0df;color:#242019;border:12px solid #ded5b9;box-shadow:inset 0 0 0 2px #777;font-family:Georgia,serif}.officeNo{font:12px monospace;letter-spacing:3px;border-bottom:1px solid #999;padding-bottom:12px}.office h1{font-size:30px;max-width:650px}.office p{font-size:19px;line-height:1.65;max-width:680px}.officeChoices{display:grid;gap:10px;max-width:660px;margin:28px 0}.officeChoices button{display:flex;align-items:center;gap:10px;text-align:left;padding:14px;background:#c0c0c0;border:3px outset #fff;font:bold 15px Tahoma;color:#000}.officeChoices button:hover,.officeChoices button:focus{background:#000080;color:#fff;outline:1px dotted #fff;outline-offset:-5px}.officeChoices u{display:grid;place-items:center;width:24px;height:24px;background:#fff;color:#000;border:1px solid #000;text-decoration:none}.officeChoices span{margin-left:auto;font-size:10px;letter-spacing:1px}.office small{font-family:monospace} +.adventure{height:100%;min-height:520px;background:#090d2a;color:#fff;display:grid;grid-template-columns:minmax(0,2fr) minmax(250px,1fr)}.scene{position:relative;overflow:hidden;background:linear-gradient(#07112d 0 55%,#cf6d46 56%,#151a3f 58%);min-height:520px;image-rendering:pixelated}.skyPixels{position:absolute;inset:8% 8% 46%;background:radial-gradient(circle,#fff 0 1px,transparent 2px);background-size:43px 31px;opacity:.7}.moon{position:absolute;right:17%;top:9%;color:#ffe9a8;font:70px Georgia;text-shadow:8px 3px #de7d52}.beam{position:absolute;left:35%;top:18%;width:42%;height:10%;background:#fff4a533;clip-path:polygon(0 40%,100% 0,100% 100%,0 60%)}.sea{position:absolute;inset:58% 0 0;background:repeating-linear-gradient(0deg,#101942 0 10px,#244574 11px 15px,#172753 16px 25px)}.cliffs{position:absolute;left:0;bottom:28%;width:58%;height:27%;background:#26353d;clip-path:polygon(0 25%,20% 8%,35% 40%,55% 15%,75% 55%,100% 38%,100% 100%,0 100%)}.lighthouse{position:absolute;left:29%;bottom:39%;width:54px;height:150px;background:repeating-linear-gradient(0deg,#eee 0 25px,#b54238 26px 48px);clip-path:polygon(17% 0,83% 0,100% 100%,0 100%);filter:drop-shadow(5px 5px #091020)}.lighthouse i{position:absolute;left:4px;top:-22px;width:46px;height:25px;background:#222;border:5px solid #eee}.lighthouse b{position:absolute;left:22px;bottom:0;width:14px;height:26px;background:#151a3f}.oracle{position:absolute;right:8%;bottom:22%;width:88px;height:150px;background:#b02f50;border:7px solid #421c3d;box-shadow:7px 7px #080b1d;text-align:center;font:bold 9px monospace}.oracle em{display:block;padding:5px;background:#f1b65a;color:#35142c;font-style:normal}.oracle i{display:block;height:54px;margin:8px;background:#1b1938;border:4px inset #df8090}.oracle b{color:#fff1a0}.dock{position:absolute;left:0;right:18%;bottom:6%;height:20px;background:#704832;transform:skewY(-4deg);box-shadow:0 10px #38271f}.scene p{position:absolute;left:4%;right:4%;bottom:1%;z-index:2;background:#050817e8;border:3px solid #fff;padding:12px;font:15px/1.4 monospace}.commandArea{padding:22px;background:#d2c4a1;color:#17152d;border-left:7px solid #080b1d}.commandArea>strong{display:block;margin:4px 0 9px;font:11px monospace;letter-spacing:2px}.verbs,.choices{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:22px}.verbs button,.choices button{min-height:42px;border:3px outset #fff;background:#c0c0c0;color:#000;font:bold 12px monospace;text-align:left;padding:8px}.verbs button:hover,.choices button:hover,.verbs button:focus,.choices button:focus{background:#fff3a1;outline:2px solid #201b48}.verbs button.selected{background:#201b48;color:#fff;border-style:inset}.choices{grid-template-columns:1fr}.choices span{float:right} +/* ants */ +.infestation{position:absolute;inset:0 0 36px;z-index:9000;pointer-events:none}.ant{--turn:0deg;position:absolute;width:15px;height:7px;pointer-events:none;animation:crawl 5s steps(12) infinite;filter:drop-shadow(1px 1px #fff8)}.ant b,.ant span,.ant em{position:absolute;top:2px;width:5px;height:5px;border-radius:50%;background:#160c08}.ant b{left:0}.ant span{left:5px}.ant em{left:10px}.ant:before,.ant:after{content:"";position:absolute;left:3px;top:-3px;width:9px;height:12px;border-top:1px solid #160c08;border-bottom:1px solid #160c08;transform:skewX(35deg)}.ant:after{transform:skewX(-35deg)}.ant.flee{animation-duration:1.2s}.file,.bin{animation-name:cluster} +.antDialog{position:absolute;right:18px;bottom:18px;width:390px;background:var(--os-face);border:3px outset #fff;box-shadow:4px 4px #111;pointer-events:auto;color:#000}.antDialog header{background:var(--os-title-a);color:#fff;padding:5px;font-weight:bold}.antDialog>div{display:flex;gap:15px;padding:18px}.antDialog>div>strong{font-size:34px;color:#d88a00}.antDialog p{margin:0;line-height:1.6}.antDialog small{background:#700;color:#fff;padding:3px;font-family:monospace}.antDialog footer{text-align:right;padding:10px;border-top:1px solid #777}.antDialog button{padding:7px 14px;border:3px outset #fff;background:var(--os-face);color:#000} +@keyframes crawl{50%{transform:translate(110px,-30px) rotate(var(--turn))}100%{transform:translate(0,-80px) rotate(var(--turn))}}@keyframes cluster{50%{transform:translate(22px,15px) rotate(var(--turn))}100%{transform:translate(-15px,-18px) rotate(var(--turn))}} +@media(max-width:650px){.os{font-size:13px}.wallmark{font-size:40px}.icons{grid-template-columns:repeat(3,88px);width:auto;padding:10px;gap:8px}.window{left:4px!important;top:4px!important;width:calc(100% - 8px)!important;height:calc(100% - 44px)!important;min-width:0}.resizer{display:none}.task{max-width:70px}.explorerBody{grid-template-columns:1fr}.tree{display:none}.office{padding:10% 6%;border-width:5px}.office h1{font-size:23px}.office p{font-size:16px}.adventure{grid-template-columns:1fr;min-height:100%}.scene{min-height:58vh}.commandArea{border-left:0;border-top:6px solid #080b1d;padding:12px}.verbs{grid-template-columns:repeat(4,1fr)}.verbs button{font-size:9px;padding:4px}.antDialog{left:8px;right:8px;bottom:8px;width:auto}.tray{padding:5px}.startMenu{width:280px}.dpad{display:grid}} +.hintMouse,.hintTouch{font-style:normal}.hintTouch{display:none}@media(hover:none){.dpad{display:grid}.hintMouse{display:none}.hintTouch{display:inline}} +@media(prefers-reduced-motion:reduce){.ant{animation:none}.beam{display:none}} diff --git a/src/components/maxwell-os/MaxwellOS.tsx b/src/components/maxwell-os/MaxwellOS.tsx index 1b50eff0..5f8bef66 100644 --- a/src/components/maxwell-os/MaxwellOS.tsx +++ b/src/components/maxwell-os/MaxwellOS.tsx @@ -1,23 +1,186 @@ "use client"; -import {FormEvent,useEffect,useReducer,useRef,useState} from "react"; -import {ADVENTURE_GRAPH,AntState,AppId,OFFICE_GRAPH,antEscalation,antPosition,parseCommand,windowReducer,initialOSState} from "@/lib/maxwellOS"; +// The Maxwell OS shell: desktop, windows, taskbar, Start menu, persistence. +// Rendered client-only (see MaxwellOSLoader) so the saved session can be read +// synchronously during the first render instead of after a hydration flash. +import { useCallback, useEffect, useReducer, useRef, useState, type PointerEvent as ReactPointerEvent } from "react"; import Link from "next/link"; +import { defaultPrefs, initialOSState, schemeById, topWindow, wallpaperById, windowReducer, type AppId, type OSPrefs, type OSState, type OSWindow, type WindowPayload } from "@/lib/maxwellOS"; import styles from "./MaxwellOS.module.css"; +import { About, Browser, Files, Help, Notepad, Settings, Terminal, Trash } from "./apps"; +import { Ants, Mines, Snake, Story } from "./games"; -const apps:{id:AppId;title:string;icon:string}[]=[{id:"files",title:"My Computer",icon:"PC"},{id:"about",title:"About Maxwell",icon:"MY"},{id:"browser",title:"Internet",icon:"WWW"},{id:"notes",title:"Notepad",icon:"TXT"},{id:"terminal",title:"MS-DOS Prompt",icon:"C:"},{id:"mines",title:"Minesweeper",icon:"*"},{id:"snake",title:"Snake",icon:"S"},{id:"adventure",title:"Elsewhere",icon:"MOON"},{id:"office",title:"The Office",icon:"DOOR"},{id:"trash",title:"Recycle Bin",icon:"BIN"}]; -function PixelIcon({name,small=false}:{name:string;small?:boolean}){return } -export default function MaxwellOS(){const [os,dispatch]=useReducer(windowReducer,initialOSState);const[start,setStart]=useState(false);const[menu,setMenu]=useState<{x:number;y:number}|null>(null);const[ants,setAnts]=useState({active:false,count:0,generation:0});const[clock,setClock]=useState("");const open=(id:AppId)=>{const a=apps.find(x=>x.id===id)!;dispatch({type:"open",app:id,title:a.title});setStart(false)}; -useEffect(()=>{const tick=()=>setClock(new Date().toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}));tick();const id=setInterval(tick,1000);return()=>clearInterval(id)},[]); -useEffect(()=>{if(!ants.active)return;const id=setInterval(()=>setAnts(s=>({...s,count:antEscalation(s.count)})),3500);return()=>clearInterval(id)},[ants.active]); -useEffect(()=>()=>{document.title="Maxwell Young"},[]); -useEffect(()=>{const key=(event:KeyboardEvent)=>{if(event.key!=="Escape")return;const top=[...os.windows].filter(w=>!w.minimized).sort((a,b)=>b.z-a.z)[0];if(top)dispatch({type:"close",id:top.id});else location.assign("/")};addEventListener("keydown",key);return()=>removeEventListener("keydown",key)},[os.windows]); -return
menu&&setMenu(null)} onContextMenu={e=>{e.preventDefault();setMenu({x:e.clientX,y:e.clientY})}}>
MAXWELL
PROFESSIONAL
{apps.filter(a=>["files","about","browser","adventure","office","trash"].includes(a.id)).map(a=>)}
-{os.windows.map(w=>!w.minimized&&
dispatch({type:"focus",id:w.id})}>
a.id===w.app)?.icon||"?"} small/>{w.title}
setAnts(s=>({active:true,count:Math.min(80,s.count+28),generation:s.generation}))}/>
)} -{ants.active&&setAnts(s=>({active:false,count:0,generation:s.generation+1}))}/>} {menu&&

} -
{os.windows.map(w=>)}
{ants.active?`⚠ ANTS ${ants.count}`:"VOL"} {clock}
{start&&
{apps.map(a=>)}
⏻ Shut Down...
}
} -function App({id,open,release}:{id:AppId;open:(x:AppId)=>void;release:()=>void}){if(id==="terminal")return ;if(id==="adventure")return ;if(id==="office")return ;if(id==="mines")return ;if(id==="snake")return ;if(id==="notes")return