Compare commits

...

19 Commits

Author SHA1 Message Date
Gregory Schier
aadfbfdfca Fix lint errors 2025-06-10 08:16:02 -07:00
Gregory Schier
383fd05c6c Split appearance settings into theme/interface 2025-06-09 21:19:44 -07:00
Gregory Schier
be0a8fc27a Add proxy bypass setting and rewrite proxy logic 2025-06-09 14:29:12 -07:00
Gregory Schier
648a1ac53c Update DEVELOPMENT.md 2025-06-08 22:49:43 -07:00
Gregory Schier
9fab37fb17 Custom font selection (#226) 2025-06-08 22:48:27 -07:00
Gregory Schier
e0aaa33ccb Update README 2025-06-08 08:17:11 -07:00
Gregory Schier
20f7d20031 Enable socks reqwest feature 2025-06-08 08:10:55 -07:00
Gregory Schier
4d90bc78b1 Link docs in readme 2025-06-08 08:05:59 -07:00
Gregory Schier
97763a1301 Add README to types package 2025-06-08 08:03:11 -07:00
Gregory Schier
d8b5a201b6 I'm stupid 2025-06-07 20:17:28 -07:00
Gregory Schier
88e87a1999 Fix stupid typo 2025-06-07 20:15:32 -07:00
Gregory Schier
2c4c1abd20 Pin tauri cli 2025-06-07 20:04:04 -07:00
Gregory Schier
67026fc5b3 Tweak 2025-06-07 19:37:28 -07:00
Gregory Schier
423a1a0a52 Fix environment color editing 2025-06-07 19:32:23 -07:00
Gregory Schier
1abe01aa5a Embed migrations into Rust binary 2025-06-07 19:25:36 -07:00
Gregory Schier
d0fde99b1c Environment colors (#225) 2025-06-07 18:21:54 -07:00
Gregory Schier
27901231dc Clarify proxy HTTP/HTTPS setting 2025-06-06 20:34:23 -07:00
Gregory Schier
1d9d80319b Upgrade dependencies 2025-06-06 19:32:25 -07:00
Gregory Schier
f62e90297d Fix recent workspaces when open in new window 2025-06-06 14:10:30 -07:00
106 changed files with 2162 additions and 1628 deletions

View File

@@ -50,11 +50,9 @@ New migrations can be created from the `src-tauri/` directory:
npm run migration
```
Run the app to apply the migrations.
Rerun the app to apply the migrations.
If nothing happens, try `cargo clean` and run the app again.
_Note: Development builds use a separate database location from production builds._
_Note: For safety, development builds use a separate database location from production builds._
## Lezer Grammer Generation

72
package-lock.json generated
View File

@@ -36,6 +36,7 @@
"plugins/template-function-xml",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -50,7 +51,7 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
@@ -63,7 +64,7 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"workspaces-run": "^1.0.2"
}
},
@@ -2801,9 +2802,9 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
"integrity": "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz",
"integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
@@ -3037,36 +3038,36 @@
}
},
"node_modules/@tauri-apps/plugin-dialog": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.1.tgz",
"integrity": "sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.2.2.tgz",
"integrity": "sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-fs": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.2.1.tgz",
"integrity": "sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.3.0.tgz",
"integrity": "sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-log": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.3.1.tgz",
"integrity": "sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.4.0.tgz",
"integrity": "sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.6.tgz",
"integrity": "sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==",
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.2.7.tgz",
"integrity": "sha512-uduEyvOdjpPOEeDRrhwlCspG/f9EQalHumWBtLBnp3fRp++fKGLqDOyUhSIn7PzX45b/rKep//ZQSAQoIxobLA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.0.0"
@@ -3606,6 +3607,10 @@
"resolved": "src-tauri/yaak-crypto",
"link": true
},
"node_modules/@yaakapp-internal/fonts": {
"resolved": "src-tauri/yaak-fonts",
"link": true
},
"node_modules/@yaakapp-internal/git": {
"resolved": "src-tauri/yaak-git",
"link": true
@@ -12946,6 +12951,16 @@
"node": ">=0.10.0"
}
},
"node_modules/react-colorful": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-dnd": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
@@ -16097,9 +16112,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -17284,7 +17299,7 @@
},
"packages/plugin-runtime-types": {
"name": "@yaakapp/api",
"version": "0.6.0",
"version": "0.6.4",
"dependencies": {
"@types/node": "^22.5.4"
},
@@ -17533,6 +17548,10 @@
"name": "@yaakapp-internal/crypto",
"version": "1.0.0"
},
"src-tauri/yaak-fonts": {
"name": "@yaakapp-internal/fonts",
"version": "1.0.0"
},
"src-tauri/yaak-git": {
"name": "@yaakapp-internal/git",
"version": "1.0.0"
@@ -17598,12 +17617,12 @@
"@tanstack/react-query": "^5.76.1",
"@tanstack/react-router": "^1.120.3",
"@tanstack/react-virtual": "^3.13.8",
"@tauri-apps/api": "^2.4.1",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-log": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-dialog": "^2.2.2",
"@tauri-apps/plugin-fs": "^2.3.0",
"@tauri-apps/plugin-log": "^2.4.0",
"@tauri-apps/plugin-opener": "^2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-shell": "^2.2.1",
"buffer": "^6.0.3",
@@ -17626,6 +17645,7 @@
"papaparse": "^5.4.1",
"parse-color": "^1.0.0",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.3.1",

View File

@@ -35,6 +35,7 @@
"plugins/template-function-xml",
"src-tauri/yaak-crypto",
"src-tauri/yaak-git",
"src-tauri/yaak-fonts",
"src-tauri/yaak-license",
"src-tauri/yaak-mac-window",
"src-tauri/yaak-models",
@@ -66,7 +67,7 @@
"jotai": "^2.12.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "2.4.1",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"@yaakapp/cli": "^0.1.5",
@@ -79,7 +80,7 @@
"nodejs-file-downloader": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^3.4.2",
"typescript": "^5.8.2",
"typescript": "^5.8.3",
"workspaces-run": "^1.0.2"
}
}

View File

@@ -0,0 +1,28 @@
# Yaak Plugin API
Yaak is a desktop [API client](https://yaak.app/blog/yet-another-api-client) for
interacting with REST, GraphQL, Server Sent Events (SSE), WebSocket, and gRPC APIs. It's
built using Tauri, Rust, and ReactJS.
Plugins can be created in TypeScript, which are executed alongside Yaak in a NodeJS
runtime. This package contains the TypeScript type definitions required to make building
Yaak plugins a breeze.
## Quick Start
The easiest way to get started is by generating a plugin with the Yaak CLI:
```shell
npx @yaakapp/cli generate
```
For more details on creating plugins, check out
the [Quick Start Guide](https://feedback.yaak.app/help/articles/6911763-plugins-quick-start)
## Installation
If you prefer starting from scratch, manually install the types package:
```shell
npm install @yaakapp/api
```

View File

@@ -1,6 +1,20 @@
{
"name": "@yaakapp/api",
"version": "0.6.0",
"version": "0.6.4",
"keywords": [
"api-client",
"insomnia-alternative",
"bruno-alternative",
"postman-alternative"
],
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak"
},
"bugs": {
"url": "https://feedback.yaak.app"
},
"homepage": "https://yaak.app",
"main": "lib/index.js",
"typings": "./lib/index.d.ts",
"files": [

View File

@@ -1,12 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -27,7 +27,7 @@ async function createMigration() {
const timestamp = generateTimestamp();
const fileName = `${timestamp}_${slugify(String(migrationName), { lower: true })}.sql`;
const migrationsDir = path.join(__dirname, '../src-tauri/migrations');
const migrationsDir = path.join(__dirname, '../src-tauri/yaak-models/migrations');
const filePath = path.join(migrationsDir, fileName);
if (!fs.existsSync(migrationsDir)) {

2760
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ members = [
"yaak-crypto",
"yaak-git",
"yaak-grpc",
"yaak-fonts",
"yaak-http",
"yaak-license",
"yaak-mac-window",
@@ -33,7 +34,7 @@ strip = true # Automatically strip symbols from the binary.
cargo-clippy = []
[build-dependencies]
tauri-build = { version = "2.1.1", features = [] }
tauri-build = { version = "2.2.0", features = [] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
@@ -48,7 +49,7 @@ log = "0.4.27"
md5 = "0.7.0"
mime_guess = "2.0.5"
rand = "0.9.0"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider", "socks"] }
reqwest_cookie_store = "0.8.0"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["raw_value"] }
@@ -75,6 +76,7 @@ yaak-http = { workspace = true }
yaak-license = { path = "yaak-license" }
yaak-mac-window = { path = "yaak-mac-window" }
yaak-models = { workspace = true }
yaak-fonts = { workspace = true }
yaak-plugins = { workspace = true }
yaak-sse = { workspace = true }
yaak-sync = { workspace = true }
@@ -82,19 +84,20 @@ yaak-templates = { workspace = true }
yaak-ws = { path = "yaak-ws" }
[workspace.dependencies]
reqwest = "0.12.15"
reqwest = "0.12.19"
serde = "1.0.219"
serde_json = "1.0.140"
tauri = "2.4.1"
tauri-plugin = "2.1.1"
tauri-plugin-dialog = "2.2.0"
tauri = "2.5.1"
tauri-plugin = "2.2.0"
tauri-plugin-dialog = "2.2.2"
tauri-plugin-shell = "2.2.1"
tokio = "1.44.2"
tokio = "1.45.1"
thiserror = "2.0.12"
ts-rs = "10.1.0"
rustls = { version = "0.23.25", default-features = false }
rustls-platform-verifier = "0.5.1"
ts-rs = "11.0.1"
rustls = { version = "0.23.27", default-features = false }
rustls-platform-verifier = "0.6.0"
yaak-common = { path = "yaak-common" }
yaak-fonts = { path = "yaak-fonts" }
yaak-http = { path = "yaak-http" }
yaak-models = { path = "yaak-models" }
yaak-plugins = { path = "yaak-plugins" }

View File

@@ -53,6 +53,7 @@
"shell:allow-open",
"yaak-crypto:default",
"yaak-git:default",
"yaak-fonts:default",
"yaak-license:default",
"yaak-mac-window:default",
"yaak-models:default",

View File

@@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderName, HeaderValue};
use log::{debug, error, warn};
use mime_guess::Mime;
use reqwest::redirect::Policy;
use reqwest::{Method, Response};
use reqwest::{Method, NoProxy, Response};
use reqwest::{Proxy, Url, multipart};
use serde_json::Value;
use std::collections::BTreeMap;
@@ -120,25 +120,39 @@ pub async fn send_http_request<R: Runtime>(
https,
auth,
disabled,
bypass,
}) if !disabled => {
debug!("Using proxy http={http} https={https}");
let mut proxy = Proxy::custom(move |url| {
let http = if http.is_empty() { None } else { Some(http.to_owned()) };
let https = if https.is_empty() { None } else { Some(https.to_owned()) };
let proxy_url = match (url.scheme(), http, https) {
("http", Some(proxy_url), _) => Some(proxy_url),
("https", _, Some(proxy_url)) => Some(proxy_url),
_ => None,
debug!("Using proxy http={http} https={https} bypass={bypass}");
if !http.is_empty() {
match Proxy::http(http) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth.clone() {
debug!("Using http proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply http proxy {e:?}");
}
};
}
if !https.is_empty() {
match Proxy::https(https) {
Ok(mut proxy) => {
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using https proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
proxy = proxy.no_proxy(NoProxy::from_string(&bypass));
client_builder = client_builder.proxy(proxy);
}
Err(e) => {
warn!("Failed to apply https proxy {e:?}");
}
};
proxy_url
});
if let Some(ProxySettingAuth { user, password }) = auth {
debug!("Using proxy auth");
proxy = proxy.basic_auth(user.as_str(), password.as_str());
}
client_builder = client_builder.proxy(proxy);
}
_ => {} // Nothing to do for this one, as it is the default
}

View File

@@ -1265,6 +1265,7 @@ pub fn run() {
.plugin(yaak_models::init())
.plugin(yaak_plugins::init())
.plugin(yaak_crypto::init())
.plugin(yaak_fonts::init())
.plugin(yaak_git::init())
.plugin(yaak_ws::init())
.plugin(yaak_sync::init());

View File

@@ -57,7 +57,6 @@
],
"longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC",
"resources": [
"migrations",
"vendored/protoc/include",
"vendored/plugins",
"vendored/plugin-runtime"

View File

@@ -0,0 +1,16 @@
[package]
name = "yaak-fonts"
links = "yaak-fonts"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
font-loader = "0.11.0"
tauri = { workspace = true }
ts-rs = { workspace = true }
serde = "1.0"
thiserror = { workspace = true }
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Fonts = { editorFonts: Array<string>, uiFonts: Array<string>, };

View File

@@ -0,0 +1,5 @@
const COMMANDS: &[&str] = &["list"];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();
}

View File

@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
import { Fonts } from './bindings/gen_fonts';
export async function listFonts() {
return invoke<Fonts>('plugin:yaak-fonts|list', {});
}
export function useFonts() {
return useQuery({
queryKey: ['list_fonts'],
queryFn: () => listFonts(),
});
}

View File

@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/fonts",
"private": true,
"version": "1.0.0",
"main": "index.ts"
}

View File

@@ -0,0 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-list"]

View File

@@ -0,0 +1,41 @@
use crate::Result;
use font_loader::system_fonts;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use tauri::command;
use ts_rs::TS;
#[derive(Default, Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_fonts.ts")]
pub struct Fonts {
pub editor_fonts: Vec<String>,
pub ui_fonts: Vec<String>,
}
#[command]
pub(crate) async fn list() -> Result<Fonts> {
let mut ui_fonts = HashSet::new();
let mut editor_fonts = HashSet::new();
let mut property = system_fonts::FontPropertyBuilder::new().monospace().build();
for font in &system_fonts::query_specific(&mut property) {
editor_fonts.insert(font.to_string());
}
for font in &system_fonts::query_all() {
if !editor_fonts.contains(font) {
ui_fonts.insert(font.to_string());
}
}
let mut ui_fonts: Vec<String> = ui_fonts.into_iter().collect();
let mut editor_fonts: Vec<String> = editor_fonts.into_iter().collect();
ui_fonts.sort();
editor_fonts.sort();
Ok(Fonts {
ui_fonts,
editor_fonts,
})
}

View File

@@ -0,0 +1,15 @@
use serde::{ser::Serializer, Serialize};
#[derive(Debug, thiserror::Error)]
pub enum Error {}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -0,0 +1,15 @@
use tauri::{
generate_handler,
plugin::{Builder, TauriPlugin},
Runtime,
};
mod commands;
mod error;
use crate::commands::list;
pub use error::{Error, Result};
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("yaak-fonts").invoke_handler(generate_handler![list]).build()
}

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -8,9 +8,8 @@ publish = false
anyhow = "1.0.97"
async-recursion = "1.1.1"
dunce = "1.0.4"
hyper = "1.5.2"
hyper-rustls = { version = "0.27.5", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.10", default-features = false, features = ["client-legacy"] }
hyper-rustls = { version = "0.27.7", default-features = false, features = ["http2"] }
hyper-util = { version = "0.1.13", default-features = false, features = ["client-legacy"] }
log = "0.4.20"
md5 = "0.7.0"
prost = "0.13.4"
@@ -25,4 +24,4 @@ tokio-stream = "0.1.14"
tonic = { version = "0.12.3", default-features = false, features = ["transport"] }
tonic-reflection = "0.12.3"
uuid = { version = "1.7.0", features = ["v4"] }
yaak-http = { workspace = true }
yaak-http = { workspace = true }

View File

@@ -6,7 +6,7 @@ publish = false
[dependencies]
yaak-models = { workspace = true }
regex = "1.11.0"
regex = "1.11.1"
rustls = { workspace = true, default-features = false, features = ["ring"] }
rustls-platform-verifier = { workspace = true }
urlencoding = "2.1.3"

View File

@@ -1,9 +1,9 @@
use std::sync::Arc;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls::crypto::ring;
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls_platform_verifier::BuilderVerifierExt;
use std::sync::Arc;
pub fn get_config(validate_certificates: bool) -> ClientConfig {
let arc_crypto_provider = Arc::new(ring::default_provider());
@@ -12,9 +12,7 @@ pub fn get_config(validate_certificates: bool) -> ClientConfig {
.unwrap();
if validate_certificates {
// Use platform-native verifier to validate certificates
config_builder
.with_platform_verifier()
.with_no_client_auth()
config_builder.with_platform_verifier().unwrap().with_no_client_auth()
} else {
config_builder
.dangerous()

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use hex_color::HexColor;
use objc::{msg_send, sel, sel_impl};
use tauri::{Emitter, Runtime, Window};
@@ -65,12 +66,7 @@ pub(crate) fn update_window_theme<R: Runtime>(window: Window<R>, color: HexColor
}
}
fn position_traffic_lights(
ns_window_handle: UnsafeWindowHandle,
x: f64,
y: f64,
label: String,
) {
fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64, label: String) {
if !label.starts_with(MAIN_WINDOW_PREFIX) {
return;
}

View File

@@ -7,6 +7,8 @@ publish = false
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
hex = "0.4.3"
include_dir = "0.7"
log = "0.4.22"
nanoid = "0.4.0"
r2d2 = "0.8.10"
@@ -22,7 +24,6 @@ tauri-plugin-dialog = { workspace = true }
thiserror = "2.0.11"
tokio = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
hex = "0.4.3"
[build-dependencies]
tauri-plugin = { workspace = true, features = ["build"] }

View File

@@ -14,7 +14,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
export type EncryptedKey = { encryptedKey: string, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
@@ -58,11 +58,11 @@ export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, } | { "type": "disabled" };
export type ProxySetting = { "type": "enabled", disabled: boolean, http: string, https: string, auth: ProxySettingAuth | null, bypass: string, } | { "type": "disabled" };
export type ProxySettingAuth = { user: string, password: string, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, editorFontSize: number, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, editorKeymap: EditorKeymap, coloredMethods: boolean, };
export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, };
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };

View File

@@ -1,9 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models.js";
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };

View File

@@ -0,0 +1,2 @@
ALTER TABLE settings ADD COLUMN interface_font TEXT;
ALTER TABLE settings ADD COLUMN editor_font TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE environments ADD COLUMN color TEXT;

View File

@@ -59,7 +59,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.build(manager)
.unwrap();
if let Err(e) = migrate_db(app_handle.app_handle(), &pool) {
if let Err(e) = migrate_db(&pool) {
error!("Failed to run database migration {e:?}");
app_handle
.dialog()

View File

@@ -1,26 +1,16 @@
use crate::error::Error::MigrationError;
use crate::error::Result;
use log::info;
use include_dir::{include_dir, Dir, DirEntry};
use log::{debug, info};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, TransactionBehavior, params};
use rusqlite::{params, OptionalExtension, TransactionBehavior};
use sha2::{Digest, Sha384};
use std::fs;
use std::path::Path;
use std::result::Result as StdResult;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager, Runtime};
pub(crate) fn migrate_db<R: Runtime>(
app_handle: &AppHandle<R>,
pool: &Pool<SqliteConnectionManager>,
) -> Result<()> {
let migrations_dir = app_handle
.path()
.resolve("migrations", BaseDirectory::Resource)
.expect("failed to resolve resource");
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
info!("Running database migrations from: {:?}", migrations_dir);
pub(crate) fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
info!("Running database migrations");
// Ensure the table exists
// NOTE: Yaak used to use sqlx for migrations, so we need to mirror that table structure. We
@@ -39,20 +29,22 @@ pub(crate) fn migrate_db<R: Runtime>(
)?;
// Read and sort all .sql files
let mut entries = fs::read_dir(migrations_dir)
.expect("Failed to find migrations directory")
.filter_map(StdResult::ok)
let mut entries = MIGRATIONS_DIR
.entries()
.into_iter()
.filter(|e| e.path().extension().map(|ext| ext == "sql").unwrap_or(false))
.collect::<Vec<_>>();
// Ensure they're in the correct order
entries.sort_by_key(|e| e.file_name());
entries.sort_by_key(|e| e.path());
// Run each migration in a transaction
let mut num_migrations = 0;
for entry in entries {
num_migrations += 1;
let mut conn = pool.get()?;
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
match run_migration(entry.path().as_path(), &mut tx) {
match run_migration(entry, &mut tx) {
Ok(_) => tx.commit()?,
Err(e) => {
let msg = format!(
@@ -66,16 +58,15 @@ pub(crate) fn migrate_db<R: Runtime>(
};
}
info!("Finished running migrations");
info!("Finished running {} migrations", num_migrations);
Ok(())
}
fn run_migration(migration_path: &Path, tx: &mut rusqlite::Transaction) -> Result<bool> {
fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> {
let start = std::time::Instant::now();
let (version, description) =
split_migration_filename(migration_path.file_name().unwrap().to_str().unwrap())
.expect("Failed to parse migration filename");
let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap())
.expect("Failed to parse migration filename");
// Skip if already applied
let row: Option<i64> = tx
@@ -85,11 +76,13 @@ fn run_migration(migration_path: &Path, tx: &mut rusqlite::Transaction) -> Resul
.optional()?;
if row.is_some() {
debug!("Skipping migration {description}");
// Migration was already run
return Ok(false);
}
let sql = fs::read_to_string(migration_path).expect("Failed to read migration file");
let sql =
migration_path.as_file().unwrap().contents_utf8().expect("Failed to read migration file");
info!("Applying migration {description}");
// Split on `;`? → optional depending on how your SQL is structured

View File

@@ -31,12 +31,15 @@ macro_rules! impl_model {
#[ts(export, export_to = "gen_models.ts")]
pub enum ProxySetting {
Enabled {
#[serde(default)]
// This was added after on so give it a default to be able to deserialize older values
disabled: bool,
http: String,
https: String,
auth: Option<ProxySettingAuth>,
// These were added later, so give them defaults
#[serde(default)]
bypass: String,
#[serde(default)]
disabled: bool,
},
Disabled,
}
@@ -103,9 +106,13 @@ pub struct Settings {
pub updated_at: NaiveDateTime,
pub appearance: String,
pub colored_methods: bool,
pub editor_font: Option<String>,
pub editor_font_size: i32,
pub editor_keymap: EditorKeymap,
pub editor_soft_wrap: bool,
pub hide_window_controls: bool,
pub interface_font: Option<String>,
pub interface_font_size: i32,
pub interface_scale: f32,
pub open_workspace_new_window: Option<bool>,
@@ -113,8 +120,6 @@ pub struct Settings {
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
pub editor_keymap: EditorKeymap,
pub colored_methods: bool,
}
impl UpsertModelInfo for Settings {
@@ -154,6 +159,8 @@ impl UpsertModelInfo for Settings {
(EditorFontSize, self.editor_font_size.into()),
(EditorKeymap, self.editor_keymap.to_string().into()),
(EditorSoftWrap, self.editor_soft_wrap.into()),
(EditorFont, self.editor_font.into()),
(InterfaceFont, self.interface_font.into()),
(InterfaceFontSize, self.interface_font_size.into()),
(InterfaceScale, self.interface_scale.into()),
(HideWindowControls, self.hide_window_controls.into()),
@@ -173,8 +180,10 @@ impl UpsertModelInfo for Settings {
SettingsIden::EditorFontSize,
SettingsIden::EditorKeymap,
SettingsIden::EditorSoftWrap,
SettingsIden::EditorFont,
SettingsIden::InterfaceFontSize,
SettingsIden::InterfaceScale,
SettingsIden::InterfaceFont,
SettingsIden::HideWindowControls,
SettingsIden::OpenWorkspaceNewWindow,
SettingsIden::Proxy,
@@ -198,10 +207,12 @@ impl UpsertModelInfo for Settings {
updated_at: row.get("updated_at")?,
appearance: row.get("appearance")?,
editor_font_size: row.get("editor_font_size")?,
editor_font: row.get("editor_font")?,
editor_keymap: EditorKeymap::from_str(editor_keymap.as_str()).unwrap(),
editor_soft_wrap: row.get("editor_soft_wrap")?,
interface_font_size: row.get("interface_font_size")?,
interface_scale: row.get("interface_scale")?,
interface_font: row.get("interface_font")?,
open_workspace_new_window: row.get("open_workspace_new_window")?,
proxy: proxy.map(|p| -> ProxySetting { serde_json::from_str(p.as_str()).unwrap() }),
theme_dark: row.get("theme_dark")?,
@@ -520,6 +531,7 @@ pub struct Environment {
pub public: bool,
pub base: bool,
pub variables: Vec<EnvironmentVariable>,
pub color: Option<String>,
}
impl UpsertModelInfo for Environment {
@@ -553,6 +565,7 @@ impl UpsertModelInfo for Environment {
(UpdatedAt, upsert_date(source, self.updated_at)),
(WorkspaceId, self.workspace_id.into()),
(Base, self.base.into()),
(Color, self.color.into()),
(Name, self.name.trim().into()),
(Public, self.public.into()),
(Variables, serde_json::to_string(&self.variables)?.into()),
@@ -563,6 +576,7 @@ impl UpsertModelInfo for Environment {
vec![
EnvironmentIden::UpdatedAt,
EnvironmentIden::Base,
EnvironmentIden::Color,
EnvironmentIden::Name,
EnvironmentIden::Public,
EnvironmentIden::Variables,
@@ -581,6 +595,7 @@ impl UpsertModelInfo for Environment {
created_at: row.get("created_at")?,
updated_at: row.get("updated_at")?,
base: row.get("base")?,
color: row.get("color")?,
name: row.get("name")?,
public: row.get("public")?,
variables: serde_json::from_str(variables.as_str()).unwrap_or_default(),

View File

@@ -19,10 +19,12 @@ impl<'a> DbContext<'a> {
appearance: "system".to_string(),
editor_font_size: 13,
editor_font: None,
editor_keymap: EditorKeymap::Default,
editor_soft_wrap: true,
interface_font_size: 15,
interface_scale: 1.0,
interface_font: None,
hide_window_controls: false,
open_workspace_new_window: None,
proxy: None,

View File

@@ -1,12 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { Environment, Folder, GrpcRequest, HttpRequest, HttpResponse, WebsocketRequest, Workspace } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { WebsocketRequest } from "./gen_models.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, };
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, base: boolean, variables: Array<EnvironmentVariable>, color: string | null, };
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };

View File

@@ -1,6 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel } from "./gen_models.js";
import type { SyncState } from "./gen_models.js";
import type { SyncModel, SyncState } from "./gen_models.js";
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };

View File

@@ -4,11 +4,12 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
import { toggleDialog } from '../lib/dialog';
import { setWorkspaceSearchParams } from '../lib/setWorkspaceSearchParams';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { ButtonProps } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
type Props = {
@@ -38,6 +39,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
(e) => ({
key: e.id,
label: e.name,
rightSlot: <EnvironmentColorIndicator environment={e} />,
leftSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
if (e.id !== activeEnvironment?.id) {
@@ -80,6 +82,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onClick={subEnvironments.length === 0 ? showEnvironmentDialog : undefined}
{...buttonProps}
>
<EnvironmentColorIndicator environment={activeEnvironment ?? null} />
{activeEnvironment?.name ?? (hasBaseVars ? 'Environment' : 'No Environment')}
</Button>
</Dropdown>

View File

@@ -0,0 +1,29 @@
import type { Environment } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { showColorPicker } from '../lib/showColorPicker';
export function EnvironmentColorIndicator({
environment,
clickToEdit,
}: {
environment: Environment | null;
clickToEdit?: boolean;
}) {
if (environment?.color == null) return null;
const style = { backgroundColor: environment.color };
const className =
'inline-block w-[0.75em] h-[0.75em] rounded-full mr-1.5 border border-transparent';
if (clickToEdit) {
return (
<button
onClick={() => showColorPicker(environment)}
style={style}
className={classNames(className, 'hover:border-text')}
/>
);
} else {
return <span style={style} className={className} />;
}
}

View File

@@ -0,0 +1,32 @@
import { useState } from 'react';
import { Button } from './core/Button';
import { ColorPicker } from './core/ColorPicker';
export function EnvironmentColorPicker({
color: defaultColor,
onChange,
}: {
color: string | null;
onChange: (color: string | null) => void;
}) {
const [color, setColor] = useState<string | null>(defaultColor);
return (
<form
className="flex flex-col items-stretch gap-3 pb-2 w-full"
onSubmit={(e) => {
e.preventDefault();
onChange(color);
}}
>
<ColorPicker color={color} onChange={setColor} />
<div className="grid grid-cols-[1fr_1fr] gap-1.5">
<Button variant="border" color="secondary" onClick={() => onChange(null)}>
Clear
</Button>
<Button type="submit" color="primary">
Save
</Button>
</div>
</form>
);
}

View File

@@ -17,6 +17,7 @@ import {
setupOrConfigureEncryption,
withEncryptionEnabled,
} from '../lib/setupOrConfigureEncryption';
import { showColorPicker } from '../lib/showColorPicker';
import { BadgeButton } from './core/BadgeButton';
import { Banner } from './core/Banner';
import { Button } from './core/Button';
@@ -35,6 +36,7 @@ import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout';
import { VStack } from './core/Stacks';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
interface Props {
initialEnvironment: Environment | null;
@@ -123,7 +125,7 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
))}
{subEnvironments.length > 0 && (
<div className="px-2">
<Separator className="my-3"></Separator>
<Separator className="my-3" />
</div>
)}
{subEnvironments.map((e) => (
@@ -237,6 +239,7 @@ const EnvironmentEditor = function ({
return (
<VStack space={4} className={classNames(className, 'pl-4')}>
<Heading className="w-full flex items-center gap-0.5">
<EnvironmentColorIndicator clickToEdit environment={selectedEnvironment ?? null} />
<div className="mr-2">{selectedEnvironment?.name}</div>
{isEncryptionEnabled ? (
promptToEncrypt ? (
@@ -344,6 +347,7 @@ function SidebarButton({
onContextMenu={handleContextMenu}
rightSlot={rightSlot}
>
<EnvironmentColorIndicator environment={environment} />
{children}
</Button>
{outerRightSlot}
@@ -385,6 +389,12 @@ function SidebarButton({
},
]
: []) as DropdownItem[]),
{
label: environment.color ? 'Change Color' : 'Assign Color',
leftSlot: <Icon icon="palette" />,
hidden: environment.base,
onSelect: async () => showColorPicker(environment),
},
{
label: `Make ${environment.public ? 'Private' : 'Sharable'}`,
leftSlot: <Icon icon={environment.public ? 'eye_closed' : 'eye'} />,

View File

@@ -8,22 +8,24 @@ import { capitalize } from '../../lib/capitalize';
import { HStack } from '../core/Stacks';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsInterface } from './SettingsInterface';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsLicense } from './SettingsLicense';
import { SettingsPlugins } from './SettingsPlugins';
import { SettingsProxy } from './SettingsProxy';
import { SettingsTheme } from './SettingsTheme';
interface Props {
hide?: () => void;
}
const TAB_GENERAL = 'general';
const TAB_APPEARANCE = 'appearance';
const TAB_INTERFACE = 'interface';
const TAB_THEME = 'theme';
const TAB_PROXY = 'proxy';
const TAB_PLUGINS = 'plugins';
const TAB_LICENSE = 'license';
const tabs = [TAB_GENERAL, TAB_APPEARANCE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
const tabs = [TAB_GENERAL, TAB_THEME, TAB_INTERFACE, TAB_PROXY, TAB_PLUGINS, TAB_LICENSE] as const;
export type SettingsTab = (typeof tabs)[number];
export default function Settings({ hide }: Props) {
@@ -73,8 +75,11 @@ export default function Settings({ hide }: Props) {
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
<SettingsGeneral />
</TabContent>
<TabContent value={TAB_APPEARANCE} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
<TabContent value={TAB_INTERFACE} className="pt-3 overflow-y-auto h-full px-4">
<SettingsInterface />
</TabContent>
<TabContent value={TAB_THEME} className="pt-3 overflow-y-auto h-full px-4">
<SettingsTheme />
</TabContent>
<TabContent value={TAB_PLUGINS} className="pt-3 overflow-y-auto h-full px-4">
<SettingsPlugins />

View File

@@ -0,0 +1,137 @@
import { type } from '@tauri-apps/plugin-os';
import { useFonts } from '@yaakapp-internal/fonts';
import type { EditorKeymap } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { clamp } from '../../lib/clamp';
import { Checkbox } from '../core/Checkbox';
import { Icon } from '../core/Icon';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
const NULL_FONT_VALUE = '__NULL_FONT__';
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: { value: EditorKeymap; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'vim', label: 'Vim' },
{ value: 'vscode', label: 'VSCode' },
{ value: 'emacs', label: 'Emacs' },
];
export function SettingsInterface() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom);
const fonts = useFonts();
if (settings == null || workspace == null) {
return null;
}
return (
<VStack space={3} className="mb-4">
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="uiFont"
label="Interface Font"
value={settings.interfaceFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System Default', value: NULL_FONT_VALUE },
...(fonts.data.uiFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
// Some people like monospace fonts for the UI
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
]}
onChange={async (v) => {
const interfaceFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { interfaceFont });
}}
/>
)}
<Select
hideLabel
size="sm"
name="interfaceFontSize"
label="Interface Font Size"
defaultValue="15"
value={`${settings.interfaceFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: parseInt(v) })}
/>
</HStack>
<HStack space={2} alignItems="end">
{fonts.data && (
<Select
size="sm"
name="editorFont"
label="Editor Font"
value={settings.editorFont ?? NULL_FONT_VALUE}
options={[
{ label: 'System Default', value: NULL_FONT_VALUE },
...(fonts.data.editorFonts.map((f) => ({
label: f,
value: f,
})) ?? []),
]}
onChange={async (v) => {
const editorFont = v === NULL_FONT_VALUE ? null : v;
await patchModel(settings, { editorFont });
}}
/>
)}
<Select
hideLabel
size="sm"
name="editorFontSize"
label="Editor Font Size"
defaultValue="13"
value={`${settings.editorFontSize}`}
options={fontSizeOptions}
onChange={(v) =>
patchModel(settings, { editorFontSize: clamp(parseInt(v) || 14, 8, 30) })
}
/>
</HStack>
<Select
leftSlot={<Icon icon="keyboard" color="secondary" />}
size="sm"
name="editorKeymap"
label="Editor Keymap"
value={`${settings.editorKeymap}`}
options={keymaps}
onChange={(v) => patchModel(settings, { editorKeymap: v })}
/>
<Checkbox
checked={settings.editorSoftWrap}
title="Wrap Editor Lines"
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
<Checkbox
checked={settings.coloredMethods}
title="Colorize Request Methods"
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
/>
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide Window Controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
</VStack>
);
}

