mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-27 12:26:25 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 693768ffc6 | |||
| 98794fa031 | |||
| 4092511f22 |
@@ -4,6 +4,7 @@ import { Banner, VStack } from "@yaakapp-internal/ui";
|
||||
import { useState } from "react";
|
||||
import { openWorkspaceFromSyncDir } from "../commands/openWorkspaceFromSyncDir";
|
||||
import { appInfo } from "../lib/appInfo";
|
||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||
import { showErrorToast } from "../lib/toast";
|
||||
import { Button } from "./core/Button";
|
||||
import { Checkbox } from "./core/Checkbox";
|
||||
@@ -89,6 +90,10 @@ export function CloneGitRepositoryDialog({ hide }: Props) {
|
||||
</Banner>
|
||||
)}
|
||||
|
||||
<CommercialUseBanner source="git-clone" title="Using Git for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
<PlainInput
|
||||
required
|
||||
label="Repository URL"
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import type { LicenseCheckStatus } from "@yaakapp-internal/license";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useKeyValue } from "../hooks/useKeyValue";
|
||||
import { appInfo } from "../lib/appInfo";
|
||||
import { pricingUrl } from "../lib/pricingUrl";
|
||||
import { DismissibleBanner } from "./core/DismissibleBanner";
|
||||
|
||||
const COMMERCIAL_USE_SNOOZE_DAYS = 7;
|
||||
|
||||
export function CommercialUseBanner({
|
||||
children,
|
||||
source,
|
||||
title,
|
||||
}: {
|
||||
children: string;
|
||||
source: string;
|
||||
title: string;
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const {
|
||||
isLoading: isSnoozeLoading,
|
||||
set: setSnoozedAt,
|
||||
value: snoozedAt,
|
||||
} = useKeyValue<string | null>({
|
||||
namespace: "global",
|
||||
key: "commercial-use-banner-snoozed-at",
|
||||
fallback: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let canceled = false;
|
||||
|
||||
shouldShowCommercialUsePrompt()
|
||||
.then((shouldShow) => {
|
||||
if (!canceled) setVisible(shouldShow);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [source]);
|
||||
|
||||
if (
|
||||
!visible ||
|
||||
isSnoozeLoading ||
|
||||
isWithinDays(snoozedAt, COMMERCIAL_USE_SNOOZE_DAYS)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<DismissibleBanner
|
||||
id={`commercial-use:${source}`}
|
||||
color="info"
|
||||
className="w-full"
|
||||
onDismiss={() => setSnoozedAt(new Date().toISOString())}
|
||||
actions={[
|
||||
{
|
||||
label: "View plans",
|
||||
color: "info",
|
||||
variant: "solid",
|
||||
onClick: () => {
|
||||
openCommercialUsePricing(source).catch(console.error);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="text-sm">
|
||||
<p className="font-semibold text-text">{title}</p>
|
||||
<p className="mt-0.5 text-text-subtle">{children}</p>
|
||||
</div>
|
||||
</DismissibleBanner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function shouldShowCommercialUsePrompt(): Promise<boolean> {
|
||||
// Open-source builds omit the Rust license plugin, so never show commercial-use prompts there.
|
||||
if (appInfo.featureLicense !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const license = await invoke<LicenseCheckStatus>("plugin:yaak-license|check");
|
||||
return license.status !== "active" && license.status !== "trialing";
|
||||
} catch (err) {
|
||||
console.log("Failed to check license before commercial-use prompt", err);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function openCommercialUsePricing(source: string): Promise<void> {
|
||||
await openUrl(pricingUrl(`app.commercial-use.${source}`)).catch(console.error);
|
||||
}
|
||||
|
||||
function isWithinDays(date: string | null, days: number): boolean {
|
||||
if (date == null) return false;
|
||||
|
||||
const time = new Date(date).getTime();
|
||||
if (Number.isNaN(time)) return false;
|
||||
|
||||
return Date.now() - time < days * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import slugify from "slugify";
|
||||
import { activeWorkspaceAtom } from "../hooks/useActiveWorkspace";
|
||||
import { pluralizeCount } from "../lib/pluralize";
|
||||
import { invokeCmd } from "../lib/tauri";
|
||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||
import { Button } from "./core/Button";
|
||||
import { Checkbox } from "./core/Checkbox";
|
||||
import { DetailsBanner } from "./core/DetailsBanner";
|
||||
@@ -85,8 +86,12 @@ function ExportDataDialogContent({
|
||||
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||
const noneSelected = numSelected === 0;
|
||||
return (
|
||||
<div className="w-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||
<div className="h-full w-full grid grid-rows-[minmax(0,1fr)_auto] overflow-hidden rounded-b-lg">
|
||||
<VStack space={3} className="overflow-auto px-5 pb-6">
|
||||
<CommercialUseBanner source="data-export" title="Exporting work data?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -137,9 +142,9 @@ function ExportDataDialogContent({
|
||||
/>
|
||||
</DetailsBanner>
|
||||
</VStack>
|
||||
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface-highlight py-2 border-t border-border-subtle">
|
||||
<footer className="px-5 grid grid-cols-[1fr_auto] items-center bg-surface py-3 border-t border-border-subtle">
|
||||
<div>
|
||||
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtle">
|
||||
<Link href="https://yaak.app/button/new" noUnderline className="text-text-subtlest">
|
||||
Create Run Button
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { VStack } from "@yaakapp-internal/ui";
|
||||
import { useState } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { CommercialUseBanner } from "./CommercialUseBanner";
|
||||
import { Button } from "./core/Button";
|
||||
import { SelectFile } from "./SelectFile";
|
||||
|
||||
@@ -14,6 +15,10 @@ export function ImportDataDialog({ importData }: Props) {
|
||||
|
||||
return (
|
||||
<VStack space={5} className="pb-4">
|
||||
<CommercialUseBanner source="data-import" title="Importing work data?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
<VStack space={1}>
|
||||
<ul className="list-disc pl-5">
|
||||
<li>OpenAPI 3.0, 3.1</li>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Heading, HStack, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useRef } from "react";
|
||||
import { showConfirmDelete } from "../../lib/confirm";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import { Button } from "../core/Button";
|
||||
import { Checkbox } from "../core/Checkbox";
|
||||
import { DetailsBanner } from "../core/DetailsBanner";
|
||||
@@ -232,6 +233,10 @@ export function SettingsCertificates() {
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<CommercialUseBanner source="client-certificates" title="Using certificates for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
|
||||
{certificates.length > 0 && (
|
||||
<VStack space={3}>
|
||||
{certificates.map((cert, index) => (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../../lib/requestSettings";
|
||||
import { revealInFinderText } from "../../lib/reveal";
|
||||
import { CargoFeature } from "../CargoFeature";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import { IconButton } from "../core/IconButton";
|
||||
import {
|
||||
ModelSettingRowBoolean,
|
||||
@@ -38,10 +39,15 @@ export function SettingsGeneral() {
|
||||
|
||||
return (
|
||||
<VStack space={1.5} className="mb-4">
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<Heading>General</Heading>
|
||||
<p className="text-text-subtle">Configure general settings for update behavior and more.</p>
|
||||
</div>
|
||||
<div className="mt-3 mb-5">
|
||||
<CommercialUseBanner source="settings-general" title="Using Yaak for work?">
|
||||
A Yaak license is required for commercial use and helps support future development.
|
||||
</CommercialUseBanner>
|
||||
</div>
|
||||
<SettingsList className="space-y-8">
|
||||
<CargoFeature feature="updater">
|
||||
<SettingsSection title="Updates">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useAtomValue } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||
import { showConfirm } from "../../lib/confirm";
|
||||
import { pricingUrl } from "../../lib/pricingUrl";
|
||||
import { invokeCmd } from "../../lib/tauri";
|
||||
import { CargoFeature } from "../CargoFeature";
|
||||
import { Button } from "../core/Button";
|
||||
@@ -252,7 +253,9 @@ function LicenseSettings({ settings }: { settings: Settings }) {
|
||||
</p>
|
||||
<p>
|
||||
Licenses help keep Yaak independent and sustainable.{" "}
|
||||
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
||||
<Link href={pricingUrl("app.license.badge-hide-confirm")}>
|
||||
Purchase a License →
|
||||
</Link>
|
||||
</p>
|
||||
</VStack>
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { formatDate } from "date-fns/format";
|
||||
import { useState } from "react";
|
||||
import { useToggle } from "../../hooks/useToggle";
|
||||
import { pluralizeCount } from "../../lib/pluralize";
|
||||
import { pricingUrl } from "../../lib/pricingUrl";
|
||||
import { CargoFeature } from "../CargoFeature";
|
||||
import { Button } from "../core/Button";
|
||||
import { Link } from "../core/Link";
|
||||
@@ -48,7 +49,7 @@ function SettingsLicenseCmp() {
|
||||
<span className="opacity-50">Personal use is always free, forever.</span>
|
||||
<Separator className="my-2" />
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
|
||||
<Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}>
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
@@ -68,7 +69,7 @@ function SettingsLicenseCmp() {
|
||||
</span>
|
||||
<Separator className="my-2" />
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
|
||||
<Link noUnderline href={pricingUrl(`app.license.learn.${check.data.status}`)}>
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
@@ -134,7 +135,7 @@ function SettingsLicenseCmp() {
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => openUrl("https://yaak.app/dashboard?s=support&ref=app.yaak.desktop")}
|
||||
onClick={() => openUrl("https://yaak.app/dashboard?intent=app.license.support")}
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
>
|
||||
Direct Support
|
||||
@@ -150,9 +151,7 @@ function SettingsLicenseCmp() {
|
||||
color="primary"
|
||||
rightSlot={<Icon icon="external_link" />}
|
||||
onClick={() =>
|
||||
openUrl(
|
||||
`https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop&t=${check.data?.status ?? ""}`,
|
||||
)
|
||||
openUrl(pricingUrl(`app.license.purchase.${check.data?.status ?? "unknown"}`))
|
||||
}
|
||||
>
|
||||
Purchase License
|
||||
|
||||
@@ -2,6 +2,7 @@ import { patchModel, settingsAtom } from "@yaakapp-internal/models";
|
||||
import type { ProxySetting } from "@yaakapp-internal/models";
|
||||
import { Heading, InlineCode, VStack } from "@yaakapp-internal/ui";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import {
|
||||
SettingRowBoolean,
|
||||
SettingRowSelect,
|
||||
@@ -33,6 +34,9 @@ export function SettingsProxy() {
|
||||
traffic, or routing through specific infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
<CommercialUseBanner source="proxy-settings" title="Using a proxy for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
<SettingsList className="space-y-8">
|
||||
<SettingsSection title="Proxy">
|
||||
<SettingRowSelect
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useExportData } from "../hooks/useExportData";
|
||||
import { appInfo } from "../lib/appInfo";
|
||||
import { showDialog } from "../lib/dialog";
|
||||
import { importData } from "../lib/importData";
|
||||
import { pricingUrl } from "../lib/pricingUrl";
|
||||
import type { DropdownRef } from "./core/Dropdown";
|
||||
import { Dropdown } from "./core/Dropdown";
|
||||
import { Icon } from "@yaakapp-internal/ui";
|
||||
@@ -76,7 +77,8 @@ export function SettingsDropdown() {
|
||||
hidden: check.data == null || check.data.status === "active",
|
||||
leftSlot: <Icon icon="circle_dollar_sign" />,
|
||||
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
|
||||
onSelect: () => openUrl("https://yaak.app/pricing"),
|
||||
onSelect: () =>
|
||||
openUrl(pricingUrl(`app.menu.purchase.${check.data?.status ?? "unknown"}`)),
|
||||
},
|
||||
{
|
||||
label: "Install CLI",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseBulkPairLine } from "./BulkPairEditor";
|
||||
|
||||
describe("parseBulkPairLine", () => {
|
||||
test("parses colon-space pairs as name and value", () => {
|
||||
expect(parseBulkPairLine("foo: bar")).toMatchObject({
|
||||
enabled: true,
|
||||
name: "foo",
|
||||
value: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves colon-without-space lines as a name with an empty value", () => {
|
||||
expect(parseBulkPairLine("foo:bar")).toMatchObject({
|
||||
enabled: true,
|
||||
name: "foo:bar",
|
||||
value: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("preserves malformed lines instead of dropping their contents", () => {
|
||||
expect(parseBulkPairLine("not a pair")).toMatchObject({
|
||||
enabled: true,
|
||||
name: "not a pair",
|
||||
value: "",
|
||||
});
|
||||
});
|
||||
|
||||
test("unescapes newlines in parsed values", () => {
|
||||
expect(parseBulkPairLine("foo: bar\\nbaz")).toMatchObject({
|
||||
enabled: true,
|
||||
name: "foo",
|
||||
value: "bar\nbaz",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ export function BulkPairEditor({
|
||||
const pairsText = useMemo(() => {
|
||||
return pairs
|
||||
.filter((p) => !(p.name.trim() === "" && p.value.trim() === ""))
|
||||
.map(formatBulkPairLine)
|
||||
.map(pairToLine)
|
||||
.join("\n");
|
||||
}, [pairs]);
|
||||
|
||||
@@ -26,7 +26,7 @@ export function BulkPairEditor({
|
||||
const pairs = text
|
||||
.split("\n")
|
||||
.filter((l: string) => l.trim())
|
||||
.map(parseBulkPairLine);
|
||||
.map(lineToPair);
|
||||
onChange(pairs);
|
||||
},
|
||||
[onChange],
|
||||
@@ -47,16 +47,16 @@ export function BulkPairEditor({
|
||||
);
|
||||
}
|
||||
|
||||
export function formatBulkPairLine(pair: Pair) {
|
||||
function pairToLine(pair: Pair) {
|
||||
const value = pair.value.replaceAll("\n", "\\n");
|
||||
return `${pair.name}: ${value}`;
|
||||
}
|
||||
|
||||
export function parseBulkPairLine(line: string): PairWithId {
|
||||
const [, name, value] = line.match(/^([^:]+):\s+(.*)$/) ?? [];
|
||||
function lineToPair(line: string): PairWithId {
|
||||
const [, name, value] = line.match(/^(:?[^:]+):\s+(.*)$/) ?? [];
|
||||
return {
|
||||
enabled: true,
|
||||
name: (name ?? line).trim(),
|
||||
name: (name ?? "").trim(),
|
||||
value: (value ?? "").replaceAll("\\n", "\n").trim(),
|
||||
id: generateId(),
|
||||
};
|
||||
|
||||
@@ -1,57 +1,73 @@
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import type { BannerProps } from "@yaakapp-internal/ui";
|
||||
import { Banner, HStack } from "@yaakapp-internal/ui";
|
||||
import { Banner } from "@yaakapp-internal/ui";
|
||||
import classNames from "classnames";
|
||||
import { useKeyValue } from "../../hooks/useKeyValue";
|
||||
import type { ButtonProps } from "./Button";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function DismissibleBanner({
|
||||
children,
|
||||
className,
|
||||
id,
|
||||
onDismiss,
|
||||
actions,
|
||||
...props
|
||||
}: BannerProps & {
|
||||
id: string;
|
||||
actions?: { label: string; onClick: () => void; color?: Color }[];
|
||||
onDismiss?: () => void | Promise<void>;
|
||||
actions?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
color?: Color;
|
||||
variant?: ButtonProps["variant"];
|
||||
}[];
|
||||
}) {
|
||||
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
|
||||
const {
|
||||
isLoading,
|
||||
set: setDismissed,
|
||||
value: dismissed,
|
||||
} = useKeyValue<boolean>({
|
||||
namespace: "global",
|
||||
key: ["dismiss-banner", id],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
if (dismissed) return null;
|
||||
if (isLoading || dismissed) return null;
|
||||
|
||||
return (
|
||||
<Banner
|
||||
className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<HStack space={1.5}>
|
||||
{actions?.map((a) => (
|
||||
<Button
|
||||
key={a.label}
|
||||
variant="border"
|
||||
color={a.color ?? props.color}
|
||||
size="xs"
|
||||
onClick={a.onClick}
|
||||
title={a.label}
|
||||
>
|
||||
{a.label}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="border"
|
||||
color={props.color}
|
||||
size="xs"
|
||||
onClick={() => setDismissed((d) => !d)}
|
||||
title="Dismiss message"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
</HStack>
|
||||
<Banner className={classNames(className, "relative")} {...props}>
|
||||
<div className="@container">
|
||||
<div className="grid gap-2 @[34rem]:grid-cols-[minmax(0,1fr)_auto] @[34rem]:items-center @[34rem]:gap-3">
|
||||
{children}
|
||||
<div className="flex flex-wrap gap-1.5 @[34rem]:justify-end">
|
||||
<Button
|
||||
variant="border"
|
||||
color={props.color}
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
setDismissed(true).catch(console.error);
|
||||
Promise.resolve(onDismiss?.()).catch(console.error);
|
||||
}}
|
||||
title="Dismiss message"
|
||||
>
|
||||
Dismiss
|
||||
</Button>
|
||||
{actions?.map((a) => (
|
||||
<Button
|
||||
key={a.label}
|
||||
variant={a.variant ?? "border"}
|
||||
color={a.color ?? props.color}
|
||||
size="xs"
|
||||
onClick={a.onClick}
|
||||
title={a.label}
|
||||
>
|
||||
{a.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Banner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@top pairs { (Key Sep Value "\n")* }
|
||||
|
||||
@tokens {
|
||||
Sep { ":" $[ \t]+ }
|
||||
Sep { ":" }
|
||||
Key { ":"? ![:]+ }
|
||||
Value { ![\n]+ }
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parser } from "./pairs";
|
||||
|
||||
function getNodeNames(input: string): string[] {
|
||||
const tree = parser.parse(input);
|
||||
const nodes: string[] = [];
|
||||
const cursor = tree.cursor();
|
||||
do {
|
||||
if (cursor.name !== "pairs") {
|
||||
nodes.push(cursor.name);
|
||||
}
|
||||
} while (cursor.next());
|
||||
return nodes;
|
||||
}
|
||||
|
||||
describe("pairs grammar", () => {
|
||||
test("parses colon-space pairs with a value", () => {
|
||||
expect(getNodeNames("foo: bar\n")).toEqual(["Key", "Sep", "Value"]);
|
||||
});
|
||||
|
||||
test("does not parse colon-without-space as a value", () => {
|
||||
const nodes = getNodeNames("foo:bar\n");
|
||||
|
||||
expect(nodes).not.toContain("Value");
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ export const parser = LRParser.deserialize({
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData:
|
||||
"%]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#tYSSOXhXY$dYZ!UZphpq$dq![h![!]!m!];'Sh;'S;=`#U<%lOhV$mYQPRQSSOXhXY$dYZ!UZphpq$dq![h![!]!m!];'Sh;'S;=`#U<%lOh",
|
||||
"$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh",
|
||||
tokenizers: [0, 1, 2],
|
||||
topRules: { pairs: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { resolvedModelName } from "../../lib/resolvedModelName";
|
||||
import { showConfirm } from "../../lib/confirm";
|
||||
import { showErrorToast } from "../../lib/toast";
|
||||
import { sync } from "../../init/sync";
|
||||
import { CommercialUseBanner } from "../CommercialUseBanner";
|
||||
import { Button } from "../core/Button";
|
||||
import type { CheckboxProps } from "../core/Checkbox";
|
||||
import { Checkbox } from "../core/Checkbox";
|
||||
@@ -205,7 +206,10 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) {
|
||||
layout="horizontal"
|
||||
defaultRatio={0.6}
|
||||
firstSlot={({ style }) => (
|
||||
<div style={style} className="h-full px-4">
|
||||
<div style={style} className="h-full px-4 grid grid-rows-[auto_minmax(0,1fr)] gap-3">
|
||||
<CommercialUseBanner source="git-commit" title="Using Git for work?">
|
||||
A Yaak license is required for commercial use and helps support features like this.
|
||||
</CommercialUseBanner>
|
||||
<SplitLayout
|
||||
storageKey="commit-vertical"
|
||||
layout="vertical"
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function addGitRemote(dir: string, defaultName?: string): Promise<G
|
||||
title: "Add Remote",
|
||||
inputs: [
|
||||
{ type: "text", label: "Name", name: "name", defaultValue: defaultName },
|
||||
{ type: "text", label: "URL", name: "url" },
|
||||
{ type: "text", label: "URL", name: "url", placeholder: "git@github.com:org/repo.git" },
|
||||
],
|
||||
});
|
||||
if (r == null) throw new Error("Cancelled remote prompt");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Appearance } from "@yaakapp-internal/theme";
|
||||
import { getCSSAppearance, subscribeToPreferredAppearance } from "@yaakapp-internal/theme";
|
||||
import type { Appearance } from "../lib/theme/appearance";
|
||||
import { getCSSAppearance, subscribeToPreferredAppearance } from "../lib/theme/appearance";
|
||||
|
||||
export function usePreferredAppearance() {
|
||||
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(getCSSAppearance());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { settingsAtom } from "@yaakapp-internal/models";
|
||||
import { resolveAppearance } from "@yaakapp-internal/theme";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { resolveAppearance } from "../lib/theme/appearance";
|
||||
import { usePreferredAppearance } from "./usePreferredAppearance";
|
||||
|
||||
export function useResolvedAppearance() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { settingsAtom } from "@yaakapp-internal/models";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { getResolvedTheme, getThemes } from "../lib/themes";
|
||||
import { getResolvedTheme, getThemes } from "../lib/theme/themes";
|
||||
import { usePluginsKey } from "./usePlugins";
|
||||
import { usePreferredAppearance } from "./usePreferredAppearance";
|
||||
|
||||
|
||||
@@ -44,19 +44,6 @@ export function initGlobalListeners() {
|
||||
color: "danger",
|
||||
timeout: null,
|
||||
message: `Failed to load plugin "${name}": ${err}`,
|
||||
action: ({ hide }) => (
|
||||
<Button
|
||||
size="xs"
|
||||
color="danger"
|
||||
variant="border"
|
||||
onClick={() => {
|
||||
hide();
|
||||
openSettings.mutate("plugins:installed");
|
||||
}}
|
||||
>
|
||||
Manage Plugins
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function pricingUrl(intent: string): string {
|
||||
return `https://yaak.app/pricing?intent=${encodeURIComponent(intent)}`;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type { Appearance } from "@yaakapp-internal/theme";
|
||||
export {
|
||||
getCSSAppearance,
|
||||
getWindowAppearance,
|
||||
resolveAppearance,
|
||||
subscribeToPreferredAppearance,
|
||||
subscribeToWindowAppearanceChange,
|
||||
} from "@yaakapp-internal/theme";
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { GetThemesResponse } from "@yaakapp-internal/plugins";
|
||||
import {
|
||||
defaultDarkTheme,
|
||||
defaultLightTheme,
|
||||
resolveAppearance,
|
||||
type Appearance,
|
||||
} from "@yaakapp-internal/theme";
|
||||
import { invokeCmd } from "./tauri";
|
||||
import { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme";
|
||||
import { invokeCmd } from "../tauri";
|
||||
import type { Appearance } from "./appearance";
|
||||
import { resolveAppearance } from "./appearance";
|
||||
|
||||
export { defaultDarkTheme, defaultLightTheme } from "@yaakapp-internal/theme";
|
||||
|
||||
export async function getThemes() {
|
||||
const themes = (await invokeCmd<GetThemesResponse[]>("cmd_get_themes")).flatMap((t) => t.themes);
|
||||
@@ -0,0 +1,9 @@
|
||||
export type { YaakColorKey, YaakColors, YaakTheme } from "@yaakapp-internal/theme";
|
||||
export {
|
||||
addThemeStylesToDocument,
|
||||
applyThemeToDocument,
|
||||
completeTheme,
|
||||
getThemeCSS,
|
||||
indent,
|
||||
setThemeOnDocument,
|
||||
} from "@yaakapp-internal/theme";
|
||||
@@ -0,0 +1 @@
|
||||
export { YaakColor } from "@yaakapp-internal/theme";
|
||||
@@ -2,14 +2,11 @@ import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { setWindowTheme } from "@yaakapp-internal/mac-window";
|
||||
import type { ModelPayload } from "@yaakapp-internal/models";
|
||||
import type { Appearance } from "@yaakapp-internal/theme";
|
||||
import {
|
||||
applyThemeToDocument,
|
||||
getCSSAppearance,
|
||||
subscribeToPreferredAppearance,
|
||||
} from "@yaakapp-internal/theme";
|
||||
import { getSettings } from "./lib/settings";
|
||||
import { getResolvedTheme } from "./lib/themes";
|
||||
import type { Appearance } from "./lib/theme/appearance";
|
||||
import { getCSSAppearance, subscribeToPreferredAppearance } from "./lib/theme/appearance";
|
||||
import { getResolvedTheme } from "./lib/theme/themes";
|
||||
import { applyThemeToDocument } from "@yaakapp-internal/theme";
|
||||
|
||||
// NOTE: CSS appearance isn't as accurate as getting it async from the window (next step), but we want
|
||||
// a good appearance guess so we're not waiting too long
|
||||
|
||||
@@ -54,7 +54,7 @@ use yaak_plugins::events::{
|
||||
InternalEventPayload, JsonPrimitive, PluginContext, RenderPurpose, ShowToastRequest,
|
||||
};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::plugin_meta::{PluginMetadata, get_plugin_meta};
|
||||
use yaak_plugins::plugin_meta::PluginMetadata;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_sse::sse::ServerSentEvent;
|
||||
use yaak_tauri_utils::window::WorkspaceWindowTrait;
|
||||
@@ -1512,36 +1512,11 @@ async fn cmd_plugin_info<R: Runtime>(
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> YaakResult<PluginMetadata> {
|
||||
let plugin = app_handle.db().get_plugin(id)?;
|
||||
if let Some(plugin_handle) = plugin_manager
|
||||
Ok(plugin_manager
|
||||
.get_plugin_by_dir(plugin.directory.as_str())
|
||||
.await
|
||||
{
|
||||
return Ok(plugin_handle.info());
|
||||
}
|
||||
|
||||
if let Ok(metadata) = get_plugin_meta(&PathBuf::from(&plugin.directory)) {
|
||||
return Ok(metadata);
|
||||
}
|
||||
|
||||
Ok(fallback_plugin_metadata(&plugin.directory))
|
||||
}
|
||||
|
||||
fn fallback_plugin_metadata(directory: &str) -> PluginMetadata {
|
||||
let display_name = PathBuf::from(directory)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or(directory)
|
||||
.to_string();
|
||||
|
||||
PluginMetadata {
|
||||
version: "Unavailable".to_string(),
|
||||
name: directory.to_string(),
|
||||
display_name,
|
||||
description: Some(format!("Plugin metadata could not be loaded from {directory}")),
|
||||
homepage_url: None,
|
||||
repository_url: None,
|
||||
}
|
||||
.ok_or(GenericError("Failed to find plugin for info".to_string()))?
|
||||
.info())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::{PluginVersion, download_plugin_archive, get_plugin};
|
||||
use crate::checksum::compute_checksum;
|
||||
use crate::error::Error::{PluginErr, PluginNotFoundErr};
|
||||
use crate::error::Error::PluginErr;
|
||||
use crate::error::Result;
|
||||
use crate::events::PluginContext;
|
||||
use crate::manager::PluginManager;
|
||||
@@ -29,14 +29,7 @@ pub async fn delete_and_uninstall(
|
||||
let db = query_manager.connect();
|
||||
db.delete_plugin_by_id(plugin_id, &update_source)?
|
||||
};
|
||||
if let Err(err) = plugin_manager
|
||||
.uninstall(plugin_context, plugin.directory.as_str())
|
||||
.await
|
||||
{
|
||||
if !matches!(err, PluginNotFoundErr(_)) {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
plugin_manager.uninstall(plugin_context, plugin.directory.as_str()).await?;
|
||||
Ok(plugin)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,6 @@ export type { DocumentPlatform, YaakColorKey, YaakColors, YaakTheme } from "./wi
|
||||
export {
|
||||
addThemeStylesToDocument,
|
||||
applyThemeToDocument,
|
||||
completeColorVariables,
|
||||
completeFullColorVariables,
|
||||
completePartialColorVariables,
|
||||
completeTheme,
|
||||
getThemeCSS,
|
||||
indent,
|
||||
|
||||
+107
-126
@@ -47,10 +47,18 @@ export type YaakTheme = {
|
||||
export type YaakColorKey = keyof ThemeComponentColors;
|
||||
export type DocumentPlatform = "linux" | "macos" | "windows" | "unknown";
|
||||
|
||||
type ComponentName = keyof NonNullable<Theme["components"]>;
|
||||
type ComponentName = keyof NonNullable<YaakTheme["components"]>;
|
||||
type CSSVariables = Record<YaakColorKey, string | undefined>;
|
||||
|
||||
export function completeFullColorVariables(theme: Theme, cmp: Partial<CSSVariables>): CSSVariables {
|
||||
function themeVariables(
|
||||
theme: Theme,
|
||||
component?: ComponentName,
|
||||
base?: CSSVariables,
|
||||
): CSSVariables | null {
|
||||
const cmp =
|
||||
component == null
|
||||
? theme.base
|
||||
: (theme.components?.[component] ?? ({} as ThemeComponentColors));
|
||||
const color = (value: string | undefined) => yc(theme, value);
|
||||
const vars: CSSVariables = {
|
||||
surface: cmp.surface,
|
||||
@@ -58,12 +66,12 @@ export function completeFullColorVariables(theme: Theme, cmp: Partial<CSSVariabl
|
||||
surfaceActive: cmp.surfaceActive ?? color(cmp.primary)?.lower(0.2).translucify(0.8).css(),
|
||||
backdrop: cmp.backdrop ?? color(cmp.surface)?.lower(0.2).translucify(0.2).css(),
|
||||
selection: cmp.selection ?? color(cmp.primary)?.lower(0.1).translucify(0.7).css(),
|
||||
border: cmp.border,
|
||||
borderSubtle: cmp.borderSubtle,
|
||||
borderFocus: cmp.borderFocus ?? color(cmp.info)?.translucify(0.5)?.css(),
|
||||
border: cmp.border ?? color(cmp.surface)?.lift(0.11)?.css(),
|
||||
borderSubtle: cmp.borderSubtle ?? color(cmp.border)?.lower(0.06)?.css(),
|
||||
borderFocus: color(cmp.info)?.translucify(0.5)?.css(),
|
||||
text: cmp.text,
|
||||
textSubtle: cmp.textSubtle,
|
||||
textSubtlest: cmp.textSubtlest,
|
||||
textSubtle: cmp.textSubtle ?? color(cmp.text)?.lower(0.2)?.css(),
|
||||
textSubtlest: cmp.textSubtlest ?? color(cmp.text)?.lower(0.3)?.css(),
|
||||
shadow:
|
||||
cmp.shadow ??
|
||||
YaakColor.black()
|
||||
@@ -78,126 +86,96 @@ export function completeFullColorVariables(theme: Theme, cmp: Partial<CSSVariabl
|
||||
danger: cmp.danger,
|
||||
};
|
||||
|
||||
const themeColor = (value: string) => new YaakColor(value, theme.dark ? "dark" : "light");
|
||||
const themeSurface = themeColor(theme.dark ? "oklch(23% 0 0)" : "oklch(100% 0 0)");
|
||||
const surface = themeColor(vars.surface ?? themeSurface.css());
|
||||
const reference = surface.compositeOver(themeSurface);
|
||||
const seed = themeColor(vars.surface ?? vars.surfaceHighlight ?? vars.border ?? surface.css());
|
||||
const textBase = seed.desaturate(0.6).opacify(1);
|
||||
const borderBase = seed.opacify(1);
|
||||
const text = vars.text ?? textBase.withContrast(reference, 11).css();
|
||||
const textColor = themeColor(text);
|
||||
|
||||
return normalizeColorVariables(theme, {
|
||||
...vars,
|
||||
text,
|
||||
textSubtle: vars.textSubtle ?? textColor.lower(0.2).css(),
|
||||
textSubtlest: vars.textSubtlest ?? textColor.lower(0.4).css(),
|
||||
border: vars.border ?? borderBase.desaturate(0.2).withContrast(reference, 3).css(),
|
||||
borderSubtle:
|
||||
vars.borderSubtle ?? borderBase.desaturate(0.2).withContrast(reference, 1.2).css(),
|
||||
});
|
||||
}
|
||||
|
||||
export function completePartialColorVariables(
|
||||
theme: Theme,
|
||||
cmp: Partial<CSSVariables>,
|
||||
): CSSVariables {
|
||||
const color = (value: string | undefined) => yc(theme, value);
|
||||
const text = color(cmp.text);
|
||||
|
||||
return normalizeColorVariables(theme, {
|
||||
surface: cmp.surface,
|
||||
surfaceHighlight: cmp.surfaceHighlight ?? color(cmp.surface)?.lift(0.06).css(),
|
||||
surfaceActive: cmp.surfaceActive ?? color(cmp.primary)?.lower(0.2).translucify(0.8).css(),
|
||||
backdrop: cmp.backdrop ?? color(cmp.surface)?.lower(0.2).translucify(0.2).css(),
|
||||
selection: cmp.selection ?? color(cmp.primary)?.lower(0.1).translucify(0.7).css(),
|
||||
border: cmp.border ?? color(cmp.surface)?.lift(0.11).css(),
|
||||
borderSubtle: cmp.borderSubtle ?? color(cmp.border)?.lower(0.06).css(),
|
||||
borderFocus: cmp.borderFocus ?? color(cmp.info)?.translucify(0.5).css(),
|
||||
text: cmp.text,
|
||||
textSubtle: cmp.textSubtle ?? text?.lower(0.3).css(),
|
||||
textSubtlest: cmp.textSubtlest ?? text?.lower(0.5).css(),
|
||||
shadow:
|
||||
cmp.shadow ??
|
||||
YaakColor.black()
|
||||
.translucify(theme.dark ? 0.7 : 0.93)
|
||||
.css(),
|
||||
primary: cmp.primary,
|
||||
secondary: cmp.secondary,
|
||||
info: cmp.info,
|
||||
success: cmp.success,
|
||||
notice: cmp.notice,
|
||||
warning: cmp.warning,
|
||||
danger: cmp.danger,
|
||||
});
|
||||
}
|
||||
|
||||
export const completeColorVariables = completeFullColorVariables;
|
||||
|
||||
function normalizeColorVariables(theme: Theme, vars: CSSVariables): CSSVariables {
|
||||
const normalized: CSSVariables = {} as CSSVariables;
|
||||
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
normalized[key as YaakColorKey] = value == null ? undefined : yc(theme, value).css();
|
||||
if (!value && base?.[key as YaakColorKey]) {
|
||||
vars[key as YaakColorKey] = base[key as YaakColorKey];
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
return vars;
|
||||
}
|
||||
|
||||
function templateTagColorVariables(theme: Theme, color: YaakColor): CSSVariables {
|
||||
return completeFullColorVariables(theme, {
|
||||
text: color.liftMax().lower(0.05).css(),
|
||||
textSubtle: color.liftMax().lower(0.08).css(),
|
||||
function templateTagColorVariables(color: YaakColor | null): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
return {
|
||||
text: color.lift(0.7).css(),
|
||||
textSubtle: color.lift(0.4).css(),
|
||||
textSubtlest: color.css(),
|
||||
surface: color.lower(0.2).translucify(0.8).css(),
|
||||
border: color.translucify(0.6).css(),
|
||||
borderSubtle: color.translucify(0.8).css(),
|
||||
surfaceHighlight: color.lower(0.1).translucify(0.7).css(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function toastColorVariables(theme: Theme, color: YaakColor): CSSVariables {
|
||||
return completeFullColorVariables(theme, {
|
||||
function toastColorVariables(color: YaakColor | null): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
return {
|
||||
text: color.lift(0.8).css(),
|
||||
textSubtle: color.lift(0.8).translucify(0.3).css(),
|
||||
surface: color.translucify(0.9).css(),
|
||||
surfaceHighlight: color.translucify(0.8).css(),
|
||||
});
|
||||
border: color.lift(0.3).translucify(0.6).css(),
|
||||
};
|
||||
}
|
||||
|
||||
function bannerColorVariables(theme: Theme, color: YaakColor): CSSVariables {
|
||||
return completeFullColorVariables(theme, {
|
||||
function bannerColorVariables(color: YaakColor | null): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
return {
|
||||
text: color.desaturate(0.5).lift(0.12).css(),
|
||||
textSubtle: color.desaturate(0.58).lift(0.04).translucify(0.04).css(),
|
||||
textSubtlest: color.desaturate(0.65).translucify(0.18).css(),
|
||||
surface: color.translucify(0.95).css(),
|
||||
surfaceHighlight: color.translucify(0.85).css(),
|
||||
border: color.lift(0.3).translucify(0.8).css(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function _inputCSS(color: YaakColor | null): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
const theme: Partial<ThemeComponentColors> = {
|
||||
border: color.css(),
|
||||
};
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
function buttonSolidColorVariables(
|
||||
theme: Theme,
|
||||
color: YaakColor,
|
||||
color: YaakColor | null,
|
||||
isDefault = false,
|
||||
): CSSVariables {
|
||||
const vars: Partial<CSSVariables> = {
|
||||
): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
const theme: Partial<ThemeComponentColors> = {
|
||||
text: "white",
|
||||
surface: color.lower(0.3).css(),
|
||||
surfaceHighlight: color.lower(0.1).css(),
|
||||
border: color.css(),
|
||||
};
|
||||
|
||||
if (isDefault) {
|
||||
vars.surface = undefined;
|
||||
vars.surfaceHighlight = color.lift(0.08).css();
|
||||
theme.text = undefined;
|
||||
theme.surface = undefined;
|
||||
theme.surfaceHighlight = color.lift(0.08).css();
|
||||
}
|
||||
|
||||
return completeFullColorVariables(theme, vars);
|
||||
return theme;
|
||||
}
|
||||
|
||||
function buttonBorderColorVariables(
|
||||
theme: Theme,
|
||||
color: YaakColor,
|
||||
color: YaakColor | null,
|
||||
isDefault = false,
|
||||
): CSSVariables {
|
||||
): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
const vars: Partial<CSSVariables> = {
|
||||
text: color.desaturate(0.4).lift(1).css(),
|
||||
textSubtle: color.desaturate(0.4).lift(0.55).css(),
|
||||
text: color.lift(0.8).css(),
|
||||
textSubtle: color.lift(0.55).css(),
|
||||
textSubtlest: color.lift(0.4).translucify(0.6).css(),
|
||||
surfaceHighlight: color.translucify(0.8).css(),
|
||||
borderSubtle: color.translucify(0.5).css(),
|
||||
border: color.translucify(0.3).css(),
|
||||
@@ -208,7 +186,7 @@ function buttonBorderColorVariables(
|
||||
vars.border = color.lift(0.5).css();
|
||||
}
|
||||
|
||||
return completeFullColorVariables(theme, vars);
|
||||
return vars;
|
||||
}
|
||||
|
||||
function variablesToCSS(
|
||||
@@ -225,8 +203,9 @@ function variablesToCSS(
|
||||
return selector == null ? css : `${selector} {\n${indent(css)}\n}`;
|
||||
}
|
||||
|
||||
function componentCSS(component: ComponentName, vars: CSSVariables): string | null {
|
||||
return variablesToCSS(`.x-theme-${component}`, vars);
|
||||
function componentCSS(theme: Theme, component: ComponentName): string | null {
|
||||
if (theme.components == null) return null;
|
||||
return variablesToCSS(`.x-theme-${component}`, themeVariables(theme, component));
|
||||
}
|
||||
|
||||
function buttonCSS(
|
||||
@@ -238,11 +217,8 @@ function buttonCSS(
|
||||
if (color == null) return null;
|
||||
|
||||
return [
|
||||
variablesToCSS(`.x-theme-button--solid--${colorKey}`, buttonSolidColorVariables(theme, color)),
|
||||
variablesToCSS(
|
||||
`.x-theme-button--border--${colorKey}`,
|
||||
buttonBorderColorVariables(theme, color),
|
||||
),
|
||||
variablesToCSS(`.x-theme-button--solid--${colorKey}`, buttonSolidColorVariables(color)),
|
||||
variablesToCSS(`.x-theme-button--border--${colorKey}`, buttonBorderColorVariables(color)),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
@@ -254,7 +230,7 @@ function bannerCSS(
|
||||
const color = yc(theme, colors?.[colorKey]);
|
||||
if (color == null) return null;
|
||||
|
||||
return variablesToCSS(`.x-theme-banner--${colorKey}`, bannerColorVariables(theme, color));
|
||||
return variablesToCSS(`.x-theme-banner--${colorKey}`, bannerColorVariables(color));
|
||||
}
|
||||
|
||||
function toastCSS(
|
||||
@@ -265,7 +241,7 @@ function toastCSS(
|
||||
const color = yc(theme, colors?.[colorKey]);
|
||||
if (color == null) return null;
|
||||
|
||||
return variablesToCSS(`.x-theme-toast--${colorKey}`, toastColorVariables(theme, color));
|
||||
return variablesToCSS(`.x-theme-toast--${colorKey}`, toastColorVariables(color));
|
||||
}
|
||||
|
||||
function templateTagCSS(
|
||||
@@ -276,10 +252,7 @@ function templateTagCSS(
|
||||
const color = yc(theme, colors?.[colorKey]);
|
||||
if (color == null) return null;
|
||||
|
||||
return variablesToCSS(
|
||||
`.x-theme-templateTag--${colorKey}`,
|
||||
templateTagColorVariables(theme, color),
|
||||
);
|
||||
return variablesToCSS(`.x-theme-templateTag--${colorKey}`, templateTagColorVariables(color));
|
||||
}
|
||||
|
||||
export function getThemeCSS(theme: Theme): string {
|
||||
@@ -292,26 +265,18 @@ export function getThemeCSS(theme: Theme): string {
|
||||
|
||||
let themeCSS = "";
|
||||
try {
|
||||
const baseCss = variablesToCSS(null, completeFullColorVariables(theme, theme.base));
|
||||
const baseSurface = yc(theme, theme.base.surface);
|
||||
|
||||
const baseCss = variablesToCSS(null, themeVariables(theme));
|
||||
themeCSS = [
|
||||
baseCss,
|
||||
...Object.entries(components).map(([key, value]) =>
|
||||
componentCSS(key as ComponentName, completePartialColorVariables(theme, value ?? {})),
|
||||
...Object.keys(components).map((key) => componentCSS(theme, key as ComponentName)),
|
||||
variablesToCSS(
|
||||
".x-theme-button--solid--default",
|
||||
buttonSolidColorVariables(yc(theme, theme.base.surface), true),
|
||||
),
|
||||
variablesToCSS(
|
||||
".x-theme-button--border--default",
|
||||
buttonBorderColorVariables(yc(theme, theme.base.surface), true),
|
||||
),
|
||||
baseSurface == null
|
||||
? null
|
||||
: variablesToCSS(
|
||||
".x-theme-button--solid--default",
|
||||
buttonSolidColorVariables(theme, baseSurface, true),
|
||||
),
|
||||
baseSurface == null
|
||||
? null
|
||||
: variablesToCSS(
|
||||
".x-theme-button--border--default",
|
||||
buttonBorderColorVariables(theme, baseSurface, true),
|
||||
),
|
||||
...Object.keys(colors).map((key) =>
|
||||
buttonCSS(theme, key as YaakColorKey, theme.components?.button ?? colors),
|
||||
),
|
||||
@@ -396,10 +361,26 @@ function yc<T extends string | null | undefined>(
|
||||
|
||||
export function completeTheme(theme: Theme): Theme {
|
||||
const fallback = theme.dark ? defaultDarkTheme.base : defaultLightTheme.base;
|
||||
const color = (value: string | null | undefined) => yc(theme, value);
|
||||
|
||||
for (const [key, value] of Object.entries(fallback)) {
|
||||
theme.base[key as YaakColorKey] ??= value;
|
||||
}
|
||||
theme.base.primary ??= fallback.primary;
|
||||
theme.base.secondary ??= fallback.secondary;
|
||||
theme.base.info ??= fallback.info;
|
||||
theme.base.success ??= fallback.success;
|
||||
theme.base.notice ??= fallback.notice;
|
||||
theme.base.warning ??= fallback.warning;
|
||||
theme.base.danger ??= fallback.danger;
|
||||
|
||||
theme.base.surface ??= fallback.surface;
|
||||
theme.base.surfaceHighlight ??= color(theme.base.surface)?.lift(0.06)?.css();
|
||||
theme.base.surfaceActive ??= color(theme.base.primary)?.lower(0.2).translucify(0.8).css();
|
||||
|
||||
theme.base.border ??= color(theme.base.surface)?.lift(0.12)?.css();
|
||||
theme.base.borderSubtle ??= color(theme.base.border)?.lower(0.08)?.css();
|
||||
|
||||
theme.base.text ??= fallback.text;
|
||||
theme.base.textSubtle ??= color(theme.base.text)?.lower(0.3)?.css();
|
||||
theme.base.textSubtlest ??= color(theme.base.text)?.lower(0.5)?.css();
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
+17
-254
@@ -3,9 +3,9 @@ import parseColor from "parse-color";
|
||||
export class YaakColor {
|
||||
private readonly appearance: "dark" | "light" = "light";
|
||||
|
||||
private lightness = 0;
|
||||
private chroma = 0;
|
||||
private hue = 0;
|
||||
private saturation = 0;
|
||||
private lightness = 0;
|
||||
private alpha = 1;
|
||||
|
||||
constructor(cssColor: string, appearance: "dark" | "light" = "light") {
|
||||
@@ -22,11 +22,11 @@ export class YaakColor {
|
||||
}
|
||||
|
||||
static white(): YaakColor {
|
||||
return new YaakColor("rgb(0,0,0)", "light").lower(999);
|
||||
return new YaakColor("rgb(0,0,0)", "light").lower(1);
|
||||
}
|
||||
|
||||
static black(): YaakColor {
|
||||
return new YaakColor("rgb(0,0,0)", "light").lift(999);
|
||||
return new YaakColor("rgb(0,0,0)", "light").lift(1);
|
||||
}
|
||||
|
||||
set(cssColor: string): YaakColor {
|
||||
@@ -35,22 +35,11 @@ export class YaakColor {
|
||||
const [r, g, b, a] = hexToRgba(cssColor);
|
||||
fixedCssColor = `rgba(${r},${g},${b},${a})`;
|
||||
}
|
||||
|
||||
const oklch = parseOklch(fixedCssColor);
|
||||
if (oklch != null) {
|
||||
this.lightness = oklch.lightness;
|
||||
this.chroma = oklch.chroma;
|
||||
this.hue = oklch.hue;
|
||||
this.alpha = oklch.alpha;
|
||||
return this;
|
||||
}
|
||||
|
||||
const { rgba } = parseColor(fixedCssColor);
|
||||
const [lightness, chroma, hue] = rgbToOklch(rgba[0], rgba[1], rgba[2]);
|
||||
this.lightness = lightness;
|
||||
this.chroma = chroma;
|
||||
this.hue = hue;
|
||||
this.alpha = rgba[3] ?? 1;
|
||||
const { hsla } = parseColor(fixedCssColor);
|
||||
this.hue = hsla[0];
|
||||
this.saturation = hsla[1];
|
||||
this.lightness = hsla[2];
|
||||
this.alpha = hsla[3] ?? 1;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -58,10 +47,6 @@ export class YaakColor {
|
||||
return new YaakColor(this.css(), this.appearance);
|
||||
}
|
||||
|
||||
themeColor(cssColor: string): YaakColor {
|
||||
return new YaakColor(cssColor, this.appearance);
|
||||
}
|
||||
|
||||
lower(mod: number): YaakColor {
|
||||
return this.appearance === "dark" ? this._darken(mod) : this._lighten(mod);
|
||||
}
|
||||
@@ -70,21 +55,6 @@ export class YaakColor {
|
||||
return this.appearance === "dark" ? this._lighten(mod) : this._darken(mod);
|
||||
}
|
||||
|
||||
liftMax(): YaakColor {
|
||||
return this.lift(999);
|
||||
}
|
||||
|
||||
lowerMax(): YaakColor {
|
||||
return this.lower(999);
|
||||
}
|
||||
|
||||
themeSurface(): YaakColor {
|
||||
return new YaakColor(
|
||||
this.appearance === "dark" ? "oklch(23% 0 0)" : "oklch(100% 0 0)",
|
||||
this.appearance,
|
||||
);
|
||||
}
|
||||
|
||||
minLightness(n: number): YaakColor {
|
||||
const color = this.clone();
|
||||
if (color.lightness < n) {
|
||||
@@ -99,25 +69,25 @@ export class YaakColor {
|
||||
|
||||
translucify(mod: number): YaakColor {
|
||||
const color = this.clone();
|
||||
color.alpha = clamp(color.alpha - color.alpha * mod, 0, 1);
|
||||
color.alpha = color.alpha - color.alpha * mod;
|
||||
return color;
|
||||
}
|
||||
|
||||
opacify(mod: number): YaakColor {
|
||||
const color = this.clone();
|
||||
color.alpha = clamp(this.alpha + (1 - this.alpha) * mod, 0, 1);
|
||||
color.alpha = this.alpha + (100 - this.alpha) * mod;
|
||||
return color;
|
||||
}
|
||||
|
||||
desaturate(mod: number): YaakColor {
|
||||
const color = this.clone();
|
||||
color.chroma = color.chroma - color.chroma * mod;
|
||||
color.saturation = color.saturation - color.saturation * mod;
|
||||
return color;
|
||||
}
|
||||
|
||||
saturate(mod: number): YaakColor {
|
||||
const color = this.clone();
|
||||
color.chroma = this.chroma + this.chroma * mod;
|
||||
color.saturation = this.saturation + (100 - this.saturation) * mod;
|
||||
return color;
|
||||
}
|
||||
|
||||
@@ -125,236 +95,29 @@ export class YaakColor {
|
||||
return this.lightness > color.lightness;
|
||||
}
|
||||
|
||||
contrastRatio(background: YaakColor): number {
|
||||
const foreground = this.alpha < 1 ? this.compositeOver(background) : this;
|
||||
const foregroundLuminance = foreground.relativeLuminance();
|
||||
const backgroundLuminance = background.relativeLuminance();
|
||||
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
||||
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
withContrast(background: YaakColor, minContrast: number): YaakColor {
|
||||
const darker = this.clone();
|
||||
darker.lightness = 0;
|
||||
darker.chroma = 0;
|
||||
darker.hue = 0;
|
||||
|
||||
const lighter = this.clone();
|
||||
lighter.lightness = 100;
|
||||
lighter.chroma = 0;
|
||||
lighter.hue = 0;
|
||||
|
||||
const darkerContrast = darker.contrastRatio(background);
|
||||
const lighterContrast = lighter.contrastRatio(background);
|
||||
let useLighterColor = lighterContrast >= darkerContrast;
|
||||
|
||||
// Saturated accent surfaces often read better with white text even when
|
||||
// black has the higher numeric contrast. Keep yellow-ish light accents dark
|
||||
// by requiring white to clear a modest contrast floor first.
|
||||
if (minContrast >= 3 && lighterContrast >= 2.5) {
|
||||
useLighterColor = true;
|
||||
}
|
||||
|
||||
const selectedContrast = useLighterColor ? lighterContrast : darkerContrast;
|
||||
if (selectedContrast < minContrast) {
|
||||
return useLighterColor ? lighter : darker;
|
||||
}
|
||||
|
||||
let minLightness = 0;
|
||||
let maxLightness = 100;
|
||||
const color = this.clone();
|
||||
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
color.lightness = (minLightness + maxLightness) / 2;
|
||||
const contrast = color.contrastRatio(background);
|
||||
|
||||
if (useLighterColor) {
|
||||
if (contrast >= minContrast) {
|
||||
maxLightness = color.lightness;
|
||||
} else {
|
||||
minLightness = color.lightness;
|
||||
}
|
||||
} else if (contrast >= minContrast) {
|
||||
minLightness = color.lightness;
|
||||
} else {
|
||||
maxLightness = color.lightness;
|
||||
}
|
||||
}
|
||||
|
||||
color.lightness = useLighterColor ? maxLightness : minLightness;
|
||||
return color;
|
||||
}
|
||||
|
||||
compositeOver(background: YaakColor): YaakColor {
|
||||
const [fgR, fgG, fgB] = this.rgb();
|
||||
const [bgR, bgG, bgB] = background.rgb();
|
||||
const alpha = this.alpha + background.alpha * (1 - this.alpha);
|
||||
|
||||
if (alpha <= 0) {
|
||||
return YaakColor.transparent();
|
||||
}
|
||||
|
||||
const r = (fgR * this.alpha + bgR * background.alpha * (1 - this.alpha)) / alpha;
|
||||
const g = (fgG * this.alpha + bgG * background.alpha * (1 - this.alpha)) / alpha;
|
||||
const b = (fgB * this.alpha + bgB * background.alpha * (1 - this.alpha)) / alpha;
|
||||
|
||||
return new YaakColor(`rgba(${r},${g},${b},${alpha})`, this.appearance);
|
||||
}
|
||||
|
||||
css(): string {
|
||||
const [r, g, b] = this.rgb();
|
||||
const [r, g, b] = parseColor(`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`).rgb;
|
||||
return rgbaToHex(r, g, b, this.alpha);
|
||||
}
|
||||
|
||||
hexNoAlpha(): string {
|
||||
const [r, g, b] = this.rgb();
|
||||
const [r, g, b] = parseColor(`hsl(${this.hue},${this.saturation}%,${this.lightness}%)`).rgb;
|
||||
return rgbaToHexNoAlpha(r, g, b);
|
||||
}
|
||||
|
||||
private relativeLuminance(): number {
|
||||
const [r, g, b] = this.rgb();
|
||||
const red = srgbToLinear(r / 255);
|
||||
const green = srgbToLinear(g / 255);
|
||||
const blue = srgbToLinear(b / 255);
|
||||
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
|
||||
}
|
||||
|
||||
private rgb(): [number, number, number] {
|
||||
return oklchToRgb(this.lightness, this.chroma, this.hue);
|
||||
}
|
||||
|
||||
private _lighten(mod: number): YaakColor {
|
||||
const color = this.clone();
|
||||
color.lightness = clamp(this.lightness + (100 - this.lightness) * mod, 0, 100);
|
||||
color.lightness = this.lightness + (100 - this.lightness) * mod;
|
||||
return color;
|
||||
}
|
||||
|
||||
private _darken(mod: number): YaakColor {
|
||||
const color = this.clone();
|
||||
color.lightness = clamp(this.lightness - this.lightness * mod, 0, 100);
|
||||
color.lightness = this.lightness - this.lightness * mod;
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
function parseOklch(
|
||||
cssColor: string,
|
||||
): { lightness: number; chroma: number; hue: number; alpha: number } | null {
|
||||
const match = cssColor
|
||||
.trim()
|
||||
.match(
|
||||
/^oklch\(\s*([^\s,]+)(?:\s+|,\s*)([^\s,]+)(?:\s+|,\s*)([^\s,/]+)(?:\s*\/\s*([^)]+)|(?:\s*,\s*([^)]*))?)\s*\)$/i,
|
||||
);
|
||||
if (match == null) return null;
|
||||
|
||||
const [, lightnessValue, chromaValue, hueValue, slashAlpha, commaAlpha] = match;
|
||||
if (lightnessValue == null || chromaValue == null || hueValue == null) return null;
|
||||
|
||||
const lightness = parseOklchLightness(lightnessValue);
|
||||
const chroma = parseCssNumber(chromaValue, 1);
|
||||
const hue = normalizeHue(parseCssNumber(hueValue.replace(/deg$/i, ""), 1));
|
||||
const alpha = parseCssNumber(slashAlpha ?? commaAlpha ?? "1", 1);
|
||||
|
||||
if (
|
||||
!Number.isFinite(lightness) ||
|
||||
!Number.isFinite(chroma) ||
|
||||
!Number.isFinite(hue) ||
|
||||
!Number.isFinite(alpha)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
lightness: clamp(lightness, 0, 100),
|
||||
chroma: Math.max(0, chroma),
|
||||
hue,
|
||||
alpha: clamp(alpha, 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
function parseCssNumber(value: string, percentScale: number): number {
|
||||
const normalized = value.trim();
|
||||
if (normalized.endsWith("%")) {
|
||||
return (Number.parseFloat(normalized) / 100) * percentScale;
|
||||
}
|
||||
return Number.parseFloat(normalized);
|
||||
}
|
||||
|
||||
function parseOklchLightness(value: string): number {
|
||||
const parsed = parseCssNumber(value, 100);
|
||||
return value.trim().endsWith("%") || parsed > 1 ? parsed : parsed * 100;
|
||||
}
|
||||
|
||||
function rgbToOklch(r: number, g: number, b: number): [number, number, number] {
|
||||
const red = srgbToLinear(r / 255);
|
||||
const green = srgbToLinear(g / 255);
|
||||
const blue = srgbToLinear(b / 255);
|
||||
|
||||
const l = 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue;
|
||||
const m = 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue;
|
||||
const s = 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue;
|
||||
|
||||
const lRoot = Math.cbrt(l);
|
||||
const mRoot = Math.cbrt(m);
|
||||
const sRoot = Math.cbrt(s);
|
||||
|
||||
const lightness = 0.2104542553 * lRoot + 0.793617785 * mRoot - 0.0040720468 * sRoot;
|
||||
const a = 1.9779984951 * lRoot - 2.428592205 * mRoot + 0.4505937099 * sRoot;
|
||||
const okb = 0.0259040371 * lRoot + 0.7827717662 * mRoot - 0.808675766 * sRoot;
|
||||
|
||||
return [
|
||||
lightness * 100,
|
||||
Math.sqrt(a * a + okb * okb),
|
||||
normalizeHue(radToDeg(Math.atan2(okb, a))),
|
||||
];
|
||||
}
|
||||
|
||||
function oklchToRgb(lightness: number, chroma: number, hue: number): [number, number, number] {
|
||||
const l = clamp(lightness, 0, 100) / 100;
|
||||
const a = Math.cos(degToRad(hue)) * chroma;
|
||||
const b = Math.sin(degToRad(hue)) * chroma;
|
||||
|
||||
const lRoot = l + 0.3963377774 * a + 0.2158037573 * b;
|
||||
const mRoot = l - 0.1055613458 * a - 0.0638541728 * b;
|
||||
const sRoot = l - 0.0894841775 * a - 1.291485548 * b;
|
||||
|
||||
const lCube = lRoot * lRoot * lRoot;
|
||||
const mCube = mRoot * mRoot * mRoot;
|
||||
const sCube = sRoot * sRoot * sRoot;
|
||||
|
||||
const red = 4.0767416621 * lCube - 3.3077115913 * mCube + 0.2309699292 * sCube;
|
||||
const green = -1.2684380046 * lCube + 2.6097574011 * mCube - 0.3413193965 * sCube;
|
||||
const blue = -0.0041960863 * lCube - 0.7034186147 * mCube + 1.707614701 * sCube;
|
||||
|
||||
return [linearToSrgb(red) * 255, linearToSrgb(green) * 255, linearToSrgb(blue) * 255];
|
||||
}
|
||||
|
||||
function srgbToLinear(value: number): number {
|
||||
return value <= 0.04045 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
function linearToSrgb(value: number): number {
|
||||
const srgb = value <= 0.0031308 ? value * 12.92 : 1.055 * Math.pow(value, 1 / 2.4) - 0.055;
|
||||
return clamp(srgb, 0, 1);
|
||||
}
|
||||
|
||||
function normalizeHue(value: number): number {
|
||||
const hue = value % 360;
|
||||
return hue < 0 ? hue + 360 : hue;
|
||||
}
|
||||
|
||||
function degToRad(value: number): number {
|
||||
return (value * Math.PI) / 180;
|
||||
}
|
||||
|
||||
function radToDeg(value: number): number {
|
||||
return (value * 180) / Math.PI;
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function rgbaToHex(r: number, g: number, b: number, a: number): string {
|
||||
const toHex = (n: number): string => {
|
||||
const hex = Number(Math.round(n)).toString(16);
|
||||
|
||||
@@ -69,6 +69,9 @@ const config = JSON.stringify({
|
||||
const normalizedAdditionalArgs = [];
|
||||
for (let i = 0; i < additionalArgs.length; i++) {
|
||||
const arg = additionalArgs[i];
|
||||
if (arg === "--") {
|
||||
continue;
|
||||
}
|
||||
if (arg === "--config" && i + 1 < additionalArgs.length) {
|
||||
const value = additionalArgs[i + 1];
|
||||
const isInlineJson = value.trimStart().startsWith("{");
|
||||
|
||||
Reference in New Issue
Block a user