feat: gemini support

This commit is contained in:
Artemy Egorov 2024-08-04 16:55:03 +03:00
parent 15c43e3482
commit d96b863ea8
26 changed files with 448 additions and 93 deletions

34
src-tauri/Cargo.lock generated
View file

@ -258,9 +258,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.1" version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -603,9 +603,9 @@ dependencies = [
[[package]] [[package]]
name = "dalet" name = "dalet"
version = "1.0.0-pre6" version = "1.0.0-pre9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bc4c347533f8341633bd820799dea680f600e50891310b74bc914740681e8c2" checksum = "2095f83b5256dc9a981639c3250aba53c02736a3601c1e6b2c54c27ec786274a"
dependencies = [ dependencies = [
"clap", "clap",
"enum-procs", "enum-procs",
@ -3093,6 +3093,15 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@ -3685,8 +3694,11 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -3704,6 +3716,17 @@ dependencies = [
"webpki-roots", "webpki-roots",
] ]
[[package]]
name = "tokio-macros"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-native-tls"
version = "0.3.1" version = "0.3.1"
@ -4006,6 +4029,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
name = "vigi" name = "vigi"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"bytes",
"dalet", "dalet",
"mime", "mime",
"reqwest 0.12.5", "reqwest 0.12.5",
@ -4013,7 +4037,9 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tokio",
"tokio-gemini", "tokio-gemini",
"tokio-rustls",
"url", "url",
] ]

View file

@ -26,7 +26,14 @@ tauri = { version = "1", features = [
] } ] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
dalet = "1.0.0-pre6" dalet = "1.0.0-pre9"
tokio = { version = "1.39.2", features = ["full"] }
tokio-rustls = { version = "0.26.0", default-features = false, features = [
"ring",
] }
bytes = "1.7.1"
reqwest = "0.12.5" reqwest = "0.12.5"
tokio-gemini = "0.1.0" tokio-gemini = "0.1.0"

View file

@ -7,7 +7,7 @@ mod process_input;
mod types; mod types;
mod utils; mod utils;
use tauri::async_runtime::Mutex; use tokio::sync::Mutex;
use types::{VigiError, VigiJsState, VigiState}; use types::{VigiError, VigiJsState, VigiState};
use utils::{read_or_create_jsonl, read_or_create_number}; use utils::{read_or_create_jsonl, read_or_create_number};

View file

@ -0,0 +1,56 @@
use tokio_rustls::rustls::{
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
ClientConfig, SignatureScheme,
};
/// TODO: update to secure version when supported
pub fn insecure_gemini_client() -> tokio_gemini::Client {
tokio_gemini::Client::from(
ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(NoCertVerification {}))
.with_no_client_auth(),
)
}
#[derive(Debug)]
struct NoCertVerification;
impl ServerCertVerifier for NoCertVerification {
fn verify_server_cert(
&self,
_end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
_intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>],
_server_name: &tokio_rustls::rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: tokio_rustls::rustls::pki_types::UnixTime,
) -> Result<ServerCertVerified, tokio_rustls::rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
_dss: &tokio_rustls::rustls::DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
_dss: &tokio_rustls::rustls::DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, tokio_rustls::rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
vec![
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::ECDSA_NISTP521_SHA512,
]
}
}

View file

