Init rebase to Svelte and TS

This commit is contained in:
2025-11-08 09:43:14 +01:00
committed by Kamil Olszewski
parent 1af139201d
commit 4428cc7e8a
61 changed files with 12624 additions and 13 deletions

50
src/app.css Normal 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 Normal 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 Normal 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 Normal 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

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

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

File diff suppressed because it is too large Load Diff

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 Normal file

Binary file not shown.

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

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

152
src/lib/stores/bash/bash.ts Normal file
View File

@@ -0,0 +1,152 @@
import { command } from '$app/server';
import { COMMANDS, GROUP, HELP_ARGS, PASSWD, type CommandArg, type ICommand } from './static';
import { VirtualFS } from './fs';
import { Terminal, type PrintData } from '../terminal';
import { Stack } from '../stack';
import path from 'path';
export interface Permission {
r: boolean;
w: boolean;
x: boolean;
}
export interface BashInitArgs {
stdio?: Terminal;
user: User;
fs: any;
}
// TODO: Finish this
export enum ExitCode {
SUCCESS = 0,
ERROR = 1
}
export interface User {
username: string;
passwd: string; //HASHED PASSWORD
uid: number; // Normal user 1000+ System user 1-999 root - 0
gid: number; // Primary group | 'Users' 1000 - Others - 1000+ root - 0
home: string;
history: string[];
cwd?: string[];
pwd?: string[];
}
export interface Group {
groupname: string;
gid: number; // Primary group 'Users' 1000 - Others - 1000+ root - 0
members: string[];
}
export class Bash {
private vfs: VirtualFS;
private _passwd: User[];
private _instances: Stack<User>;
private _group: Group[];
private _terminal!: Terminal;
private user: User;
private _helpArgs: CommandArg[];
private _commands: Record<string, ICommand>;
constructor(args: BashInitArgs) {
this.user = args.user;
this._helpArgs = HELP_ARGS;
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 });
console.log(this._commands);
}
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(): string[] {
return this.vfs.cwd;
}
getPwd(): string[] {
return this.vfs.pwd;
}
getUser(): User {
return this.user;
}
getFs(): VirtualFS {
return this.vfs;
}
changeUser(user: User) {
this.user = user;
this.vfs.home = this.vfs._splitPathString(user.home);
this.vfs.cwd = user.cwd ? user.cwd : this.vfs._splitPathString(user.home);
this.vfs.pwd = user.pwd ? user.pwd : this.vfs._splitPathString(user.home);
}
executeCommand(commandName: string, ...args: string[]): void {
const command = this._commands[commandName];
if (!command) this.throwError(ExitCode.ERROR);
if (command.root) {
if (this._group[1].members.includes(this.user.username)) {
let out: ExitCode = command.method.call(this, ...args);
this.throwError(out);
}
this.throwError(ExitCode.ERROR);
}
let out: ExitCode = command.method.call(this, ...args);
this.throwError(out);
}
throwError(code: ExitCode, data?: any): void {
//TODO: Make data some interface format or smh.
switch (code) {
default:
this.appendNewResult(this.vfs.pwd, 'Success!');
break;
}
}
userLogin(username: string, passwd: string): ExitCode {
const user: User | undefined = this._passwd.find((u) => u.username === username);
if (user === undefined) return ExitCode.ERROR;
if (user.passwd === passwd) {
this._instances.push(user);
this.changeUser(user);
return ExitCode.ERROR; //TODO: Make it return the exitcode of changeUser() if needed
} else return ExitCode.ERROR;
}
userLogout() {
this._instances.pop();
if (this._instances.size() === 0) {
//TODO: Implement system logout
} else {
this.changeUser(this._instances.peek()!);
}
}
appendNewResult(path: string[], output: any) {
const data: PrintData = {
path: this.vfs.formatPath(this.vfs.pathArrayToString(path)),
output: output
};
console.log('NEW RESULT - ', data);
this._terminal.PrintOutput(data);
}
}

130
src/lib/stores/bash/fs.ts Normal file
View File

