Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/JAVA/Openclaw/ui/src/ui/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 18 kB image not shown  

Quelle  custom-theme.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { z } from "zod";
import { normalizeOptionalString } from "./string-coerce.ts";

const TWEAKCN_HOSTS = new Set(["tweakcn.com", "www.tweakcn.com"]);
const THEME_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/;
const CUSTOM_THEME_STYLE_ID = "openclaw-custom-theme";
const MAX_TWEAKCN_THEME_BYTES = 200_000;
const MAX_CSS_TOKEN_LENGTH = 240;
const TWEAKCN_FETCH_TIMEOUT_MS = 10_000;
const DEFAULT_FONT_BODY =
  '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
const DEFAULT_MONO =
  '"JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace';
const FORBIDDEN_CSS_VALUE_PARTS = [
  "url(",
  "image(",
  "image-set(",
  "-webkit-image-set(",
  "cross-fade(",
  "element(",
  "-moz-element(",
  "paint(",
  "@import",
  "expression(",
] as const;
const SAFE_COLOR_KEYWORDS = new Set(["black", "white", "transparent", "currentcolor"]);
const SAFE_COLOR_FUNCTION_PATTERN =
  /^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([a-z0-9+\-.,/%\s]+\)$/i;
const SAFE_HEX_COLOR_PATTERN = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
const SAFE_FONT_FAMILY_PATTERN = /^[a-z0-9\s,'"._-]+(?:,\s*[a-z0-9\s'"._-]+)*$/i;

const MODE_TOKEN_ORDER = [
  "bg",
  "bg-accent",
  "bg-elevated",
  "bg-hover",
  "bg-muted",
  "bg-content",
  "card",
  "card-foreground",
  "card-highlight",
  "popover",
  "popover-foreground",
  "panel",
  "panel-strong",
  "panel-hover",
  "chrome",
  "chrome-strong",
  "text",
  "text-strong",
  "chat-text",
  "muted",
  "muted-strong",
  "muted-foreground",
  "border",
  "border-strong",
  "border-hover",
  "input",
  "ring",
  "accent",
  "accent-hover",
  "accent-muted",
  "accent-subtle",
  "accent-foreground",
  "accent-glow",
  "primary",
  "primary-foreground",
  "secondary",
  "secondary-foreground",
  "accent-2",
  "accent-2-muted",
  "accent-2-subtle",
  "destructive",
  "destructive-foreground",
  "danger",
  "danger-muted",
  "danger-subtle",
  "focus",
  "focus-ring",
  "focus-glow",
  "font-body",
  "font-display",
  "mono",
  "grid-line",
] as const;

type ModeTokenName = (typeof MODE_TOKEN_ORDER)[number];
type ThemeTokenMap = Record<ModeTokenName, string>;

const REQUIRED_TWEAKCN_MODE_VARS = [
  "background",
  "foreground",
  "card",
  "card-foreground",
  "popover",
  "popover-foreground",
  "primary",
  "primary-foreground",
  "secondary",
  "secondary-foreground",
  "muted",
  "muted-foreground",
  "accent",
  "accent-foreground",
  "destructive",
  "destructive-foreground",
  "border",
  "input",
  "ring",
] as const;
type RequiredTweakcnModeVar = (typeof REQUIRED_TWEAKCN_MODE_VARS)[number];

export type ImportedCustomTheme = {
  sourceUrl: string;
  themeId: string;
  label: string;
  importedAt: string;
  light: ThemeTokenMap;
  dark: ThemeTokenMap;
};

const cssTokenSchema = z.string().max(MAX_CSS_TOKEN_LENGTH);

function createStringShape<const T extends readonly string[]>(keys: T) {
  return Object.fromEntries(keys.map((key) => [key, cssTokenSchema])) as Record<
    T[number],
    typeof cssTokenSchema
  >;
}

const tweakcnThemeSchema = z.object({
  name: z.string().max(80).optional(),
  cssVars: z.object({
    theme: z
      .object({
        "font-sans": cssTokenSchema.optional(),
        "font-mono": cssTokenSchema.optional(),
      })
      .optional(),
    light: z.object(createStringShape(REQUIRED_TWEAKCN_MODE_VARS)),
    dark: z.object(createStringShape(REQUIRED_TWEAKCN_MODE_VARS)),
  }),
});

const importedCustomThemeSchema = z.object({
  sourceUrl: z.string(),
  themeId: z.string(),
  label: z.string(),
  importedAt: z.string(),
  light: z.object(createStringShape(MODE_TOKEN_ORDER)),
  dark: z.object(createStringShape(MODE_TOKEN_ORDER)),
});

type TweakcnThemePayload = z.infer<typeof tweakcnThemeSchema>;

type TweakcnThemeResolution = {
  sourceUrl: string;
  fetchUrl: string;
  themeId: string;
};

function requireThemeId(value: string) {
  if (!THEME_ID_PATTERN.test(value)) {
    throw new Error("Unsupported tweakcn link. Expected a theme share URL.");
  }
}

function normalizeThemeIdFromPath(pathname: string): string {
  const segments = pathname.split("/").filter(Boolean);
  if (segments.length === 2 && segments[0] === "themes") {
    requireThemeId(segments[1]);
    return segments[1];
  }
  if (segments.length === 3 && segments[0] === "r" && segments[1] === "themes") {
    requireThemeId(segments[2]);
    return segments[2];
  }
  throw new Error("Unsupported tweakcn link. Expected a theme share URL.");
}

function requireSafeCssValue(value: unknown, label: string) {
  const normalized = normalizeOptionalString(value);
  if (!normalized) {
    throw new Error(`Unsupported tweakcn token: ${label}`);
  }
  if (normalized.length > MAX_CSS_TOKEN_LENGTH) {
    throw new Error(`Unsupported tweakcn token: ${label}`);
  }
  const lowered = normalized.toLowerCase();
  if (FORBIDDEN_CSS_VALUE_PARTS.some((part) => lowered.includes(part))) {
    throw new Error(`Unsupported tweakcn token: ${label}`);
  }
  if (normalized.includes("/*") || normalized.includes("*/") || normalized.includes("\\")) {
    throw new Error(`Unsupported tweakcn token: ${label}`);
  }
  for (const char of normalized) {
    const code = char.charCodeAt(0);
    if (
      code < 0x20 ||
      code === 0x7f ||
      char === "{" ||
      char === "}" ||
      char === ";" ||
      char === "<" ||
      char === ">" ||
      char === "`"
    ) {
      throw new Error(`Unsupported tweakcn token: ${label}`);
    }
  }
  return normalized;
}

function requireSafeExternalColorValue(value: unknown, label: string) {
  const normalized = requireSafeCssValue(value, label);
  const lowered = normalized.toLowerCase();
  if (
    SAFE_COLOR_KEYWORDS.has(lowered) ||
    SAFE_HEX_COLOR_PATTERN.test(normalized) ||
    SAFE_COLOR_FUNCTION_PATTERN.test(normalized)
  ) {
    return normalized;
  }
  throw new Error(`Unsupported tweakcn token: ${label}`);
}

function requireSafeFontFamilyValue(value: unknown, label: string) {
  const normalized = requireSafeCssValue(value, label);
  if (
    normalized.includes("(") ||
    normalized.includes(")") ||
    !SAFE_FONT_FAMILY_PATTERN.test(normalized)
  ) {
    throw new Error(`Unsupported tweakcn token: ${label}`);
  }
  return normalized;
}

function requireSafeExternalModeValue(value: unknown, label: string) {
  if (label === "font-sans" || label === "font-mono") {
    return requireSafeFontFamilyValue(value, label);
  }
  return requireSafeExternalColorValue(value, label);
}

function makeTokenMap(entries: Array<[ModeTokenName, string]>): ThemeTokenMap {
  return Object.fromEntries(entries) as ThemeTokenMap;
}

function normalizeStoredTokenMap(value: Record<string, string> | undefined): ThemeTokenMap | null {
  if (!value || typeof value !== "object") {
    return null;
  }
  const entries: Array<[ModeTokenName, string]> = [];
  for (const key of MODE_TOKEN_ORDER) {
    const normalized =
      key === "font-body" || key === "font-display" || key === "mono"
        ? requireSafeFontFamilyValue(value[key], key)
        : requireSafeCssValue(value[key], key);
    entries.push([key, normalized]);
  }
  return makeTokenMap(entries);
}

function resolveModeVar(
  theme: Record<string, string | undefined>,
  shared: Record<string, string | undefined> | undefined,
  key: string,
  fallback?: string,
) {
  const themeValue = normalizeOptionalString(theme[key]);
  if (themeValue) {
    return requireSafeExternalModeValue(themeValue, key);
  }
  const sharedValue = normalizeOptionalString(shared?.[key]);
  if (sharedValue) {
    return requireSafeExternalModeValue(sharedValue, key);
  }
  if (fallback != null) {
    return key === "font-sans" || key === "font-mono"
      ? requireSafeFontFamilyValue(fallback, key)
      : requireSafeCssValue(fallback, key);
  }
  throw new Error(`tweakcn theme is missing required token: ${key}`);
}

function normalizeModeTokenMap(
  mode: "light" | "dark",
  theme: Record<RequiredTweakcnModeVar, string>,
  shared: Record<string, string | undefined> | undefined,
): ThemeTokenMap {
  const isLight = mode === "light";
  const contrastTarget = isLight ? "black" : "white";
  const background = resolveModeVar(theme, shared, "background");
  const foreground = resolveModeVar(theme, shared, "foreground");
  const card = resolveModeVar(theme, shared, "card");
  const cardForeground = resolveModeVar(theme, shared, "card-foreground");
  const popover = resolveModeVar(theme, shared, "popover");
  const popoverForeground = resolveModeVar(theme, shared, "popover-foreground");
  const primary = resolveModeVar(theme, shared, "primary");
  const primaryForeground = resolveModeVar(theme, shared, "primary-foreground");
  const secondary = resolveModeVar(theme, shared, "secondary");
  const secondaryForeground = resolveModeVar(theme, shared, "secondary-foreground");
  const muted = resolveModeVar(theme, shared, "muted");
  const mutedForeground = resolveModeVar(theme, shared, "muted-foreground");
  const accent = resolveModeVar(theme, shared, "accent");
  const accentForeground = resolveModeVar(theme, shared, "accent-foreground");
  const destructive = resolveModeVar(theme, shared, "destructive");
  const destructiveForeground = resolveModeVar(theme, shared, "destructive-foreground");
  const border = resolveModeVar(theme, shared, "border");
  const input = resolveModeVar(theme, shared, "input");
  const ring = resolveModeVar(theme, shared, "ring");
  const fontBody = resolveModeVar(theme, shared, "font-sans", DEFAULT_FONT_BODY);
  const mono = resolveModeVar(theme, shared, "font-mono", DEFAULT_MONO);

  return makeTokenMap([
    ["bg", background],
    ["bg-accent", "color-mix(in srgb, var(--bg) 88%, var(--card) 12%)"],
    ["bg-elevated", card],
    ["bg-hover", "color-mix(in srgb, var(--muted) 68%, var(--bg) 32%)"],
    ["bg-muted", muted],
    ["bg-content", "color-mix(in srgb, var(--bg) 92%, var(--card) 8%)"],
    ["card", card],
    ["card-foreground", cardForeground],
    ["card-highlight", `color-mix(in srgb, var(--text) ${isLight ? "3" : "5"}%, transparent)`],
    ["popover", popover],
    ["popover-foreground", popoverForeground],
    ["panel", background],
    ["panel-strong", card],
    ["panel-hover", "color-mix(in srgb, var(--card) 76%, var(--muted) 24%)"],
    ["chrome", "color-mix(in srgb, var(--bg) 96%, transparent)"],
    ["chrome-strong", "color-mix(in srgb, var(--bg) 98%, transparent)"],
    ["text", foreground],
    ["text-strong", foreground],
    ["chat-text", foreground],
    ["muted", mutedForeground],
    ["muted-strong", "color-mix(in srgb, var(--muted) 84%, var(--text) 16%)"],
    ["muted-foreground", mutedForeground],
    ["border", border],
    ["border-strong", "color-mix(in srgb, var(--border) 72%, var(--text) 28%)"],
    ["border-hover", "color-mix(in srgb, var(--border) 55%, var(--text) 45%)"],
    ["input", input],
    ["ring", ring],
    ["accent", accent],
    ["accent-hover", `color-mix(in srgb, var(--accent) 82%, ${contrastTarget} 18%)`],
    ["accent-muted", accent],
    ["accent-subtle", `color-mix(in srgb, var(--accent) ${isLight ? "10" : "16"}%, transparent)`],
    ["accent-foreground", accentForeground],
    ["accent-glow", `color-mix(in srgb, var(--accent) ${isLight ? "18" : "30"}%, transparent)`],
    ["primary", primary],
    ["primary-foreground", primaryForeground],
    ["secondary", secondary],
    ["secondary-foreground", secondaryForeground],
    ["accent-2", primary],
    ["accent-2-muted", "color-mix(in srgb, var(--accent-2) 72%, transparent)"],
    [
      "accent-2-subtle",
      `color-mix(in srgb, var(--accent-2) ${isLight ? "8" : "12"}%, transparent)`,
    ],
    ["destructive", destructive],
    ["destructive-foreground", destructiveForeground],
    ["danger", destructive],
    ["danger-muted", "color-mix(in srgb, var(--danger) 75%, transparent)"],
    ["danger-subtle", `color-mix(in srgb, var(--danger) ${isLight ? "8" : "12"}%, transparent)`],
    ["focus", `color-mix(in srgb, var(--ring) ${isLight ? "14" : "22"}%, transparent)`],
    [
      "focus-ring",
      `0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) ${isLight ? "70" : "80"}%, transparent)`,
    ],
    ["focus-glow", "0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow)"],
    ["font-body", fontBody],
    ["font-display", fontBody],
    ["mono", mono],
    ["grid-line", `color-mix(in srgb, var(--text) ${isLight ? "4" : "3"}%, transparent)`],
  ]);
}

function describeThemeLabel(value: string | undefined) {
  const normalized = normalizeOptionalString(value);
  if (!normalized) {
    return "Custom";
  }
  return normalized.slice(0, 80);
}

export function normalizeTweakcnThemeUrl(input: string): TweakcnThemeResolution {
  const normalized = normalizeOptionalString(input);
  if (!normalized) {
    throw new Error("Paste a tweakcn theme link to import.");
  }
  let parsed: URL;
  try {
    parsed = new URL(normalized);
  } catch {
    throw new Error("Paste a full tweakcn URL.");
  }
  if (!TWEAKCN_HOSTS.has(parsed.hostname)) {
    throw new Error("Only tweakcn.com theme links are supported.");
  }
  const themeId = normalizeThemeIdFromPath(parsed.pathname);
  return {
    themeId,
    sourceUrl: `https://tweakcn.com/themes/${themeId}`,
    fetchUrl: `https://tweakcn.com/r/themes/${themeId}`,
  };
}

export function parseImportedCustomTheme(value: unknown): ImportedCustomTheme | null {
  const parsed = importedCustomThemeSchema.safeParse(value);
  if (!parsed.success) {
    return null;
  }
  try {
    requireThemeId(parsed.data.themeId);
    const light = normalizeStoredTokenMap(parsed.data.light);
    const dark = normalizeStoredTokenMap(parsed.data.dark);
    if (!light || !dark) {
      return null;
    }
    return {
      sourceUrl: parsed.data.sourceUrl,
      themeId: parsed.data.themeId,
      label: describeThemeLabel(parsed.data.label),
      importedAt: parsed.data.importedAt,
      light,
      dark,
    };
  } catch {
    return null;
  }
}

export function normalizeImportedCustomTheme(
  payload: unknown,
  resolution: Pick<TweakcnThemeResolution, "sourceUrl" | "themeId">,
): ImportedCustomTheme {
  const parsed = tweakcnThemeSchema.safeParse(payload);
  if (!parsed.success) {
    throw new Error("tweakcn returned an invalid theme payload.");
  }
  const data: TweakcnThemePayload = parsed.data;
  const shared = data.cssVars.theme;
  return {
    sourceUrl: resolution.sourceUrl,
    themeId: resolution.themeId,
    label: describeThemeLabel(data.name),
    importedAt: new Date().toISOString(),
    light: normalizeModeTokenMap("light", data.cssVars.light, shared),
    dark: normalizeModeTokenMap("dark", data.cssVars.dark, shared),
  };
}

function assertTweakcnResponseUrl(value: string | undefined) {
  if (!value) {
    return;
  }
  let parsed: URL;
  try {
    parsed = new URL(value);
  } catch {
    throw new Error("Unexpected tweakcn import response URL.");
  }
  if (parsed.protocol !== "https:" || !TWEAKCN_HOSTS.has(parsed.hostname)) {
    throw new Error("Unexpected redirect during tweakcn import.");
  }
}

function parseContentLength(headers: Headers): number | null {
  const raw = headers.get("content-length");
  if (!raw) {
    return null;
  }
  const parsed = Number(raw);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
}

async function readResponseTextWithLimit(response: Response): Promise<string> {
  const contentLength = parseContentLength(response.headers);
  if (contentLength != null && contentLength > MAX_TWEAKCN_THEME_BYTES) {
    throw new Error("tweakcn theme payload is too large.");
  }

  if (!response.body) {
    throw new Error("tweakcn returned an unreadable theme payload.");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let bytes = 0;
  let text = "";
  try {
    while (true) {
      const chunk = await reader.read();
      if (chunk.done) {
        break;
      }
      bytes += chunk.value.byteLength;
      if (bytes > MAX_TWEAKCN_THEME_BYTES) {
        await reader.cancel().catch(() => undefined);
        throw new Error("tweakcn theme payload is too large.");
      }
      text += decoder.decode(chunk.value, { stream: true });
    }
    text += decoder.decode();
    return text;
  } finally {
    reader.releaseLock();
  }
}

async function readJsonResponseWithLimit(response: Response): Promise<unknown> {
  const text = await readResponseTextWithLimit(response);
  try {
    return JSON.parse(text) as unknown;
  } catch {
    throw new Error("tweakcn returned invalid JSON.");
  }
}

export async function importCustomThemeFromUrl(
  input: string,
  fetchImpl: typeof fetch = fetch,
): Promise<ImportedCustomTheme> {
  const resolution = normalizeTweakcnThemeUrl(input);
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), TWEAKCN_FETCH_TIMEOUT_MS);
  try {
    const response = await fetchImpl(resolution.fetchUrl, {
      headers: { accept: "application/json" },
      redirect: "error",
      signal: controller.signal,
    });
    assertTweakcnResponseUrl(response.url);
    if (!response.ok) {
      throw new Error(`tweakcn import failed (${response.status}).`);
    }
    const payload = await readJsonResponseWithLimit(response);
    return normalizeImportedCustomTheme(payload, resolution);
  } catch (error) {
    if (controller.signal.aborted) {
      throw new Error("tweakcn import timed out.", { cause: error });
    }
    throw error;
  } finally {
    clearTimeout(timeout);
  }
}

