Files

694 lines
40 KiB
HTML
Raw Permalink Normal View History

2026-07-12 21:17:31 +02:00
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debian 13 Bootstrap Configurator</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- React & ReactDOM -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Babel for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="antialiased h-screen flex flex-col font-mono text-sm overflow-hidden">
<div id="root" class="flex-1 flex flex-col h-full"></div>
<script type="text/babel">
const { useState, useEffect } = React;
// ==============================================================================
// 1. UI & THEME CONFIGURATION (Texte, Farben, Bilder)
// ==============================================================================
const uiConfigJSON = {
text: {
appTitle: "Debian 13 // JSON Configurator",
logoLetter: "D",
navTitle: "Nav-Gruppe: Checkliste",
pipelineTitle: "Pipeline: Script Output",
btnCopyDefault: "[ SICHTBARE MODULE KOPIEREN ]",
btnCopySuccess: "[ KOMPLETTES SKRIPT KOPIERT ]",
emptyStateTitle: "[ KEINE AKTIVEN/SICHTBAREN MODULE ]",
emptyStateSub: "Bitte wähle links Einträge aus oder klappe die Navigation auf.",
logMessage: ">> LOG: Generiere {{count}} sichtbare Pipeline-Schritte..."
},
theme: {
accent: "#10b981", // Haupt-Akzent (Emerald Green)
accentHover: "#34d399",
accentDark: "#064e3b",
bgApp: "#05080f", // Tiefster Hintergrund
bgPanelDark: "#0f172a", // Dunkler Kasten-Hintergrund
bgPanelLight: "#1e293b", // Heller Kasten-Hintergrund
textMain: "#f8fafc", // Helle Schrift
textMuted: "#94a3b8", // Abgedunkelte Schrift
wmBorderLight: "#94a3b8", // Wmaker 3D Rand (Hell)
wmBorderDark: "#0f172a", // Wmaker 3D Rand (Dunkel)
wmBorderMedium: "#64748b" // Wmaker Rahmenfarbe
},
images: {
// Ein cyber-artiges Terminal-Bild für den Header (Unsplash)
headerBg: "https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?q=80&w=1200&auto=format&fit=crop"
}
};
// ==============================================================================
// 2. MASTER SETUP CONFIGURATION (Inhalte, Pakete, Befehle)
// ==============================================================================
const setupConfigJSON = [
{
id: "basis",
title: "Basis & System",
items: [
{
id: "baseUpdate", type: "checkbox", label: "apt update & upgrade", defaultChecked: true,
commands: [ { text: "Paketquellen aktualisieren und System upgraden", code: "apt update && apt upgrade -y" } ]
},
{
id: "baseDeps", type: "checkbox", label: "Curl, Wget, Git, GnuPG", defaultChecked: true,
commands: [
{ text: "Curl, Git, GnuPG und Zertifikate installieren", code: "apt install -y curl wget git build-essential apt-transport-https ca-certificates gnupg software-properties-common" },
{ text: "Apt Keyring-Verzeichnis sicher anlegen", code: "install -m 0755 -d /etc/apt/keyrings" }
]
}
]
},
{
id: "terminal",
title: "Terminal & UI",
items: [
{
id: "keyboardUsIntl", type: "checkbox", label: "US Tastatur mit Umlauten", defaultChecked: true,
commands: [
{ text: "Konfiguration schreiben", code: "cat << 'EOF' > /etc/default/keyboard\nXKBMODEL=\"pc105\"\nXKBLAYOUT=\"us\"\nXKBVARIANT=\"altgr-intl\"\nXKBOPTIONS=\"\"\nBACKSPACE=\"guess\"\nEOF" },
{ text: "System-Dienste neu laden", code: "dpkg-reconfigure -f noninteractive keyboard-configuration\nsetupcon || true" }
]
},
{
id: "promptColor", type: "select", label: "Bash Prompt (PS1)", defaultSelect: "green",
options: [
{ value: "none", label: "STANDARD" },
{ value: "green", label: "GRÜN (HACKER STYLE)", meta: "\\[\\033[01;32m\\]" },
{ value: "blue", label: "BLAU (CLEAN)", meta: "\\[\\033[01;34m\\]" },
{ value: "cyan", label: "CYAN (MODERN)", meta: "\\[\\033[01;36m\\]" },
{ value: "red", label: "ROT (WARNING/ROOT)", meta: "\\[\\033[01;31m\\]" }
],
commandTemplate: [
{ text: "PS1 Variable in ~/.bashrc anhängen", code: "TARGET_USER=\"${SUDO_USER:-$USER}\"\nUSER_HOME=$(getent passwd \"$TARGET_USER\" | cut -d: -f6)\nif ! grep -q \"# BOOTSTRAP_PS1\" \"$USER_HOME/.bashrc\"; then\n echo -e \"\\n# BOOTSTRAP_PS1\\nPS1='{{meta}}\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\\\$ '\" >> \"$USER_HOME/.bashrc\"\nfi" }
]
}
]
},
{
id: "dev",
title: "Sprachen & Editoren",
items: [
{
id: "neovim", type: "checkbox_with_select", label: "Neovim", defaultChecked: false, selectId: "neovimSrc", selectLabel: "QUELLE:", selectDefault: "apt",
options: [ { value: "apt", label: "Debian Repo (Stable)" }, { value: "web", label: "Website (Latest)" } ],
commandsByValue: {
"apt": [ { text: "Installation von: neovim", code: "apt install -y neovim" } ],
"web": [
{ text: "Archiv laden & entpacken", code: "curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux64.tar.gz\nrm -rf /opt/nvim\ntar -C /opt -xzf nvim-linux64.tar.gz" },
{ text: "Globale Symlinks setzen", code: "ln -sf /opt/nvim-linux64/bin/nvim /usr/local/bin/nvim\nrm nvim-linux64.tar.gz" }
]
}
},
{
id: "golang", type: "checkbox_with_select", label: "Golang", defaultChecked: false, selectId: "golangSrc", selectLabel: "QUELLE:", selectDefault: "apt",
options: [ { value: "apt", label: "Debian Repo (Stable)" }, { value: "web", label: "Website (Latest)" } ],
commandsByValue: {
"apt": [ { text: "Installation von: golang", code: "apt install -y golang" } ],
"web": [
{ text: "Neueste Version ermitteln", code: "GO_LATEST=$(curl -sL https://go.dev/VERSION?m=text | head -n 1)\necho \"Lade ${GO_LATEST} herunter...\"" },
{ text: "Archiv laden & nach /usr/local entpacken", code: "curl -LO \"https://go.dev/dl/${GO_LATEST}.linux-amd64.tar.gz\"\nrm -rf /usr/local/go\ntar -C /usr/local -xzf \"${GO_LATEST}.linux-amd64.tar.gz\"" },
{ text: "Globale Symlinks setzen", code: "ln -sf /usr/local/go/bin/go /usr/local/bin/go\nln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt\nrm \"${GO_LATEST}.linux-amd64.tar.gz\"" }
]
}
},
{
id: "elixir", type: "checkbox_with_select", label: "Elixir & Erlang", defaultChecked: false, selectId: "elixirSrc", selectLabel: "QUELLE:", selectDefault: "apt",
options: [ { value: "apt", label: "Debian Repo (Stable)" }, { value: "web", label: "Website (Latest)" } ],
commandsByValue: {
"apt": [ { text: "Installation von: elixir erlang", code: "apt install -y elixir erlang" } ],
"web": [
{ text: "Erlang & Build-Tools vorbereiten", code: "apt install -y erlang make git" },
{ text: "Elixir Quellcode klonen & kompilieren", code: "rm -rf /usr/local/src/elixir\ngit clone https://github.com/elixir-lang/elixir.git /usr/local/src/elixir\ncd /usr/local/src/elixir\nmake install clean" },
{ text: "Globale Symlinks setzen", code: "ln -sf /usr/local/src/elixir/bin/elixir /usr/local/bin/elixir\nln -sf /usr/local/src/elixir/bin/elixirc /usr/local/bin/elixirc\nln -sf /usr/local/src/elixir/bin/iex /usr/local/bin/iex\nln -sf /usr/local/src/elixir/bin/mix /usr/local/bin/mix\ncd -" }
]
}
}
]
},
{
id: "thirdparty",
title: "Externe Quellen (GPG/Curl)",
items: [
{
id: "docker", type: "checkbox", label: "Docker (CE, CLI, Compose)", defaultChecked: false,
commands: [
{ text: "Docker GPG-Schlüssel speichern", code: "curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg\nchmod a+r /etc/apt/keyrings/docker.gpg" },
{ text: "Apt-Repository eintragen", code: "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null" },
{ text: "Docker installieren", code: "apt update\napt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin" },
{ text: "User in Docker-Gruppe aufnehmen", code: "TARGET_USER=\"${SUDO_USER:-$USER}\"\nusermod -aG docker \"$TARGET_USER\"" }
]
},
{
id: "nodejs", type: "checkbox_with_select", label: "Node.js", defaultChecked: false, selectId: "nodeVersion", selectLabel: "VER:", selectDefault: "20.x",
options: [ { value: "22.x", label: "22.x (Current)" }, { value: "20.x", label: "20.x (LTS)" }, { value: "18.x", label: "18.x (Legacy)" } ],
commandTemplate: [
{ text: "NodeSource GPG-Schlüssel importieren", code: "curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg" },
{ text: "Repository-Eintrag erstellen", code: "echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_{{selectValue}} nodistro main\" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null" },
{ text: "Node.js installieren", code: "apt update\napt install -y nodejs" }
]
},
{
id: "syncthing", type: "checkbox", label: "Syncthing", defaultChecked: false,
commands: [
{ text: "Syncthing Release Key importieren", code: "curl -fsSL https://syncthing.net/release-key.txt | gpg --dearmor -o /etc/apt/keyrings/syncthing-archive-keyring.gpg" },
{ text: "Repository hinzufügen", code: "echo \"deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] https://apt.syncthing.net/ syncthing stable\" | tee /etc/apt/sources.list.d/syncthing.list > /dev/null" },
{ text: "Syncthing installieren", code: "apt update\napt install -y syncthing" }
]
}
]
}
];
// ==============================================================================
// --- Dynamsiche CSS Injection basierend auf uiConfigJSON ---
const DynamicStyles = () => (
<style>{`
:root {
--c-accent: ${uiConfigJSON.theme.accent};
--c-accent-hover: ${uiConfigJSON.theme.accentHover};
--c-accent-dark: ${uiConfigJSON.theme.accentDark};
--c-bg-app: ${uiConfigJSON.theme.bgApp};
--c-bg-panel-dark: ${uiConfigJSON.theme.bgPanelDark};
--c-bg-panel-light: ${uiConfigJSON.theme.bgPanelLight};
--c-text-main: ${uiConfigJSON.theme.textMain};
--c-text-muted: ${uiConfigJSON.theme.textMuted};
--wm-light: ${uiConfigJSON.theme.wmBorderLight};
--wm-dark: ${uiConfigJSON.theme.wmBorderDark};
--wm-med: ${uiConfigJSON.theme.wmBorderMedium};
}
body { background-color: var(--c-bg-panel-dark); color: var(--c-text-main); }
.custom-scrollbar::-webkit-scrollbar { width: 12px; }
.custom-scrollbar::-webkit-scrollbar-track { background: var(--c-bg-panel-dark); border-left: 2px solid var(--c-bg-panel-light); }
.custom-scrollbar::-webkit-scrollbar-thumb { background: var(--wm-med); border: 2px solid; border-top-color: var(--wm-light); border-left-color: var(--wm-light); border-bottom-color: var(--wm-dark); border-right-color: var(--wm-dark); }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: var(--c-text-muted); }
/* Wmaker Components */
.wm-btn {
background-color: var(--wm-med);
color: var(--c-text-main);
border: 2px solid;
border-top-color: var(--wm-light);
border-left-color: var(--wm-light);
border-bottom-color: var(--wm-dark);
border-right-color: var(--wm-dark);
text-transform: uppercase;
font-weight: bold;
padding: 4px 12px;
cursor: pointer;
}
.wm-btn:active, .wm-btn-active-state {
border-top-color: var(--wm-dark);
border-left-color: var(--wm-dark);
border-bottom-color: var(--wm-light);
border-right-color: var(--wm-light);
background-color: var(--c-bg-panel-light);
}
.wm-btn-success {
background-color: var(--c-accent-dark);
border-top-color: var(--c-accent-hover);
border-left-color: var(--c-accent-hover);
border-bottom-color: var(--wm-dark);
border-right-color: var(--wm-dark);
}
.wm-btn-success:active {
border-top-color: var(--wm-dark);
border-left-color: var(--wm-dark);
border-bottom-color: var(--c-accent-hover);
border-right-color: var(--c-accent-hover);
}
.wm-block {
border: 2px solid var(--wm-med);
background-color: var(--c-bg-panel-light);
box-shadow: 4px 4px 0px rgba(0,0,0,0.8);
}
.wm-titlebar {
background: linear-gradient(to right, var(--c-bg-panel-light), var(--c-bg-panel-dark));
border-bottom: 2px solid var(--wm-med);
}
.wm-header-bg {
background-image: linear-gradient(to right, rgba(15, 23, 42, 0.95), rgba(15, 23, 42, 0.7)), url('${uiConfigJSON.images.headerBg}');
background-size: cover;
background-position: center;
}
.wm-checkbox-inner {
border: 2px solid;
border-top-color: var(--wm-dark);
border-left-color: var(--wm-dark);
border-bottom-color: var(--wm-light);
border-right-color: var(--wm-light);
background-color: var(--c-bg-panel-light);
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.wm-checkbox-inner.checked { background-color: var(--c-accent); }
.wm-input {
border: 2px solid;
border-top-color: var(--wm-dark);
border-left-color: var(--wm-dark);
border-bottom-color: var(--wm-light);
border-right-color: var(--wm-light);
background-color: var(--wm-dark);
color: var(--c-text-main);
outline: none;
}
.wm-toggle-box {
width: 16px;
height: 16px;
border: 2px solid;
border-top-color: var(--c-text-muted);
border-left-color: var(--c-text-muted);
border-bottom-color: var(--wm-dark);
border-right-color: var(--wm-dark);
background-color: var(--wm-med);
display: flex;
align-items: center;
justify-content: center;
}
.wm-toggle-box:active {
border-top-color: var(--wm-dark);
border-left-color: var(--wm-dark);
border-bottom-color: var(--c-text-muted);
border-right-color: var(--c-text-muted);
}
/* Helfer-Klassen für dynamische Farben */
.text-accent { color: var(--c-accent); }
.bg-accent { background-color: var(--c-accent); }
`}</style>
);
// --- UI Komponenten ---
const CommandItem = ({ command }) => {
const [copied, setCopied] = useState(false);
const copyCode = () => {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(command.code).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
} else {
const textArea = document.createElement("textarea");
textArea.value = command.code;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) { }
document.body.removeChild(textArea);
}
};
return (
<div className="mb-4 last:mb-0">
<div className="flex items-center justify-between mb-1" style={{ backgroundColor: 'var(--c-bg-panel-light)', border: '1px solid var(--wm-med)', padding: '4px 8px' }}>
<p className="text-xs flex items-center gap-2" style={{ color: 'var(--c-text-muted)' }}>
<span className="w-2 h-2 bg-accent inline-block"></span>
<span dangerouslySetInnerHTML={{ __html: command.text }}></span>
</p>
<button
onClick={copyCode}
className={`wm-btn text-[10px] py-0 px-2 ml-4 ${copied ? 'wm-btn-success' : ''}`}
>
{copied ? 'KOPIERT' : 'COPY'}
</button>
</div>
<div className="p-3 overflow-x-auto custom-scrollbar" style={{ backgroundColor: 'black', border: '2px solid var(--wm-med)' }}>
<pre className="text-xs font-mono text-accent">
<code>{command.code}</code>
</pre>
</div>
</div>
);
};
const Checkbox = ({ label, checked, onChange }) => (
<label className="flex items-start space-x-3 cursor-pointer group mb-3">
<div className="mt-0.5">
<input type="checkbox" checked={checked} onChange={onChange} className="hidden" />
<div className={`wm-checkbox-inner ${checked ? 'checked' : ''}`}>
{checked && (
<svg className="w-3.5 h-3.5" style={{color: '#fff'}} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="none" strokeLinejoin="miter" strokeWidth="3" d="M5 13l4 4L19 7" />
</svg>
)}
</div>
</div>
<div className="flex flex-col">
<span className="text-sm font-bold uppercase" style={{ color: 'var(--c-text-main)' }}>{label}</span>
</div>
</label>
);
const CollapsibleNavGroup = ({ title, sectionId, collapsed, onToggle, children }) => (
<div className="mb-4 wm-block" style={{ backgroundColor: 'var(--c-bg-panel-light)' }}>
<div
className="wm-titlebar px-3 py-2 flex items-center justify-between cursor-pointer select-none"
onClick={() => onToggle(sectionId)}
>
<h4 className="text-xs font-bold uppercase tracking-widest text-accent">{title}</h4>
<div className={`wm-toggle-box ${collapsed ? '' : 'wm-btn-active-state'}`}>
<span className="text-[10px] text-black font-bold leading-none mt-0.5">{collapsed ? '+' : '-'}</span>
</div>
</div>
{!collapsed && (
<div className="p-3" style={{ borderTop: '1px solid var(--wm-med)' }}>
{children}
</div>
)}
</div>
);
const App = () => {
const buildInitialConfig = () => {
const initConf = {};
setupConfigJSON.forEach(section => {
section.items.forEach(item => {
if (item.type === 'checkbox' || item.type === 'checkbox_with_select') {
initConf[item.id] = item.defaultChecked;
if (item.type === 'checkbox_with_select') {
initConf[item.selectId] = item.selectDefault;
}
} else if (item.type === 'select') {
initConf[item.id] = item.defaultSelect;
}
});
});
return initConf;
};
const buildInitialCollapsed = () => {
const initColl = {};
setupConfigJSON.forEach(section => { initColl[section.id] = false; });
return initColl;
};
const [config, setConfig] = useState(() => {
try {
const saved = localStorage.getItem('wmaker-config-state');
if (saved) return JSON.parse(saved);
} catch (e) {}
return buildInitialConfig();
});
const [collapsed, setCollapsed] = useState(() => {
try {
const saved = localStorage.getItem('wmaker-nav-collapsed');
if (saved) return JSON.parse(saved);
} catch (e) {}
return buildInitialCollapsed();
});
const [copiedAll, setCopiedAll] = useState(false);
useEffect(() => { localStorage.setItem('wmaker-nav-collapsed', JSON.stringify(collapsed)); }, [collapsed]);
useEffect(() => { localStorage.setItem('wmaker-config-state', JSON.stringify(config)); }, [config]);
const handleConfigToggle = (key) => setConfig(prev => ({ ...prev, [key]: !prev[key] }));
const handleConfigSelect = (key, val) => setConfig(prev => ({ ...prev, [key]: val }));
const handleCollapseToggle = (sectionId) => setCollapsed(prev => ({ ...prev, [sectionId]: !prev[sectionId] }));
// ==============================================================================
// PIPELINE ENGINE
// ==============================================================================
const getVisibleSteps = () => {
const steps = [];
setupConfigJSON.forEach(section => {
if (collapsed[section.id]) return;
section.items.forEach(item => {
let finalCommands = [];
let displayTitle = item.label;
if (item.type === 'checkbox' && config[item.id]) {
finalCommands = item.commands;
}
else if (item.type === 'select' && config[item.id] !== 'none') {
const activeOption = item.options.find(o => o.value === config[item.id]);
displayTitle = `${item.label} (${activeOption.label})`;
finalCommands = item.commandTemplate.map(c => ({
text: c.text, code: c.code.replaceAll('{{meta}}', activeOption.meta || '')
}));
}
else if (item.type === 'checkbox_with_select' && config[item.id]) {
const selVal = config[item.selectId];
const activeOption = item.options.find(o => o.value === selVal);
displayTitle = `${item.label} (${activeOption.label})`;
if (item.commandsByValue) {
finalCommands = item.commandsByValue[selVal];
} else if (item.commandTemplate) {
finalCommands = item.commandTemplate.map(c => ({
text: c.text, code: c.code.replaceAll('{{selectValue}}', selVal)
}));
}
}
if (finalCommands && finalCommands.length > 0) {
steps.push({
title: displayTitle,
commands: finalCommands
});
}
});
});
return steps;
};
const visibleSteps = getVisibleSteps();
const copyFullScript = () => {
if(visibleSteps.length === 0) return;
let text = `#!/bin/bash\n# Debian 13 Bootstrap Skript\n# Generiert durch den JSON Setup-Builder\n\n`;
text += `# ==========================================\n`;
text += `# Init & Root-Check\n`;
text += `# ==========================================\n`;
text += `set -e\n`;
text += `if [ "$EUID" -ne 0 ]; then\n echo "Bitte als root ausführen (z.B. sudo ./bootstrap.sh)"\n exit 1\nfi\n\n`;
visibleSteps.forEach(s => {
text += `# ==========================================\n`;
text += `# ${s.title}\n`;
text += `# ==========================================\n`;
s.commands.forEach(c => {
text += `# ${c.text}\n${c.code}\n\n`;
});
});
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => showCopiedAll());
} else {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showCopiedAll();
} catch (err) { }
document.body.removeChild(textArea);
}
};
const showCopiedAll = () => {
setCopiedAll(true);
setTimeout(() => setCopiedAll(false), 2000);
};
const renderNavItem = (item) => {
if (item.type === 'checkbox') {
return <Checkbox key={item.id} label={item.label} checked={config[item.id]} onChange={() => handleConfigToggle(item.id)} />;
}
else if (item.type === 'select') {
return (
<div key={item.id} className="mt-3 mb-3 ml-8 pl-3" style={{ borderLeft: '2px solid var(--wm-med)' }}>
<label className="text-[10px] font-bold block mb-1 uppercase" style={{ color: 'var(--c-text-muted)' }}>{item.label}</label>
<select
value={config[item.id]}
onChange={(e) => handleConfigSelect(item.id, e.target.value)}
className="wm-input w-full p-1"
>
{item.options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
</div>
);
}
else if (item.type === 'checkbox_with_select') {
return (
<div key={item.id}>
<Checkbox label={item.label} checked={config[item.id]} onChange={() => handleConfigToggle(item.id)} />
{config[item.id] && (
<div className="ml-8 mt-1 mb-3 flex items-center gap-2 pl-3" style={{ borderLeft: '2px solid var(--wm-med)' }}>
<label className="text-[10px] uppercase font-bold" style={{ color: 'var(--c-text-muted)' }}>{item.selectLabel}</label>
<select
value={config[item.selectId]}
onChange={(e) => handleConfigSelect(item.selectId, e.target.value)}
className="wm-input flex-1 p-1"
>
{item.options.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
</div>
)}
</div>
);
}
return null;
};
return (
<>
<DynamicStyles />
<div className="flex flex-col h-full w-full p-4 gap-4" style={{ backgroundColor: 'var(--c-bg-app)' }}>
{/* Header mit dynamischem Image Background */}
<header className="wm-block wm-titlebar wm-header-bg p-3 flex justify-between items-center shrink-0 z-20">
<div className="flex items-center gap-4">
<div className="w-6 h-6 bg-accent flex items-center justify-center" style={{ border: '2px solid', borderTopColor: 'var(--c-accent-hover)', borderLeftColor: 'var(--c-accent-hover)', borderBottomColor: 'var(--wm-dark)', borderRightColor: 'var(--wm-dark)' }}>
<span className="text-black font-bold text-xs">{uiConfigJSON.text.logoLetter}</span>
</div>
<div>
<h1 className="text-lg font-bold tracking-wider" style={{ color: 'var(--c-text-main)', textShadow: '2px 2px 0px rgba(0,0,0,0.8)' }}>
{uiConfigJSON.text.appTitle}
</h1>
</div>
</div>
<button
onClick={copyFullScript}
className={`wm-btn ${copiedAll ? 'wm-btn-success' : ''} ${visibleSteps.length === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
disabled={visibleSteps.length === 0}
>
{copiedAll ? uiConfigJSON.text.btnCopySuccess : uiConfigJSON.text.btnCopyDefault}
</button>
</header>
{/* Main Content Area */}
<div className="flex flex-1 overflow-hidden gap-4">
{/* Sidebar: Nav-Gruppe */}
<div className="w-1/3 flex flex-col shrink-0">
<div className="wm-block flex flex-col h-full">
<div className="wm-titlebar px-3 py-2 flex items-center justify-between shrink-0">
<h3 className="font-bold uppercase tracking-wide" style={{ color: 'var(--c-text-main)' }}>{uiConfigJSON.text.navTitle}</h3>
<div className="w-4 h-4" style={{ border: '2px solid', borderTopColor: 'var(--wm-light)', borderLeftColor: 'var(--wm-light)', borderBottomColor: 'var(--wm-dark)', borderRightColor: 'var(--wm-dark)', backgroundColor: 'var(--wm-med)' }}></div>
</div>
<div className="p-3 flex-1 overflow-y-auto custom-scrollbar" style={{ backgroundColor: 'var(--c-bg-panel-dark)' }}>
{setupConfigJSON.map(section => (
<CollapsibleNavGroup
key={section.id}
title={section.title}
sectionId={section.id}
collapsed={collapsed[section.id]}
onToggle={handleCollapseToggle}
>
{section.items.map(item => renderNavItem(item))}
</CollapsibleNavGroup>
))}
</div>
</div>
</div>
{/* Preview Panel: Pipeline */}
<div className="flex-1 flex flex-col min-w-0">
<div className="wm-block flex flex-col h-full">
<div className="wm-titlebar px-3 py-2 flex items-center justify-between shrink-0">
<h3 className="font-bold uppercase tracking-wide" style={{ color: 'var(--c-text-main)' }}>{uiConfigJSON.text.pipelineTitle}</h3>
<div className="w-4 h-4" style={{ border: '2px solid', borderTopColor: 'var(--wm-light)', borderLeftColor: 'var(--wm-light)', borderBottomColor: 'var(--wm-dark)', borderRightColor: 'var(--wm-dark)', backgroundColor: 'var(--wm-med)' }}></div>
</div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-6" style={{ backgroundColor: 'var(--c-bg-app)' }}>
<div className="max-w-4xl mx-auto">
{visibleSteps.length === 0 ? (
<div className="flex flex-col items-center justify-center h-[50vh] space-y-4" style={{ color: 'var(--wm-med)' }}>
<div className="w-16 h-16 border-4 border-dashed flex items-center justify-center" style={{ borderColor: 'var(--wm-med)' }}>
<span className="text-2xl font-bold">?</span>
</div>
<p className="text-sm font-bold uppercase tracking-widest text-center">
{uiConfigJSON.text.emptyStateTitle}<br/>
<span className="text-xs mt-2 block" style={{ color: 'var(--c-text-muted)' }}>{uiConfigJSON.text.emptyStateSub}</span>
</p>
</div>
) : (
<>
<p className="text-xs mb-8 uppercase tracking-widest pb-2 text-accent" style={{ borderBottom: '1px solid var(--c-accent-dark)' }}>
{uiConfigJSON.text.logMessage.replace('{{count}}', visibleSteps.length)}
</p>
{visibleSteps.map((step, index) => (
<div key={index} className="mb-10 last:mb-0">
<div className="flex items-center gap-3 mb-3">
<span className="text-accent w-6 h-6 flex items-center justify-center font-bold text-xs" style={{ backgroundColor: 'var(--c-bg-panel-dark)', border: '1px solid var(--wm-med)' }}>
{index + 1}
</span>
<h3 className="font-bold uppercase tracking-wide text-base" style={{ color: 'var(--c-text-main)' }}>{step.title}</h3>
</div>
<div className="pl-9">
{step.commands.map((cmd, idx) => (
<CommandItem key={idx} command={cmd} />
))}
</div>
</div>
))}
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>