@@ -0,0 +1,130 @@
import type { Permission, User } from './bash';
export enum Type {
Directory = 16384,
File = 32768
}
type NodePerms = {
user: Permission;
group: Permission;
other: Permission;
};
export interface FsInitArgs {
fs: any;
user: User;
}
export interface TreeNode {
name: string;
type: Type;
readonly: boolean;
interactible: boolean;
func: any;
children: TreeNode[];
content: string; // Path to the content of the file
link: string[]; // Symlink
permission: NodePerms;
owner: string;
group: string;
modtime: Date;
}
export class VirtualFS {
private root: TreeNode; // TODO make this the correct type
home: string[];
cwd: string[];
pwd: string[];
constructor(args: FsInitArgs) {
this.root = args.fs;
this.home = this._splitPathString(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);
console.log('VFS INIT ', this._getNodeByPathArray(['/', 'home', 'kamil']));
}
_splitPathString(path: string): string[] {
if (path === '/') return ['/'];
const raw: string[] = path.split('/');
const parts: string[] = [];
for (let i = 0; i < raw.length; i++) {
if (raw[i].length > 0) parts.push(raw[i]);
}
return parts;
}
_isAbsolutePath = (path: string): boolean => {
return typeof path === 'string' && path.startsWith('/');
};
pathArrayToString(path: string[]): string {
if (path.length === 1 && path[0] === '/') return '/';
return '/' + path.join('/');
}
formatPath(path: string): string {
console.log('FORMAT PATH ', path);
const prefix = this.pathArrayToString(this.home);
if (path.startsWith(prefix)) {
return path.replace(prefix, '~');
} else return path;
}
resolvePath(path: string): string[] {
if (path === '' || path === undefined || path === null) return this.cwd.slice();
if (path.startsWith('/') && path.length === 1) return [];
if (path.startsWith('~')) {
const trail: string = path === '~' ? '' : path.slice(1);
const home: string = this.pathArrayToString(this.home);
path = home + (trail ? (trail.startsWith('/') ? '' : '/') + trail : '');
}
const start = this._isAbsolutePath(path) ? [] : this.cwd.slice();
console.log('START', start);
const parts = this._splitPathString(path);
console.log('PARTS', parts);
for (let i = 0; i < parts.length; i++) {
const seg = parts[i];
if (seg === '.' || seg === '') continue;
if (seg === '..') {
if (start.length > 1) start.pop();
continue;
}
start.push(seg);
}
if (start.length === 0) return [];
console.log('OUTPUT', start);
return start;
}
_getNodeByPathArray(path: string[]): TreeNode {
if (path.length === 1 && path[0] === '/') return this.root;
let node: TreeNode = this.root;
const parts: string[] = path.slice(path[0] === '/' ? 1 : 0);
for (let i = 0; i < parts.length; i++) {
const seg: string = parts[i];
if (node.type === Type.File) return node;
const newNode = node.children.find((child) => child.name === seg);
if (newNode !== undefined) node = newNode;
}
return node;
}
}

View File