export function buildCustomThemeStyles(theme: ImportedCustomTheme) {
  const light = normalizeStoredTokenMap(theme.light);
  const dark = normalizeStoredTokenMap(theme.dark);
  if (!light || !dark) {
    throw new Error("Stored custom theme is missing required tokens.");
  }
  const renderDeclarations = (modeTokens: ThemeTokenMap) =>
    MODE_TOKEN_ORDER.map((key) => `  --${key}: ${modeTokens[key]};`).join("\n");
  return [
    `:root[data-theme="custom"] {`,
    renderDeclarations(dark),
    `}`,
    `:root[data-theme="custom-light"] {`,
    renderDeclarations(light),
    `}`,
  ].join("\n");
}

export function syncCustomThemeStyleTag(theme: ImportedCustomTheme | null | undefined) {
  if (typeof document === "undefined") {
    return;
  }
  let style = document.getElementById(CUSTOM_THEME_STYLE_ID) as HTMLStyleElement | null;
  if (!theme) {
    style?.remove();
    return;
  }
  let cssText = "";
  try {
    cssText = buildCustomThemeStyles(theme);
  } catch {
    style?.remove();
    return;
  }
  if (!cssText) {
    style?.remove();
    return;
  }
  if (!style) {
    style = document.createElement("style");
    style.id = CUSTOM_THEME_STYLE_ID;
    document.head.appendChild(style);
  }
  style.textContent = cssText;
}

¤ Dauer der Verarbeitung: 0.1 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.