chore: ARTEX 初始化
ARTEX — AI 自主渗透测试系统(Go 后端 + Next.js 前端,单二进制内嵌)。 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
commit
0ef8561411
287 files changed
+70178
No files matched your search
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Script: generate-theme-presets.ts
|
||||
*
|
||||
* This script scans the /styles/presets directory for CSS files containing theme definitions.
|
||||
* It extracts `label:`, `value:`, and primary color definitions (`--primary`) for both light and dark modes.
|
||||
* These primary colors are used to visually represent each theme in the UI (e.g., colored dots or theme previews).
|
||||
* Default theme colors are fetched from /app/globals.css.
|
||||
* All extracted metadata is injected into a marked section of the /lib/preferences/theme.ts file.
|
||||
*
|
||||
* Usage:
|
||||
* - During local development, run manually after adding any new theme preset:
|
||||
* npm run generate:presets
|
||||
* - Ensure that each new CSS preset includes `label:` and `value:` comments.
|
||||
* - This generation step is currently automated using a Husky pre-push hook.
|
||||
* - You may optionally integrate it directly into a build step if preferred.
|
||||
*/
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const presetDir = path.resolve(__dirname, "../styles/presets");
|
||||
|
||||
if (!fs.existsSync(presetDir)) {
|
||||
console.error(`❌ Preset directory not found at: ${presetDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputPath = path.resolve(__dirname, "../lib/preferences/theme.ts");
|
||||
|
||||
const files = fs.readdirSync(presetDir).filter((file) => file.endsWith(".css"));
|
||||
|
||||
if (files.length === 0) {
|
||||
console.warn("⚠️ No preset CSS files found. Only default preset will be included.");
|
||||
}
|
||||
|
||||
const presets = files.map((file) => {
|
||||
const filePath = path.join(presetDir, file);
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
const labelMatch = content.match(/label:\s*(.+)/);
|
||||
const valueMatch = content.match(/value:\s*(.+)/);
|
||||
|
||||
if (!labelMatch) {
|
||||
console.warn(`⚠️ No 'label:' found in ${file}, using filename as fallback.`);
|
||||
}
|
||||
if (!valueMatch) {
|
||||
console.warn(`⚠️ No 'value:' found in ${file}, using filename as fallback.`);
|
||||
}
|
||||
|
||||
const label = labelMatch?.[1]?.trim() ?? file.replace(".css", "");
|
||||
const value = valueMatch?.[1]?.trim() ?? file.replace(".css", "");
|
||||
|
||||
const lightPrimaryMatch = content.match(/:root\[data-theme-preset="[^"]*"\][\s\S]*?--primary:\s*([^;]+);/);
|
||||
const darkPrimaryMatch = content.match(/\.dark:root\[data-theme-preset="[^"]*"\][\s\S]*?--primary:\s*([^;]+);/);
|
||||
|
||||
const primary = {
|
||||
light: lightPrimaryMatch?.[1]?.trim() ?? "",
|
||||
dark: darkPrimaryMatch?.[1]?.trim() ?? "",
|
||||
};
|
||||
|
||||
if (!lightPrimaryMatch || !darkPrimaryMatch) {
|
||||
console.warn(`⚠️ Missing --primary for ${file} (light or dark). Check CSS syntax.`);
|
||||
}
|
||||
|
||||
return { label, value, primary };
|
||||
});
|
||||
|
||||
const globalStylesPath = path.resolve(__dirname, "../app/globals.css");
|
||||
|
||||
let globalContent = "";
|
||||
try {
|
||||
globalContent = fs.readFileSync(globalStylesPath, "utf8");
|
||||
} catch (err) {
|
||||
console.error(`❌ Could not read globals.css at ${globalStylesPath}`);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const defaultLightPrimaryRegex = /:root\s*{[^}]*--primary:\s*([^;]+);/;
|
||||
const defaultDarkPrimaryRegex = /\.dark\s*{[^}]*--primary:\s*([^;]+);/;
|
||||
|
||||
const defaultLightPrimaryMatch = defaultLightPrimaryRegex.exec(globalContent);
|
||||
const defaultDarkPrimaryMatch = defaultDarkPrimaryRegex.exec(globalContent);
|
||||
|
||||
const defaultPrimary = {
|
||||
light: defaultLightPrimaryMatch?.[1]?.trim() ?? "",
|
||||
dark: defaultDarkPrimaryMatch?.[1]?.trim() ?? "",
|
||||
};
|
||||
|
||||
presets.unshift({ label: "Default", value: "default", primary: defaultPrimary });
|
||||
|
||||
const generatedBlock = `// --- generated:themePresets:start ---
|
||||
|
||||
export const THEME_PRESET_OPTIONS = ${JSON.stringify(presets, null, 2)} as const;
|
||||
|
||||
export const THEME_PRESET_VALUES = THEME_PRESET_OPTIONS.map((p) => p.value);
|
||||
|
||||
export type ThemePreset = (typeof THEME_PRESET_OPTIONS)[number]["value"];
|
||||
|
||||
// --- generated:themePresets:end ---`;
|
||||
|
||||
const fileContent = fs.readFileSync(outputPath, "utf8");
|
||||
|
||||
const updated = fileContent.replace(
|
||||
/\/\/ --- generated:themePresets:start ---[\s\S]*?\/\/ --- generated:themePresets:end ---/,
|
||||
generatedBlock,
|
||||
);
|
||||
|
||||
function main() {
|
||||
const biomeBin = require.resolve("@biomejs/biome/bin/biome");
|
||||
const formatted = execFileSync(process.execPath, [biomeBin, "format", "--stdin-file-path", outputPath], {
|
||||
input: updated,
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
if (formatted === fileContent) {
|
||||
console.log("ℹ️ No changes in theme.ts");
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, formatted);
|
||||
console.log("✅ theme.ts updated with new theme presets");
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error("❌ Unexpected error while generating theme presets:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Boot script that reads user preference values (theme mode, theme preset,
|
||||
* content layout, navbar style) from cookies or localStorage based on the
|
||||
* configured persistence mode.
|
||||
*
|
||||
* Runs early in <head> to apply the correct data attributes before hydration,
|
||||
* preventing layout or theme flicker and keeping RootLayout fully static.
|
||||
*/
|
||||
import { PREFERENCE_DEFAULTS, PREFERENCE_PERSISTENCE } from "@/lib/preferences/preferences-config";
|
||||
|
||||
export function ThemeBootScript() {
|
||||
const persistence = JSON.stringify({
|
||||
theme_mode: PREFERENCE_PERSISTENCE.theme_mode,
|
||||
theme_preset: PREFERENCE_PERSISTENCE.theme_preset,
|
||||
font: PREFERENCE_PERSISTENCE.font,
|
||||
content_layout: PREFERENCE_PERSISTENCE.content_layout,
|
||||
navbar_style: PREFERENCE_PERSISTENCE.navbar_style,
|
||||
sidebar_variant: PREFERENCE_PERSISTENCE.sidebar_variant,
|
||||
sidebar_collapsible: PREFERENCE_PERSISTENCE.sidebar_collapsible,
|
||||
});
|
||||
|
||||
const defaults = JSON.stringify({
|
||||
theme_mode: PREFERENCE_DEFAULTS.theme_mode,
|
||||
theme_preset: PREFERENCE_DEFAULTS.theme_preset,
|
||||
font: PREFERENCE_DEFAULTS.font,
|
||||
content_layout: PREFERENCE_DEFAULTS.content_layout,
|
||||
navbar_style: PREFERENCE_DEFAULTS.navbar_style,
|
||||
sidebar_variant: PREFERENCE_DEFAULTS.sidebar_variant,
|
||||
sidebar_collapsible: PREFERENCE_DEFAULTS.sidebar_collapsible,
|
||||
});
|
||||
|
||||
const code = `
|
||||
(function () {
|
||||
try {
|
||||
var root = document.documentElement;
|
||||
var PERSISTENCE = ${persistence};
|
||||
var DEFAULTS = ${defaults};
|
||||
|
||||
function readCookie(name) {
|
||||
var match = document.cookie.split("; ").find(function(c) {
|
||||
return c.startsWith(name + "=");
|
||||
});
|
||||
return match ? decodeURIComponent(match.split("=")[1]) : null;
|
||||
}
|
||||
|
||||
function readLocal(name) {
|
||||
try {
|
||||
return window.localStorage.getItem(name);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readPreference(key, fallback) {
|
||||
var mode = PERSISTENCE[key];
|
||||
var value = null;
|
||||
|
||||
if (mode === "localStorage") {
|
||||
value = readLocal(key);
|
||||
}
|
||||
|
||||
if (!value && (mode === "client-cookie" || mode === "server-cookie")) {
|
||||
value = readCookie(key);
|
||||
}
|
||||
|
||||
if (!value || typeof value !== "string") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
var rawMode = readPreference("theme_mode", DEFAULTS.theme_mode);
|
||||
var rawPreset = readPreference("theme_preset", DEFAULTS.theme_preset);
|
||||
var rawFont = readPreference("font", DEFAULTS.font);
|
||||
var rawContentLayout = readPreference("content_layout", DEFAULTS.content_layout);
|
||||
var rawNavbarStyle = readPreference("navbar_style", DEFAULTS.navbar_style);
|
||||
var rawSidebarVariant = readPreference("sidebar_variant", DEFAULTS.sidebar_variant);
|
||||
var rawSidebarCollapsible = readPreference("sidebar_collapsible", DEFAULTS.sidebar_collapsible);
|
||||
|
||||
var isValidMode = rawMode === "dark" || rawMode === "light" || rawMode === "system";
|
||||
var mode = isValidMode ? rawMode : DEFAULTS.theme_mode;
|
||||
var resolvedMode =
|
||||
mode === "system" && window.matchMedia
|
||||
? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
: mode;
|
||||
var preset = rawPreset || DEFAULTS.theme_preset;
|
||||
var font = rawFont || DEFAULTS.font;
|
||||
var contentLayout = rawContentLayout || DEFAULTS.content_layout;
|
||||
var navbarStyle = rawNavbarStyle || DEFAULTS.navbar_style;
|
||||
var sidebarVariant = rawSidebarVariant || DEFAULTS.sidebar_variant;
|
||||
var sidebarCollapsible = rawSidebarCollapsible || DEFAULTS.sidebar_collapsible;
|
||||
|
||||
root.classList.toggle("dark", resolvedMode === "dark");
|
||||
root.setAttribute("data-theme-mode", mode);
|
||||
root.setAttribute("data-theme-preset", preset);
|
||||
root.setAttribute("data-font", font);
|
||||
root.setAttribute("data-content-layout", contentLayout);
|
||||
root.setAttribute("data-navbar-style", navbarStyle);
|
||||
root.setAttribute("data-sidebar-variant", sidebarVariant);
|
||||
root.setAttribute("data-sidebar-collapsible", sidebarCollapsible);
|
||||
|
||||
root.style.colorScheme = resolvedMode === "dark" ? "dark" : "light";
|
||||
|
||||
} catch (e) {
|
||||
console.warn("ThemeBootScript error:", e);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
/* biome-ignore lint/security/noDangerouslySetInnerHtml: required for pre-hydration boot script */
|
||||
return <script dangerouslySetInnerHTML={{ __html: code }} />;
|
||||
}
|
||||
Reference in new issue
Block a user