@@ -0,0 +1,229 @@
import { Bash, ExitCode, type Group, type User } from './bash';
import { Type, type TreeNode } from './fs';
export type CommandArg = `-${string}`;
export interface ICommand {
method: (this: Bash, ...args: any[]) => ExitCode;
args: CommandArg[] | string[] | null;
help: string;
root: boolean;
}
export const GROUP: Group[] = [
{
groupname: 'sudo',
gid: 69,
members: ['root', 'admin']
},
{
groupname: 'users',
gid: 1000,
members: ['admin', 'user']
}
];
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: 1001,
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: 1002,
gid: 1000,
home: '/home/user',
history: [] //TODO: Delete this and declare a new history array when logging the user in.
}
];
export const HELP_ARGS: CommandArg[] = ['-h', '--help'];
export const cmd_return = function (this: Bash, ...args: string[]): ExitCode {
return 0;
};
export const cmd_cd = function (this: Bash, ...args: string[]): ExitCode {
const path = args[0];
let targetNode: TreeNode;
if (args.length > 1) return ExitCode.ERROR; // Too many args
// if no args cd into home dir
if (args.length === 0) {
this.getFs().cwd = this.getFs().home;
return ExitCode.SUCCESS;
}
// if the arg is - cd make your current dir the prev dir and vice versa
if (args[0] === '-') {
[this.getFs().cwd, this.getFs().pwd] = [this.getFs().pwd, this.getFs().cwd];
return ExitCode.SUCCESS;
}
// Change the input STRING path from relative to absolute by replacing ~ with the home directory path
//TODO: Change that to a global function inside fs class to parse all possible path formats????? already exists, need to verify
let resolvedPath = path.startsWith('~')
? path.replace('~', this.getFs().pathArrayToString(this.getFs().home))
: path;
this.getFs().pwd = this.getFs().cwd;
targetNode = this.getFs()._getNodeByPathArray(this.getFs().resolvePath(resolvedPath)); // Conversion from STRING path to ARRAY
if (!targetNode) return ExitCode.ERROR;
if (targetNode.type !== Type.Directory) return ExitCode.ERROR;
//if () return ExitCode.ERROR; // Check for read permissions on node and user
this.getFs().cwd = this.getFs().resolvePath(resolvedPath); // CD was successfull, change current dir to the verified target dir
return ExitCode.SUCCESS;
};
export const COMMANDS = {
return: {
method: cmd_return,
args: [] as CommandArg[],
help: 'PATH TO HELP.MD',
root: false
},
cd: {
method: cmd_cd,
args: [] as string[],
help: 'PATH TO HELP.MD',
root: false
}
} 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: "",
}
} */

13
src/lib/stores/lang.ts Normal 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 Normal 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,95 @@
import { FileOutput } from '@lucide/svelte';
import Cursor from '../../modules/terminal/Cursor.svelte';
import { Bash, ExitCode, type BashInitArgs, type User } from './bash/bash';
import { Stack } from './stack';
import type { VirtualFS } from './bash/fs';
export interface TerminalMode {}
export interface TermInitArgs {
bash: BashInitArgs;
}
export interface ParsedInput {
command: string;
args: string[];
}
export interface PrintData {
path: string;
output: any; // TODO: Make this be any predefined format of outputs like ls, ls w/ flags and so on;
}
export interface PageCallbacks {
print: (data: PrintData) => void;
}
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 {
const result: ParsedInput = { command: '', args: [] };
let current: string = '';
let inQuotes: boolean = false;
let quoteChar: Stack<string> = new Stack<string>();
for (let i = 0; i < input.length; i++) {
const char = input[i];
if ((char === '"' || char === "'") && !inQuotes) {
inQuotes = true;
quoteChar.push(char);
continue;
} else if (char === quoteChar.peek() && inQuotes) {
inQuotes = false;
quoteChar.pop();
continue;
}
if (char === ' ' && !inQuotes) {
if (current !== '') {
result.command = current;
current = '';
}
} else {
current += char;
}
}
if (current !== '') result.args.push(current);
return result;
}
executeCommand(input: string): void {
this.bash.updateHistory(input);
const parsed: ParsedInput = this._parseInput(input);
this.bash.executeCommand(parsed.command, ...parsed.args);
}
registerCallbacks(callbacks: PageCallbacks): void {
this.callbacks = callbacks;
}
getUser(): User {
return this.bash.getUser();
}
getCwd(): string {
const fs: VirtualFS = this.bash.getFs();
let temp: string = fs.formatPath(fs.pathArrayToString(this.bash.getCwd()));
return temp;
}
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 Normal 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 Normal 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>

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 Normal file
View File

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

110
src/modules/Settings.svelte Normal 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>

217
src/modules/Terminal.svelte Normal file
View File

