up
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
- 支持搜索、筛选、排序以及桌面表格和移动端卡片。
|
||||
- 从任意地区一键带入国家和货币,生成 ChatGPT Team 结账脚本。
|
||||
- 脚本生成器支持 39 种货币、实时预览、复制和 `.js` 下载。
|
||||
- 优惠码仅在当前浏览器内处理,不写入网址或本地存储。
|
||||
- 优惠码和手动提供的 Token 仅在当前浏览器内处理,不写入网址或本地存储。
|
||||
- GitHub Actions 每两天刷新,结构异常或覆盖率骤降时停止部署。
|
||||
- 单个地区暂时失败时,最多沿用 14 天的上次成功结果并标记为“数据暂旧”。
|
||||
|
||||
@@ -59,7 +59,9 @@ npm run build
|
||||
|
||||
顶部导航可切换到脚本生成器,也可以从价格表或移动端价格卡片直接进入。地区入口会通过 `view=generator&country=XX¤cy=XXX` 预填国家和货币,优惠码不会出现在网址中。
|
||||
|
||||
生成器默认使用 `US / EGP`、月付、两个席位和工作区名称 `xxx`。它只生成文本,不会在本站请求登录凭证、支付接口或代替用户执行代码。请仅在有权操作的账号中使用,并以实际结账页结果为准。
|
||||
生成器默认使用 `US / EGP`、月付、两个席位和工作区名称 `xxx`。Access Token 可以在脚本运行时从登录 Session 自动获取,也可以手动粘贴原始 `accessToken` 或 `/api/auth/session` 返回的完整 JSON;完整 JSON 只会提取其中的 `accessToken` 写入脚本。切回自动获取时,手动输入会立即清空。
|
||||
|
||||
生成器只生成文本,不会在本站请求登录凭证、支付接口或代替用户执行代码。请仅在有权操作的账号中使用,并以实际结账页结果为准。
|
||||
|
||||
### 发布保护
|
||||
|
||||
|
||||
+35
-1
@@ -1,4 +1,4 @@
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import sample from "../public/data/sample-prices.json";
|
||||
@@ -134,4 +134,38 @@ describe("pricing explorer", () => {
|
||||
expect(screen.getByLabelText(/国家 ISO 缩写/)).toHaveValue("US");
|
||||
expect(screen.getByLabelText(/货币 ISO 缩写/)).toHaveValue("EGP");
|
||||
});
|
||||
|
||||
it("accepts complete session JSON in manual token mode without storing it", async () => {
|
||||
const user = userEvent.setup();
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
value: { writeText },
|
||||
});
|
||||
render(<App />);
|
||||
await user.click(screen.getByRole("button", { name: /脚本生成器/ }));
|
||||
|
||||
expect(screen.getByRole("radio", { name: /自动获取/ })).toBeChecked();
|
||||
await user.click(screen.getByRole("radio", { name: /手动粘贴/ }));
|
||||
const tokenInput = screen.getByPlaceholderText("粘贴 accessToken 或完整 Session JSON");
|
||||
const sessionJson = JSON.stringify({
|
||||
user: { email: "private@example.com" },
|
||||
accessToken: "session-access-token",
|
||||
});
|
||||
fireEvent.change(tokenInput, { target: { value: sessionJson } });
|
||||
await user.type(screen.getByPlaceholderText("例如:XXXXXXXXXXXX"), "SAVE20");
|
||||
await user.click(screen.getByRole("button", { name: /复制代码/ }));
|
||||
|
||||
const copiedScript = writeText.mock.calls[0][0] as string;
|
||||
expect(copiedScript).toContain('const accessToken = "session-access-token"');
|
||||
expect(copiedScript).not.toContain("private@example.com");
|
||||
expect(copiedScript).not.toContain("/api/auth/session");
|
||||
expect(window.location.search).not.toContain("session-access-token");
|
||||
expect(Object.values(window.localStorage)).not.toContain("session-access-token");
|
||||
|
||||
await user.click(screen.getByRole("radio", { name: /自动获取/ }));
|
||||
expect(screen.queryByPlaceholderText("粘贴 accessToken 或完整 Session JSON")).not.toBeInTheDocument();
|
||||
await user.click(screen.getByRole("radio", { name: /手动粘贴/ }));
|
||||
expect(screen.getByPlaceholderText("粘贴 accessToken 或完整 Session JSON")).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
CHECKOUT_CURRENCIES,
|
||||
DEFAULT_ACCESS_TOKEN_MODE,
|
||||
DEFAULT_CHECKOUT_COUNTRY,
|
||||
DEFAULT_CHECKOUT_CURRENCY,
|
||||
generateCheckoutScript,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
normalizeIsoInput,
|
||||
validateCheckoutInput,
|
||||
type CheckoutScriptInput,
|
||||
type CheckoutInputField,
|
||||
type CheckoutValidationErrors,
|
||||
} from "./checkout-generator";
|
||||
import type { PriceRow } from "./types";
|
||||
@@ -41,12 +43,15 @@ export default function CheckoutGenerator({
|
||||
const [coupon, setCoupon] = useState("");
|
||||
const [country, setCountry] = useState(initialCountry);
|
||||
const [currency, setCurrency] = useState(initialCurrency);
|
||||
const [accessTokenMode, setAccessTokenMode] = useState<"auto" | "manual">(DEFAULT_ACCESS_TOKEN_MODE);
|
||||
const [accessToken, setAccessToken] = useState("");
|
||||
const [errors, setErrors] = useState<CheckoutValidationErrors>({});
|
||||
const [notice, setNotice] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
const couponRef = useRef<HTMLInputElement>(null);
|
||||
const countryRef = useRef<HTMLInputElement>(null);
|
||||
const currencyRef = useRef<HTMLInputElement>(null);
|
||||
const accessTokenRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setCountry(initialCountry || DEFAULT_CHECKOUT_COUNTRY);
|
||||
@@ -64,6 +69,8 @@ export default function CheckoutGenerator({
|
||||
coupon: coupon.trim(),
|
||||
country: country.toUpperCase(),
|
||||
currency: currency.toUpperCase(),
|
||||
accessTokenMode,
|
||||
accessToken: accessToken.trim(),
|
||||
};
|
||||
const previewInput: CheckoutScriptInput = {
|
||||
coupon: normalized.coupon || "XXXXXXXXXXXX",
|
||||
@@ -73,15 +80,25 @@ export default function CheckoutGenerator({
|
||||
currency: isSupportedCheckoutCurrency(normalized.currency)
|
||||
? normalized.currency
|
||||
: DEFAULT_CHECKOUT_CURRENCY,
|
||||
accessTokenMode,
|
||||
accessToken: accessTokenMode === "manual"
|
||||
? normalized.accessToken || "PASTE_ACCESS_TOKEN_HERE"
|
||||
: "",
|
||||
};
|
||||
const source = useMemo(
|
||||
() => generateCheckoutScript(previewInput),
|
||||
[previewInput.coupon, previewInput.country, previewInput.currency],
|
||||
[
|
||||
previewInput.coupon,
|
||||
previewInput.country,
|
||||
previewInput.currency,
|
||||
previewInput.accessTokenMode,
|
||||
previewInput.accessToken,
|
||||
],
|
||||
);
|
||||
const isInputValid = Object.keys(validateCheckoutInput(normalized)).length === 0;
|
||||
const currencyName = CHECKOUT_CURRENCIES.find(([code]) => code === normalized.currency)?.[1];
|
||||
|
||||
const clearError = (field: keyof CheckoutScriptInput) => {
|
||||
const clearError = (field: CheckoutInputField) => {
|
||||
if (!errors[field]) return;
|
||||
setErrors((current) => ({ ...current, [field]: undefined }));
|
||||
};
|
||||
@@ -89,10 +106,11 @@ export default function CheckoutGenerator({
|
||||
const validateAndFocus = () => {
|
||||
const nextErrors = validateCheckoutInput(normalized);
|
||||
setErrors(nextErrors);
|
||||
const firstError = Object.keys(nextErrors)[0] as keyof CheckoutScriptInput | undefined;
|
||||
const firstError = Object.keys(nextErrors)[0] as CheckoutInputField | undefined;
|
||||
if (firstError === "coupon") couponRef.current?.focus();
|
||||
if (firstError === "country") countryRef.current?.focus();
|
||||
if (firstError === "currency") currencyRef.current?.focus();
|
||||
if (firstError === "accessToken") accessTokenRef.current?.focus();
|
||||
return !firstError;
|
||||
};
|
||||
|
||||
@@ -148,8 +166,10 @@ export default function CheckoutGenerator({
|
||||
setCoupon("");
|
||||
setCountry(DEFAULT_CHECKOUT_COUNTRY);
|
||||
setCurrency(DEFAULT_CHECKOUT_CURRENCY);
|
||||
setAccessTokenMode(DEFAULT_ACCESS_TOKEN_MODE);
|
||||
setAccessToken("");
|
||||
setErrors({});
|
||||
setNotice("已恢复默认值 US / EGP");
|
||||
setNotice("已恢复自动获取与默认值 US / EGP");
|
||||
couponRef.current?.focus();
|
||||
};
|
||||
|
||||
@@ -162,11 +182,11 @@ export default function CheckoutGenerator({
|
||||
</button>
|
||||
<span className="eyebrow"><Code2 size={15} /> BUSINESS TOOL · GENERATOR</span>
|
||||
<h1>参数填好,<br /><em>脚本即刻就绪。</em></h1>
|
||||
<p>输入优惠码、国家和货币,生成可复制的 ChatGPT Team 结账脚本。全部处理都在当前浏览器中完成。</p>
|
||||
<p>选择自动获取或手动提供 Access Token,再输入优惠码、国家和货币,生成可复制的 ChatGPT Team 结账脚本。</p>
|
||||
</div>
|
||||
<div className="generator-promise">
|
||||
<ShieldCheck size={22} />
|
||||
<div><strong>本地生成</strong><span>不上传、不保存优惠码,也不会在本站执行脚本。</span></div>
|
||||
<div><strong>本地生成</strong><span>不上传、不保存优惠码或 Access Token,也不会在本站执行脚本。</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -192,6 +212,56 @@ export default function CheckoutGenerator({
|
||||
{errors.coupon ? <em role="alert">{errors.coupon}</em> : null}
|
||||
</label>
|
||||
|
||||
<fieldset className="token-source-field">
|
||||
<legend>Access Token 来源</legend>
|
||||
<div className="token-source-options">
|
||||
<label className={accessTokenMode === "auto" ? "active" : ""}>
|
||||
<input
|
||||
type="radio"
|
||||
name="access-token-mode"
|
||||
value="auto"
|
||||
checked={accessTokenMode === "auto"}
|
||||
onChange={() => {
|
||||
setAccessTokenMode("auto");
|
||||
setAccessToken("");
|
||||
clearError("accessToken");
|
||||
}}
|
||||
/>
|
||||
<span><strong>自动获取</strong><small>运行时读取登录 Session</small></span>
|
||||
</label>
|
||||
<label className={accessTokenMode === "manual" ? "active" : ""}>
|
||||
<input
|
||||
type="radio"
|
||||
name="access-token-mode"
|
||||
value="manual"
|
||||
checked={accessTokenMode === "manual"}
|
||||
onChange={() => {
|
||||
setAccessTokenMode("manual");
|
||||
clearError("accessToken");
|
||||
}}
|
||||
/>
|
||||
<span><strong>手动粘贴</strong><small>支持 Token 或 Session JSON</small></span>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{accessTokenMode === "manual" ? (
|
||||
<label className={`generator-field token-value-field ${errors.accessToken ? "has-error" : ""}`}>
|
||||
<span><strong>Access Token / Session JSON</strong><small>仅用于当前生成结果</small></span>
|
||||
<input
|
||||
ref={accessTokenRef}
|
||||
type="password"
|
||||
value={accessToken}
|
||||
onChange={(event) => { setAccessToken(event.target.value); clearError("accessToken"); }}
|
||||
placeholder="粘贴 accessToken 或完整 Session JSON"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
aria-invalid={Boolean(errors.accessToken)}
|
||||
/>
|
||||
{errors.accessToken ? <em role="alert">{errors.accessToken}</em> : <small className="field-note">Session JSON 会自动提取 accessToken;切换回自动获取时立即清空</small>}
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<div className="generator-field-grid">
|
||||
<label className={`generator-field ${errors.country ? "has-error" : ""}`}>
|
||||
<span><strong>国家 ISO 缩写</strong><small>2 位字母</small></span>
|
||||
@@ -248,6 +318,7 @@ export default function CheckoutGenerator({
|
||||
<div className="generator-steps">
|
||||
<span>如何使用</span>
|
||||
<ol>
|
||||
<li>选择自动获取,或手动粘贴 Access Token。</li>
|
||||
<li>登录 ChatGPT 并打开浏览器控制台。</li>
|
||||
<li>复制生成的脚本,粘贴后执行。</li>
|
||||
<li>根据控制台输出查看 Stripe 长链接。</li>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CHECKOUT_CURRENCIES,
|
||||
DEFAULT_CHECKOUT_COUNTRY,
|
||||
DEFAULT_CHECKOUT_CURRENCY,
|
||||
extractAccessToken,
|
||||
generateCheckoutScript,
|
||||
isSupportedCheckoutCurrency,
|
||||
normalizeIsoInput,
|
||||
@@ -20,7 +21,13 @@ describe("checkout script generator", () => {
|
||||
|
||||
it("normalizes ISO input and validates every field", () => {
|
||||
expect(normalizeIsoInput(" u-s1 ", 2)).toBe("US");
|
||||
expect(validateCheckoutInput({ coupon: "", country: "USA", currency: "TRY" }))
|
||||
expect(validateCheckoutInput({
|
||||
coupon: "",
|
||||
country: "USA",
|
||||
currency: "TRY",
|
||||
accessTokenMode: "auto",
|
||||
accessToken: "",
|
||||
}))
|
||||
.toEqual({
|
||||
coupon: "请输入优惠码",
|
||||
country: "请输入 2 位英文字母国家代码",
|
||||
@@ -33,6 +40,8 @@ describe("checkout script generator", () => {
|
||||
coupon: 'SAVE "20" & 更多',
|
||||
country: "SG",
|
||||
currency: "SGD",
|
||||
accessTokenMode: "auto",
|
||||
accessToken: "",
|
||||
});
|
||||
|
||||
expect(script).toContain('const COUPON = "SAVE \\"20\\" & 更多"');
|
||||
@@ -42,4 +51,44 @@ describe("checkout script generator", () => {
|
||||
expect(script).toContain("promoCode=SAVE%20%2220%22%20%26%20%E6%9B%B4%E5%A4%9A");
|
||||
expect(() => new Function(script)).not.toThrow();
|
||||
});
|
||||
|
||||
it("accepts a raw access token or extracts one from complete session JSON", () => {
|
||||
expect(extractAccessToken(" eyJraw.token ")).toBe("eyJraw.token");
|
||||
expect(extractAccessToken(JSON.stringify({ user: { name: "Kai" }, accessToken: "eyJsession.token" })))
|
||||
.toBe("eyJsession.token");
|
||||
expect(extractAccessToken(JSON.stringify({ session: { access_token: "nested-token" } })))
|
||||
.toBe("nested-token");
|
||||
expect(extractAccessToken('{"user":true}')).toBeNull();
|
||||
expect(extractAccessToken("{")).toBeNull();
|
||||
});
|
||||
|
||||
it("writes only the extracted token in manual mode and removes the session fetch", () => {
|
||||
const sessionJson = JSON.stringify({
|
||||
user: { email: "private@example.com" },
|
||||
accessToken: 'eyJmanual."token"',
|
||||
expires: "2099-01-01",
|
||||
});
|
||||
const script = generateCheckoutScript({
|
||||
coupon: "SAVE20",
|
||||
country: "US",
|
||||
currency: "EGP",
|
||||
accessTokenMode: "manual",
|
||||
accessToken: sessionJson,
|
||||
});
|
||||
|
||||
expect(script).toContain('const accessToken = "eyJmanual.\\"token\\""');
|
||||
expect(script).not.toContain("/api/auth/session");
|
||||
expect(script).not.toContain("private@example.com");
|
||||
expect(() => new Function(script)).not.toThrow();
|
||||
});
|
||||
|
||||
it("requires an extractable token in manual mode", () => {
|
||||
expect(validateCheckoutInput({
|
||||
coupon: "SAVE20",
|
||||
country: "US",
|
||||
currency: "EGP",
|
||||
accessTokenMode: "manual",
|
||||
accessToken: '{"user":true}',
|
||||
})).toEqual({ accessToken: "未能从输入内容中提取 accessToken" });
|
||||
});
|
||||
});
|
||||
|
||||
+66
-13
@@ -42,16 +42,22 @@ export const CHECKOUT_CURRENCIES = [
|
||||
|
||||
export const DEFAULT_CHECKOUT_COUNTRY = "US";
|
||||
export const DEFAULT_CHECKOUT_CURRENCY = "EGP";
|
||||
export const DEFAULT_ACCESS_TOKEN_MODE = "auto" as const;
|
||||
|
||||
const currencyCodes = new Set<string>(CHECKOUT_CURRENCIES.map(([code]) => code));
|
||||
|
||||
export type AccessTokenMode = "auto" | "manual";
|
||||
|
||||
export type CheckoutScriptInput = {
|
||||
coupon: string;
|
||||
country: string;
|
||||
currency: string;
|
||||
accessTokenMode: AccessTokenMode;
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
export type CheckoutValidationErrors = Partial<Record<keyof CheckoutScriptInput, string>>;
|
||||
export type CheckoutInputField = "coupon" | "country" | "currency" | "accessToken";
|
||||
export type CheckoutValidationErrors = Partial<Record<CheckoutInputField, string>>;
|
||||
|
||||
export function normalizeIsoInput(value: string, maxLength: number): string {
|
||||
return value.toUpperCase().replace(/[^A-Z]/g, "").slice(0, maxLength);
|
||||
@@ -61,11 +67,45 @@ export function isSupportedCheckoutCurrency(value: string): boolean {
|
||||
return currencyCodes.has(value.toUpperCase());
|
||||
}
|
||||
|
||||
function findAccessToken(value: unknown, depth = 0): string | null {
|
||||
if (depth > 4 || value == null || typeof value !== "object") return null;
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if ((key === "accessToken" || key === "access_token") && typeof child === "string" && child.trim()) {
|
||||
return child.trim();
|
||||
}
|
||||
}
|
||||
for (const child of Object.values(value)) {
|
||||
const token = findAccessToken(child, depth + 1);
|
||||
if (token) return token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractAccessToken(value: string): string | null {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[") && !trimmed.startsWith('"')) {
|
||||
return trimmed;
|
||||
}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(trimmed);
|
||||
if (typeof parsed === "string") return parsed.trim() || null;
|
||||
return findAccessToken(parsed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateCheckoutInput(input: CheckoutScriptInput): CheckoutValidationErrors {
|
||||
const errors: CheckoutValidationErrors = {};
|
||||
if (!input.coupon.trim()) errors.coupon = "请输入优惠码";
|
||||
if (!/^[A-Z]{2}$/.test(input.country)) errors.country = "请输入 2 位英文字母国家代码";
|
||||
if (!isSupportedCheckoutCurrency(input.currency)) errors.currency = "请选择支持的货币代码";
|
||||
if (input.accessTokenMode === "manual" && !input.accessToken.trim()) {
|
||||
errors.accessToken = "请输入 Access Token 或 Session JSON";
|
||||
} else if (input.accessTokenMode === "manual" && !extractAccessToken(input.accessToken)) {
|
||||
errors.accessToken = "未能从输入内容中提取 accessToken";
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
@@ -74,17 +114,19 @@ export function generateCheckoutScript(input: CheckoutScriptInput): string {
|
||||
const country = input.country || DEFAULT_CHECKOUT_COUNTRY;
|
||||
const currency = input.currency || DEFAULT_CHECKOUT_CURRENCY;
|
||||
const encodedCoupon = encodeURIComponent(coupon);
|
||||
|
||||
return `(async function generateAUTeamLink() {
|
||||
// ================= 配置项 =================
|
||||
const WORKSPACE_NAME = "xxx";
|
||||
const COUPON = ${JSON.stringify(coupon)}; // 优惠码
|
||||
const SEAT_QUANTITY = 2; // 席位数量(Team 最少 2 个)
|
||||
// ==========================================
|
||||
|
||||
console.log("⏳ 正在获取 ChatGPT Session Token...");
|
||||
|
||||
// 1. 自动获取登录凭证
|
||||
const tokenStatusMessage = input.accessTokenMode === "manual"
|
||||
? "⏳ 正在使用手动 Access Token..."
|
||||
: "⏳ 正在获取 ChatGPT Session Token...";
|
||||
const manualAccessToken = extractAccessToken(input.accessToken);
|
||||
const tokenSource = input.accessTokenMode === "manual"
|
||||
? ` // 1. 使用手动提供的登录凭证
|
||||
const accessToken = ${JSON.stringify(manualAccessToken || "PASTE_ACCESS_TOKEN_HERE")};
|
||||
if (!accessToken || accessToken === "PASTE_ACCESS_TOKEN_HERE") {
|
||||
console.error("❌ Access Token 为空,请重新生成并填入 Token");
|
||||
return;
|
||||
}
|
||||
console.log("✅ 已使用手动 Access Token");`
|
||||
: ` // 1. 自动获取登录凭证
|
||||
let accessToken;
|
||||
try {
|
||||
const s = await fetch("/api/auth/session").then(r => r.json());
|
||||
@@ -94,7 +136,18 @@ export function generateCheckoutScript(input: CheckoutScriptInput): string {
|
||||
console.error("❌ 获取 Token 失败:", e.message);
|
||||
return;
|
||||
}
|
||||
console.log("✅ Token 获取成功");
|
||||
console.log("✅ Token 获取成功");`;
|
||||
|
||||
return `(async function generateAUTeamLink() {
|
||||
// ================= 配置项 =================
|
||||
const WORKSPACE_NAME = "xxx";
|
||||
const COUPON = ${JSON.stringify(coupon)}; // 优惠码
|
||||
const SEAT_QUANTITY = 2; // 席位数量(Team 最少 2 个)
|
||||
// ==========================================
|
||||
|
||||
console.log(${JSON.stringify(tokenStatusMessage)});
|
||||
|
||||
${tokenSource}
|
||||
|
||||
// 2. 构建请求 Payload
|
||||
const payload = {
|
||||
|
||||
@@ -171,6 +171,19 @@ tbody tr:hover { background: #f9fbf8; }
|
||||
.generator-field > input:focus, .iso-input:focus-within { border-color: var(--green); box-shadow: 0 0 0 3px rgba(23,107,77,.1); }
|
||||
.generator-field.has-error > input, .generator-field.has-error .iso-input { border-color: #bd5b4f; }
|
||||
.generator-field > em { display: block; margin-top: 7px; color: #a74338; font-size: 9px; font-style: normal; }
|
||||
.token-source-field { margin: 0 0 18px; padding: 0; border: 0; }
|
||||
.token-source-field legend { margin-bottom: 8px; font-size: 12px; font-weight: 750; }
|
||||
.token-source-options { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
||||
.token-source-options label { position: relative; display: flex; min-height: 61px; align-items: center; padding: 11px 12px; border: 1px solid #d7ddd7; border-radius: 11px; background: white; cursor: pointer; transition: border-color .18s ease, background .18s ease, box-shadow .18s ease; }
|
||||
.token-source-options label.active { border-color: var(--green); background: #f1f7f1; box-shadow: 0 0 0 2px rgba(23,107,77,.07); }
|
||||
.token-source-options input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||
.token-source-options label::before { width: 13px; height: 13px; flex: 0 0 auto; margin-right: 9px; border: 1px solid #aab5ae; border-radius: 50%; background: white; content: ""; box-shadow: inset 0 0 0 3px white; }
|
||||
.token-source-options label.active::before { border-color: var(--green); background: var(--green); }
|
||||
.token-source-options span { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.token-source-options strong { font-size: 10px; }
|
||||
.token-source-options small { color: #87918d; font-size: 8px; line-height: 1.4; }
|
||||
.token-value-field { margin-top: -2px; }
|
||||
.token-value-field > input { font: 600 11px/1.4 "SFMono-Regular", Consolas, monospace; }
|
||||
.generator-field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.iso-input { display: grid; grid-template-columns: auto 1fr; align-items: center; }
|
||||
.iso-input b { padding-left: 12px; color: #87918d; font: 700 9px/1 monospace; }
|
||||
@@ -304,6 +317,7 @@ footer > span:last-child { text-align: right; }
|
||||
.section-heading p { display: none; }
|
||||
.generator-form-panel, .generator-result-panel { padding: 22px 16px; }
|
||||
.generator-field-grid { grid-template-columns: 1fr; gap: 0; }
|
||||
.token-source-options { grid-template-columns: 1fr; }
|
||||
.result-head { align-items: flex-end; }
|
||||
.code-actions button:first-child { display: none; }
|
||||
.code-preview pre { padding: 16px; }
|
||||
|
||||
Reference in New Issue
Block a user