Compare commits

...

15 Commits

Author SHA1 Message Date
f678c0fa54 Code duplication cleanup after last commit 2026-02-08 10:42:58 +01:00
ef09b642d6 Newest Commit - code same as last commit 2026-02-08 10:42:58 +01:00
7852a77a7c added getWidth method to bash and terminal + callbackf unction on the end for width rertival and calculations in the ls command 2026-02-08 10:42:58 +01:00
582eb68139 ls command almost finished, needed cleanup and rework of the code 2026-02-08 10:42:58 +01:00
d0ff245582 path formatting fixed, long flag support, command execution path now gets passed with the command result to not make it dependant on dynamic bash variable unlike previously implemented with pwd (prev. working dir) 2026-02-08 10:42:58 +01:00
a109c7115e progress, dont really know what to put in here... 2026-02-08 10:42:58 +01:00
8a06d7cb93 ls command working, need to debug sort, and figure out how to prevent sorting on flag (will need further debugging probs) 2026-02-08 10:42:58 +01:00
60eb56da56 LS technically fixed (still debugging pos interpreter errors). Sort class added, witht he ability to sort nodes by inode and the object through a common method. Added an enum SortBy that contains sorting types for the node. Backend for sorting implemented, modification of the ls command still required (will need long flag support for other sorting methods, and timestamp display). 2026-02-08 10:42:58 +01:00
0877993b41 some work, i dont really know what and how much i did 2026-02-08 10:42:58 +01:00
d404f5daab Fixed FS class logic, with full support for table fs structure. Terminal sucressfully initializes (havent checked the commands) ls broken. 2026-02-08 10:42:58 +01:00
8e0ae3dd83 Partially fixed logic in the file system class with changes to support fs table structure instead of a tree 2026-02-08 10:42:58 +01:00
85d98a41c1 new fs.json in the form of a table. support for symlinks and hardlinks (data) support for other times (data - mtime, ctime, atime) inode support (data)
!!PRE `fs` class refactoring. Everything still on the old string array based system.
2026-02-08 10:42:58 +01:00
e1fe000608 Changelog:
- Added `getGroupByName() getGroupByGid() getUserByName getUserByUid()` methods to the `Bash` class.

- Fixed a bug inside the `localStorage` caching logic that saved only 32 characters out of 36 in the signature UUID.

- Added support for reverse order sorting in the quick sort implementation.

**Commands:**

- ls
	-l
	-a
	-A
	-U
	-g
	-G
	-h
	-f
	-n
	-N
	-r
	-Q
	-p
	-o
- cd
2026-02-08 10:42:58 +01:00
e853268e52 Bash and IO working, basic error setup. Changelog:
Commands:
    - ls (only with -l)
    - cd (basic, probably unfinished)
2026-02-08 10:42:58 +01:00
4428cc7e8a Init rebase to Svelte and TS 2026-02-08 10:42:58 +01:00
70 changed files with 13578 additions and 13 deletions

32
.gitignore vendored Normal file → Executable file
View File

@@ -1,14 +1,24 @@
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
test-results
node_modules
# Local History for Visual Studio Code
.history/
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# Built Visual Studio Code Extensions
*.vsix
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Executable file
View File

@@ -0,0 +1 @@
engine-strict=true

9
.prettierignore Executable file
View File

@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

16
.prettierrc Executable file
View File

@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/app.css"
}

39
README.md Normal file → Executable file
View File

@@ -1,3 +1,38 @@
# olszewski.ink
# sv
Repository for vite driven svelte project of a portfolio with The Bash
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

6
e2e/demo.test.ts Executable file
View File

@@ -0,0 +1,6 @@
import { expect, test } from '@playwright/test';
test('home page has expected h1', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});

41
eslint.config.js Executable file
View File

@@ -0,0 +1,41 @@
import prettier from 'eslint-config-prettier';
import { fileURLToPath } from 'node:url';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

4874
package-lock.json generated Executable file

File diff suppressed because it is too large Load Diff

49
package.json Executable file
View File

@@ -0,0 +1,49 @@
{
"name": "svelte-portfolio",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint .",
"test:e2e": "playwright test",
"test": "npm run test:e2e && npm run test:unit -- --run",
"test:unit": "vitest"
},
"devDependencies": {
"@eslint/compat": "^1.4.0",
"@eslint/js": "^9.36.0",
"@playwright/test": "^1.55.1",
"@sveltejs/adapter-auto": "^6.1.0",
"@sveltejs/kit": "^2.43.2",
"@sveltejs/vite-plugin-svelte": "^6.2.0",
"@tailwindcss/vite": "^4.1.16",
"@types/node": "^22",
"@vitest/browser": "^3.2.4",
"eslint": "^9.36.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"globals": "^16.4.0",
"playwright": "^1.55.1",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"prettier-plugin-tailwindcss": "^0.6.14",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.2",
"typescript-eslint": "^8.44.1",
"vite": "^7.1.7",
"vitest": "^3.2.4",
"vitest-browser-svelte": "^1.1.0"
},
"dependencies": {
"@lucide/svelte": "^0.548.0"
}
}

9
playwright.config.ts Executable file
View File

@@ -0,0 +1,9 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run build && npm run preview',
port: 4173
},
testDir: 'e2e'
});

3101
pnpm-lock.yaml generated Executable file

File diff suppressed because it is too large Load Diff

50
src/app.css Executable file
View File

@@ -0,0 +1,50 @@
@import './style/global.css';
@import 'tailwindcss';
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body,
#app {
margin: 0;
padding: 0;
color: var(--text);
background-color: var(--bg-dark);
background-image: radial-gradient(circle at center, var(--bg-light) 1px, transparent 1px);
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
.card {
padding: 2em;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}

13
src/app.d.ts vendored Executable file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

11
src/app.html Executable file
View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/demo.spec.ts Executable file
View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

10
src/lib/assets/deFlag.svg Executable file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<path style="fill:#464655;" d="M473.655,88.276H38.345C17.167,88.276,0,105.443,0,126.621v73.471h512v-73.471
C512,105.443,494.833,88.276,473.655,88.276z"/>
<path style="fill:#FFE15A;" d="M0,385.379c0,21.177,17.167,38.345,38.345,38.345h435.31c21.177,0,38.345-17.167,38.345-38.345
v-73.471H0V385.379z"/>
<rect y="200.09" style="fill:#FF4B55;" width="512" height="111.81"/>
</svg>

After

Width:  |  Height:  |  Size: 668 B

2
src/lib/assets/enFlag.svg Executable file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--twemoji" preserveAspectRatio="xMidYMid meet"><path fill="#00247D" d="M0 9.059V13h5.628zM4.664 31H13v-5.837zM23 25.164V31h8.335zM0 23v3.941L5.63 23zM31.337 5H23v5.837zM36 26.942V23h-5.631zM36 13V9.059L30.371 13zM13 5H4.664L13 10.837z"></path><path fill="#CF1B2B" d="M25.14 23l9.712 6.801a3.977 3.977 0 0 0 .99-1.749L28.627 23H25.14zM13 23h-2.141l-9.711 6.8c.521.53 1.189.909 1.938 1.085L13 23.943V23zm10-10h2.141l9.711-6.8a3.988 3.988 0 0 0-1.937-1.085L23 12.057V13zm-12.141 0L1.148 6.2a3.994 3.994 0 0 0-.991 1.749L7.372 13h3.487z"></path><path fill="#EEE" d="M36 21H21v10h2v-5.836L31.335 31H32a3.99 3.99 0 0 0 2.852-1.199L25.14 23h3.487l7.215 5.052c.093-.337.158-.686.158-1.052v-.058L30.369 23H36v-2zM0 21v2h5.63L0 26.941V27c0 1.091.439 2.078 1.148 2.8l9.711-6.8H13v.943l-9.914 6.941c.294.07.598.116.914.116h.664L13 25.163V31h2V21H0zM36 9a3.983 3.983 0 0 0-1.148-2.8L25.141 13H23v-.943l9.915-6.942A4.001 4.001 0 0 0 32 5h-.663L23 10.837V5h-2v10h15v-2h-5.629L36 9.059V9zM13 5v5.837L4.664 5H4a3.985 3.985 0 0 0-2.852 1.2l9.711 6.8H7.372L.157 7.949A3.968 3.968 0 0 0 0 9v.059L5.628 13H0v2h15V5h-2z"></path><path fill="#CF1B2B" d="M21 15V5h-6v10H0v6h15v10h6V21h15v-6z"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
src/lib/assets/favicon.svg Executable file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

10
src/lib/assets/frFlag.svg Executable file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<path style="fill:#41479B;" d="M38.345,88.273C17.167,88.273,0,105.44,0,126.618v258.759c0,21.177,17.167,38.345,38.345,38.345
h132.322V88.273H38.345z"/>
<rect x="170.67" y="88.277" style="fill:#F5F5F5;" width="170.67" height="335.45"/>
<path style="fill:#FF4B55;" d="M473.655,88.273H341.333v335.448h132.322c21.177,0,38.345-17.167,38.345-38.345V126.618
C512,105.44,494.833,88.273,473.655,88.273z"/>
</svg>

After

Width:  |  Height:  |  Size: 695 B

1804
src/lib/assets/fs/fs.json Executable file

File diff suppressed because it is too large Load Diff

1
src/lib/assets/fs/signature Executable file
View File

@@ -0,0 +1 @@
32aac83d-ce2b-4def-977c-dcdcb6f514ef

9
src/lib/assets/jaFlag.svg Executable file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<path style="fill:#F5F5F5;" d="M473.655,88.275H38.345C17.167,88.275,0,105.442,0,126.62V385.38
c0,21.177,17.167,38.345,38.345,38.345h435.31c21.177,0,38.345-17.167,38.345-38.345V126.62
C512,105.442,494.833,88.275,473.655,88.275z"/>
<circle style="fill:#FF4B55;" cx="256" cy="255.999" r="97.1"/>
</svg>

After

Width:  |  Height:  |  Size: 620 B

2
src/lib/assets/plFlag.svg Executable file
View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--twemoji" preserveAspectRatio="xMidYMid meet"><path fill="#EEE" d="M32 5H4a4 4 0 0 0-4 4v9h36V9a4 4 0 0 0-4-4z"></path><path fill="#DC143C" d="M0 27a4 4 0 0 0 4 4h28a4 4 0 0 0 4-4v-9H0v9z"></path></svg>

After

Width:  |  Height:  |  Size: 506 B

BIN
src/lib/assets/quan.ttf Executable file

Binary file not shown.

1
src/lib/index.ts Executable file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

186
src/lib/stores/bash/bash.ts Executable file
View File