@@ -0,0 +1,217 @@
<script lang="ts">
import type { User } from '$lib/stores/bash/bash';
import type { TreeNode } from '$lib/stores/bash/fs';
import { Terminal, type TermInitArgs } from '$lib/stores/terminal';
import {
onDestroy,
onMount,
type Snippet,
type Component,
type ComponentProps,
mount
} from 'svelte';
let { children, username, cwd }: { children: Snippet; username: string; cwd: string } = $props();
/* function jsonToTreeNode(data: any): TreeNode {
return {
name: data.Name,
type: data.Type,
readonly: data.ReadOnly,
interactible: data.Interactible,
func: data.Func,
children: data.Children ? data.Children.map((child: any) => jsonToTreeNode(child)) : [],
content: data.Content,
link: data.Link || [],
permission: {
user: {
r: data.Permission[0]?.Read,
w: data.Permission[0]?.Write,
x: data.Permission[0]?.Exec
},
group: {
r: data.Permission[1]?.Read,
w: data.Permission[1]?.Write,
x: data.Permission[1]?.Exec
},
other: {
r: data.Permission[2]?.Read,
w: data.Permission[2]?.Write,
x: data.Permission[2]?.Exec
}
},
owner: data.Owner,
group: data.Group,
modtime: new Date(data.Mtime)
};
}
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();
const node: TreeNode = jsonToTreeNode(data);
return node;
}
let callbackInit = {
print: (data: any) => {
console.log('print callback executed');
//print(data);
}
};
let testUser: User = {
username: 'kamil',
passwd: '123',
uid: 0,
gid: 0,
home: '/home/kamil',
history: []
};
let terminal: Terminal;
let username: string = $state(testUser.username);
let cwd: string = $state(testUser.home);
let isInitializing = $state(true);
onMount(async () => {
try {
let fsJson = await fetchFileSystem('/src/lib/assets/fs/fs.json');
let args: TermInitArgs = {
bash: {
user: testUser,
fs: fsJson
}
};
terminal = new Terminal(args);
terminal.registerCallbacks(callbackInit);
username = terminal.getUser().username;
cwd = terminal.getCwd();
} catch (error) {
console.error('Failed to initialize terminal:', error);
} finally {
updateTerminal();
isInitializing = false;
}
});
function updateTerminal() {
username = terminal!.getUser().username;
cwd = terminal!.getCwd();
} */
let outputContainer = $state<HTMLElement>();
let instances = $state<Set<ReturnType<typeof mount>>>(new Set());
onMount(() => {
const scrollable = document.getElementById('cout');
const config = { childList: true };
const callback = function (mutationList: any, observer: any) {
for (let mutation of mutationList) {
if (mutation.type === 'childList') {
scrollable?.scrollTo(0, scrollable.scrollHeight);
}
}
};
const observer = new MutationObserver(callback);
observer.observe(scrollable!, config);
});
export function addComponent<T extends Component>(
component: T,
props: ComponentProps<T> = {} as ComponentProps<T>
): ReturnType<typeof mount> | undefined {
if (!outputContainer) return;
const instance = mount(component, {
target: outputContainer,
props
});
instances.add(instance);
return instance;
}
export function clearComponents(): void {
for (const instance of instances) {
instance.$destroy();
}
instances.clear();
}
onDestroy(() => {
for (const instance of instances) {
instance.$destroy();
}
});
</script>
<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 bind:this={outputContainer}></div>
{@render children()}
</div>
</div>
<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,10 @@
<script lang="ts">
let { path, output }: { path: string; output: any } = $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-row items-center font-terminal">
<span class="pointer pr-2">$</span>
<!-- prettier-ignore -->
<div style="white-space: preserve;" class=" relative wrap-break-word">{output}</div>
</div>

View File

@@ -0,0 +1,50 @@
<script lang="ts">
let inputElement = $state<HTMLInputElement>();
let { value, isFocused, cwd }: { value?: string; isFocused?: boolean; cwd: string } = $props();
export function focus() {
console.log('WOOOOW');
inputElement?.focus();
}
export function blur() {
console.log('beeeeeeeee');
inputElement?.blur();
}
export function setValue(newValue: string) {
if (inputElement) {
inputElement.value = newValue;
}
}
export function clear() {
setValue('');
}
function handleInput(event: Event) {
value = (event.target as HTMLInputElement).value;
}
</script>
<div class=" relative">
<input
bind:this={inputElement}
oninput={handleInput}
onfocus={() => (isFocused = true)}
onblur={() => (isFocused = false)}
type="text"
class=" pointer-events-none absolute left-0 m-0 w-0 border-none p-0 opacity-0"
/>
<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">{value}</div>
<span id="cursor" class={isFocused ? 'animate-cursor-blink' : ''}>_</span>
</div>
</div>
<style>
</style>