View File

@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { Checkbox } from '../core/Checkbox';
import { InlineCode } from '../core/InlineCode';
import { PlainInput } from '../core/PlainInput';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
@@ -29,6 +30,7 @@ export function SettingsProxy() {
http: '',
https: '',
auth: { user: '', password: '' },
bypass: '',
},
});
} else {
@@ -52,22 +54,28 @@ export function SettingsProxy() {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = !enabled;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
<HStack space={1.5}>
<PlainInput
size="sm"
label="HTTP"
label={
<>
Proxy for <InlineCode>http://</InlineCode> traffic
</>
}
placeholder="localhost:9090"
defaultValue={settings.proxy?.http}
onChange={async (http) => {
const { proxy } = settings;
const https = proxy?.type === 'enabled' ? proxy.https : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
await patchModel(settings, {
@@ -77,22 +85,28 @@ export function SettingsProxy() {
https,
auth,
disabled,
bypass,
},
});
}}
/>
<PlainInput
size="sm"
label="HTTPS"
label={
<>
Proxy for <InlineCode>https://</InlineCode> traffic
</>
}
placeholder="localhost:9090"
defaultValue={settings.proxy?.https}
onChange={async (https) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = proxy?.type === 'enabled' ? proxy.auth : null;
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
@@ -106,9 +120,10 @@ export function SettingsProxy() {
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const auth = enabled ? { user: '', password: '' } : null;
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
@@ -126,10 +141,11 @@ export function SettingsProxy() {
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
@@ -144,15 +160,39 @@ export function SettingsProxy() {
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const bypass = proxy?.type === 'enabled' ? proxy.bypass : '';
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled },
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
</HStack>
)}
{settings.proxy.type === 'enabled' && (
<>
<Separator className="my-6" />
<PlainInput
label="Proxy Bypass"
help="Comma-separated list to bypass the proxy."
defaultValue={settings.proxy.bypass}
placeholder="127.0.0.1, *.example.com, localhost:3000"
onChange={async (bypass) => {
const { proxy } = settings;
const http = proxy?.type === 'enabled' ? proxy.http : '';
const https = proxy?.type === 'enabled' ? proxy.https : '';
const disabled = proxy?.type === 'enabled' ? proxy.disabled : false;
const user = proxy?.type === 'enabled' ? (proxy.auth?.user ?? '') : '';
const password = proxy?.type === 'enabled' ? (proxy.auth?.password ?? '') : '';
const auth = { user, password };
await patchModel(settings, {
proxy: { type: 'enabled', http, https, auth, disabled, bypass },
});
}}
/>
</>
)}
</VStack>
)}
</VStack>

View File

@@ -1,35 +1,19 @@
import type { EditorKeymap } from '@yaakapp-internal/models';
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
import { useAtomValue } from 'jotai';
import React from 'react';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
import { clamp } from '../../lib/clamp';
import { getThemes } from '../../lib/theme/themes';
import { isThemeDark } from '../../lib/theme/window';
import type { ButtonProps } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { Editor } from '../core/Editor/Editor';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import type { SelectProps } from '../core/Select';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import type { SelectProps } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
import { type } from '@tauri-apps/plugin-os';
const fontSizeOptions = [
8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
].map((n) => ({ label: `${n}`, value: `${n}` }));
const keymaps: { value: EditorKeymap; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'vim', label: 'Vim' },
{ value: 'vscode', label: 'VSCode' },
{ value: 'emacs', label: 'Emacs' },
];
const buttonColors: ButtonProps['color'][] = [
'primary',
@@ -62,7 +46,7 @@ const icons: IconProps['icon'][] = [
const { themes } = getThemes();
export function SettingsAppearance() {
export function SettingsTheme() {
const workspace = useAtomValue(activeWorkspaceAtom);
const settings = useAtomValue(settingsAtom);
const appearance = useResolvedAppearance();
@@ -87,58 +71,7 @@ export function SettingsAppearance() {
}));
return (
<VStack space={2} className="mb-4">
<Select
size="sm"
name="interfaceFontSize"
label="Font Size"
labelPosition="left"
defaultValue="15"
value={`${settings.interfaceFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { interfaceFontSize: parseInt(v) })}
/>
<Select
size="sm"
name="editorFontSize"
label="Editor Font Size"
labelPosition="left"
defaultValue="13"
value={`${settings.editorFontSize}`}
options={fontSizeOptions}
onChange={(v) => patchModel(settings, { editorFontSize: clamp(parseInt(v) || 14, 8, 30) })}
/>
<Select
size="sm"
name="editorKeymap"
label="Editor Keymap"
labelPosition="left"
value={`${settings.editorKeymap}`}
options={keymaps}
onChange={(v) => patchModel(settings, { editorKeymap: v })}
/>
<Checkbox
checked={settings.editorSoftWrap}
title="Wrap Editor Lines"
onChange={(editorSoftWrap) => patchModel(settings, { editorSoftWrap })}
/>
<Checkbox
checked={settings.coloredMethods}
title="Colorize Request Methods"
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
/>
{type() !== 'macos' && (
<Checkbox
checked={settings.hideWindowControls}
title="Hide Window Controls"
help="Hide the close/maximize/minimize controls on Windows or Linux"
onChange={(hideWindowControls) => patchModel(settings, { hideWindowControls })}
/>
)}
<Separator className="my-4" />
<VStack space={3} className="mb-4">
<Select
name="appearance"
label="Appearance"
@@ -156,7 +89,7 @@ export function SettingsAppearance() {
{(settings.appearance === 'system' || settings.appearance === 'light') && (
<Select
hideLabel
leftSlot={<Icon icon="sun" />}
leftSlot={<Icon icon="sun" color="secondary" />}
name="lightTheme"
label="Light Theme"
size="sm"
@@ -172,7 +105,7 @@ export function SettingsAppearance() {
name="darkTheme"
className="flex-1"
label="Dark Theme"
leftSlot={<Icon icon="moon" />}
leftSlot={<Icon icon="moon" color="secondary" />}
size="sm"
value={activeTheme.dark.id}
options={darkThemes}

View File

@@ -4,11 +4,8 @@ import { useAtomValue } from 'jotai';
import * as m from 'motion/react-m';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import {
useEnsureActiveCookieJar,
useSubscribeActiveCookieJarId,
} from '../hooks/useActiveCookieJar';
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import { useEnsureActiveCookieJar, useSubscribeActiveCookieJarId } from '../hooks/useActiveCookieJar';
import { activeEnvironmentAtom, useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
import { activeRequestAtom } from '../hooks/useActiveRequest';
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
@@ -32,6 +29,7 @@ import { HotKeyList } from './core/HotKeyList';
import { FeedbackLink } from './core/Link';
import { HStack } from './core/Stacks';
import { CreateDropdown } from './CreateDropdown';
import { ErrorBoundary } from './ErrorBoundary';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HeaderSize } from './HeaderSize';
import { HttpRequestLayout } from './HttpRequestLayout';
@@ -41,7 +39,6 @@ import { Sidebar } from './sidebar/Sidebar';
import { SidebarActions } from './sidebar/SidebarActions';
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
import { WorkspaceHeader } from './WorkspaceHeader';
import { ErrorBoundary } from './ErrorBoundary';
const side = { gridArea: 'side' };
const head = { gridArea: 'head' };
@@ -56,6 +53,7 @@ export function Workspace() {
const { setWidth, width, resetWidth } = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
const activeEnvironment = useAtomValue(activeEnvironmentAtom);
const floating = useShouldFloatSidebar();
const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
@@ -117,6 +115,12 @@ export function Workspace() {
[sideWidth, floating],
);
const environmentBgStyle = useMemo(() => {
if (activeEnvironment?.color == null) return undefined;
const background = `linear-gradient(to right, ${activeEnvironment.color} 15%, transparent 40%)`;
return { background };
}, [activeEnvironment?.color]);
// We're loading still
if (workspaces.length === 0) {
return null;
@@ -175,9 +179,19 @@ export function Workspace() {
<HeaderSize
data-tauri-drag-region
size="lg"
className="x-theme-appHeader bg-surface"
className="relative x-theme-appHeader bg-surface"
style={head}
>
<div className="absolute inset-0 pointer-events-none">
<div // Add subtle background
style={environmentBgStyle}
className="absolute inset-0 opacity-5"
/>
<div // Add subtle border bottom
style={environmentBgStyle}
className="absolute left-0 right-0 bottom-0 h-[0.5px] opacity-20"
/>
</div>
<WorkspaceHeader className="pointer-events-none" />
</HeaderSize>
<ErrorBoundary name="Workspace Body">

View File

@@ -0,0 +1,32 @@
import { HexColorPicker } from 'react-colorful';
import { useRandomKey } from '../../hooks/useRandomKey';
import { PlainInput } from './PlainInput';
interface Props {
onChange: (value: string | null) => void;
color: string | null;
}
export function ColorPicker({ onChange, color }: Props) {
const [updateKey, regenerateKey] = useRandomKey();
return (
<div>
<HexColorPicker
color={color ?? undefined}
className="!w-full"
onChange={(color) => {
onChange(color);
regenerateKey(); // To force input to change
}}
/>
<PlainInput
hideLabel
label="Plain Color"
forceUpdateKey={updateKey}
defaultValue={color ?? ''}
onChange={onChange}
validate={(color) => color.match(/#[0-9a-fA-F]{6}$/) !== null}
/>
</div>
);
}

View File

@@ -26,9 +26,9 @@ const icons = {
cake: lucide.CakeIcon,
chat: lucide.MessageSquare,
check: lucide.CheckIcon,
check_circle: lucide.CheckCircleIcon,
check_square_checked: lucide.SquareCheckIcon,
check_square_unchecked: lucide.SquareIcon,
check_circle: lucide.CheckCircleIcon,
chevron_down: lucide.ChevronDownIcon,
chevron_right: lucide.ChevronRightIcon,
circle_alert: lucide.CircleAlertIcon,
@@ -78,6 +78,7 @@ const icons = {
minus_circle: lucide.MinusCircleIcon,
moon: lucide.MoonIcon,
more_vertical: lucide.MoreVerticalIcon,
palette: lucide.PaletteIcon,
paste: lucide.ClipboardPasteIcon,
pencil: lucide.PencilIcon,
pin: lucide.PinIcon,

View File

@@ -16,11 +16,15 @@ export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type
};
export function PlainInput({
autoFocus,
autoSelect,
className,
containerClassName,
defaultValue,
forceUpdateKey,
help,
hideLabel,
hideObscureToggle,
label,
labelClassName,
labelPosition = 'top',
@@ -29,19 +33,16 @@ export function PlainInput({
onBlur,
onChange,
onFocus,
onFocusRaw,
onKeyDownCapture,
onPaste,
placeholder,
required,
rightSlot,
hideObscureToggle,
size = 'md',
type = 'text',
tint,
type = 'text',
validate,
autoSelect,
placeholder,
autoFocus,
onKeyDownCapture,
onFocusRaw,
}: PlainInputProps) {
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [focused, setFocused] = useState(false);
@@ -101,7 +102,7 @@ export function PlainInput({
labelPosition === 'top' && 'flex-row gap-0.5',
)}
>
<Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required}>
<Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required} help={help}>
{label}
</Label>
<HStack

19
src-web/font.ts Normal file
View File

@@ -0,0 +1,19 @@
// Listen for settings changes, the re-compute theme
import { listen } from '@tauri-apps/api/event';
import type { ModelPayload, Settings } from '@yaakapp-internal/models';
import { getSettings } from './lib/settings';
function setFonts(settings: Settings) {
document.documentElement.style.setProperty('--font-family-editor', settings.editorFont ?? '');
document.documentElement.style.setProperty(
'--font-family-interface',
settings.interfaceFont ?? '',
);
}
listen<ModelPayload>('upserted_model', async (event) => {
if (event.payload.model.model !== 'settings') return;
setFonts(event.payload.model);
}).catch(console.error);
getSettings().then((settings) => setFonts(settings));

View File

@@ -26,18 +26,23 @@ export function useRecentWorkspaces() {
export function useSubscribeRecentWorkspaces() {
useEffect(() => {
return jotaiStore.sub(activeWorkspaceIdAtom, async () => {
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (activeWorkspaceId == null) return;
const key = kvKey();
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
if (recentIds[0] === activeWorkspaceId) return; // Short-circuit
const withoutActiveId = recentIds.filter((id) => id !== activeWorkspaceId);
const value = [activeWorkspaceId, ...withoutActiveId];
await setKeyValue({ namespace, key, value });
});
const unsub = jotaiStore.sub(activeWorkspaceIdAtom, updateRecentWorkspaces);
updateRecentWorkspaces().catch(console.error); // Update when opened in a new window
return unsub;
}, []);
}
async function updateRecentWorkspaces() {
const activeWorkspaceId = jotaiStore.get(activeWorkspaceIdAtom);
if (activeWorkspaceId == null) return;
const key = kvKey();
const recentIds = getKeyValue<string[]>({ namespace, key, fallback });
if (recentIds[0] === activeWorkspaceId) return; // Short-circuit
const withoutActiveId = recentIds.filter((id) => id !== activeWorkspaceId);
const value = [activeWorkspaceId, ...withoutActiveId];
console.log('Recent workspaces update', activeWorkspaceId);
await setKeyValue({ namespace, key, value });
}

View File

@@ -28,6 +28,7 @@
<div id="radix-portal" class="cm-portal"></div>
<script type="module" src="/theme.ts"></script>
<script type="module" src="/font-size.ts"></script>
<script type="module" src="/font.ts"></script>
<script type="module" src="/main.tsx"></script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More