@ -1,15 +1,16 @@
use crate::types::{VigiError, VigiOutput}; use crate::types::{VigiError, VigiOutput};
use bytes::Bytes;
use mime::Mime; use mime::Mime;
use url::Url; use url::Url;
mod insecure_gemini_client;
mod process_data; mod process_data;
mod process_url; mod process_url;
use process_data::process_data; use process_data::process_data;
use process_url::process_url; use process_url::process_url;
type Data = Vec<u8>; type ReqResult = (Mime, Bytes);
type ReqResult = (Mime, Data);
pub async fn process_input(input: &String) -> Result<VigiOutput, VigiError> { pub async fn process_input(input: &String) -> Result<VigiOutput, VigiError> {
let parsed = Url::parse(input); let parsed = Url::parse(input);

View file

@ -1,25 +1,41 @@
use dalet::{daletl::ToDaletlPage, typed::Tag::*}; use bytes::Bytes;
use dalet::{daletl::ToDaletlPage, parsers::gemtext::parse_gemtext, typed::Tag::*};
use mime::Mime; use mime::Mime;
use std::str;
use crate::types::{VigiError, VigiOutput}; use crate::types::{VigiError, VigiOutput};
pub async fn process_data(mime: Mime, data: Vec<u8>) -> Result<VigiOutput, VigiError> { pub async fn process_data(mime: Mime, data: Bytes) -> Result<VigiOutput, VigiError> {
let result = match (mime.type_().as_str(), mime.subtype().as_str()) { let result = match (mime.type_().as_str(), mime.subtype().as_str()) {
("text", "plain") => { ("text", "plain") => {
process_text(String::from_utf8(data).map_err(|_| VigiError::TextIsNotUtf8)?).await process_text(str::from_utf8(&data).map_err(|_| VigiError::InvalidCharset)?).await
}
("text", "gemini") => {
process_gemini(str::from_utf8(&data).map_err(|_| VigiError::InvalidCharset)?).await?
} }
// ("text", "gemini") => {
// process_text(String::from_utf8(data).map_err(|_| VigiError::TextIsNotUtf8)?).await
// }
_ => Err(VigiError::UnsupportedMimeType)?, _ => Err(VigiError::UnsupportedMimeType)?,
}; };
Ok(result) Ok(result)
} }
async fn process_text(data: String) -> VigiOutput { async fn process_text(data: &str) -> VigiOutput {
let mut truncated = data.clone(); let mut truncated = data.to_owned();
truncated.truncate(50); truncated.truncate(50);
VigiOutput::new(truncated, vec![El(data.into())].to_dl_page()) VigiOutput::new(truncated, vec![El(data.into())].to_dl_page())
} }
async fn process_gemini(data: &str) -> Result<VigiOutput, VigiError> {
let mut truncated = data.to_owned();
truncated.truncate(50);
let res = VigiOutput::new(
truncated,
parse_gemtext(data)
.map_err(|_| VigiError::Parse)?
.to_dl_page(),
);
Ok(res)
}

View file

@ -1,15 +1,17 @@
use bytes::Bytes;
use mime::Mime; use mime::Mime;
use reqwest::header::CONTENT_TYPE; use reqwest::header::CONTENT_TYPE;
use tokio::io::AsyncReadExt;
use url::Url; use url::Url;
use crate::types::VigiError; use crate::types::VigiError;
use super::ReqResult; use super::{insecure_gemini_client, ReqResult};
pub async fn process_url(url: Url) -> Result<ReqResult, VigiError> { pub async fn process_url(url: Url) -> Result<ReqResult, VigiError> {
let result = match url.scheme() { let result = match url.scheme() {
"http" | "https" => process_http(url.to_string()).await?, "http" | "https" => process_http(url.to_string()).await?,
// "gemini" => process_gemini(url.to_string()).await?, "gemini" => process_gemini(url.to_string()).await?,
_ => Err(VigiError::UnsupportedProtocol)?, _ => Err(VigiError::UnsupportedProtocol)?,
}; };
@ -37,16 +39,21 @@ async fn process_http(url: String) -> Result<ReqResult, VigiError> {
)) ))
} }
// async fn process_gemini(url: String) -> Result<ReqResult, VigiError> { async fn process_gemini(url: String) -> Result<ReqResult, VigiError> {
// let res = tokio_gemini::Client::default() let mut res = insecure_gemini_client::insecure_gemini_client()
// .request(&url) .request(&url)
// .await .await
// .map_err(|e| { .map_err(|_| VigiError::Network)?;
// println!("{:#?}", e);
// VigiError::Network
// })?;
// let mime_type = res.mime().map_err(|_| VigiError::InvalidMimeType)?; let mime_type = res.mime().map_err(|_| VigiError::InvalidMimeType)?;
// Ok((mime_type, res.message().as_bytes().into())) let mut buffer = Vec::new();
// }
let tls_stream = res.body();
tls_stream
.read_to_end(&mut buffer)
.await
.map_err(|_| VigiError::Network)?;
Ok((mime_type, Bytes::from(buffer).into()))
}