54
src/routes/+layout.svelte Normal 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>

166
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,166 @@
<script lang="ts">
import TerminalModule from '../modules/Terminal.svelte';
import { Terminal, type PrintData, type TermInitArgs } from '$lib/stores/terminal';
import type { User } from '$lib/stores/bash/bash';
import { onMount } from 'svelte';
import Settings from '../modules/Settings.svelte';
import Loading from '../modules/Loading.svelte';
import type { TreeNode } from '$lib/stores/bash/fs';
import Input from '../modules/terminal/Input.svelte';
import Cursor from '../modules/terminal/Cursor.svelte';
//let terminalMode =
function jsonToTreeNode(data: any): TreeNode {
return {
name: data.Name,
type: data.Type,
readonly: data.ReadOnly,
interactible: data.Interactible,
func: data.Func,
children: data.Children ? data.Children.map((child: any) => jsonToTreeNode(child)) : [],
content: data.Content,
link: data.Link || [],
permission: {
user: {
r: data.Permission[0]?.Read,
w: data.Permission[0]?.Write,
x: data.Permission[0]?.Exec
},
group: {
r: data.Permission[1]?.Read,
w: data.Permission[1]?.Write,
x: data.Permission[1]?.Exec
},
other: {
r: data.Permission[2]?.Read,
w: data.Permission[2]?.Write,
x: data.Permission[2]?.Exec
}
},
owner: data.Owner,
group: data.Group,
modtime: new Date(data.Mtime)
};
}
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();
const node: TreeNode = jsonToTreeNode(data);
return node;
}
let callbackInit = {
print: (data: any) => {
console.log('print callback executed');
print(data);
}
};
let testUser: User = {
username: 'kamil',
passwd: '123',
uid: 0,
gid: 0,
home: '/home/kamil',
history: []
};
let terminal: Terminal;
let username: string = $state(testUser.username);
let cwd: string = $state(testUser.home);
let isInitializing = $state(true);
onMount(async () => {
try {
let fsJson = await fetchFileSystem('/src/lib/assets/fs/fs.json');
let args: TermInitArgs = {
bash: {
user: testUser,
fs: fsJson
}
};
terminal = new Terminal(args);
terminal.registerCallbacks(callbackInit);
username = terminal.getUser().username;
cwd = terminal.getCwd();
} catch (error) {
console.error('Failed to initialize terminal:', error);
} finally {
updateTerminal();
isInitializing = false;
}
});
function updateTerminal() {
username = terminal!.getUser().username;
cwd = terminal!.getCwd();
}
let terminalComponent = $state<any>();
let inputComponent = $state<any>();
function focusInput() {
console.log('focus');
inputComponent.focus();
}
function blurInput() {
console.log('blur');
inputComponent.blur();
}
function clearInput() {
inputComponent.value = '';
}
function print(data: PrintData): void {
if (isInitializing) {
console.error('Terminal is initializing! Skipping Print');
return;
}
terminalComponent.addComponent(Cursor, {
path: data.path,
output: data.output
});
updateTerminal();
}
function testAction() {
if (!terminal || isInitializing) {
console.error('Terminal is initializing!');
return;
}
terminal.executeCommand('cd ~/.config');
}
</script>
<Settings></Settings>
{#if !isInitializing}
<div class="h-dvh w-full p-24">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div onclick={() => focusInput()}>
<TerminalModule bind:this={terminalComponent} {username} {cwd}>
<Input {cwd} bind:this={inputComponent} />
</TerminalModule>
<button title="" onclick={() => testAction()}>Test Action</button>
</div>
</div>
{:else}
<Loading></Loading>
{/if}
<style>
</style>

View 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();
});
});

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>

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

26
src/style/animations.css Normal 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 Normal 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 Normal 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);
}
}