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

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