View file

@ -21,7 +21,7 @@ pub enum VigiError {
UnsupportedMimeType, UnsupportedMimeType,
InvalidMimeType, InvalidMimeType,
TextIsNotUtf8, InvalidCharset,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -1,3 +1,5 @@
@import "tags.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@ -12,14 +14,14 @@
--min-bg: #96e28a; --min-bg: #96e28a;
--max-bg: #193815; --max-bg: #193815;
--min-text: #bef5b5; --min-text: #82de73;
--max-text: #fff; --max-text: #fff;
/* Dark */ /* Dark */
/* --min-bg: #555; /* --min-bg: #555;
--max-bg: #000; --max-bg: #000;
--min-text: #cfcfcf; --min-text: #555;
--max-text: #fff; */ --max-text: #fff; */
} }
@ -27,7 +29,7 @@
/* Components */ /* Components */
body { body {
@apply color-vigi-90 cursor-default; @apply color-vigi-90 cursor-default overflow-clip;
user-select: none; user-select: none;
} }
@ -43,11 +45,11 @@ body {
} }
.main-window { .main-window {
@apply grow flex flex-col gap-3; @apply grow flex flex-col gap-3 w-3/4;
} }
.browser-window { .browser-window {
@apply grow overflow-auto text-wrap select-text cursor-auto; @apply grow overflow-y-auto text-wrap select-text cursor-auto overflow-x-hidden;
} }
.window-controls { .window-controls {
@ -110,7 +112,7 @@ body {
} }
.search-input { .search-input {
@apply px-2 py-1 rounded-xl grow; @apply px-2 py-1 rounded-xl grow min-w-0;
@apply color-vigi-60 outline-none; @apply color-vigi-60 outline-none;
@apply focus:color-vigi-50 hover:color-vigi-55; @apply focus:color-vigi-50 hover:color-vigi-55;
@ -160,24 +162,26 @@ input::placeholder {
@apply color-vigi-60; @apply color-vigi-60;
} }
/* width */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 15px; width: 15px;
height: 15px;
} }
/* Track */
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@apply bg-transparent; @apply bg-transparent;
} }
/* Handle */
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@apply rounded-xl color-vigi-70 bg-clip-content; @apply rounded-xl color-vigi-70 bg-clip-content;
border: 6px solid transparent; border: 6px solid transparent;
} }
/* Handle on hover */
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
@apply color-vigi-75; @apply color-vigi-75;
border: 5px solid transparent; border: 5px solid transparent;
} }
::-webkit-scrollbar-thumb:active {
@apply color-vigi-100;
border: 4px solid transparent;
}

View file