@@ -0,0 +1,186 @@
import { COMMANDS, GROUP, PASSWD, type CommandArgs, type ICommand, type Result } from './static';
import { VirtualFS } from './fs';
import { Terminal, type PrintData } from '../terminal/terminal';
import { Stack } from '../stack';
export type Permission = {
r: boolean;
w: boolean;
x: boolean;
};
export type BashInitArgs = {
stdio?: Terminal;
user: User;
fs: any;
};
export type TimeStamps = {
modified: Date;
changed: Date;
accessed: Date;
};
// TODO: Finish this
// TODO: Change into a type instead of an enum for performance (low priority)
export enum ExitCode {
SUCCESS = 0,
ERROR = 1
}
export type User = {
username: string;
passwd: string; //HASHED PASSWORD //TODO: Make a formated type
readonly uid: number; // Normal user 1000+ System user 1-999 root - 0 //TODO: Make a formated type
readonly gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0 //TODO: Make a formated type
home: string; //TODO: Make a formated type
history: string[];
cwd?: number; //TODO: Make a formated type
pwd?: number; //TODO: Make a formated type
};
export type Group = {
groupname: string;
gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0
members: number[]; //TODO: Make a formated type UID
};
export class Bash {
private vfs: VirtualFS;
private _passwd: User[];
private _instances: Stack<User>;
private _group: Group[];
private _terminal!: Terminal;
private user: User;
private readonly _commands: Record<string, ICommand>;
constructor(args: BashInitArgs) {
this.user = args.user;
this._commands = COMMANDS;
this._passwd = PASSWD;
this._group = GROUP;
this._terminal = args.stdio!;
this._instances = new Stack<User>();
this.vfs = new VirtualFS({ fs: args.fs, user: args.user });
}
private _appendNewResult(inode: number, output: any, cmd: string) {
const data: PrintData = {
path: this.vfs.formatPath(this.vfs.getPathByInode(inode)),
output: output,
cmd: cmd
};
console.log(data);
this._terminal.PrintOutput(data);
}
updateHistory(input: string): void {
if ((this.user.history.length = 255)) {
this.user.history.unshift(...this.user.history.splice(-1));
this.user.history[0] = input;
} else {
this.user.history.push(input);
this.user.history.unshift(...this.user.history.splice(-1));
}
}
getCwd(): number {
return this.vfs.cwd;
}
getPwd(): number {
return this.vfs.pwd;
}
getUser(): User {
return this.user;
}
getFs(): VirtualFS {
return this.vfs;
}
getTerminalWidth(): number {
return this._terminal.getTerminalWidth();
}
hasSudoPerms(uid: number): boolean {
return this._group[1].members.includes(uid);
}
executeCommand(commandName: string, args: CommandArgs): void {
let result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd() };
const command = this._commands[commandName];
if (!command) this.throwError(result);
if (command.root) {
if (this.hasSudoPerms(this.user.uid)) {
let out: Result = command.method.call(this, args);
this._appendNewResult(this.getCwd(), out, this.user.history[0]);
}
this.throwError(result);
}
let out: Result = command.method.call(this, args);
console.log(out);
this._appendNewResult(out.path, out.data?.data, this.user.history[0]);
}
throwError(result: Result): void {
switch (result.exitCode) {
default: {
throw new Error(`Error, dont know where, just look for it;`);
}
}
}
userLogout() {
this._instances.pop();
if (this._instances.size() === 0) {
//TODO: Implement system logout
} else {
//this.changeUser(this._instances.peek()!);
}
}
formatBytes(bytes: number, dPoint?: number, pow: 1024 | 1000 = 1024): string {
if (!+bytes) return '0';
const k: number = pow;
const dp: number = dPoint ? (dPoint < 0 ? 0 : dPoint) : 1;
const units: string[] = ['', 'K', 'M', 'G', 'T', 'P'];
const i: number = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(dp)}${units[i]}`;
}
getGroupByName(name: string): Group {
const out: Group | undefined = this._group.find((group) => group.groupname === name);
if (out) return out;
else throw new Error(`Cannot find a user group named ${name}`);
}
getUserByName(name: string): User {
const out: User | undefined = this._passwd.find((user) => user.username === name);
if (out) return out;
else throw new Error(`Cannot find a user named ${name}`);
}
getGroupByGid(gid: number): Group {
const out: Group | undefined = this._group.find((group) => group.gid === gid);
if (out) return out;
else throw new Error(`Cannot find a user group with id of ${gid}`);
}
getUserByUid(uid: number): User {
const out: User | undefined = this._passwd.find((user) => user.uid === uid);
if (out) return out;
else throw new Error(`Cannot find a user with id of ${uid}`);
}
}

View File

@@ -0,0 +1,46 @@
import { ExitCode, type Bash } from '../bash';
import { Type, type TreeNode } from '../fs';
import type { CommandArgs, ICommand, Result } from '../static';
export const cmd_cd = function (this: Bash, args: CommandArgs): Result {
let result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd() };
const path = args.args[0];
let targetNode: TreeNode | null;
if (args.args.length > 1) return result; // Too many args
// if no args cd into home dir
if (args.args.length === 0) {
this.getFs().cwd = this.getFs().home;
result.exitCode = ExitCode.SUCCESS;
return result;
}
// if the arg is - cd make your current dir the prev dir and vice versa
if (args.args[0] === '-') {
[this.getFs().cwd, this.getFs().pwd] = [this.getFs().pwd, this.getFs().cwd];
result.exitCode = ExitCode.SUCCESS;
return result;
}
this.getFs().pwd = this.getFs().cwd;
targetNode = this.getFs().resolvePath(path); // Conversion from STRING path to TREENODE
console.log(targetNode, path, 'CD OUTPUT');
if (targetNode === null) return result;
if (targetNode.type !== Type.Directory) return result;
//if () return ExitCode.ERROR; // Check for read permissions on node and user
this.getFs().cwd = targetNode.inode; // CD was successfull, change current dir to the verified target dir
result.exitCode = ExitCode.SUCCESS;
return result;
};
export const cd: ICommand = {
method: cmd_cd,
flags: [] as string[],
help: 'PATH TO HELP.md',
root: false
};

View File

@@ -0,0 +1,387 @@
import { Bash, ExitCode, type Permission, type TimeStamps } from '../bash';
import { Type, type NodePerms, type TreeNode } from '../fs';
import { Sort, SortNodeBy } from '../sort';
import type { CommandArgs, ICommand, Result, resultData } from '../static';
type LsEntry = {
inode: number | null;
perms: string;
children: string;
owners: string;
size: string;
modt: string;
name: string;
};
const LsEntryUtils = {
toString(entry: LsEntry): string {
return Object.entries(entry)
.filter(([_, value]) => value !== '')
.map(([_, value]) => value)
.join(' ');
}
};
const months: readonly string[] = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
export const cmd_ls = function (this: Bash, args: CommandArgs): Result {
const resultData: resultData = { cmd: 'ls', data: null, args: args };
const result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd(), data: resultData };
const nodes: TreeNode[] = [];
//Check if any args contain the long flags with value and are valid flags inside the ls const
const valuedArgs = args.flags.filter((flag: string) =>
flag.includes('=') && ls.flags.includes(flag.split('=')[0]));
console.log(valuedArgs);
//Check if args contain any nonexistent flags, if so add it to an array and check its length. if 0 no bad flags
const invalidArgs = args.flags.filter((flag) => !ls.flags.includes(flag) && !valuedArgs.includes(flag));
console.log(invalidArgs);
if (invalidArgs.length > 0) {
this.throwError(result); //No such flag/s
}
if (args.args.length === 0) nodes.push(this.getFs().getNodeByINode(this.getFs().cwd));
for (let i = 0; i < args.args.length; i++) {
const node = this.getFs().resolvePath(args.args[i]);
if (node === null) this.throwError(result); //no such path (i think this will never occur as backed methods have error cases implemented - which is wrong)
nodes.push(node);
}
result.exitCode = ExitCode.SUCCESS;
resultData.data = result_ls.call(this, nodes, args);
return result;
};
function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
const dummysonoerror: HTMLElement = document.createElement('div');
const flagInfo = checkFlags(args.flags);
const nodes: TreeNode[] = data;
const f_a: boolean = flagInfo.has('a') || flagInfo.has('all');
const f_A: boolean = flagInfo.has('A') || flagInfo.has('almost-all');
const f_G: boolean = flagInfo.has('G') || flagInfo.has('no-group');
const f_h: boolean = flagInfo.has('h') || flagInfo.has('human-readable');
const f_r: boolean = flagInfo.has('r') || flagInfo.has('reverse');
const f_Q: boolean = flagInfo.has('Q') || flagInfo.has('quote-name');
const f_n: boolean = flagInfo.has('n') || flagInfo.has('numeric-uid-gid');
const f_N: boolean = flagInfo.has('N') || flagInfo.has('literal');
const f_L: boolean = flagInfo.has('L') || flagInfo.has('dereference');
const f_i: boolean = flagInfo.has('i') || flagInfo.has('inode');
const f_help: boolean = flagInfo.has('help');
const f_si: boolean = flagInfo.has('si');
const f_X: boolean = flagInfo.has('X');
const f_S: boolean = flagInfo.has('S');
const f_t: boolean = flagInfo.has('t');
const f_l: boolean = flagInfo.has('l');
const f_U: boolean = flagInfo.has('U');
const f_f: boolean = flagInfo.has('f');
const f_g: boolean = flagInfo.has('g');
const f_o: boolean = flagInfo.has('o');
let shouldShift: boolean = false
const valuedArgs = args.flags.filter((flag: string) =>
flag.includes('=') && ls.flags.includes(flag.split('=')[0]));
if (f_l || f_g || f_o) {
const w: HTMLElement = document.createElement('div');
for (const node of nodes) {
const elem: HTMLElement = document.createElement('div');
const children: TreeNode[] = node.children.map((child) => this.getFs().getNodeByINode(child));
const rows: string[] = [];
const timeArg = valuedArgs.find((flag) => flag.startsWith('time'));
let timestamp: SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME = SortNodeBy.MTIME;
if(timeArg) {
let value: string = timeArg.split('=')[1];
if (value && isValidNodeTimestamp(value)) {
timestamp = value;
console.log(timestamp);
}
}
if (!f_U && !f_f) {
const sortArg = valuedArgs.find((flag) => flag.startsWith('sort'));
let sortBy: SortNodeBy = SortNodeBy.NAME;
if(f_t) sortBy = timestamp;
if(f_S) sortBy = SortNodeBy.SIZE;
if(f_X) sortBy = SortNodeBy.EXTENSION;
if(sortArg) {
let value = sortArg.split('=')[1];
if(value && isValidNodeSortMethod(value)) {
sortBy = value;
console.log(sortBy, 'sortBy');
}
}
Sort.nodeArraySort.call(this, children, f_r, sortBy);
}
const sizes = children.map((child) => child.size);
const maxSizeWidth = Math.max(...sizes.map((size) => size));
for (const child of children) {
if (child.name.startsWith('.') && !(f_a || f_A)) continue;
const cols: LsEntry = {
inode: null,
perms: formatPermission(child),
children: formatChildren(child),
owners: formatOwners.call(this, child, flagInfo),
size: formatSize.call(this, f_h, child, maxSizeWidth, f_si),
modt: formatModtime(child, timestamp),
name: formatName(child, flagInfo, shouldShift)
};
if (f_i) cols.inode = child.inode;
rows.push(LsEntryUtils.toString(cols));
}
if (f_a && !f_A) {
const current: LsEntry = {
inode: null,
perms: formatPermission(node),
children: formatChildren(node),
owners: formatOwners.call(this, node, flagInfo),
size: formatSize.call(this, f_h, node, maxSizeWidth, f_si),
modt: formatModtime(node, timestamp),
name: '.'
};
let parent: LsEntry = {
...current,
name: '..'
};
if (node.parent) {
const parentNode: TreeNode = this.getFs().getNodeByINode(node.parent);
parent = {
inode: null,
perms: formatPermission(parentNode),
children: formatChildren(parentNode),
owners: formatOwners.call(this, parentNode, flagInfo),
size: formatSize.call(this, f_h, parentNode, maxSizeWidth, f_si),
modt: formatModtime(parentNode, timestamp),
name: '..'
};
}
if (f_i) {
current.inode = node.inode;
parent.inode = node.parent ? node.parent : node.inode;
}
if (f_r) {
rows.push(LsEntryUtils.toString(parent), LsEntryUtils.toString(current));
} else {
rows.unshift(LsEntryUtils.toString(current), LsEntryUtils.toString(parent));
}
}
//TODO: Calculate the total size of contents in the node
rows.unshift('total ' + node.children.length.toString());
if (nodes.length > 1) {
const nodePath: string =
node.name === '/' ? '/:' : `${this.getFs().getPathByInode(node.inode).slice(1)}:`;
rows.unshift(nodePath);
rows.push('\n');
}
for(const row of rows) {
const name: string = row[row.length - 1];
if(!name.startsWith('"') || !name.startsWith("'"))
name.padStart(1, ' ');
else continue;
}
for (let i = 0; i < rows.length; i++) {
const p: HTMLElement = document.createElement('p');
p.innerText = rows[i];
elem.appendChild(p);
}
w.appendChild(elem);
}
return w;
}
return dummysonoerror; //TEMP SO NO ERROR CUZ RETURNS HTMLElement EVERY TIME, DELETE LATER
}
function isValidNodeSortMethod(value: string): value is SortNodeBy {
return Object.values(SortNodeBy).includes(value as SortNodeBy);
}
function isValidNodeTimestamp(value: string): value is SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME {
return Object.values(SortNodeBy).includes(value as SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME);
}
function parsePerms(perms: NodePerms): string {
const parts: string[] = [];
//for each key (key representing key name and p representing the key contents) of entries in perms as types keyof NodePerms and Permission
for (const [key, p] of Object.entries(perms) as [keyof NodePerms, Permission][]) {
const perms: string = `${p.r ? 'r' : '-'}${p.w ? 'w' : '-'}${p.x ? 'x' : '-'}`;
parts.push(perms);
}
return parts.join('');
}
function formatOwners(this: Bash, node: TreeNode, flag: any): string {
const owner: string = this.getUserByUid(node.owner).username;
const group: string = this.getGroupByGid(node.group).groupname;
if (flag.has('G') || flag.has('o')) {
if (flag.has('n')) return `${node.owner}`;
return `${owner}`;
}
if (flag.has('g')) {
if (flag.has('n')) return `${node.group}`;
return `${group}`;
}
if (flag.has('n')) return `${node.owner} ${node.group}`;
return `${owner} ${group}`;
}
function formatPermission(node: TreeNode): string {
switch(node.type) {
case Type.Directory:
return `d${parsePerms(node.permission)}`;
case Type.File:
return `-${parsePerms(node.permission)}`;
case Type.SymbolicLink:
return `l${parsePerms(node.permission)}`;
default:
throw new Error(`Node of unexpected type - ${node.type}`);
}
}
function formatChildren(node: TreeNode): string {
if (node.type !== Type.Directory) return ' 0';
if (!node.children) throw new Error('children array is null on this node');
const c = node.children.length.toString();
return c.length > 1 ? c : ` ${c}`;
}
function formatSize(
this: Bash,
humanReadable: boolean,
node: TreeNode,
max: number,
f_si: boolean
): string {
let size: string;
if (humanReadable) {
size = this.formatBytes(node.size, 1, f_si ? 1000 : 1024);
} else size = node.size.toString();
return size.padStart(max, ' ');
}
function formatModtime(node: TreeNode, sortBy: SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME): string {
const now = new Date();
//TODO: Change this to be dynamic based on the --time value passed
const hours: string = node.timestamps[sortBy].getHours().toString().padStart(2, '0');
const minutes: string = node.timestamps[sortBy].getMinutes().toString().padStart(2, '0');
const time: string =
now.getFullYear() === node.timestamps[sortBy].getFullYear()
? `${hours}:${minutes}`
: node.timestamps[sortBy].getFullYear().toString();
return [
months[node.timestamps[sortBy].getMonth()],
node.timestamps[sortBy].getDate().toString().padStart(2, ' '),
`${time}`
].join(' ');
}
function formatName(node: TreeNode, flag: any, shouldShift: boolean) {
let name: string;
const char: string = flag.has('Q') ? '"' : "'";
if (/\s/.test(node.name)) {
name = `${char}${node.name}${char}`
shouldShift = true;
} else {
name = `${node.name}`;
}
return flag.has('p') && node.type === Type.Directory ? `${name}/` : name;
}
const checkFlags = (pFlags: string[]) => {
const flagSet = new Set(pFlags);
return { has: (flag: string) => flagSet.has(flag) };
};
export const ls: ICommand = {
method: cmd_ls,
flags: [
'l',
'a',
'A',
'c',
'U',
'g',
'G',
'h',
'f',
'x',
'X',
'u',
't',
'S',
'r',
'Q',
'p',
'o',
'n',
'N',
'L',
'm',
'i',
'sort',
'time',
'help',
'all',
'almost-all',
'no-group',
'human-readable',
'reverse',
'quote-name',
'indicator-style',
'literal',
'numeric-uid-gid',
'inode',
'si',
'dereference'
] as string[],
help: 'PATH TO HELP.MD',
root: false
};

View File

@@ -0,0 +1,13 @@
import type { ICommand } from "../static"
import { cmd_ls } from "./ls"
type LsEntry
export class ls {
public const ls: ICommand = {
method: ls.cmd_ls
}
}

165
src/lib/stores/bash/fs.ts Executable file
View File

@@ -0,0 +1,165 @@
import type { Permission, TimeStamps, User } from './bash';
export enum Type {
Directory = 16384,
File = 32768,
SymbolicLink = 40960
}
export type NodePerms = {
user: Permission;
group: Permission;
other: Permission;
};
export type FsInitArgs = {
fs: any;
user: User;
};
export type TreeNode = {
inode: number;
parent?: number;
name: string;
type: Type;
size: number; //Size in Bytes
children: number[];
content: string; // GUID of the cache file that contains the file contents.
link: number; // Links
permission: NodePerms;
owner: number;
group: number;
timestamps: TimeStamps;
interactible: boolean;
func: any;
};
export class VirtualFS {
private FsTable: Map<number, TreeNode>;
private rootINode: number;
home: number;
cwd: number;
pwd: number;
constructor(args: FsInitArgs) {
this.FsTable = args.fs;
this.rootINode = 1;
this.home = this._pathStringToINode(args.user.home);
this.cwd = args.user.cwd ? args.user.cwd : this.home;
this.pwd = args.user.pwd ? args.user.pwd : this.cwd;
console.log(this.home);
console.log(this.cwd);
console.log(this.pwd);
}
private _iNodeToPathString(inode: number): string {
const currentNode = this.FsTable.get(inode);
if (!currentNode)
throw new Error('could not find the node in the fs table - inodetopathstring');
if (!currentNode.parent) {
return '/';
}
const parentPath: string = this._iNodeToPathString(currentNode.parent);
return parentPath === '/' ? `/${currentNode.name}` : `${parentPath}/${currentNode.name}`;
}
//TODO: Make all backend methods NOT throw errors. Just return null, and let more closely connected with bash functions call throwError() so user can see the error.
// this will save a lot of ass pain later.
private _pathStringToINode(path: string): number {
const normalizedPath = path.replace(/^\/+|\/+$/g, '');
const pathComponents = normalizedPath.split('/').filter((component) => component.length > 0);
console.log(path, 'pathstringtoinode');
if (pathComponents.length === 0) return this.rootINode;
let currentNode = this.FsTable.get(this.rootINode);
if (!currentNode) throw new Error('iNode does not exist,');
for (const component of pathComponents) {
const childINode = this._findChildNodeByName(currentNode, component);
if (childINode === null) throw new Error('this child iNode does not exist,');
const nextNode = this.FsTable.get(childINode);
if (!nextNode) throw new Error('iNode child does not exist,');
currentNode = nextNode;
}
console.log(path, currentNode.inode);
return currentNode.inode;
}
private _findChildNodeByName(node: TreeNode, name: string): number {
console.log(name, node);
for (const childINode of node.children) {
const child = this.FsTable.get(childINode);
if (child && child.name === name) {
return childINode;
}
}
throw new Error('could not find the specified child node');
}
private _isAbsolutePath = (path: string): boolean => {
return typeof path === 'string' && path.startsWith('/');
};
getPathByInode(inode: number): string {
return this._iNodeToPathString(inode);
}
formatPath(path: string): string {
console.log(path, 'formatPath');
const prefix = this._iNodeToPathString(this.home);
if (path.startsWith(prefix)) {
return path.replace(prefix, '~');
} else return path;
}
resolvePath(path: string): TreeNode {
if (path === '/') return this.getNodeByINode(this.rootINode);
let parsedPath: string = path;
if (!this._isAbsolutePath(path)) {
const trail: string = this._iNodeToPathString(this.cwd);
parsedPath = `${trail}/${path}`;
console.log(parsedPath);
}
if (path.startsWith('~')) {
const trail: string = this._iNodeToPathString(this.home);
parsedPath = `${trail}/${path.replace('~', '')}`;
console.log(parsedPath);
}
const INode: number = this._pathStringToINode(parsedPath);
const Node: TreeNode = this.getNodeByINode(INode);
return Node;
}
getNodeByINode(inode: number): TreeNode {
const node: TreeNode | undefined = this.FsTable.get(inode);
if (!node) throw new Error('Could not get the node, no such i node exists');
return node;
}
/* private _getPathToNode(node: TreeNode): string[] {
const path: string[] = [];
let current = node;
path.push(node.name);
while (current.parent) {
current = current.parent;
path.unshift(current.name);
}
return path;
} */
}

139
src/lib/stores/bash/sort.ts Executable file
View File

@@ -0,0 +1,139 @@
import type { TreeNode } from './fs';
import type { Bash } from './bash';
export enum SortNodeBy {
NAME = 'name',
INODE = 'inode',
SIZE = 'size',
EXTENSION = 'extension',
TYPE = 'type',
MTIME = 'modified',
ATIME = 'accessed',
CTIME = 'changed'
}
export class Sort {
public static nodeArraySort(
this: Bash,
nodes: TreeNode[] | number[],
reverse: boolean = false,
sortBy: SortNodeBy = SortNodeBy.NAME
): TreeNode[] {
if (nodes.length === 0) throw new Error('Tried to sort an empty node array!');
const parsedNodes: TreeNode[] = [];
if (typeof nodes[0] === 'number') {
for (const inode of nodes as number[]) {
const node = this.getFs().getNodeByINode(inode);
parsedNodes.push(node);
}
Sort.nodeQSort(parsedNodes, reverse, sortBy, 0, parsedNodes.length - 1);
console.log(parsedNodes);
return parsedNodes;
} else {
Sort.nodeQSort(nodes as TreeNode[], reverse, sortBy, 0, nodes.length - 1);
console.log(nodes);
return nodes as TreeNode[];
}
}
private static nodeQSort(
array: TreeNode[],
reverse: boolean,
sortBy: SortNodeBy,
start: number,
end: number
) {
if (end <= start) return;
let pivot: number = this.nodePartition(array, reverse, sortBy, start, end);
this.nodeQSort(array, reverse, sortBy, start, pivot - 1);
this.nodeQSort(array, reverse, sortBy, pivot + 1, end);
}
private static nodePartition(
part: TreeNode[],
reverse: boolean,
sortBy: SortNodeBy,
start: number,
end: number
): number {
let pivot: TreeNode = part[end];
let i: number = start - 1;
for (let j = start; j <= end; j++) {
if (this.nodeCompareElements(part[j], pivot, sortBy, reverse) < 0) {
i++;
let temp = part[i];
part[i] = part[j];
part[j] = temp;
}
}
i++;
let temp = part[i];
part[i] = part[end];
part[end] = temp;
return i;
}
private static nodeCompareElements(
a: TreeNode,
b: TreeNode,
sortBy: SortNodeBy,
reverse: boolean
): number {
switch (sortBy) {
case SortNodeBy.NAME: {
const minLength = Math.min(a.name.length, b.name.length);
for (let i = 0; i < minLength; i++) {
const charCodeA = a.name.charCodeAt(i);
const charCodeB = b.name.charCodeAt(i);
if (charCodeA !== charCodeB) {
return reverse ? charCodeB - charCodeA : charCodeA - charCodeB;
}
}
return reverse ? b.name.length - a.name.length : a.name.length - b.name.length;
}
case SortNodeBy.MTIME:
case SortNodeBy.ATIME:
case SortNodeBy.CTIME: {
console.log(sortBy, 'sortby');
// The sortBy serves as the lookup key in the timestamps object.
// It works because the times in SortBy enum have assigned values matching the names of the keys in the TreeNode object
const timeA: number = a.timestamps[sortBy].getTime();
const timeB: number = b.timestamps[sortBy].getTime();
return reverse ? timeA - timeB : timeB - timeA;
}
case SortNodeBy.SIZE: {
return reverse ? a.size - b.size : b.size - a.size;
}
case SortNodeBy.EXTENSION: {
const extA: string = a.name.split('.').pop() ?? '';
const extB: string = b.name.split('.').pop() ?? '';
const minLength = Math.min(extA.length, extB.length);
for (let i = 0; i < minLength; i++) {
const charCodeA = extA.charCodeAt(i);
const charCodeB = extB.charCodeAt(i);
if (charCodeA !== charCodeB) {
return reverse ? charCodeB - charCodeA : charCodeA - charCodeB;
}
}
return reverse ? extB.length - extA.length : extA.length - extB.length;
}
case SortNodeBy.INODE: {
return reverse ? b.inode - a.inode : a.inode - b.inode;
}
case SortNodeBy.TYPE: {
return reverse ? b.type - a.type : a.type - b.type;
}
default:
throw new Error(`Sorting basis outside of the declared scope. - `);
}
}
}

198
src/lib/stores/bash/static.ts Executable file
View File

@@ -0,0 +1,198 @@
import { Bash, ExitCode, type Group, type User } from './bash';
import { ls } from './commands/ls';
import { cd } from './commands/cd';
export type ICommand = {
method: (this: Bash, args: CommandArgs) => Result;
flags: string[];
help: string;
root: boolean;
};
export type CommandArgs = {
flags: string[];
args: string[];
};
export type resultData = {
cmd: string; //the string that contains the shorthand for the command that was executed - used in a switch statement in parseResult
data: any; //the data that the commmand may have returned like TreeNodes[] from ls
args?: CommandArgs;
};
export type Result = {
exitCode: ExitCode;
path: number; //the inode of the place that the command was executed in
data?: resultData;
};
export const GROUP: Group[] = [
{
groupname: 'sudo',
gid: 69,
members: [0, 1001]
},
{
groupname: 'users',
gid: 984,
members: [1001, 1002]
}
] as const;
export const PASSWD: User[] = [
{
username: 'root',
passwd: '123',
uid: 0,
gid: 0,
home: '/',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
},
{
username: 'admin',
passwd: '456',
uid: 1000,
gid: 1000,
home: '/home/admin',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
},
{
username: 'user',
passwd: '789',
uid: 1001,
gid: 1000,
home: '/home/user',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
},
{
username: 'kamil',
passwd: '000',
uid: 1002,
gid: 1000,
home: '/home/kamil',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
}
];
export const COMMANDS = {
cd,
ls
} as const satisfies Record<string, ICommand>;
/* //export const commands {
return: {
method: this.cmd_return,
flags: [],
help: "Help about this command",
},
ls: {
method: this.cmd_ls,
flags: [],
help: "./help/ls.md",
},
echo: {
method: this.cmd_echo,
flags: [],
help: "",
},
touch: {
method: this.cmd_touch,
flags: [],
help: "",
},
mkdir: {
method: this.cmd_mkdir,
flags: [],
help: "",
},
pwd: {
method: this.cmd_pwd,
flags: [],
help: "",
},
cd: {
method: this.cmd_cd,
flags: [],
help: "",
},
exit: {
method: this.cmd_exit,
flags: [],
help: "",
},
cp: {
method: this.cmd_cp,
flags: [],
help: "",
},
mv: {
method: this.cmd_mv,
flags: [],
help: "",
},
rmdir: {
method: this.cmd_rmdir,
flags: [],
help: "",
},
cat: {
method: this.cmd_cat,
flags: [],
help: "",
},
dir: {
method: this.cmd_dir,
flags: [],
help: "",
},
less: {
method: this.cmd_less,
flags: [],
help: "",
},
chown: {
method: this.cmd_chown,
flags: [],
help: "",
},
chmod: {
method: this.cmd_chmod,
flags: [],
help: "",
},
reboot: {
method: this.cmd_reboot,
flags: [],
help: "",
},
help: {
method: this.cmd_help,
flags: [],
help: "",
},
whoami: {
method: this.cmd_whoami,
flags: [],
help: "",
},
rm: {
method: this.cmd_rm,
flags: [],
help: "",
},
sudo: {
method: this.cmd_sudo,
flags: [],
help: "",
},
su: {
method: this.cmd_su,
flags: [],
help: "",
},
clear: {
method: this.cmd_clear,
flags: [],
help: "",
}
} */

72
src/lib/stores/char.ts Executable file
View File

@@ -0,0 +1,72 @@
type BrandedChar = string & { readonly __brand: unique symbol };
export class Char {
private readonly value: BrandedChar;
private constructor(char: string) {
if (char.length !== 1) {
throw new Error('Char must be exactly one character');
}
this.value = char as BrandedChar;
}
static from(char: string): Char {
return new Char(char);
}
valueOf(): string {
return this.value;
}
toString(): string {
return this.value;
}
charCode(): number {
return this.value.charCodeAt(0);
}
isDigit(): boolean {
return /^\d$/.test(this.value);
}
isLetter(): boolean {
return /^[a-zA-Z]$/.test(this.value);
}
isAlphanumeric(): boolean {
return /^[a-zA-Z0-9]$/.test(this.value);
}
isWhitespace(): boolean {
return /^\s$/.test(this.value);
}
isUpperCase(): boolean {
return this.value === this.value.toUpperCase() && this.value !== this.value.toLowerCase();
}
isLowerCase(): boolean {
return this.value === this.value.toLowerCase() && this.value !== this.value.toUpperCase();
}
toUpperCase(): Char {
return Char.from(this.value.toUpperCase());
}
toLowerCase(): Char {
return Char.from(this.value.toLowerCase());
}
equals(other: Char): boolean {
return this.value === other.value;
}
equalsIgnoreCase(other: Char): boolean {
return this.value.toLowerCase() === other.value.toLowerCase();
}
static fromCharCode(charCode: number): Char {
return Char.from(String.fromCharCode(charCode));
}
}

13
src/lib/stores/lang.ts Executable file
View File

@@ -0,0 +1,13 @@
import plFlag from '$lib/assets/plFlag.svg';
import enFlag from '$lib/assets/enFlag.svg';
import deFlag from '$lib/assets/deFlag.svg';
import frFlag from '$lib/assets/frFlag.svg';
import jaFlag from '$lib/assets/jaFlag.svg';
export const langs = {
pl: {},
en: {},
de: {},
ja: {},
fr: {}
};

31
src/lib/stores/stack.ts Executable file
View File

@@ -0,0 +1,31 @@
export class Stack<T> {
private items: T[] = [];
push(element: T): void {
this.items.push(element);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
clear(): void {
this.items = [];
}
toArray(): T[] {
return [...this.items];
}
}

View File

@@ -0,0 +1,125 @@
import type { User } from '../bash/bash';
import type { TreeNode } from '../bash/fs';
import { Terminal, type TermInitArgs } from './terminal';
let initializing = $state(true);
export function isInitializing(): boolean {
return initializing;
}
function jsonToNodeTable(data: any, parent?: number): Map<number, TreeNode> {
const FsTable: Map<number, TreeNode> = new Map<number, TreeNode>();
const entryList = Object.entries(data);
for (let i = 0; i < entryList.length; i++) {
const object: any = entryList[i][1];
const node: TreeNode = {
inode: object.Inode,
name: object.Name,
type: object.Type,
size: object.Size,
interactible: object.Interactible,
func: object.Func,
children: object.Children,
content: object.Content,
link: object.Link,
permission: {
user: {
r: object.Permission[0]?.Read,
w: object.Permission[0]?.Write,
x: object.Permission[0]?.Exec
},
group: {
r: object.Permission[1]?.Read,
w: object.Permission[1]?.Write,
x: object.Permission[1]?.Exec
},
other: {
r: object.Permission[2]?.Read,
w: object.Permission[2]?.Write,
x: object.Permission[2]?.Exec
}
},
owner: object.Owner,
group: object.Group,
timestamps: {
modified: new Date(object.TimeStamps.MTime),
changed: new Date(object.TimeStamps.CTime),
accessed: new Date(object.TimeStamps.ATime)
},
parent: object.Parent
};
FsTable.set(object.Inode, node);
}
return FsTable;
}
async function fetchFsJson(sig: string): Promise<any> {
const signature: string | null = localStorage.getItem('signature');
if (signature !== sig || signature === null || localStorage.getItem('fs') === null) {
const fs: JSON = await fetchFileSystem('/src/lib/assets/fs/fs.json');
localStorage.setItem('signature', sig);
localStorage.setItem('fs', JSON.stringify(fs));
console.info('FS fetched from file, new sinature:', sig);
return fs;
} else {
const fs: string | null = localStorage.getItem('fs');
if (fs === null) throw new Error('FS in LocalStorage is null!');
console.info('FS fetched from localStorage with signature:', sig);
return JSON.parse(fs);
}
}
async function fetchFsSignature(path: string): Promise<any> {
try {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.text();
return data.slice(0, 36); //32 characters of uuid + 4 dashes
} catch (error) {
console.error('Failed to fetch the file system signature', error);
}
}
async function fetchFileSystem(path: string): Promise<any> {
const response = await fetch(path);
if (!response.ok) throw new Error('Failed to fetch the file system json');
const data = await response.json();
return data;
}
export async function initTerminal(user: User, callbackInit: any): Promise<Terminal> {
try {
const sig = await fetchFsSignature('/src/lib/assets/fs/signature');
const fsJson = await fetchFsJson(sig);
const fs: Map<number, TreeNode> = jsonToNodeTable(fsJson);
const args: TermInitArgs = {
bash: {
user,
fs
}
};
const terminal = new Terminal(args);
terminal.registerCallbacks(callbackInit);
return terminal;
} catch (error) {
console.error('Failed to initialize terminal:', error);
throw error;
} finally {
initializing = false;
}
}

View File

@@ -0,0 +1,41 @@
import { mount, unmount } from 'svelte';
import Output from '../../../modules/terminal/Output.svelte';
import { isInitializing } from './init.svelte';
import type { PrintData } from './terminal';
interface OutputProps {
path: string;
output: any;
cmd: string;
}
const outputInstances = new Set<Output>();
function appendOutput(container: HTMLElement, props: OutputProps): Output | undefined {
if (!container) return;
const instance = mount(Output, {
target: container,
props
});
outputInstances.add(instance);
return instance;
}
export function print(e: HTMLElement, data: PrintData): void {
if (isInitializing()) {
console.error('Terminal is initializing! Skipping Print');
return;
}
appendOutput(e, {
path: data.path,
output: data.output,
cmd: data.cmd
});
}
export function clear(): void {
for (const n of outputInstances) {
unmount(n);
}
}

View File

@@ -0,0 +1,127 @@
import { Bash, ExitCode, type BashInitArgs, type User } from '../bash/bash';
import type { VirtualFS } from '../bash/fs';
import type { CommandArgs } from '../bash/static';
import { Char } from '../char';
export type TerminalMode = {};
export type TermInitArgs = {
bash: BashInitArgs;
};
export type ParsedInput = {
command: string;
args: CommandArgs;
};
export type PrintData = {
path: string;
output: any; // TODO: Make this be any predefined format of outputs like ls, ls w/ flags and so on;
cmd: string;
};
export type PageCallbacks = {
print: (data: PrintData) => void;
getWidth: () => number;
};
export class Terminal {
private bash: Bash;
private callbacks: Partial<PageCallbacks> = {};
constructor(args: TermInitArgs) {
args.bash.stdio = this;
this.bash = new Bash(args.bash);
}
private _parseInput(input: string): ParsedInput {
let args: string[] = [];
const result: ParsedInput = { command: '', args: { flags: [], args: [] } };
let current: string = '';
let inQuotes: boolean = false;
let quoteChar: string = '';
for (let i = 0; i < input.length; i++) {
const char = input[i];
if ((char === '"' || char === "'") && !inQuotes) {
inQuotes = true;
quoteChar = char;
continue;
} else if (char === quoteChar && inQuotes) {
inQuotes = false;
continue;
}
if (char === ' ' && !inQuotes) {
if (current === '') continue;
result.command === '' ? (result.command = current) : args.push(current);
current = '';
} else {
current += char;
}
}
if (current !== '') result.command === '' ? (result.command = current) : args.push(current);
for (let i = 0; i < args.length; i++) {
let curr = args[i];
console.log(curr);
if (curr.startsWith('--')) {
curr = curr.replaceAll('--', '');
if (curr.length === 0) continue;
result.args.flags.push(curr);
} else if (curr.startsWith('-')) {
curr = curr.replaceAll('-', '');
if (curr.length === 0) continue;
for (let n = 0; n < curr.length; n++) {
result.args.flags.push(curr[n]);
}
} else result.args.args.push(curr);
}
return result;
}
executeCommand(input: string): void {
this.bash.updateHistory(input);
const parsed: ParsedInput = this._parseInput(input);
console.log(parsed, 'executeCommand output');
this.bash.executeCommand(parsed.command, parsed.args);
}
registerCallbacks(callbacks: PageCallbacks): void {
this.callbacks = callbacks;
}
getUser(): User {
return this.bash.getUser();
}
getTerminalWidth(): number {
const width = this.callbacks.getWidth?.();
if(!width) { throw new Error('somehow width is undefined still after all the checks'); }
console.log(width);
return width;
}
getCwd(): string {
const fs: VirtualFS = this.bash.getFs();
console.log(fs.getPathByInode(this.bash.getCwd()));
return fs.formatPath(fs.getPathByInode(this.bash.getCwd()));
}
//TODO: Later reimplement the backend helper methods
/* userLogin(username: string, passwd: string): ExitCode {
return this.bash.userLogin(username, passwd);
} */
PrintOutput(data: PrintData) {
this.callbacks.print?.(data);
}
}

17
src/lib/stores/theme.ts Executable file
View File

@@ -0,0 +1,17 @@
import { writable } from 'svelte/store';
function getInitalTheme(): string {
if (typeof window === 'undefined') return 'dark';
const savedTheme: string | null = localStorage.getItem('theme');
if (savedTheme === 'dark' || savedTheme === 'light') return savedTheme;
const sysPrefTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
return sysPrefTheme ? 'dark' : 'light';
}
export const theme = writable(getInitalTheme());
theme.subscribe((value) => {
if (typeof window !== 'undefined') localStorage.setItem('theme', value);
});

55
src/modules/Footer.svelte Executable file
View File

@@ -0,0 +1,55 @@
<script lang="ts">
import { Languages, SunMoon } from '@lucide/svelte';
import { theme } from '$lib/stores/theme';
import plFlag from '$lib/assets/plFlag.svg';
import enFlag from '$lib/assets/enFlag.svg';
</script>
<div
id="footer"
class="fixed bottom-0 flex w-max flex-row-reverse items-center justify-between p-2"
>
<div class="footer-child flex flex-row-reverse content-start items-center" id="footer-lang-child">
<button
type="button"
id="lang-switch"
class="button rounded-lg bg-bg text-primary hover:text-primary-hover"
>
<Languages class="m-2.5 size-10" />
</button>
<div id="lang-wrapper" class="">
<div id="pl" class="lang visible">
<img class="flags" src={plFlag} alt="PL" height="30" width="30" />
</div>
<div id="en" class="lang">
<img class="flags" src={enFlag} alt="EN" height="30" width="30" />
</div>
<!-- <div id="ja" class="lang"><img class="flags" src="/images/japan-svgrepo-com.svg" alt="JA" height="30" width="30"/></div>
<div id="de" class="lang"><img class="flags" src="/images/germany-svgrepo-com.svg" alt="DE" height="30" width="30"/></div>
<div id="fr" class="lang"><img class="flags" src="/images/france-svgrepo-com.svg" alt="FR" height="30" width="30"/></div> -->
</div>
</div>
<div
class="footer-child flex flex-row content-start items-center bg-bg text-primary hover:text-primary-hover"
id="footer-theme-child"
>
<button
type="button"
id="theme-switch"
class="button rounded-lg"
onclick={() => {
$theme = $theme === 'dark' ? 'light' : 'dark';
}}
>
<SunMoon class="m-2.5 size-10" />
</button>
</div>
</div>
<style>
.button {
transition: var(--transition-standard);
padding: 0;
border: none;
}
</style>

57
src/modules/Loading.svelte Executable file
View File

@@ -0,0 +1,57 @@
<div
id="loader-master"
class=" light:bg-dots-bg-2-light pointer-events-none visible absolute z-49 size-full bg-bg-dark select-none"
>
<div id="loader" class="tran absolute top-1/2 left-1/2 z-50 -translate-x-1/2 -translate-y-1/2">
<div id="bar-wrapper" class="flex h-full flex-row items-center gap-2">
<div
class="bar h-10 w-2 origin-center rounded-[3px] bg-loader-primary-dark light:bg-loader-primary-light"
></div>
<div
class="bar h-10 w-2 origin-center rounded-[3px] bg-loader-primary-dark light:bg-loader-primary-light"
></div>
<div
class="bar h-10 w-2 origin-center rounded-[3px] bg-loader-primary-dark light:bg-loader-primary-light"
></div>
<div
class="bar h-10 w-2 origin-center rounded-[3px] bg-loader-primary-dark light:bg-loader-primary-light"
></div>
</div>
</div>
</div>
<style>
#loader-master {
transition: all 0.3s cubic-bezier(0.41, 0.68, 0.45, 0.96);
}
.bar {
animation: pulse 1s ease-in-out infinite;
}
.bar:nth-child(1) {
animation-delay: 0s;
}
.bar:nth-child(2) {
animation-delay: 0.1s;
}
.bar:nth-child(3) {
animation-delay: 0.2s;
}
.bar:nth-child(4) {
animation-delay: 0.3s;
}
@keyframes pulse {
0% {
transform: scaleY(1);
}
30% {
transform: scaleY(0.35);
background-color: oklch(0.8 0.15 292);
}
60% {
transform: scaleY(1);
}
}
</style>

4
src/modules/Panel.svelte Executable file
View File

@@ -0,0 +1,4 @@
<section>
<section></section>
<section></section>
</section>

110
src/modules/Settings.svelte Executable file
View File

@@ -0,0 +1,110 @@
<script lang="ts">
import { Languages, Settings2, SunMoon } from '@lucide/svelte';
import { theme } from '$lib/stores/theme';
import plFlag from '$lib/assets/plFlag.svg';
import enFlag from '$lib/assets/enFlag.svg';
import deFlag from '$lib/assets/deFlag.svg';
import frFlag from '$lib/assets/frFlag.svg';
import jaFlag from '$lib/assets/jaFlag.svg';
function toggleSettings() {
const settingsMenu = window.document.getElementById('settings-menu');
const langsMenu = window.document.getElementById('langs-menu');
langsMenu?.classList.add('hide');
settingsMenu?.classList.toggle('hide');
}
function toggleLangs() {
const langsMenu = window.document.getElementById('langs-menu');
langsMenu?.classList.toggle('hide');
}
</script>
<div
id="options"
class="absolute top-0 left-0 isolate mx-4 rounded-b-xl bg-bg-dark light:bg-bg-light"
>
<section class="hide grid" id="settings-menu">
<div class="flex flex-col items-center justify-center gap-4 overflow-hidden">
<div class=" mt-4 flex items-center justify-center rounded-lg">
<button
class=" rounded-lg bg-bg-lighter-dark p-1.5 text-text-dark shadow-button duration-75 hover:text-primary-hover-dark active:translate-y-0.5 light:bg-bg-lighter-light light:text-text-muted-light light:hover:text-primary-hover-light"
id="theme-btn"
onclick={() => {
$theme = $theme === 'dark' ? 'light' : 'dark';
}}
>
<SunMoon strokeWidth={1} size={32} />
</button>
</div>
<div class="flex flex-col content-center items-center">
<button
class="z-3 mb-4 rounded-lg bg-bg-lighter-dark p-1.5 text-text-dark duration-75 hover:text-primary-hover-dark active:translate-y-0.5 light:bg-bg-lighter-light light:text-text-muted-light light:hover:text-primary-hover-light"
id="langs-btn"
onclick={toggleLangs}
>
<Languages strokeWidth={1} size={32} />
</button>
<form id="langs-menu" class="hide grid">
<div class="z-2 mb-4 flex flex-col items-center gap-2 overflow-hidden">
<label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
>
<input name="langs" type="radio" class="lang hidden" id="pl" autocomplete="off" />
<img class="flags mx-2" src={plFlag} alt="PL" height="26" width="26" />
</label>
<label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
>
<input name="langs" type="radio" class="lang hidden" id="en" autocomplete="off" />
<img class="flags mx-2" src={enFlag} alt="EN" height="26" width="26" />
</label>
<label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
>
<input name="langs" type="radio" class="lang hidden" id="en" autocomplete="off" />
<img class="flags mx-2" src={deFlag} alt="DE" height="26" width="26" />
</label>
<label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
>
<input name="langs" type="radio" class="lang hidden" id="en" autocomplete="off" />
<img class="flags mx-2" src={jaFlag} alt="JA" height="26" width="26" />
</label>
<label
class="border-primary-dark duration-100 hover:ml-1 hover:pl-1 has-checked:border-l-3 has-checked:hover:m-0 has-checked:hover:p-0 light:border-primary-light"
>
<input name="langs" type="radio" class="lang hidden" id="en" autocomplete="off" />
<img class="flags mx-2" src={frFlag} alt="FR" height="26" width="26" />
</label>
</div>
</form>
</div>
</div>
</section>
<button
class="rounded-b-xl border-t bg-bg-light-dark p-2 text-text-dark shadow-button hover:text-primary-dark light:bg-bg-lighter-light light:text-text-muted-light light:hover:text-primary-light"
id="settings-btn"
onclick={toggleSettings}
>
<Settings2 strokeWidth={1} size={38} />
</button>
</div>
<style>
#langs-menu {
transition: 0.2s ease-in;
grid-template-rows: 1fr;
&.hide {
grid-template-rows: 0fr;
}
}
#settings-menu {
transition: 0.2s ease-in;
grid-template-rows: 1fr;
&.hide {
grid-template-rows: 0fr;
}
}
</style>

147
src/modules/Terminal.svelte Executable file
View File

@@ -0,0 +1,147 @@
<script lang="ts">
import type { User } from '$lib/stores/bash/bash';
import { Terminal, type PrintData } from '$lib/stores/terminal/terminal';
import { onMount } from 'svelte';
import Input from './terminal/Input.svelte';
import { initTerminal, isInitializing } from '$lib/stores/terminal/init.svelte';
import { clear, print } from '$lib/stores/terminal/stdio';
const clearTerminal = (): void => clear();
const printOutput = (e: HTMLElement, d: PrintData): void => print(e, d);
function updateTerminal() {
username = terminal!.getUser().username;
cwd = terminal!.getCwd();
}
function getWidth() {
const e = document.getElementById('cout');
if(!e){
throw new Error('cant get width of the teminal element. Its null');
}
const padding: number = parseInt(window.getComputedStyle(e, null).getPropertyValue('padding').slice(0, -2));
console.log(padding);
return e.clientWidth - (padding * 2);
}
function handleInput(e: KeyboardEvent) {
switch (e.key) {
case 'Enter': {
terminal.executeCommand(inputValue);
updateTerminal();
break;
}
case 'ArrowRight': {
//TODO: Move cursor visually
}
case 'ArrowLeft': {
//TODO: Move cursor visually
}
case 'ArrowUp': {
//TODO: Make a traverse history function with up/down args
}
case 'ArrowDown': {
//TODO: Make a traverse history function with up/down args
}
}
}
//Callback initializer
const callbackInit = {
print: (data: any) => {
const e = document.getElementById('outputWrapper');
if (!e) return;
printOutput(e, data);
},
getWidth: getWidth
};
//Test user with basic data so the bash can run
let testUser: User = {
username: 'kamil',
passwd: '123',
uid: 0,
gid: 0,
home: '/home/kamil',
history: []
};
//Empty terminal variable where the terminal class instance will be stored
let terminal: Terminal;
let username: string = $state(testUser.username);
let cwd: string = $state(testUser.home);
let inputValue = $state<string>('');
onMount(async () => {
try {
terminal = await initTerminal(testUser, callbackInit);
updateTerminal();
} catch (error) {
console.error('onMount trycatch failed', error);
}
});
</script>
<label for="input" onkeydowncapture={(e) => handleInput(e)}>
<div id="terminal" class="terminal-window shadow-() size-full rounded-md shadow-bg">
<div
class="terminal-bar flex h-9 w-full flex-row items-center rounded-t-md bg-bg-dark text-center font-terminal text-sm font-bold text-primary-dark light:bg-bg-dark-light light:text-primary-light"
>
<div class="dots-wrapper mx-2.5 flex h-full flex-row items-center justify-center gap-2.5">
<button class="size-2.5 cursor-pointer rounded-full p-0" title=""></button>
<button class="size-2.5 cursor-pointer rounded-full p-0" title=""></button>
<button class="size-2.5 cursor-pointer rounded-full p-0" title=""></button>
</div>
<div class=" flex">
<h5>{username}</h5>
<!-- prettier-ignore -->
<h5 class=" mr-2">@terminal: </h5>
<h5>{cwd}</h5>
</div>
</div>
<div
class="inner-content scroll-hidden h-[860px] origin-top overflow-y-auto rounded-b-md bg-bg-light-dark p-4 text-text-dark shadow-subtle light:bg-bg-lighter-light light:text-text-light"
id="cout"
>
<div id="outputWrapper"></div>
<Input {cwd} bind:inputValue />
</div>
</div>
</label>
<style>
* {
transition: var(--transition-standard);
}
.dots-wrapper {
& > button {
border: none;
&:hover {
transform: translateY(-0.1rem);
}
&:active {
transform: translate(0);
}
&:nth-child(1) {
background-color: rgb(255, 0, 0);
&:active {
background-color: rgb(255, 100, 100);
}
}
&:nth-child(2) {
background-color: rgb(255, 165, 0);
&:active {
background-color: rgb(255, 215, 50);
}
}
&:nth-child(3) {
background-color: rgb(50, 205, 50);
&:active {
background-color: rgb(100, 255, 100);
}
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { SquarePen } from '@lucide/svelte';
let {
langName,
icon,
checked = false
}: { langName: string; icon: string; checked?: boolean } = $props();
</script>
<div
class=" flex items-center text-text-dark saturate-50 has-checked:saturate-100 light:text-text-light"
>
<div class=" flex w-full flex-col justify-center gap-1 rounded-lg bg-bg-mid-dark">
<div class="flex items-center gap-2">
<button
class="m-2 mr-1 rounded-md bg-bg-lighter-dark p-1 shadow-subtle hover:text-primary-hover-dark active:text-primary-dark"
>
<SquarePen size={24} strokeWidth={1} />
</button>
<img alt="" width="32" height="32" src={icon} />
<h5 class=" mr-2 font-primary font-bold">{langName}</h5>
<label
for={langName}
class="relative m-2 ml-auto block h-5 w-10 cursor-pointer rounded-full bg-bg-lighter-dark shadow-subtle"
>
<input type="checkbox" {checked} id={langName} class=" peer sr-only" />
<span
class=" absolute top-0.5 left-0.5 h-4/5 w-2/5 rounded-full bg-bg-dark peer-checked:left-5.5 peer-checked:bg-primary-dark"
></span>
</label>
</div>
</div>
<div class=" flex items-center justify-center rounded-r-lg bg-bg-dark"></div>
</div>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { ChevronRight, Power, SquarePen } from '@lucide/svelte';
let { projectName, checked = true }: { projectName: string; checked?: boolean } = $props();
</script>
<div
class=" flex items-center text-text-dark saturate-50 has-checked:saturate-100 light:text-text-light"
>
<div class=" flex w-full flex-col justify-center gap-1 rounded-lg bg-bg-mid-dark">
<h5 class=" m-4 font-primary text-xl font-bold">{projectName}</h5>
<div class="flex items-center gap-2">
<label
for={projectName}
class="relative m-2 ml-auto block h-5 w-10 cursor-pointer rounded-full bg-bg-lighter-dark shadow-subtle"
>
<input type="checkbox" {checked} id={projectName} class=" peer sr-only" />
<span
class=" absolute top-0.5 left-0.5 h-4/5 w-2/5 rounded-full bg-bg-dark peer-checked:left-5.5 peer-checked:bg-primary-dark"
></span>
</label>
</div>
</div>
<div class=" flex items-center justify-center rounded-r-lg bg-bg-dark"></div>
</div>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount, type Snippet } from 'svelte';
let {
children,
path = '/',
checked = false
}: { children: Snippet; path?: string; checked?: boolean } = $props();
onMount(() => {
if (checked) goto(path);
});
</script>
<label
class="m-0 flex size-12 items-center justify-center rounded-xl bg-bg-lighter-dark shadow-subtle hover:text-primary-hover-dark has-checked:text-primary-dark has-checked:hover:text-primary-hover-dark light:bg-bg-light-light light:hover:text-primary-hover-light light:has-checked:text-primary-light light:has-checked:hover:text-primary-hover-light"
>
<input
onclick={() => goto(path)}
name="langs"
type="radio"
class="hidden"
id="pl"
autocomplete="off"
{checked}
/>
{@render children()}
</label>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
let {
inputValue = $bindable(),
isFocused,
cwd
}: { inputValue: string; isFocused?: boolean; cwd: string } = $props();
function handleInput(event: Event) {
inputValue = (event.target as HTMLInputElement).value;
}
</script>
<div class=" relative">
<input
bind:value={inputValue}
onkeydown={(e) => e.key === 'Enter' && (inputValue = '')}
oninput={handleInput}
onfocus={() => (isFocused = true)}
onblur={() => (isFocused = false)}
type="text"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
class=" pointer-events-none absolute left-0 m-0 w-0 border-none p-0 opacity-0"
id="input"
/>
<p class="cwd" id="cwd">{cwd}</p>
<div class="w flex-column flex flex-row flex-wrap font-terminal">
<span class="pointer pr-2">$</span>
<!-- prettier-ignore -->
<div style="white-space: preserve;" class=" relative wrap-break-word">{inputValue}</div>
<span id="cursor" class={isFocused ? 'animate-cursor-blink' : ''}>_</span>
</div>
</div>
<style>
</style>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
let { path, output, cmd }: { path: string; output: any; cmd: string } = $props(); //TODO: change any to matching
</script>
<p class="cwd" id="cwd">{path}</p>
<div class="pointer-wrapper mr-4 mb-2.5 flex flex-col justify-center font-terminal">
<div class=" flex flex-row items-center">
<span class=" pointer self-start pr-2">$</span>
<p>{cmd}</p>
</div>
<div style="white-space: nowrap;" class=" relative">
{#if typeof output === 'string'}
{output}
{:else if output instanceof Element}
{@html output.outerHTML}
{:else}
{output}
{/if}
</div>
</div>

54
src/routes/+layout.svelte Executable file
View File

@@ -0,0 +1,54 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import { onMount, type Snippet } from 'svelte';
import { theme } from '$lib/stores/theme';
let { children }: { children: Snippet } = $props();
let bg1: string | null;
let bg2: string | null;
onMount(() => {
const style = window.getComputedStyle(document.documentElement);
bg1 = style.getPropertyValue('--dots-bg-1');
bg2 = style.getPropertyValue('--dots-bg-2');
if (bg2 && bg1) {
window.CSS.registerProperty({
name: '--dots-bg-2',
syntax: '<color>',
inherits: true,
initialValue: bg2
});
window.CSS.registerProperty({
name: '--dots-bg-1',
syntax: '<color>',
inherits: true,
initialValue: bg1
});
}
});
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<main
class="text-text m-auto flex min-h-dvh w-full flex-col items-center justify-center overflow-hidden bg-dots-bg bg-size-[20px_20px]"
class:dark={$theme === 'dark'}
class:light={$theme === 'light'}
>
{@render children()}
</main>
<style>
main {
transition:
--dots-bg-2 0.2s ease-in,
--dots-bg-1 0.2s ease-in;
}
</style>

14
src/routes/+page.svelte Executable file
View File

@@ -0,0 +1,14 @@
<script lang="ts">
import TerminalModule from '../modules/Terminal.svelte';
import Settings from '../modules/Settings.svelte';
import Loading from '../modules/Loading.svelte';
import { isInitializing } from '$lib/stores/terminal/init.svelte';
</script>
<Settings></Settings>
<div class="h-dvh w-full p-24">
<TerminalModule />
</div>
<style>
</style>

View File

13
src/routes/page.svelte.spec.ts Executable file
View File

@@ -0,0 +1,13 @@
import { page } from '@vitest/browser/context';
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import Page from './+page.svelte';
describe('/+page.svelte', () => {
it('should render h1', async () => {
render(Page);
const heading = page.getByRole('heading', { level: 1 });
await expect.element(heading).toBeInTheDocument();
});
});

50
src/routes/panel/+layout.svelte Executable file
View File

@@ -0,0 +1,50 @@
<script lang="ts">
import '../../app.css';
import favicon from '$lib/assets/favicon.svg';
import type { Snippet } from 'svelte';
import { BookA, ChartLine, ChartNoAxesColumn, FolderArchive, Plus } from '@lucide/svelte';
import SidebarButton from '../../modules/panel/SidebarButton.svelte';
let { children }: { children: Snippet } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
</svelte:head>
<div class="flex h-dvh w-full flex-row text-text-dark light:text-text-light">
<div class="flex h-full w-fit flex-col items-center bg-bg-dark light:bg-bg-light">
<img class="m-3 mb-8" alt="Logo" src={favicon} height="32" width="32" />
<form class="flex h-full w-fit flex-col items-center gap-3">
<label
class="m-0 mb-4 flex size-12 items-center justify-center rounded-xl bg-bg-lighter-dark shadow-subtle hover:text-primary-hover-dark has-checked:text-primary-dark has-checked:hover:text-primary-hover-dark light:bg-bg-light-light light:hover:text-primary-hover-light light:has-checked:text-primary-light light:has-checked:hover:text-primary-hover-light"
>
<input name="langs" type="radio" class="hidden" id="pl" autocomplete="off" />
<Plus strokeWidth={1} />
</label>
<SidebarButton checked path="/panel/projects">
<FolderArchive strokeWidth={1} />
</SidebarButton>
<SidebarButton path="/panel/langs">
<BookA strokeWidth={1} />
</SidebarButton>
<SidebarButton path="/panel/metrics">
<ChartLine strokeWidth={1} />
</SidebarButton>
<SidebarButton path="/panel/stats">
<ChartNoAxesColumn strokeWidth={1} />
</SidebarButton>
</form>
</div>
<div class="flex w-full grow">
{@render children()}
</div>
</div>
<style>
main {
transition:
--dots-bg-2 0.2s ease-in,
--dots-bg-1 0.2s ease-in;
}
</style>

0
src/routes/panel/+page.svelte Executable file
View File

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import LangModule from '../../../modules/panel/LangModule.svelte';
import plFlag from '$lib/assets/plFlag.svg';
import enFlag from '$lib/assets/enFlag.svg';
import deFlag from '$lib/assets/deFlag.svg';
import frFlag from '$lib/assets/frFlag.svg';
import jaFlag from '$lib/assets/jaFlag.svg';
</script>
<div>
<form name="langs" class="m-10 flex flex-col gap-4">
<LangModule checked langName="Polish" icon={plFlag} />
<LangModule checked langName="English" icon={enFlag} />
<LangModule checked langName="German" icon={deFlag} />
<LangModule langName="Japanese" icon={jaFlag} />
<LangModule langName="French" icon={frFlag} />
</form>
<div></div>
</div>

View File

@@ -0,0 +1 @@
METRICS

View File

@@ -0,0 +1,7 @@
<script lang="ts">
import ProjectModule from '../../../modules/panel/ProjectModule.svelte';
</script>
<div class=" m-10">
<ProjectModule projectName="ProjectName" />
</div>

View File

@@ -0,0 +1 @@
STATS

0
src/routes/test/+page.svelte Executable file
View File

26
src/style/animations.css Executable file
View File

@@ -0,0 +1,26 @@
@theme {
--animate-loading: loading 1s ease-in-out infinite;
--animate-cursor-blink: blink 1s step-end infinite;
@keyframes blink {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
}
@keyframes loading {
0% {
transform: scaleY(1);
}
30% {
transform: scaleY(0.35);
background-color: var(--color-loader-primary-hl);
}
60% {
transform: scaleY(1);
}
}
}

955
src/style/fonts.css Executable file
View File

@@ -0,0 +1,955 @@
/* latin-ext */
@font-face {
font-family: 'Atkinson Hyperlegible Mono';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegiblemono/v8/tssPAoFBci4C4gvhPXrt3wjT1MqSzhA4t7IIcncBiwKonlKh6PW-UyGM1JTKSRMW8qj-lw.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Atkinson Hyperlegible Mono';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegiblemono/v8/tssPAoFBci4C4gvhPXrt3wjT1MqSzhA4t7IIcncBiwKonlKh6PW-UyGM1JTKSRMY8qg.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Atkinson Hyperlegible Mono';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegiblemono/v8/tssNAoFBci4C4gvhPXrt3wjT1MqSzhA4t7IIcncBiyihrK15gZ4k_SaZnNCSBCMa6qw.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Atkinson Hyperlegible Mono';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegiblemono/v8/tssNAoFBci4C4gvhPXrt3wjT1MqSzhA4t7IIcncBiyihrK15gZ4k_SaZnNCSCiMa.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Bungee Hairline';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/bungeehairline/v26/snfys0G548t04270a_ljTLUVrv-LaBecc5Y.woff2)
format('woff2');
unicode-range:
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301,
U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Bungee Hairline';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/bungeehairline/v26/snfys0G548t04270a_ljTLUVrv-LaRecc5Y.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Bungee Hairline';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/bungeehairline/v26/snfys0G548t04270a_ljTLUVrv-LZxec.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Doto';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/doto/v3/t5t6IRMbNJ6TQG7Il_EKPqP9zTkn6IuPWhojrg.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Doto';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/doto/v3/t5t6IRMbNJ6TQG7Il_EKPqP9zTkn6IuBWho.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Just Me Again Down Here';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/justmeagaindownhere/v25/MwQmbgXtz-Wc6RUEGNMc0QpRrfUh2hSdBBMoAtwOtKHAcw.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Just Me Again Down Here';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/justmeagaindownhere/v25/MwQmbgXtz-Wc6RUEGNMc0QpRrfUh2hSdBBMoAtwAtKE.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dNIFZifjKcF5UAWdDRYERMSHK_IwU.woff2)
format('woff2');
unicode-range:
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301,
U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dNIFZifjKcF5UAWdDRYERMSXK_IwU.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Space Mono';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dNIFZifjKcF5UAWdDRYERMR3K_.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dSIFZifjKcF5UAWdDRYERE_FeqEySRV3U.woff2)
format('woff2');
unicode-range:
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301,
U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dSIFZifjKcF5UAWdDRYERE_FeqEiSRV3U.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Space Mono';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dSIFZifjKcF5UAWdDRYERE_FeqHCSR.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dPIFZifjKcF5UAWdDRYE58RWq7.woff2)
format('woff2');
unicode-range:
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301,
U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dPIFZifjKcF5UAWdDRYE98RWq7.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dPIFZifjKcF5UAWdDRYEF8RQ.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dMIFZifjKcF5UAWdDRaPpZUFqaHjyV.woff2)
format('woff2');
unicode-range:
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301,
U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dMIFZifjKcF5UAWdDRaPpZUFuaHjyV.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Space Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/spacemono/v17/i7dMIFZifjKcF5UAWdDRaPpZUFWaHg.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Atkinson Hyperlegible Mono';
font-style: italic;
font-weight: 200 800;
font-display: swap;
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegiblemono/v8/tss6AoFBci4C4gvhPXrt3wjT1MqSzhA4t7IIcncBiwKotFCJGR0i.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Atkinson Hyperlegible Mono';
font-style: italic;
font-weight: 200 800;
font-display: swap;
src: url(https://fonts.gstatic.com/s/atkinsonhyperlegiblemono/v8/tss6AoFBci4C4gvhPXrt3wjT1MqSzhA4t7IIcncBiwKotF6JGQ.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Quicksand';
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/quicksand/v37/6xKtdSZaM9iE8KbpRA_hJFQNcOM.woff2)
format('woff2');
unicode-range:
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301,
U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Quicksand';
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/quicksand/v37/6xKtdSZaM9iE8KbpRA_hJVQNcOM.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Quicksand';
font-style: normal;
font-weight: 300 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/quicksand/v37/6xKtdSZaM9iE8KbpRA_hK1QN.woff2) format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Silkscreen';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/silkscreen/v6/m8JXjfVPf62XiF7kO-i9YL1la1OD.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Silkscreen';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/silkscreen/v6/m8JXjfVPf62XiF7kO-i9YLNlaw.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Silkscreen';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/silkscreen/v6/m8JUjfVPf62XiF7kO-i9aAhAfmKi2Oud.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Silkscreen';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/silkscreen/v6/m8JUjfVPf62XiF7kO-i9aAhAfmyi2A.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
@font-face {
font-family: 'Smooch Sans';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/smoochsans/v15/c4mk1n5uGsXss2LJh1QH6Zd13KeHWA.woff2)
format('woff2');
unicode-range:
U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301,
U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Smooch Sans';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/smoochsans/v15/c4mk1n5uGsXss2LJh1QH6Zd03KeHWA.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Smooch Sans';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/smoochsans/v15/c4mk1n5uGsXss2LJh1QH6Zd63Kc.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.3.woff2)
format('woff2');
unicode-range:
U+fa10, U+fa12-fa6d, U+fb00-fb04, U+fe10-fe19, U+fe30-fe42, U+fe44-fe52, U+fe54-fe66,
U+fe68-fe6b, U+ff02, U+ff04, U+ff07, U+ff51, U+ff5b, U+ff5d, U+ff5f-ff60, U+ff66, U+ff69,
U+ff87, U+ffa1-ffbe, U+ffc2-ffc7, U+ffca-ffcf, U+ffd2-ffd6;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.54.woff2)
format('woff2');
unicode-range:
U+3028-303f, U+3094-3096, U+309f-30a0, U+30ee, U+30f7-30fa, U+30ff, U+3105-312f, U+3131-3163,
U+3165-318e, U+3190-31bb, U+31c0-31c7;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.58.woff2)
format('woff2');
unicode-range:
U+2105, U+2109-210a, U+210f, U+2116, U+2121, U+2126-2127, U+212b, U+212e, U+2135, U+213b,
U+2194-2199, U+21b8-21b9, U+21c4-21c6, U+21cb-21cc, U+21d0, U+21e6-21e9, U+21f5, U+2202-2203,
U+2205-2206, U+2208-220b, U+220f, U+2211, U+2213, U+2215, U+221a, U+221d, U+2220, U+2223,
U+2225-2226, U+2228, U+222a-222e, U+2234-2237, U+223d, U+2243, U+2245, U+2248, U+224c, U+2260,
U+2262, U+2264-2265, U+226e-226f, U+2272-2273, U+2276-2277, U+2283-2287, U+228a-228b,
U+2295-2299, U+22a0, U+22a5, U+22bf, U+22da-22db, U+22ef, U+2305-2307, U+2318, U+2329-232a,
U+23b0-23b1, U+23be-23cc, U+23ce, U+23da-23db, U+2423, U+2469-24d0;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.59.woff2)
format('woff2');
unicode-range:
U+a1-a4, U+a6-a7, U+aa, U+ac-ad, U+b5-b6, U+b8-ba, U+bc-c8, U+ca-cc, U+ce-d5, U+d9-db, U+dd-df,
U+e6, U+ee, U+f0, U+f5, U+f7, U+f9, U+fb, U+fe-102, U+110-113, U+11a-11b, U+128-12b, U+143-144,
U+147-148, U+14c, U+14e-14f, U+152-153, U+168-16d, U+192, U+1a0-1a1, U+1af, U+1cd-1dc,
U+1f8-1f9, U+251, U+261, U+2bb, U+2c7, U+2c9, U+2ea-2eb, U+304, U+307, U+30c, U+1e3e-1e3f,
U+1ea0-1ebe, U+1ec0-1ec6, U+1ec8-1ef9, U+2011-2012, U+2016, U+2018-201a, U+201e, U+2021, U+2030,
U+2033, U+2035, U+2042, U+2047, U+2051, U+2074, U+20a9, U+20ab-20ac, U+20dd-20de, U+2100;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.61.woff2)
format('woff2');
unicode-range:
U+a8, U+2032, U+2261, U+2282, U+3090, U+30f1, U+339c, U+535c, U+53d9, U+56a2, U+56c1, U+5806,
U+589f, U+59d0, U+5a7f, U+60e0, U+639f, U+65af, U+68fa, U+69ae, U+6d1b, U+6ef2, U+71fb, U+725d,
U+7262, U+75bc, U+7768, U+7940, U+79bf, U+7bed, U+7d68, U+7dfb, U+814b, U+8207, U+83e9, U+8494,
U+8526, U+8568, U+85ea, U+86d9, U+87ba, U+8861, U+887f, U+8fe6, U+9059, U+9061, U+916a, U+976d,
U+97ad, U+9ece;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.65.woff2)
format('woff2');
unicode-range:
U+b1, U+309b, U+4e5e, U+51f1, U+5506, U+55c5, U+58cc, U+59d1, U+5c51, U+5ef7, U+6284, U+62d7,
U+6689, U+673d, U+6a2b, U+6a8e, U+6a9c, U+6d63, U+6dd1, U+70b8, U+7235, U+72db, U+72f8, U+7560,
U+7c9b, U+7ce7, U+7e1e, U+80af, U+82eb, U+8463, U+8499, U+85dd, U+86ee, U+8a60, U+8a6e, U+8c79,
U+8e87, U+8e8a, U+8f5f, U+9010, U+918d, U+9190, U+965b, U+97fb, U+9ab8, U+9bad, U+9d3b, U+9d5c,
U+9dfa, U+9e93;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.70.woff2)
format('woff2');
unicode-range:
U+266b, U+3006, U+5176, U+5197, U+51a8, U+51c6, U+52f2, U+5614, U+5875, U+5a2f, U+5b54, U+5ce0,
U+5dba, U+5deb, U+5e63, U+5f59, U+5fcc, U+6068, U+6367, U+68b6, U+6a0b, U+6b64, U+6e15, U+6eba,
U+7272, U+72a0, U+7947, U+7985, U+79e6, U+79e9, U+7a3d, U+7a9f, U+7aaf, U+7b95, U+7f60, U+7f9e,
U+7fe0, U+8098, U+80ba, U+8106, U+82d4, U+831c, U+87f9, U+8a1f, U+8acf, U+90c1, U+920d, U+9756,
U+fe43, U+ff94;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.71.woff2)
format('woff2');
unicode-range:
U+af, U+2465, U+2517, U+33a1, U+4f10, U+50c5, U+51b4, U+5384, U+5606, U+5bb0, U+5cac, U+5ee3,
U+618e, U+61f2, U+62c9, U+66ab, U+66f9, U+6816, U+6960, U+6b3e, U+6f20, U+7078, U+72d0, U+73ed,
U+7ad9, U+7b1b, U+7be4, U+7d62, U+7f51, U+80b4, U+80f4, U+8154, U+85fb, U+865c, U+8702, U+895f,
U+8aed, U+8b90, U+8ced, U+8fbf, U+91d8, U+9418, U+9583, U+9591, U+9813, U+982c, U+9bd6, U+ff46,
U+ff7f, U+ff88;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.79.woff2)
format('woff2');
unicode-range:
U+25b3, U+30f5, U+4eae, U+4f46, U+4f51, U+5203, U+52ff, U+55a7, U+564c, U+565b, U+57f9, U+5805,
U+5b64, U+5e06, U+5f70, U+5f90, U+60e8, U+6182, U+62f3, U+62fe, U+63aa, U+64a4, U+65d7, U+673a,
U+6851, U+68cb, U+68df, U+6d1e, U+6e58, U+6e9d, U+77b3, U+7832, U+7c3f, U+7db4, U+7f70, U+80aa,
U+80c6, U+8105, U+819d, U+8276, U+8679, U+8986, U+8c9d, U+8fc5, U+916c, U+9665, U+9699, U+96c0,
U+9a19, U+ff8b;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.82.woff2)
format('woff2');
unicode-range:
U+2103, U+5049, U+52b1, U+5320, U+5553, U+572d, U+58c7, U+5b5d, U+5bc2, U+5de3, U+5e61, U+5f80,
U+61a9, U+67d0, U+67f4, U+6c88, U+6ca1, U+6ce5, U+6d78, U+6e9c, U+6f54, U+731b, U+73b2, U+74a7,
U+74f6, U+75e9, U+7b20, U+7c8b, U+7f72, U+809d, U+8108, U+82b3, U+82bd, U+84b8, U+84c4, U+88c2,
U+8ae6, U+8ef8, U+902e, U+9065, U+9326, U+935b, U+938c, U+9676, U+9694, U+96f7, U+9ed9, U+ff48,
U+ff4c, U+ff81;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.83.woff2)
format('woff2');
unicode-range:
U+2500, U+3008-3009, U+4ead, U+4f0f, U+4fca, U+53eb, U+543e, U+57a2, U+5cf0, U+5e8f, U+5fe0,
U+61b2, U+62d8, U+6442, U+64b2, U+6589, U+659c, U+67f1, U+68c4, U+6cb8, U+6d12, U+6de1, U+6fe1,
U+70c8, U+723d, U+73e0, U+7656, U+773a, U+7948, U+7b87, U+7c92, U+7d3a, U+7e1b, U+7e4a, U+819a,
U+8358, U+83c5, U+84bc, U+864e, U+8912, U+8c9e, U+8d05, U+92fc, U+9396, U+98fd, U+99d2, U+ff64,
U+ff7a, U+ff83;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.84.woff2)
format('woff2');
unicode-range:
U+3014-3015, U+4e3c, U+5036, U+5075, U+533f, U+53e9, U+5531, U+5642, U+5984, U+59e6, U+5a01,
U+5b6b, U+5c0b, U+5f25, U+6069, U+60a0, U+614e, U+62b5, U+62d2-62d3, U+6597, U+660c, U+674f,
U+67cf, U+6841, U+6905, U+6cf3, U+6d32, U+6d69, U+6f64, U+716e, U+7761, U+7b52, U+7be0, U+7dbf,
U+7de9, U+7f36, U+81d3, U+8302, U+8389, U+846c, U+84ee, U+8a69, U+9038, U+9d8f, U+ff47, U+ff4b,
U+ff76, U+ff9b;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.86.woff2)
format('woff2');
unicode-range:
U+24, U+2022, U+2212, U+221f, U+2665, U+4ecf, U+5100, U+51cd, U+52d8, U+5378, U+53f6, U+574a,
U+5982, U+5996, U+5c1a, U+5e1d, U+5f84, U+609f, U+61a7, U+61f8, U+6398, U+63ee, U+6676, U+6691,
U+6eb6, U+7126, U+71e5, U+7687, U+7965, U+7d17, U+80a1, U+8107, U+8266, U+85a6, U+8987, U+8ca2,
U+8cab, U+8e0a, U+9042, U+95c7, U+9810, U+9867, U+98fc, U+ff52-ff54, U+ff61, U+ff77, U+ff98-ff99;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.87.woff2)
format('woff2');
unicode-range:
U+b0, U+226a, U+2462, U+4e39, U+4fc3, U+4fd7, U+50be, U+50da, U+5200, U+5211, U+54f2, U+5618,
U+596a, U+5b22, U+5bb4, U+5d50, U+60a3, U+63fa, U+658e, U+65e8, U+6669, U+6795, U+679d, U+67a0,
U+6b3a, U+6e09, U+757f, U+7cd6, U+7dbe, U+7ffb, U+83cc, U+83f1, U+840c, U+845b, U+8846, U+8972,
U+8a34, U+8a50, U+8a87, U+8edf, U+8ff0, U+90a6, U+9154, U+95a3, U+9663, U+9686, U+96c7, U+ff3c,
U+ff7c, U+ff8a;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.89.woff2)
format('woff2');
unicode-range:
U+a5, U+4e80, U+4f34, U+4f73, U+4f75, U+511f, U+5192, U+52aa, U+53c8, U+570f, U+57cb, U+596e,
U+5d8b, U+5f66, U+5fd9, U+62db, U+62f6, U+6328, U+633f, U+63a7, U+6469, U+6bbf, U+6c41, U+6c57,
U+6d44, U+6dbc, U+706f, U+72c2, U+72ed, U+7551, U+75f4, U+7949, U+7e26, U+7fd4, U+8150, U+8af8,
U+8b0e, U+8b72, U+8ca7, U+934b, U+9a0e, U+9a12, U+9b42, U+ff41, U+ff43, U+ff45, U+ff49, U+ff4f,
U+ff62-ff63;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.91.woff2)
format('woff2');
unicode-range:
U+60, U+2200, U+226b, U+2461, U+517c, U+526f, U+5800, U+5b97, U+5bf8, U+5c01, U+5d29, U+5e4c,
U+5e81, U+6065, U+61d0, U+667a, U+6696, U+6843, U+6c99, U+6d99, U+6ec5, U+6f22, U+6f6e, U+6fa4,
U+6fef, U+71c3, U+72d9, U+7384, U+78e8, U+7a1a, U+7a32, U+7a3c, U+7adc, U+7ca7, U+7d2b, U+7dad,
U+7e4b, U+80a9, U+8170, U+81ed, U+820e, U+8a17, U+8afe, U+90aa, U+914e, U+963f, U+99c4, U+9eba,
U+9f3b, U+ff38;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.93.woff2)
format('woff2');
unicode-range:
U+21d2, U+25ce, U+300a-300b, U+4e89, U+4e9c, U+4ea1, U+5263, U+53cc, U+5426, U+5869, U+5947,
U+598a, U+5999, U+5e55, U+5e72, U+5e79, U+5fae, U+5fb9, U+602a, U+6163, U+624d, U+6749, U+6c5a,
U+6cbf, U+6d45, U+6dfb, U+6e7e, U+708e, U+725b, U+7763, U+79c0, U+7bc4, U+7c89, U+7e01, U+7e2e,
U+8010, U+8033, U+8c6a, U+8cc3, U+8f1d, U+8f9b, U+8fb2, U+907f, U+90f7, U+9707, U+9818, U+9b3c,
U+ff0a, U+ff4d;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.95.woff2)
format('woff2');
unicode-range:
U+2193, U+25b2, U+4e4b, U+516d, U+51c4, U+529f, U+52c9, U+5360, U+5442, U+5857, U+5915, U+59eb,
U+5a9b, U+5c3b, U+6012, U+61b6, U+62b1, U+6311, U+6577, U+65e2, U+65ec, U+6613, U+6790, U+6cb9,
U+7372, U+76ae, U+7d5e, U+7fcc, U+88ab, U+88d5, U+8caf, U+8ddd, U+8ecd, U+8f38, U+8f9e, U+8feb,
U+9063, U+90f5, U+93e1, U+968a, U+968f, U+98fe, U+9ec4, U+ff1d, U+ff27, U+ff2a, U+ff36, U+ff3b,
U+ff3d, U+ffe5;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.97.woff2)
format('woff2');
unicode-range:
U+7e, U+b4, U+25c6, U+2661, U+4e92, U+4eee, U+4ffa, U+5144, U+5237, U+5287, U+52b4, U+58c1,
U+5bff, U+5c04, U+5c06, U+5e95, U+5f31, U+5f93, U+63c3, U+640d, U+6557, U+6614, U+662f, U+67d3,
U+690d, U+6bba, U+6e6f, U+72af, U+732b, U+7518, U+7ae0, U+7ae5, U+7af6, U+822a, U+89e6, U+8a3a,
U+8a98, U+8cb8, U+8de1, U+8e8d, U+95d8, U+961c, U+96a3, U+96ea, U+9bae, U+ff20, U+ff22, U+ff29,
U+ff2b-ff2c;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.99.woff2)
format('woff2');
unicode-range:
U+2191, U+505c, U+52e4, U+5305, U+535a, U+56e0, U+59bb, U+5acc, U+5b09, U+5b87, U+5c90, U+5df1,
U+5e2d, U+5e33, U+5f3e, U+6298, U+6383, U+653b, U+6697, U+6804, U+6a39, U+6cca, U+6e90, U+6f2b,
U+702c, U+7206, U+7236, U+7559, U+7565, U+7591, U+75c7, U+75db, U+7b4b, U+7bb1, U+7d99, U+7fbd,
U+8131, U+885b, U+8b1d, U+8ff7, U+9003, U+9045, U+96a0, U+9732, U+990a, U+99d0, U+9e97, U+9f62,
U+ff25, U+ff2d;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.102.woff2)
format('woff2');
unicode-range:
U+3d, U+5e, U+25cf, U+4e0e, U+4e5d, U+4e73, U+4e94, U+4f3c, U+5009, U+5145, U+51ac, U+5238,
U+524a, U+53f3, U+547c, U+5802, U+5922, U+5a66, U+5c0e, U+5de6, U+5fd8, U+5feb, U+6797, U+685c,
U+6b7b, U+6c5f-6c60, U+6cc9, U+6ce2, U+6d17, U+6e21, U+7167, U+7642, U+76db, U+8001, U+821e,
U+8857, U+89d2, U+8b1b, U+8b70, U+8cb4, U+8cde, U+8f03, U+8f2a, U+968e, U+9b54, U+9e7f, U+9ebb,
U+ff05, U+ff33;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.103.woff2)
format('woff2');
unicode-range:
U+500d, U+5074, U+50cd, U+5175, U+52e2, U+5352, U+5354, U+53f2, U+5409, U+56fa, U+5a18, U+5b88,
U+5bdd, U+5ca9, U+5f92, U+5fa9, U+60a9, U+623f, U+6483, U+653f, U+666f, U+66ae, U+66f2, U+6a21,
U+6b66, U+6bcd, U+6d5c, U+796d, U+7a4d, U+7aef, U+7b56, U+7b97, U+7c4d, U+7e04, U+7fa9, U+8377,
U+83dc, U+83ef, U+8535, U+8863, U+88cf, U+88dc, U+8907, U+8acb, U+90ce, U+91dd, U+ff0b, U+ff0d,
U+ff19, U+ff65;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.104.woff2)
format('woff2');
unicode-range:
U+4e01, U+4e21, U+4e38, U+52a9, U+547d, U+592e, U+5931, U+5b63, U+5c40, U+5dde, U+5e78, U+5efa,
U+5fa1, U+604b, U+6075, U+62c5, U+632f, U+6a19, U+6c0f, U+6c11, U+6c96, U+6e05, U+70ba, U+71b1,
U+7387, U+7403, U+75c5, U+77ed, U+795d, U+7b54, U+7cbe, U+7d19, U+7fa4, U+8089, U+81f4, U+8208,
U+8336, U+8457, U+8a33, U+8c4a, U+8ca0, U+8ca8, U+8cc0, U+9014, U+964d, U+9803, U+983c, U+98db,
U+ff17, U+ff21;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.105.woff2)
format('woff2');
unicode-range:
U+25, U+25a0, U+4e26, U+4f4e, U+5341, U+56f2, U+5bbf, U+5c45, U+5c55, U+5c5e, U+5dee, U+5e9c,
U+5f7c, U+6255, U+627f, U+62bc, U+65cf, U+661f, U+666e, U+66dc, U+67fb, U+6975, U+6a4b, U+6b32,
U+6df1, U+6e29, U+6fc0, U+738b, U+7686, U+7a76, U+7a81, U+7c73, U+7d75, U+7dd2, U+82e5, U+82f1,
U+85ac, U+888b, U+899a, U+8a31, U+8a8c, U+8ab0, U+8b58, U+904a, U+9060, U+9280, U+95b2, U+984d,
U+9ce5, U+ff18;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.106.woff2)
format('woff2');
unicode-range:
U+30f6, U+50ac, U+5178, U+51e6, U+5224, U+52dd, U+5883, U+5897, U+590f, U+5a5a, U+5bb3, U+5c65,
U+5e03, U+5e2b, U+5e30, U+5eb7, U+6271, U+63f4, U+64ae, U+6574, U+672b, U+679a, U+6a29-6a2a,
U+6ca2, U+6cc1, U+6d0b, U+713c, U+74b0, U+7981, U+7a0b, U+7bc0, U+7d1a, U+7d61, U+7fd2, U+822c,
U+8996, U+89aa, U+8cac, U+8cbb, U+8d77, U+8def, U+9020, U+9152, U+9244, U+9662, U+967a, U+96e3,
U+9759, U+ff16;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.107.woff2)
format('woff2');
unicode-range:
U+23, U+3c, U+2192, U+4e45, U+4efb, U+4f50, U+4f8b, U+4fc2, U+5024, U+5150, U+5272, U+5370,
U+53bb, U+542b, U+56db, U+56e3, U+57ce, U+5bc4, U+5bcc, U+5f71, U+60aa, U+6238, U+6280, U+629c,
U+6539, U+66ff, U+670d, U+677e-677f, U+6839, U+69cb, U+6b4c, U+6bb5, U+6e96, U+6f14, U+72ec,
U+7389, U+7814, U+79cb, U+79d1, U+79fb, U+7a0e, U+7d0d, U+85e4, U+8d64, U+9632, U+96e2, U+9805,
U+99ac, U+ff1e;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.109.woff2)
format('woff2');
unicode-range:
U+266a, U+4f11, U+533a, U+5343, U+534a, U+53cd, U+5404, U+56f3, U+5b57-5b58, U+5bae, U+5c4a,
U+5e0c, U+5e2f, U+5eab, U+5f35, U+5f79, U+614b, U+6226, U+629e, U+65c5, U+6625, U+6751, U+6821,
U+6b69, U+6b8b, U+6bce, U+6c42, U+706b, U+7c21, U+7cfb, U+805e, U+80b2, U+82b8, U+843d, U+8853,
U+88c5, U+8a3c, U+8a66, U+8d8a, U+8fba, U+9069, U+91cf, U+9752, U+975e, U+9999, U+ff0f-ff10,
U+ff14-ff15;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.110.woff2)
format('woff2');
unicode-range:
U+40, U+4e86, U+4e95, U+4f01, U+4f1d, U+4fbf, U+5099, U+5171, U+5177, U+53cb, U+53ce, U+53f0,
U+5668, U+5712, U+5ba4, U+5ca1, U+5f85, U+60f3, U+653e, U+65ad, U+65e9, U+6620, U+6750, U+6761,
U+6b62, U+6b74, U+6e08, U+6e80, U+7248, U+7531, U+7533, U+753a, U+77f3, U+798f, U+7f6e, U+8449,
U+88fd, U+89b3, U+8a55, U+8ac7, U+8b77, U+8db3, U+8efd, U+8fd4, U+9031-9032, U+9580, U+9589,
U+96d1, U+985e;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.111.woff2)
format('woff2');
unicode-range:
U+2b, U+d7, U+300e-300f, U+4e07, U+4e8c, U+512a, U+5149, U+518d, U+5236, U+52b9, U+52d9, U+5468,
U+578b, U+57fa, U+5b8c, U+5ba2, U+5c02, U+5de5, U+5f37, U+5f62, U+623b, U+63d0, U+652f, U+672a,
U+6848, U+6d41, U+7136, U+7537, U+754c, U+76f4, U+79c1, U+7ba1, U+7d44, U+7d4c, U+7dcf, U+7dda,
U+7de8, U+82b1, U+897f, U+8ca9, U+8cfc, U+904e, U+9664, U+982d, U+9858, U+98a8, U+9a13, U+ff13,
U+ff5c;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.113.woff2)
format('woff2');
unicode-range:
U+26, U+5f, U+2026, U+203b, U+4e09, U+4eac, U+4ed5, U+4fa1, U+5143, U+5199, U+5207, U+539f,
U+53e3, U+53f7, U+5411, U+5473, U+5546, U+55b6, U+5929, U+597d, U+5bb9, U+5c11, U+5c4b, U+5ddd,
U+5f97, U+5fc5, U+6295, U+6301, U+6307, U+671b, U+76f8, U+78ba, U+795e, U+7d30, U+7d39, U+7d9a,
U+89e3, U+8a00, U+8a73, U+8a8d, U+8a9e, U+8aad, U+8abf, U+8cea, U+8eca, U+8ffd, U+904b, U+9650,
U+ff11-ff12;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.114.woff2)
format('woff2');
unicode-range:
U+3e, U+3005, U+4e0d, U+4e88, U+4ecb, U+4ee3, U+4ef6, U+4fdd, U+4fe1, U+500b, U+50cf, U+5186,
U+5316, U+53d7, U+540c, U+544a, U+54e1, U+5728, U+58f2, U+5973, U+5b89, U+5c71, U+5e02, U+5e97,
U+5f15, U+5fc3, U+5fdc, U+601d, U+611b, U+611f, U+671f, U+6728, U+6765, U+683c, U+6b21, U+6ce8,
U+6d3b, U+6d77, U+7530, U+7740, U+7acb, U+7d50, U+826f, U+8f09, U+8fbc, U+9001, U+9053, U+91ce,
U+9762, U+98df;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.115.woff2)
format('woff2');
unicode-range:
U+7c, U+3080, U+4ee5, U+5148, U+516c, U+521d, U+5225, U+529b, U+52a0, U+53ef, U+56de, U+56fd,
U+5909, U+591a, U+5b66, U+5b9f, U+5bb6, U+5bfe, U+5e73, U+5e83, U+5ea6, U+5f53, U+6027, U+610f,
U+6210, U+6240, U+660e, U+66f4, U+66f8, U+6709, U+6771, U+697d, U+69d8, U+6a5f, U+6c34, U+6cbb,
U+73fe, U+756a, U+7684, U+771f, U+793a, U+7f8e, U+898f, U+8a2d, U+8a71, U+8fd1, U+9078, U+9577,
U+96fb, U+ff5e;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.116.woff2)
format('woff2');
unicode-range:
U+a9, U+3010-3011, U+30e2, U+4e0b, U+4eca, U+4ed6, U+4ed8, U+4f53, U+4f5c, U+4f7f, U+53d6,
U+540d, U+54c1, U+5730, U+5916, U+5b50, U+5c0f, U+5f8c, U+624b, U+6570, U+6587, U+6599, U+691c,
U+696d, U+6cd5, U+7269, U+7279, U+7406, U+767a-767b, U+77e5, U+7d04, U+7d22, U+8005, U+80fd,
U+81ea, U+8868, U+8981, U+89a7, U+901a, U+9023, U+90e8, U+91d1, U+9332, U+958b, U+96c6, U+9ad8,
U+ff1a, U+ff1f;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.117.woff2)
format('woff2');
unicode-range:
U+4e, U+a0, U+3000, U+300c-300d, U+4e00, U+4e0a, U+4e2d, U+4e8b, U+4eba, U+4f1a, U+5165, U+5168,
U+5185, U+51fa, U+5206, U+5229, U+524d, U+52d5, U+5408, U+554f, U+5831, U+5834, U+5927, U+5b9a,
U+5e74, U+5f0f, U+60c5, U+65b0, U+65b9, U+6642, U+6700, U+672c, U+682a, U+6b63, U+6c17, U+7121,
U+751f, U+7528, U+753b, U+76ee, U+793e, U+884c, U+898b, U+8a18, U+9593, U+95a2, U+ff01,
U+ff08-ff09;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.118.woff2)
format('woff2');
unicode-range:
U+21-22, U+27-2a, U+2c-3b, U+3f, U+41-4d, U+4f-5d, U+61-7b, U+7d, U+ab, U+ae, U+b2-b3, U+b7,
U+bb, U+c9, U+cd, U+d6, U+d8, U+dc, U+e0-e5, U+e7-ed, U+ef, U+f1-f4, U+f6, U+f8, U+fa, U+fc-fd,
U+103, U+14d, U+1b0, U+300-301, U+1ebf, U+1ec7, U+2013-2014, U+201c-201d, U+2039-203a, U+203c,
U+2048-2049, U+2113, U+2122, U+65e5, U+6708, U+70b9;
}
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7tOBsePYl9tE4unb6zDxeJE5P6vlm-9aQ.119.woff2)
format('woff2');
unicode-range:
U+20, U+2027, U+3001-3002, U+3041-307f, U+3081-308f, U+3091-3093, U+3099-309a, U+309d-309e,
U+30a1-30e1, U+30e3-30ed, U+30ef-30f0, U+30f2-30f4, U+30fb-30fe, U+ff0c, U+ff0e;
}
/* latin-ext */
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7dNHkPD5g.woff2)
format('woff2');
unicode-range:
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Tsukimi Rounded';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/tsukimirounded/v14/sJoc3LJNksWZO0LvnZwkF3HtoB7dOnkP.woff2)
format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

167
src/style/global.css Executable file
View File

@@ -0,0 +1,167 @@
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap');
@import 'tailwindcss';
@import './animations.css';
@custom-variant light (&:where(.light, .light *));
* {
transition: all 0.2s ease-in allow-discrete;
}
@utility scroll-hidden {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
@theme {
/* Shadow variables (unchanged) */
--shadow-terminal:
rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px,
rgba(0, 0, 02, 0.5) 0px -4px 0px inset;
--shadow-button:
rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px,
rgba(0, 0, 02, 0.4) 0px -3px 0px inset;
--shadow-bg: rgba(0, 0, 0, 0.25) 0px 14px 28px, rgba(0, 0, 0, 0.22) 0px 10px 10px;
--shadow-subtle:
rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 3px 4px -3px,
rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
--background-image-dots-bg: radial-gradient(
circle at center,
var(--dots-bg-2) 1px,
var(--dots-bg-1) 1px
);
/* Background colors */
--color-bg-dark-dark: oklch(0.1 0.01 292);
--color-bg-dark: oklch(0.15 0.03 292);
--color-bg-mid-dark: oklch(0.175 0.03 292);
--color-bg-light-dark: oklch(0.2 0.035 292);
--color-bg-lighter-dark: oklch(0.25 0.04 292);
--color-bg-dark-light: oklch(0.8 0.15 280);
--color-bg-light: oklch(0.85 0.2 280);
--color-bg-light-light: oklch(0.9 0.3 280);
--color-bg-lighter-light: oklch(1 0.3 280);
/* Text colors */
--color-text-dark: oklch(0.96 0.01 292);
--color-text-muted-dark: oklch(0.76 0.01 292);
--color-text-light: oklch(0.15 0.02 292);
--color-text-muted-light: oklch(0.4 0.02 292);
/* Highlight and border colors */
--color-highlight-dark: oklch(0.6 0.02 292);
--color-border-dark: oklch(0.3 0.02 292);
--color-border-muted-dark: oklch(0.2 0.02 292);
--color-highlight-light: oklch(1 0.02 280);
--color-border-light: oklch(0.6 0.02 280);
--color-border-muted-light: oklch(0.7 0.02 280);
/* Primary and secondary colors */
--color-primary-dark: oklch(0.55 0.15 292);
--color-primary-hover-dark: oklch(0.65 0.18 292);
--color-secondary-dark: oklch(0.7 0.12 120);
--color-primary-light: oklch(0.6 0.1 292);
--color-primary-hover-light: oklch(0.65 0.1 292);
--color-secondary-light: oklch(0.4 0.1 112);
/* Loader colors (no light equivalent, using dark values) */
--color-loader-primary-dark: oklch(0.55 0.15 292);
--color-loader-primary-hl-dark: oklch(0.8 0.15 292);
--color-loader-primary-light: oklch(0.55 0.15 292);
--color-loader-primary-hl-light: oklch(0.8 0.15 292);
/* Status colors */
--color-danger-dark: oklch(0.7 0.05 30);
--color-warning-dark: oklch(0.7 0.05 100);
--color-success-dark: oklch(0.7 0.05 160);
--color-info-dark: oklch(0.7 0.05 260);
--color-danger-light: oklch(0.5 0.05 30);
--color-warning-light: oklch(0.5 0.05 100);
--color-success-light: oklch(0.5 0.05 160);
--color-info-light: oklch(0.5 0.05 260);
/* Font variables (unchanged) */
--font-primary: 'Space Mono', monospace;
--font-terminal: 'JetBrains Mono', monospace;
}
:root {
transition: all 0.2s ease-in;
--dots-bg-1: oklch(0.1 0.01 292);
--dots-bg-2: oklch(0.2 0.01 292);
--border-cart: solid 1px var(--border);
--shadow:
rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px,
rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
--gradient-bg-direction: 0deg;
--gradient-bg: linear-gradient(var(--gradient-bg-direction), var(--bg-dark) 80%, transparent);
--gradient: linear-gradient(0deg, var(--bg) 95%, var(--bg-light));
--gradient-hover: linear-gradient(0deg, var(--bg), var(--bg-light));
--transition-standard: all 0.2s ease-in;
--transition-theme: all 0.2s ease;
--lang-html: oklch(0.6 0.08207 195.16);
--lang-css: oklch(0.5 0.16037 303.368);
--lang-js: oklch(0.8 0.18261 102.094);
--lang-php: oklch(0.5923 0.09155 272.038);
--lang-cs: oklch(0.55 0.27927 141.446);
--lang-cpp: oklch(0.6 0.15576 6.88);
/*
#underline-bg
/single side/ linear-gradient(90deg, var(--text) 85%, transparent 100%);
/both sides/ radial-gradient(circle, var(--text) 85%, transparent 100%);
#misc
background-image: radial-gradient(circle at center, #9f7aea20 1px, transparent 1px); background-size: 20px 20px;
(dotted background)
*/
main.light {
--dots-bg-1: oklch(0.9 0.01 292);
--dots-bg-2: oklch(0.8 0.01 292);
--bg-dark: oklch(0.92 0.2 280);
--bg: oklch(0.94 0.3 280);
--bg-light: oklch(0.96 0.3 280);
--text: oklch(0.15 0.02 292);
--text-muted: oklch(0.4 0.02 292);
--highlight: oklch(1 0.02 280);
--border: oklch(0.6 0.02 280);
--border-muted: oklch(0.7 0.02 280);
--border-cart: solid 1px var(--bg);
--shadow:
oklch(0.72 0.01 292 / 0.4) 0px 2px 4px, oklch(0.62 0.01 292 / 0.3) 0px 7px 13px -3px,
oklch(0.52 0.01 292 / 0.2) 0px -3px 0px inset;
--primary: oklch(0.6 0.1 292);
--primary-hover: oklch(0.65 0.1 292);
--secondary: oklch(0.4 0.1 112);
--gradient: linear-gradient(0deg, var(--bg), var(--bg-light) 95%);
--gradient-hover: linear-gradient(0deg, var(--bg), var(--bg-light));
--box-shadow-muted:
rgba(14, 30, 37, 0.12) 0px 2px 4px 0px, rgba(14, 30, 37, 0.32) 0px 2px 16px 0px;
--danger: oklch(0.5 0.05 30);
--warning: oklch(0.5 0.05 100);
--success: oklch(0.5 0.05 160);
--info: oklch(0.5 0.05 260);
}
}

3
static/robots.txt Executable file
View File

@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

14
svelte-portfolio.code-workspace Executable file
View File

@@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
},
{
"path": "../../Server/www/dev.chaosmaker"
},
{
"path": "../svelte-website"
}
],
"settings": {}
}

18
svelte.config.js Executable file
View File

@@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

13
tailwind.config.ts Executable file
View File

@@ -0,0 +1,13 @@
import type { Config } from "tailwindcss"
export default {
content: [],
theme: {
extend: {
colors: {
dots-bg-p: "oklch(var(--dots-bg-1))",
dots-bg-s: "oklch(var(--dots-bg-1))",
}
}
},
plugins: []
} satisfies Config

19
tsconfig.json Executable file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

36
vite.config.ts Executable file
View File

@@ -0,0 +1,36 @@
import { defineConfig } from 'vitest/config';
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
test: {
expect: { requireAssertions: true },
projects: [
{
extends: './vite.config.ts',
test: {
name: 'client',
environment: 'browser',
browser: {
enabled: true,
provider: 'playwright',
instances: [{ browser: 'chromium' }]
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: ['src/lib/server/**'],
setupFiles: ['./vitest-setup-client.ts']
}
},
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
}
}
]
}
});

2
vitest-setup-client.ts Executable file
View File

@@ -0,0 +1,2 @@
/// <reference types="@vitest/browser/matchers" />
/// <reference types="@vitest/browser/providers/playwright" />