├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── index.html ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── FSEX302.ttf ├── brand │ ├── duck.png │ └── duck_raw.png ├── buttons │ └── button │ │ ├── Default.svg │ │ ├── Focus.svg │ │ └── Hover.svg ├── icons │ ├── applications │ │ ├── Brosen_windrose.svg │ │ ├── calculator.svg │ │ ├── cardFile.svg │ │ ├── controlPanel.svg │ │ ├── disk.svg │ │ ├── paint.svg │ │ ├── pxArt (1).png │ │ ├── pyide.png │ │ ├── quachat.png │ │ └── terminal.png │ ├── arrows │ │ ├── arrow │ │ │ ├── down.svg │ │ │ ├── left.svg │ │ │ ├── right.svg │ │ │ └── up.svg │ │ └── chevron │ │ │ ├── down.svg │ │ │ ├── left.svg │ │ │ ├── right.svg │ │ │ └── up.svg │ ├── clock │ │ └── Clock_Face.svg │ ├── cursors │ │ ├── Cursor.svg │ │ ├── textCursor.svg │ │ └── touchCursor.svg │ ├── paint │ │ ├── airbrush.svg │ │ ├── circle │ │ │ ├── filled.svg │ │ │ └── outline.svg │ │ ├── curve.svg │ │ ├── ellipse │ │ │ ├── filled.svg │ │ │ └── outline.svg │ │ ├── eraser.svg │ │ ├── frame.svg │ │ ├── line.svg │ │ ├── move.svg │ │ ├── net.svg │ │ ├── object.svg │ │ ├── paintBucket.svg │ │ ├── paintbrush.svg │ │ ├── pencil.svg │ │ ├── rectangle │ │ │ ├── filled.svg │ │ │ └── outline.svg │ │ ├── roundedRectangle │ │ │ ├── filled.svg │ │ │ └── outline.svg │ │ ├── shape │ │ │ ├── filled.svg │ │ │ └── outline.svg │ │ ├── text.svg │ │ └── triangle │ │ │ ├── filled.svg │ │ │ └── outline.svg │ └── system │ │ ├── floppyDisk.svg │ │ └── warning.svg ├── pattern │ ├── diagonal │ │ ├── dark.svg │ │ └── light.svg │ ├── dotted │ │ ├── dark.svg │ │ ├── light.svg │ │ ├── lightAlt.svg │ │ └── medium.svg │ ├── hatch │ │ ├── dark.svg │ │ └── light.svg │ ├── scroll.svg │ └── vertical │ │ ├── dark.svg │ │ └── light.svg ├── screenshots │ ├── boot.png │ ├── home.png │ └── home_start.png └── vite.svg ├── src ├── App.tsx ├── components │ ├── AppIcon │ │ ├── index.tsx │ │ └── types.ts │ ├── Application │ │ ├── AppWrapper.tsx │ │ ├── Draggable.tsx │ │ ├── helper.ts │ │ ├── index.tsx │ │ └── types.ts │ ├── Apps │ │ ├── Calculator │ │ │ ├── ReversePolishNotation.spec.ts │ │ │ ├── ReversePolishNotation.ts │ │ │ ├── helper.ts │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── Clock.tsx │ │ ├── Navigator.tsx │ │ ├── PyIDE.tsx │ │ ├── QuaChat.tsx │ │ └── Terminal │ │ │ ├── helper.ts │ │ │ ├── index.tsx │ │ │ ├── terminal.module.css │ │ │ └── types.ts │ ├── Dropdown │ │ ├── index.tsx │ │ └── types.ts │ ├── TopBar │ │ ├── helper.tsx │ │ └── index.tsx │ ├── WelcomeCard.tsx │ └── ui │ │ └── Button.tsx ├── contexts │ ├── ApplicationContext.tsx │ ├── WebContainerContext.tsx │ └── WindowContext.tsx ├── hooks │ ├── useApp.tsx │ └── usePython.tsx ├── index.css ├── libs │ ├── chat.ts │ ├── protocol.ts │ └── workers │ │ └── python-worker.ts ├── main.tsx ├── pages │ ├── Desktop │ │ └── index.tsx │ ├── Home.tsx │ └── Loading.tsx ├── stores │ ├── iconsStore.ts │ └── pyIDEStore.tsx ├── styles │ └── globals.scss ├── types │ └── ApplicationType.ts └── vite.d.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "extends": [ 8 | "plugin:react/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "settings": { 12 | "react": { 13 | "version": "detect" 14 | } 15 | }, 16 | "parserOptions": { 17 | "project": "./tsconfig.json" 18 | }, 19 | "overrides": [ 20 | ], 21 | "plugins": [ 22 | "react", 23 | "prettier" 24 | ], 25 | "ignorePatterns": [ 26 | "node_modules", 27 | "dist" 28 | ], 29 | "rules": { 30 | "react/react-in-jsx-scope": "off" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | /stats.html -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": false, 6 | "trailingComma": "all", 7 | "arrowParens": "always", 8 | "semi": false, 9 | "endOfLine": "auto" 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jônatas Araújo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
{display.slice(0, 10)}
55 |",
18 | fn: (...args: string[]) => {
19 | return new Promise((resolve) => {
20 | if (args.length === 0) {
21 | return "Please enter a code to run. Example: python print('Hello World')"
22 | }
23 |
24 | runCode(args.join(" "), (result: string) => {
25 | resolve(result)
26 | })
27 | })
28 | },
29 | },
30 | ls: {
31 | description: "List all files and folders in the current directory.",
32 | usage: "ls",
33 | fn: (...args: string[]) => {
34 | return new Promise((resolve) => {
35 | exec(["ls", ...args], (result: string) => {
36 | if (!result) {
37 | return resolve("No files or folders found")
38 | }
39 | resolve(result)
40 | })
41 | })
42 | },
43 | },
44 | pwd: {
45 | description: "Show the current directory.",
46 | usage: "pwd",
47 | fn: () => {
48 | return new Promise((resolve) => {
49 | exec(["pwd"], (result: string) => {
50 | resolve(result)
51 | })
52 | })
53 | },
54 | },
55 |
56 | rm: {
57 | description: "Remove a file or folder.",
58 | usage: "rm ",
59 | fn: (...args: string[]) => {
60 | if (args.length === 0) {
61 | return "Please enter a file or folder name. Example: rm newFolder"
62 | }
63 |
64 | const isRecursive = args[0] === "-rf"
65 |
66 | if (isRecursive) {
67 | args.shift()
68 | }
69 |
70 | if (args.length === 1 && args[0] === "/") {
71 | return "Cannot remove root directory"
72 | }
73 |
74 | return new Promise((resolve) => {
75 | webContainer.fs
76 | .rm(args.join(" "), {
77 | recursive: isRecursive,
78 | })
79 | .then(() => {
80 | resolve("File or folder removed")
81 | })
82 | .catch(() => {
83 | resolve("File or folder not found")
84 | })
85 | })
86 | },
87 | },
88 | exec: {
89 | description: "Execute a command.",
90 | usage: "exec ",
91 | fn: (...args: string[]) => {
92 | if (args.length === 0) {
93 | return "Please enter a command. Example: exec ls"
94 | }
95 |
96 | return new Promise((resolve) => {
97 | exec(args, (result: string) => {
98 | resolve(result)
99 | })
100 | })
101 | },
102 | },
103 | cat: {
104 | description: "Show the content of a file.",
105 | usage: "cat ",
106 | fn: (...args: string[]) => {
107 | if (args.length === 0) {
108 | return "Please enter a file name. Example: cat newFile"
109 | }
110 |
111 | if (args[0] === ">") {
112 | return webContainer.fs
113 | .writeFile(args[1], args.slice(2).join(" "))
114 | .then(() => {
115 | return "File created"
116 | })
117 | .catch(() => {
118 | return "File already exists"
119 | })
120 | }
121 |
122 | return new Promise((resolve) => {
123 | webContainer.fs
124 | .readFile(args.join(" "), "utf-8")
125 | .then((result) => {
126 | resolve(result)
127 | })
128 | .catch(() => {
129 | resolve("File not found")
130 | })
131 | })
132 | },
133 | },
134 | cd: {
135 | description: "Change directory.",
136 | usage: "cd ",
137 | fn: (...args: string[]) => {
138 | if (args.length === 0) {
139 | return "Please enter a directory name. Example: cd newFolder"
140 | }
141 |
142 | return new Promise(async (resolve) => {
143 | try {
144 | await cd(args.join(" "))
145 | resolve("")
146 | } catch (error: any) {
147 | resolve(error.message)
148 | }
149 | })
150 | },
151 | },
152 | mkdir: {
153 | description: "Create a new directory.",
154 | usage: "mkdir ",
155 | fn: (...args: string[]) => {
156 | if (args.length === 0) {
157 | return "Please enter a directory name. Example: mkdir newFolder"
158 | }
159 |
160 | return new Promise((resolve) => {
161 | webContainer.fs
162 | .mkdir(args.join(" "))
163 | .then(() => {
164 | resolve("Directory created")
165 | })
166 | .catch(() => {
167 | resolve("Directory already exists")
168 | })
169 | })
170 | },
171 | },
172 | ps: {
173 | description: "List all processes.",
174 | usage: "ps",
175 | fn: () => {
176 | let text = "------------------\n"
177 | text += "TITLE - ID - TIME\n"
178 | text += "------------------\n"
179 |
180 | text += apps
181 | .map(
182 | (app) =>
183 | `${app.title} - ${app.id} - ${(
184 | (Date.now() - app.start!) /
185 | 1000
186 | ).toFixed(2)} sec(s)`,
187 | )
188 | .join("\n")
189 |
190 | return text
191 | },
192 | },
193 | reboot: {
194 | description: "Reboot the computer.",
195 | usage: "reboot",
196 | fn: () => {
197 | window.location.reload()
198 | return ""
199 | },
200 | },
201 | kill: {
202 | description: "Kill a process.",
203 | usage: "kill ",
204 | fn: (...args: string[]) => {
205 | const id = args.join("")
206 | const app = apps.find((app) => app.id === id)
207 | if (app) {
208 | removeApp(id)
209 | return "Process killed"
210 | }
211 | return "Process not found"
212 | },
213 | },
214 | }
215 | }
216 |
217 | export { commands }
218 |
--------------------------------------------------------------------------------
/src/components/Apps/Terminal/index.tsx:
--------------------------------------------------------------------------------
1 | import ReactTerminal from "react-console-emulator"
2 | import { commands } from "./helper"
3 | import styles from "./terminal.module.css"
4 | import { useEffect } from "react"
5 | import { useWindow } from "../../../contexts/WindowContext"
6 | import { usePython } from "../../../hooks/usePython"
7 |
8 | export default function Terminal() {
9 | const { setInitialSize, setIsResizable } = useWindow()
10 | const { runCode } = usePython()
11 |
12 | const commandList = commands(runPython)
13 |
14 | function runPython(code: string, cb: (result: string) => void) {
15 | runCode(code, cb)
16 | }
17 |
18 | useEffect(() => {
19 | setInitialSize({
20 | width: 550,
21 | height: 380,
22 | })
23 | setIsResizable(true)
24 | }, [])
25 |
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/Apps/Terminal/terminal.module.css:
--------------------------------------------------------------------------------
1 | .terminal {
2 | background-color: theme("colors.white") !important;
3 | color: theme("colors.black") !important;
4 | border-radius: 0 !important;
5 | }
6 |
7 | .text {
8 | color: theme("colors.black") !important;
9 | font-family: "Fixedsys Excelsior", monospace !important;
10 | }
11 |
12 | .content, .promptLabel, .input {
13 | color: theme("colors.black") !important;
14 | font-size: theme("fontSize.xs") !important;
15 | }
16 |
17 | .inputArea {
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | }
22 |
23 | .message {
24 | color: theme("colors.black") !important;
25 | font-size: theme("fontSize.xs") !important;
26 | }
--------------------------------------------------------------------------------
/src/components/Apps/Terminal/types.ts:
--------------------------------------------------------------------------------
1 | interface Commands {
2 | [key: string]: TerminalCommand
3 | }
4 |
5 | interface TerminalCommand {
6 | description: string
7 | usage: string
8 | fn: (...args: string[]) => string | Promise
9 | }
10 |
11 | export type { Commands }
12 |
--------------------------------------------------------------------------------
/src/components/Dropdown/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react"
2 | import { useClickAway } from "react-use"
3 | import { IDropdownProps } from "./types"
4 | import clsx from "clsx"
5 |
6 | export default function Dropdown({
7 | items,
8 | isOpen,
9 | children,
10 | close,
11 | }: IDropdownProps) {
12 | const triggerRef = useRef(null)
13 |
14 | useClickAway(triggerRef, () => {
15 | close()
16 | })
17 | return (
18 |
19 | {children}
20 |
25 | {items.map((item) => (
26 |
30 | {item.Node}
31 |
32 | ))}
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Dropdown/types.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from "react"
2 |
3 | interface IDropdownProps {
4 | children: ReactElement
5 | isOpen: boolean
6 | close: () => void
7 | items: {
8 | id: string
9 | Node: ReactElement
10 | }[]
11 | }
12 |
13 | export type { IDropdownProps }
14 |
--------------------------------------------------------------------------------
/src/components/TopBar/helper.tsx:
--------------------------------------------------------------------------------
1 | import { ApplicationType } from "../../contexts/ApplicationContext"
2 |
3 | const items = (useApps: ApplicationType) => {
4 | const { addApp } = useApps
5 |
6 | return [
7 | {
8 | id: "1",
9 | Node: (
10 |
13 | ),
14 | },
15 | ]
16 | }
17 |
18 | export { items }
19 |
--------------------------------------------------------------------------------
/src/components/TopBar/index.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react"
2 | import { useClickAway } from "react-use"
3 | import Dropdown from "../Dropdown"
4 |
5 | import { useApps } from "../../hooks/useApp"
6 |
7 | import { items } from "./helper"
8 | import clsx from "clsx"
9 |
10 | export default function TopBar() {
11 | const apps = useApps()
12 |
13 | const [isActive, setIsActive] = useState(false)
14 | const [open, setOpen] = useState(false)
15 |
16 | const containerRef = useRef(null)
17 |
18 | const osIconRef = useRef(null)
19 |
20 | useClickAway(containerRef, () => {
21 | setIsActive(false)
22 | })
23 |
24 | return (
25 |
29 | {
32 | setOpen(false)
33 | }}
34 | items={items(apps)}
35 | >
36 |
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/WelcomeCard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx"
2 | import { useState } from "react"
3 | import { useWindowSize } from "react-use"
4 | function WelcomeCard() {
5 | const { width, height } = useWindowSize()
6 | const [isOpen, setIsOpen] = useState(true)
7 |
8 | const cardWidth = 400
9 | const cardHeight = 300
10 |
11 | const handleClose = () => setIsOpen(false)
12 |
13 | return (
14 |
24 |
25 | Welcome to QuackOS!
26 |
27 | This is a simple (and fake) operating system made with ReactJS and
28 | Vite.
29 |
30 | I hope you enjoy it!
31 |
34 |
35 |
36 | )
37 | }
38 |
39 | export default WelcomeCard
40 |
--------------------------------------------------------------------------------
/src/components/ui/Button.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx"
2 |
3 | interface Props extends React.ButtonHTMLAttributes {
4 | children: React.ReactNode
5 | }
6 |
7 | export default function Button({ children, className, ...rest }: Props) {
8 | return (
9 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/contexts/ApplicationContext.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | useState,
4 | ReactNode,
5 | useMemo,
6 | lazy,
7 | useEffect,
8 | } from "react"
9 | import { App } from "../types/ApplicationType"
10 |
11 | const APPLICATIONS = {
12 | clock: lazy(() => import("../components/Apps/Clock")),
13 | calculator: lazy(() => import("../components/Apps/Calculator")),
14 | navigator: lazy(() => import("../components/Apps/Navigator")),
15 | pyide: lazy(() => import("../components/Apps/PyIDE")),
16 | terminal: lazy(() => import("../components/Apps/Terminal")),
17 | quachat: lazy(() => import("../components/Apps/QuaChat")),
18 | }
19 |
20 | export type ApplicationName = keyof typeof APPLICATIONS
21 |
22 | interface AddAppProps {
23 | name: ApplicationName
24 | x?: number
25 | y?: number
26 | }
27 |
28 | export interface ApplicationType {
29 | apps: App[]
30 | addApp: (props: AddAppProps) => void
31 | removeApp: (id: string) => void
32 | clearApps: () => void
33 | appOnFocus: string
34 | setAppOnFocus: (id: string) => void
35 | }
36 |
37 | const ApplicationContext = createContext({} as ApplicationType)
38 |
39 | function randomFixedInteger(length: number) {
40 | return Math.floor(
41 | Math.pow(10, length - 1) +
42 | Math.random() * (Math.pow(10, length) - Math.pow(10, length - 1) - 1),
43 | )
44 | }
45 |
46 | const ApplicationProvider = ({ children }: { children: ReactNode }) => {
47 | const [apps, setApps] = useState([])
48 | const [appOnFocus, setAppOnFocus] = useState("")
49 |
50 | useEffect(() => {
51 | if (!appOnFocus) return
52 | if (apps.length <= 1) return
53 |
54 | const index = apps.findIndex(({ id }) => id === appOnFocus)
55 | const app = apps[index]
56 |
57 | const copyApps = apps.toSpliced(index, 1)
58 | copyApps.push(app)
59 |
60 | setApps(copyApps)
61 | }, [appOnFocus])
62 |
63 | const addApp = ({ name, x, y }: AddAppProps) => {
64 | const Comp = APPLICATIONS[name]
65 |
66 | const app: App = {
67 | id: randomFixedInteger(8).toString(),
68 | Node: Comp,
69 | title: name,
70 | start: Date.now(),
71 | x: x,
72 | y: y,
73 | }
74 |
75 | setAppOnFocus(app.id)
76 |
77 | setApps([...apps, app])
78 | }
79 |
80 | const removeApp = (id: string) => {
81 | setApps((prev) => prev.filter((app) => app.id !== id))
82 | }
83 |
84 | const clearApps = () => {
85 | setApps([])
86 | }
87 |
88 | return (
89 |
99 | {children}
100 |
101 | )
102 | }
103 |
104 | export { ApplicationContext }
105 |
106 | export default ApplicationProvider
107 |
--------------------------------------------------------------------------------
/src/contexts/WebContainerContext.tsx:
--------------------------------------------------------------------------------
1 | import type { WebContainer } from "@webcontainer/api"
2 | import { createContext, useContext, useEffect, useState } from "react"
3 |
4 | interface WebContainerContextProps {
5 | exec(cmd: string[], output: (chunk: string) => void): Promise
6 | webContainer: WebContainer
7 | cd(path: string): void
8 | }
9 |
10 | const WebContainerContext = createContext({} as WebContainerContextProps)
11 |
12 | export const useWebContainer = () => useContext(WebContainerContext)
13 |
14 | export const WebContainerProvider = ({
15 | children,
16 | }: {
17 | children: React.ReactNode
18 | }) => {
19 | const [webContainer, setWebContainer] = useState()
20 | const [path, setPath] = useState("/home/quackos")
21 | const [loading, setLoading] = useState(false)
22 |
23 | function cd(to: string) {
24 | return new Promise((resolve, reject) => {
25 | let newPath = ""
26 |
27 | if (to.startsWith("/")) {
28 | newPath = to
29 | } else {
30 | newPath = `${path}/${to}`
31 | }
32 |
33 | webContainer?.spawn("jsh", ["-c", `cd ${newPath}`]).then((process) => {
34 | process.exit.then((exitCode) => {
35 | if (exitCode !== 0) {
36 | reject(new Error(`cd: ${to}: No such file or directory`))
37 | } else {
38 | setPath(newPath)
39 | resolve()
40 | }
41 | })
42 | })
43 | })
44 | }
45 |
46 | async function exec(cmd: string[], output: (chunk: string) => void) {
47 | if (!webContainer || loading) {
48 | load()
49 | return output("Loading web container...")
50 | }
51 |
52 | const process = await webContainer.spawn("jsh", [
53 | "-c",
54 | `cd ${path}; ${cmd.join(" ")}`,
55 | ])
56 |
57 | const installExitCode = await process.exit
58 |
59 | if (installExitCode !== 0) {
60 | return process.output.pipeTo(
61 | new WritableStream({
62 | write(chunk) {
63 | output("Error: " + chunk)
64 | process.kill()
65 | },
66 | }),
67 | )
68 | }
69 |
70 | process.output.pipeTo(
71 | new WritableStream({
72 | write(chunk) {
73 | output(chunk)
74 | process.kill()
75 | },
76 | }),
77 | )
78 | }
79 |
80 | async function load() {
81 | if (webContainer || loading) {
82 | console.log("Web container already loaded")
83 | return
84 | }
85 | setLoading(true)
86 | console.log("Loading web container...")
87 |
88 | const WebContainer = await import("@webcontainer/api").then(
89 | (m) => m.WebContainer,
90 | )
91 |
92 | const container = await WebContainer.boot({
93 | workdirName: "quackos",
94 | })
95 | setWebContainer(container)
96 | setLoading(false)
97 | }
98 |
99 | useEffect(() => {
100 | return () => {
101 | webContainer?.teardown()
102 | }
103 | }, [])
104 |
105 | return (
106 |
109 | {children}
110 |
111 | )
112 | }
113 |
--------------------------------------------------------------------------------
/src/contexts/WindowContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from "react"
2 |
3 | interface IWindowContext {
4 | setIsResizable: (isResizable: boolean) => void
5 | isFullscreen: boolean
6 | appId: string
7 |
8 | setInitialSize: ({ width, height }: { width: number; height: number }) => void
9 | initialSize: { width: number; height: number }
10 | }
11 |
12 | const WindowContext = createContext({} as IWindowContext)
13 |
14 | export const useWindow = () => useContext(WindowContext)
15 |
16 | interface IWindowProvider extends IWindowContext {
17 | children: React.ReactNode
18 | }
19 |
20 | export function WindowProvider({
21 | children,
22 | setIsResizable,
23 | isFullscreen,
24 | appId,
25 | initialSize,
26 | setInitialSize,
27 | }: IWindowProvider) {
28 | return (
29 |
38 | {children}
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/src/hooks/useApp.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react"
2 | import { ApplicationContext } from "../contexts/ApplicationContext"
3 |
4 | export function useApps() {
5 | const context = useContext(ApplicationContext)
6 | if (!context)
7 | throw new Error("useApps must be used within an ApplicationProvider")
8 | return context
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/usePython.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react"
2 | import PythonWorker from "../libs/workers/python-worker?worker"
3 |
4 | export function usePython() {
5 | const worker = useRef(null)
6 | const interruptBuffer = useRef(new Uint8Array(new SharedArrayBuffer(1)))
7 |
8 | useEffect(() => {
9 | worker.current = new PythonWorker()
10 | worker.current.postMessage({
11 | cmd: "setInterruptBuffer",
12 | interruptBuffer: interruptBuffer.current,
13 | })
14 |
15 | return () => {
16 | console.log("Terminating worker")
17 | worker.current?.terminate()
18 | }
19 | }, [])
20 |
21 | function interruptExecution() {
22 | interruptBuffer.current[0] = 2
23 | }
24 |
25 | function runCode(code: string, cb: (output: string) => void) {
26 | interruptExecution() // Stop execution if it's running
27 | interruptBuffer.current[0] = 0 // Reset the interrupt buffer
28 |
29 | worker.current?.postMessage({ code })
30 | worker.current?.addEventListener("message", (e) => {
31 | const { stdout } = e.data
32 |
33 | cb(stdout)
34 | })
35 | }
36 |
37 | return {
38 | runCode,
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/src/libs/chat.ts:
--------------------------------------------------------------------------------
1 | import * as protocol from "./protocol"
2 |
3 | export class LetsChat {
4 | private socket?: WebSocket
5 | private account?: protocol.Account
6 | constructor(private addr: string) {}
7 |
8 | connect() {
9 | return new Promise((resolve) => {
10 | const socket = new WebSocket(this.addr)
11 | socket.binaryType = "arraybuffer"
12 |
13 | this.socket = socket
14 | socket.onopen = () => {
15 | resolve(true)
16 | }
17 | })
18 | }
19 |
20 | async ping() {
21 | return this.sendPacket(new protocol.PingMessage().toPacket())
22 | }
23 |
24 | async auth(username: string): Promise {
25 | await this.sendPacket(new protocol.ClientAuthMessage(username).toPacket())
26 |
27 | const serverAuthMsg = protocol.ServerAuthMessage.fromPacket(
28 | await this.readPacket(),
29 | )
30 | if (serverAuthMsg.status !== "ok") {
31 | console.log("failed to login")
32 | return null
33 | }
34 |
35 | this.account = serverAuthMsg.account
36 | return this.account ?? null
37 | }
38 |
39 | async sendPacket(pkt: protocol.Packet) {
40 | this.send(pkt.toBinary())
41 | }
42 |
43 | onMessage(cb: (msg: protocol.ChatMessage) => void) {
44 | if (!this.socket) {
45 | return
46 | }
47 |
48 | this.socket.onmessage = (ev) => {
49 | const pkt = protocol.Packet.fromBytes(ev.data)
50 | cb(protocol.ChatMessage.fromPacket(pkt))
51 | }
52 | }
53 |
54 | async readPacket() {
55 | const data = await await this.read()
56 | return protocol.Packet.fromBytes(data)
57 | }
58 |
59 | send(message: any) {
60 | this.socket?.send(message)
61 | }
62 |
63 | read(): Promise {
64 | return new Promise((resolve, reject) => {
65 | if (!this.socket) {
66 | return reject(new Error("Socket is not initialized"))
67 | }
68 |
69 | this.socket.onmessage = (ev) => {
70 | resolve(ev.data)
71 | }
72 |
73 | // Se desejar, você pode também adicionar um tratamento para onerror
74 | this.socket.onerror = (error) => {
75 | reject(error)
76 | }
77 | })
78 | }
79 |
80 | close() {
81 | this.socket?.close()
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/libs/protocol.ts:
--------------------------------------------------------------------------------
1 | export enum PacketProtocolVersion {
2 | ProtocolVersion = 1,
3 | }
4 |
5 | export enum PacketType {
6 | PacketTypeAuth,
7 | PacketTypeMessage,
8 | PacketTypePing,
9 | PacketTypePong,
10 | }
11 |
12 | export class PacketHeader {
13 | version: PacketProtocolVersion
14 | packetType: PacketType
15 | length: number
16 |
17 | constructor(
18 | version: PacketProtocolVersion,
19 | packetType: PacketType,
20 | length: number,
21 | ) {
22 | this.version = version
23 | this.packetType = packetType
24 | this.length = length
25 | }
26 | }
27 |
28 | export class Packet {
29 | header: PacketHeader
30 | payload: Uint8Array
31 |
32 | constructor(header: PacketHeader, payload: Uint8Array) {
33 | this.header = header
34 | this.payload = payload
35 | }
36 |
37 | static newPacket(packetType: PacketType, payload: Uint8Array): Packet {
38 | return new Packet(
39 | new PacketHeader(
40 | PacketProtocolVersion.ProtocolVersion,
41 | packetType,
42 | payload.length,
43 | ),
44 | payload,
45 | )
46 | }
47 |
48 | static fromBytes(data: ArrayBuffer): Packet {
49 | const view = new DataView(data)
50 | let offset = 0
51 |
52 | const version = view.getUint8(offset)
53 | offset += 1
54 |
55 | const packetType = view.getUint8(offset)
56 | offset += 1
57 |
58 | const length = view.getUint16(offset, false) // Big Endian
59 | offset += 2
60 |
61 | const payload = new Uint8Array(data, offset, length)
62 |
63 | const header = new PacketHeader(
64 | version as PacketProtocolVersion,
65 | packetType as PacketType,
66 | length,
67 | )
68 | return new Packet(header, payload)
69 | }
70 |
71 | toBinary(): ArrayBuffer {
72 | const buffer = new ArrayBuffer(4 + this.payload.length)
73 | const view = new DataView(buffer)
74 | let offset = 0
75 |
76 | view.setUint8(offset, this.header.version)
77 | offset += 1
78 |
79 | view.setUint8(offset, this.header.packetType)
80 | offset += 1
81 |
82 | view.setUint16(offset, this.header.length, false) // Big Endian
83 | offset += 2
84 |
85 | new Uint8Array(buffer, offset, this.payload.length).set(this.payload)
86 |
87 | return buffer
88 | }
89 | }
90 |
91 | export class ChatRoom {
92 | id: string
93 | name: string
94 |
95 | constructor(id: string, name: string) {
96 | this.id = id
97 | this.name = name
98 | }
99 | }
100 |
101 | export class ChatMessage {
102 | id: string
103 | is_server: boolean
104 | room: ChatRoom
105 | author: Account | null
106 | content: string
107 | created_at: Date
108 | is_command: boolean
109 |
110 | constructor(
111 | author: Account | null,
112 | content: string,
113 | room: ChatRoom,
114 | created_at: Date,
115 | ) {
116 | this.id = this.generateId()
117 | this.author = author
118 | this.content = content
119 | this.created_at = created_at
120 | this.room = room
121 | this.is_command = false
122 | this.is_server = false
123 | }
124 |
125 | private generateId(): string {
126 | return Math.random().toString(36).substring(2, 15) // Simulating an ID generator
127 | }
128 |
129 | static newServerMessage(
130 | content: string,
131 | room: ChatRoom,
132 | created_at: Date,
133 | ): ChatMessage {
134 | const msg = new ChatMessage(
135 | {
136 | id: "SERVER",
137 | username: "SERVER",
138 | },
139 | content,
140 | room,
141 | created_at,
142 | )
143 | msg.is_server = true
144 | return msg
145 | }
146 |
147 | static newCommandMessage(content: string, created_at: Date): ChatMessage {
148 | const room = new ChatRoom("COMMAND_RESPONSE", "Command Response")
149 | const msg = new ChatMessage(
150 | {
151 | id: "COMMAND",
152 | username: "COMMAND",
153 | },
154 | content,
155 | room,
156 | created_at,
157 | )
158 | msg.is_command = true
159 | return msg
160 | }
161 |
162 | static fromPacket(pkt: Packet): ChatMessage {
163 | const jsonString = new TextDecoder().decode(pkt.payload)
164 | const obj = JSON.parse(jsonString)
165 | return new ChatMessage(
166 | obj.author,
167 | obj.content,
168 | new ChatRoom(obj.room.id, obj.room.name),
169 | new Date(obj.created_at),
170 | )
171 | }
172 |
173 | toPacket(): Packet {
174 | const payload = new TextEncoder().encode(JSON.stringify(this))
175 | return Packet.newPacket(PacketType.PacketTypeMessage, payload)
176 | }
177 | }
178 |
179 | export class ClientAuthMessage {
180 | username: string
181 | roomId: string | undefined
182 |
183 | constructor(username: string, roomId?: string) {
184 | this.username = username
185 | this.roomId = roomId
186 | }
187 |
188 | static fromPacket(pkt: Packet): ClientAuthMessage {
189 | const jsonString = new TextDecoder().decode(pkt.payload)
190 | const obj = JSON.parse(jsonString)
191 | return new ClientAuthMessage(obj.username, obj.roomId)
192 | }
193 |
194 | toPacket(): Packet {
195 | const payload = new TextEncoder().encode(JSON.stringify(this))
196 | return Packet.newPacket(PacketType.PacketTypeAuth, payload)
197 | }
198 | }
199 |
200 | export interface Account {
201 | id: string
202 | username: string
203 | }
204 |
205 | export class ServerAuthMessage {
206 | status: string
207 | content: string
208 | roomId: string | undefined
209 | account: Account | undefined
210 |
211 | constructor(
212 | status: string,
213 | content: string,
214 | roomId?: string,
215 | account?: { id: string; username: string },
216 | ) {
217 | this.status = status
218 | this.content = content
219 | this.roomId = roomId
220 | this.account = account
221 | }
222 |
223 | static fromPacket(pkt: Packet): ServerAuthMessage {
224 | const jsonString = new TextDecoder().decode(pkt.payload)
225 | const obj = JSON.parse(jsonString)
226 | return new ServerAuthMessage(
227 | obj.status,
228 | obj.content,
229 | obj.roomId,
230 | obj.account,
231 | )
232 | }
233 |
234 | toPacket(): Packet {
235 | const payload = new TextEncoder().encode(JSON.stringify(this))
236 | return Packet.newPacket(PacketType.PacketTypeAuth, payload)
237 | }
238 | }
239 |
240 | export class PingMessage {
241 | constructor() {}
242 |
243 | static fromPacket(_: Packet): PingMessage {
244 | return new PingMessage()
245 | }
246 |
247 | toPacket(): Packet {
248 | const payload = new TextEncoder().encode(JSON.stringify(this))
249 | return Packet.newPacket(PacketType.PacketTypePing, payload)
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/src/libs/workers/python-worker.ts:
--------------------------------------------------------------------------------
1 | import { PyodideInterface, loadPyodide } from "pyodide"
2 |
3 | let pyodide: PyodideInterface
4 | async function loadPyodideAndPackages() {
5 | pyodide = await loadPyodide({
6 | indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.4/full",
7 | })
8 | }
9 |
10 | let pyodideReadyPromise = loadPyodideAndPackages()
11 |
12 | onmessage = async function (event) {
13 | await pyodideReadyPromise
14 |
15 | const { id, code, cmd, interruptBuffer } = event.data
16 |
17 | // Set the interrupt buffer for the Python worker
18 | if (cmd === "setInterruptBuffer") {
19 | pyodide.setInterruptBuffer(interruptBuffer)
20 | return
21 | }
22 |
23 | try {
24 | pyodide.setStdout({
25 | batched: (text) => {
26 | self.postMessage({ stdout: text, id })
27 | },
28 | })
29 | pyodide.setStderr({
30 | batched: (text) => {
31 | self.postMessage({ stdout: text, id })
32 | },
33 | })
34 |
35 | pyodide.runPython(code)
36 | } catch (error: any) {
37 | self.postMessage({ stdout: error.message, id })
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom/client"
3 | import App from "./App"
4 |
5 | import "./index.css"
6 |
7 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
8 |
9 |
10 | ,
11 | )
12 |
--------------------------------------------------------------------------------
/src/pages/Desktop/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo } from "react"
2 | import { useWindowSize } from "react-use"
3 | import { useApps } from "../../hooks/useApp"
4 | import AppIcon from "../../components/AppIcon"
5 | import TopBar from "../../components/TopBar"
6 | import Application from "../../components/Application"
7 | import WelcomeCard from "../../components/WelcomeCard"
8 | import { useIconsStore } from "../../stores/iconsStore"
9 | import { ApplicationName } from "../../contexts/ApplicationContext"
10 |
11 | interface Icon {
12 | title: string
13 | id: ApplicationName
14 | icon: string
15 | x: number
16 | y: number
17 | }
18 |
19 | const desktopIcons: Array = [
20 | {
21 | title: "Clock",
22 | id: "clock",
23 | icon: "icons/clock/Clock_Face.svg",
24 | x: 10,
25 | y: 10,
26 | },
27 | {
28 | title: "Terminal",
29 | id: "terminal",
30 | icon: "/icons/applications/terminal.png",
31 | x: 10,
32 | y: 10,
33 | },
34 | {
35 | title: "Duck's Boat Navigator",
36 | id: "navigator",
37 | icon: "/icons/applications/Brosen_windrose.svg",
38 | x: 10,
39 | y: 10,
40 | },
41 | {
42 | title: "Calculator",
43 | id: "calculator",
44 | icon: "/icons/applications/calculator.svg",
45 | x: 10,
46 | y: 10,
47 | },
48 | {
49 | title: "PyIDE",
50 | id: "pyide",
51 | icon: "/icons/applications/pyide.png",
52 | x: 10,
53 | y: 10,
54 | },
55 | {
56 | title: "QuaChat",
57 | id: "quachat",
58 | icon: "/icons/applications/quachat.png",
59 | x: 10,
60 | y: 10,
61 | },
62 | ]
63 |
64 | const Desktop = () => {
65 | const iconsStoreApps = useIconsStore((state) => state.apps)
66 | const { width } = useWindowSize()
67 | const { apps, addApp } = useApps()
68 |
69 | const icons = useMemo(() => {
70 | return desktopIcons.map((app) => {
71 | let x = app.x,
72 | y = app.y
73 |
74 | const ic = iconsStoreApps.find((i) => i.id === app.id)
75 | if (ic) {
76 | x = ic.x
77 | y = ic.y
78 | }
79 |
80 | return (
81 | addApp({ name: app.id })}
86 | defaultPosition={{ x: x, y: y }}
87 | icon={app.icon}
88 | title={app.title}
89 | />
90 | )
91 | })
92 | }, [addApp])
93 |
94 | useEffect(() => {
95 | addApp({ name: "clock", x: width - 400, y: 20 })
96 | }, [])
97 |
98 | return (
99 | <>
100 |
101 |
102 |
103 |
104 | {apps.map((app) => (
105 |
106 | ))}
107 |
108 | {icons}
109 |
110 |
111 |
112 |
113 | >
114 | )
115 | }
116 |
117 | export default Desktop
118 |
--------------------------------------------------------------------------------
/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 |
3 | import Desktop from "./Desktop"
4 | import Loading from "./Loading"
5 | import clsx from "clsx"
6 |
7 | export default function Main() {
8 | const [loading, setLoading] = useState(true)
9 |
10 | useEffect(() => {
11 | if (process.env.NODE_ENV === "development") setLoading(false)
12 | setTimeout(() => {
13 | setLoading(false)
14 | }, 3000)
15 | }, [])
16 |
17 | return (
18 | <>
19 | {loading && }
20 |
25 |
26 |
27 | >
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/Loading.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { useApps } from "../hooks/useApp"
3 | import clsx from "clsx"
4 |
5 | const loadingChar = ["|", "/", "—", "\\"]
6 |
7 | export default function Loading() {
8 | const [loadingCount, setLoadingCount] = useState(0)
9 |
10 | const { clearApps } = useApps()
11 |
12 | useEffect(() => {
13 | clearApps()
14 |
15 | const interval = setInterval(() => {
16 | setLoadingCount((prev) => {
17 | if (prev === 3) return 0
18 | return prev + 1
19 | })
20 | }, 300)
21 |
22 | return () => {
23 | clearInterval(interval)
24 | }
25 | }, [])
26 |
27 | return (
28 |
33 |
34 |
35 |
36 |
37 | QuackOS
38 | Beta Release
39 |
40 | {loadingChar[loadingCount]}
41 |
42 |
43 | Quackright (c) Duck Comporation, 1995.
44 |
45 | All Rights Reserved. QuackOS is a registered trademark of Quack Corp.
46 |
47 |
48 |
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/stores/iconsStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand"
2 | import { ApplicationName } from "../contexts/ApplicationContext"
3 | import { persist } from "zustand/middleware"
4 |
5 | interface Icon {
6 | id: ApplicationName
7 | x: number
8 | y: number
9 | }
10 |
11 | interface IconsStore {
12 | apps: Icon[]
13 | updatePos: (
14 | id: ApplicationName,
15 | pos: {
16 | x: number
17 | y: number
18 | },
19 | ) => void
20 | }
21 |
22 | export const useIconsStore = create(
23 | persist(
24 | (set, get) => ({
25 | apps: [],
26 | updatePos(id, pos) {
27 | set((state) => {
28 | const apps = state.apps.slice()
29 | let app = apps.find((app) => app.id === id)
30 | if (!app) {
31 | app = {
32 | id: id as any,
33 | x: 0,
34 | y: 0,
35 | }
36 | apps.push(app)
37 | }
38 |
39 | app.x = pos.x
40 | app.y = pos.y
41 | return {
42 | apps,
43 | }
44 | })
45 | },
46 | }),
47 | {
48 | name: "icons-store",
49 | },
50 | ),
51 | )
52 |
--------------------------------------------------------------------------------
/src/stores/pyIDEStore.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand"
2 | import { persist, createJSONStorage } from "zustand/middleware"
3 |
4 | const default_code = `import math
5 |
6 | print("Hello, world!")
7 | print("Square root of 2 is", math.sqrt(2))`
8 |
9 | interface PyIDEStore {
10 | code: string
11 | setCode: (code: string) => void
12 | }
13 |
14 | export const usePyIDEStore = create(
15 | persist(
16 | (set) => ({
17 | code: default_code,
18 | setCode: (code: string) => set({ code }),
19 | }),
20 | {
21 | name: "pyeditor-store",
22 | storage: createJSONStorage(() => localStorage),
23 | },
24 | ),
25 | )
26 |
--------------------------------------------------------------------------------
/src/styles/globals.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Fixedsys Excelsior';
3 | src: url('/FSEX302.ttf') format('truetype');
4 | }
5 |
6 | body,
7 | html {
8 | padding: 0;
9 | margin: 0;
10 | overflow-x: hidden;
11 | scroll-behavior: smooth;
12 | font-size: 18px;
13 |
14 |
15 | @media (max-width: 720px) {
16 | font-size: 16px;
17 | }
18 | line-height: 1.5;
19 |
20 | font-family: "Fixedsys Excelsior", monospace;
21 |
22 | cursor: url("/icons/cursors/Cursor.svg"), default;
23 | }
24 |
25 | *{
26 | box-sizing: border-box;
27 | }
28 |
29 | ::-webkit-scrollbar {
30 | width: 25px;
31 | border: 2px solid theme("colors.black");
32 | }
33 | ::-webkit-scrollbar-track {
34 | background-image: url("/pattern/scroll.svg");
35 | background-repeat: repeat-y;
36 | background-size: auto 20px;
37 | background-position: center;
38 | }
39 | ::-webkit-scrollbar-thumb {
40 | background: theme("colors.white");
41 | border: 2px solid theme("colors.black");
42 | }
43 | //up
44 | ::-webkit-scrollbar-button:single-button:vertical:decrement {
45 | background-image: url("../../public/icons/arrows/arrow/up.svg");
46 | background-repeat: no-repeat;
47 | background-size: contain;
48 | background-position: center;
49 |
50 | border: 2px solid theme("colors.black");
51 |
52 | height: 35px;
53 | }
54 | /* Down */
55 | ::-webkit-scrollbar-button:single-button:vertical:increment {
56 | background-image: url("../../public/icons/arrows/arrow/down.svg");
57 | background-repeat: no-repeat;
58 | background-size: contain;
59 | background-position: center;
60 |
61 | border: 2px solid theme("colors.black");
62 |
63 | height: 35px;
64 | }
65 |
66 | .button{
67 | all: unset;
68 |
69 | background-image: url("/buttons/button/Default.svg");
70 | background-repeat: no-repeat;
71 | background-size: contain;
72 | background-position: center;
73 |
74 | min-height: 20px;
75 | min-width: 60px;
76 |
77 | display: flex;
78 | justify-content: center;
79 | align-items: center;
80 |
81 | padding: 16px 32px;
82 |
83 | font-size: 1rem;
84 |
85 | &:hover{
86 | cursor: url("/icons/cursors/touchCursor.svg"), default;
87 | background-image: url("/buttons/button/Hover.svg");
88 | }
89 |
90 | &:focus{
91 | background-image: url("/buttons/button/Focus.svg");
92 | }
93 | }
94 |
95 | p {
96 | font-size: 1rem;
97 | line-height: 1.5;
98 | }
99 |
100 | h1,
101 | h2,
102 | h3,
103 | h4,
104 | h5,
105 | h6 {
106 | line-height: 1.3;
107 | }
108 | h1 {
109 | font-size: 1.8rem;
110 | }
111 | h2 {
112 | font-size: 1.6rem;
113 | }
114 | h3 {
115 | font-size: 1.42rem;
116 | }
117 | h4 {
118 | font-size: 1.26rem;
119 | }
120 | h5 {
121 | font-size: 1.11rem;
122 | }
123 | h6 {
124 | font-size: 0.875rem;
125 | }
--------------------------------------------------------------------------------
/src/types/ApplicationType.ts:
--------------------------------------------------------------------------------
1 | import type { LazyExoticComponent, ComponentType } from "react"
2 |
3 | interface App {
4 | id: string
5 | Node: LazyExoticComponent>
6 | title: string
7 |
8 | start?: number
9 |
10 | x?: number
11 | y?: number
12 | }
13 |
14 | export type { App }
15 |
--------------------------------------------------------------------------------
/src/vite.d.ts:
--------------------------------------------------------------------------------
1 | declare module "react-console-emulator"
2 | declare module "*.module.css"
3 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | container: {
6 | center: true,
7 | padding: "1rem",
8 | screens: {
9 | "2xl": "1400px",
10 | },
11 | },
12 | extend: {
13 | backgroundImage: {
14 | duck: "url('/brand/duck.png')",
15 | "dot-pattern": "url('/pattern/dotted/lightAlt.svg')",
16 | },
17 | },
18 | },
19 | plugins: [],
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "esModuleInterop": true,
6 | "jsx": "react-jsx",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "skipLibCheck": true,
10 | "moduleResolution": "Node",
11 | "outDir": "dist",
12 | "strictFunctionTypes": false,
13 | "types": ["vite/client"]
14 | },
15 | "exclude": [
16 | "node_modules",
17 | "dist"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite"
2 | import { visualizer } from "rollup-plugin-visualizer"
3 | import react from "@vitejs/plugin-react"
4 | import wasm from "vite-plugin-wasm"
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | plugins: [
9 | wasm(),
10 | visualizer({
11 | filename: "stats.html",
12 | gzipSize: true,
13 | }),
14 | react(),
15 | ],
16 | worker: {
17 | format: "es",
18 | },
19 | resolve: {
20 | alias: {
21 | "node-fetch": "isomorphic-fetch",
22 | },
23 | },
24 | build: {
25 | rollupOptions: {
26 | output: {
27 | manualChunks(id: string) {
28 | if (id.includes("refractor")) {
29 | return "refractor"
30 | }
31 | },
32 | },
33 | },
34 | },
35 | server: {
36 | headers: {
37 | "Cross-Origin-Embedder-Policy": "require-corp",
38 | "Cross-Origin-Opener-Policy": "same-origin",
39 | },
40 | },
41 | })
42 |
--------------------------------------------------------------------------------