@ -1,40 +1,40 @@
<script lang="ts"> <script lang="ts">
import type { Root } from "@txtdot/dalet"; import type { Root } from "@txtdot/dalet";
import Block from "./Block.svelte"; import Block from "./Block.svelte";
import Renderer from "./DaletlRenderer/Renderer.svelte"; import Renderer from "./DaletlRenderer/Renderer.svelte";
import { isLoading, state } from "$lib/stores"; import { isLoading, state } from "$lib/stores";
import type { VigiState } from "$lib/types"; import type { VigiState } from "$lib/types";
import GooLoadSpin from "$lib/icons/GooLoadSpin.svelte"; import GooLoadSpin from "$lib/icons/GooLoadSpin.svelte";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
let loading = false; let loading = false;
let data: Root; let data: Root;
let tabId = 0; let tabId = 0;
state.subscribe((st) => { state.subscribe((st) => {
data = (st as VigiState).current_data; data = (st as VigiState).current_data;
console.log("ada");
if (!loading) {
tabId = (st as VigiState).current_tab_index;
}
});
if (!loading) { isLoading.subscribe((val) => {
tabId = (st as VigiState).current_tab_index; loading = val;
}
});
isLoading.subscribe((val) => { if (loading) {
loading = val; tabId = -1;
}
if (loading) { });
tabId = -1;
}
});
</script> </script>
<Block className="browser-window"> <Block className="browser-window">
{#if loading} {#if loading}
<div transition:slide> <div transition:slide>
<GooLoadSpin /> <GooLoadSpin />
</div> </div>
{/if} {/if}
<Renderer {data} /> <Renderer {data} />
</Block> </Block>

View file

@ -1,15 +1,21 @@
<script lang="ts"> <script lang="ts">
import type { Body } from "@txtdot/dalet"; import type { Body } from "@txtdot/dalet";
import TagRenderer from "./TagRenderer.svelte"; import TagRenderer from "./TagRenderer.svelte";
import Element from "./tags/Element.svelte";
export let body: Body; export let body: Body;
export let ifNull: any = undefined;
</script> </script>
{#if typeof body === "string"} {#if typeof body === "string"}
<Element {body} /> {#each body.split("\n") as line}
{line} <br />
{/each}
{:else if body !== null} {:else if body !== null}
{#each body as tag} {#each body as tag}
<TagRenderer {tag} /> <TagRenderer {tag} />
{/each} {/each}
{/if}
{#if body === null && ifNull}
{ifNull}
{/if} {/if}

View file

@ -1,10 +1,43 @@
<script lang="ts"> <script lang="ts">
import type { Tag } from "@txtdot/dalet"; import type { Tag } from "@txtdot/dalet";
import Element from "./tags/Element.svelte"; import Element from "./tags/Element.svelte";
import Heading from "./tags/Heading.svelte";
import Paragraph from "./tags/Paragraph.svelte";
import LineBreak from "./tags/LineBreak.svelte";
import UnorderedList from "./tags/UnorderedList.svelte";
import Link from "./tags/Link.svelte";
import NavLink from "./tags/NavLink.svelte";
import Button from "./tags/Button.svelte";
import NavButton from "./tags/NavButton.svelte";
import Blockquote from "./tags/Blockquote.svelte";
import Code from "./tags/Code.svelte";
import Pre from "./tags/Pre.svelte";
export let tag: Tag; export let tag: Tag;
</script> </script>
{#if tag.id === 0} {#if tag.id === 0}
<Element body={tag.body} /> <Element {tag} />
{:else if tag.id === 1}
<Heading {tag} />
{:else if tag.id === 2}
<Paragraph {tag} />
{:else if tag.id === 3}
<LineBreak />
{:else if tag.id === 4}
<UnorderedList {tag} />
{:else if tag.id === 7}
<Link {tag} />
{:else if tag.id === 8}
<NavLink {tag} />
{:else if tag.id === 9}
<Button {tag} />
{:else if tag.id === 10}
<NavButton {tag} />
{:else if tag.id === 18}
<Blockquote {tag} />
{:else if tag.id === 28}
<Code {tag} />
{:else if tag.id === 29}
<Pre {tag} />
{/if} {/if}

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
</script>
<div class="bq">
<BodyRenderer body={tag.body} />
</div>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
</script>
<button class="btn">
<BodyRenderer body={tag.body} ifNull={tag.argument} />
</button>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
export let body = tag.body as string;
export let argument = tag.argument as string | undefined;
</script>
<pre class={`pre code${argument ? ` lang-${argument}` : ""}`}><code>{body}</code
></pre>

View file

@ -1,16 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { Body, Tag } from "@txtdot/dalet"; import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte"; import BodyRenderer from "../BodyRenderer.svelte";
export let body: Body; export let tag: Tag;
</script> </script>
<section class="el"> <section class="el">
{#if typeof body === "string"} <BodyRenderer body={tag.body} />
{#each body.split("\n") as line}
{line} <br />
{/each}
{:else}
<BodyRenderer {body} />
{/if}
</section> </section>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { Argument, Body, Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
let body = tag.body as string;
let argument = tag.argument as number | null;
</script>
{#if argument === 2}
<h2 class="h2">{body}</h2>
{:else if argument === 3}
<h3 class="h3">{body}</h3>
{:else if argument === 4}
<h4 class="h4">{body}</h4>
{:else if argument === 5}
<h5 class="h5">{body}</h5>
{:else if argument === 6}
<h6 class="h6">{body}</h6>
{:else}
<h1 class="h1">{body}</h1>
{/if}

View file

@ -0,0 +1 @@
<br />

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
</script>
<button class="link">
<BodyRenderer body={tag.body} ifNull={tag.argument} />
</button>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
</script>
<button class="navbtn">
<BodyRenderer body={tag.body} ifNull={tag.argument} />
</button>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
</script>
<button class="navlink">
<BodyRenderer body={tag.body} ifNull={tag.argument} />
</button>

View file

@ -0,0 +1,10 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
</script>
<p class="p">
<BodyRenderer body={tag.body} />
</p>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
export let tag: Tag;
export let body = tag.body as string;
</script>
<pre class="pre">{body}</pre>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { Tag } from "@txtdot/dalet";
import BodyRenderer from "../BodyRenderer.svelte";
import TagRenderer from "../TagRenderer.svelte";
export let tag: Tag;
let body = tag.body as Tag[];
</script>
<ul class="ul">
{#each body as tag}
<li class="ul-li"><TagRenderer {tag} /></li>
{/each}
</ul>

65
src/tags.css Normal file
View file

@ -0,0 +1,65 @@
.headings {
@apply font-bold my-4;
}
.h1 {
@apply text-3xl headings;
}
.h2 {
@apply text-2xl headings;
}
.h3 {
@apply text-xl headings;
}
.h4 {
@apply text-lg headings;
}
.h5 {
@apply text-base headings;
}
.h6 {
@apply text-sm headings;
}
.p {
@apply my-3;
}
.btn,
.navbtn {
@apply p-1 rounded-lg;
@apply ease-out duration-150;
@apply hover:color-vigi-90;
@apply cursor-pointer;
@apply color-vigi-70 active:color-vigi-100;
}
.link,
.navlink {
@apply underline font-semibold;
/* @apply text-bg-vigi-90 hover:text-bg-vigi-95 active:text-bg-vigi-100; */
@apply text-vigi-30 hover:text-vigi-20 active:text-vigi-0;
}
.ul {
@apply list-disc list-inside ms-3;
}
.pre {
@apply font-mono;
}
.code {
@apply color-vigi-50 p-4 rounded-xl overflow-auto;
}
.bq {
@apply color-vigi-50 p-4 rounded-xl border-s-4 text-wrap;
}

View file

@ -13,10 +13,31 @@ export default {
plugins: [ plugins: [
plugin(({ addUtilities }) => { plugin(({ addUtilities }) => {
let utilities = {}; let utilities = {};
for (let i = 0; i <= 100; i += 5) { for (let i = 0; i <= 100; i += 5) {
let text = `color-mix(in var(--colorspace), var(--max-text) ${i}%, var(--min-text))`;
let bg = `color-mix(in var(--colorspace), var(--max-bg) ${i}%, var(--min-bg))`;
utilities[`.color-vigi-${i}`] = { utilities[`.color-vigi-${i}`] = {
color: `color-mix(in var(--colorspace), var(--max-text) ${i}%, var(--min-text))`, color: text,
"background-color": `color-mix(in var(--colorspace), var(--max-bg) ${i}%, var(--min-bg))`, "background-color": bg,
"border-color": text,
};
utilities[`.text-vigi-${i}`] = {
color: text,
};
utilities[`.text-bg-vigi-${i}`] = {
color: bg,
};
utilities[`.bg-vigi-${i}`] = {
"background-color": bg,
};
utilities[`.bg-text-vigi-${i}`] = {
"background-color": text,
}; };
} }