Initial import of docroot-changer

This commit is contained in:
Marek Miklewicz
2026-07-04 12:02:31 +02:00
commit 4c883fd543
16 changed files with 3054 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
user="${USERNAME:-${LOGNAME:-${USER:-}}}"
lang="${LANGUAGE:-}"
normalize_lang() {
val="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
case "$val" in
pl*|polish*) echo "pl" ;;
en*|english*) echo "en" ;;
*) echo "" ;;
esac
}
is_enabled_for_user() {
u="$1"
[ -n "$u" ] || return 1
cfg="/usr/local/directadmin/data/users/$u/user.conf"
[ -r "$cfg" ] || return 1
val="$(grep -E '^docroot_changer=' "$cfg" 2>/dev/null | tail -n1 | cut -d= -f2-)"
case "$(printf '%s' "$val" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" in
yes|on|true|1) return 0 ;;
esac
return 1
}
lang="$(normalize_lang "$lang")"
if [ -z "$lang" ] && [ -n "$user" ] && [ -r "/usr/local/directadmin/data/users/$user/user.conf" ]; then
cfg_lang="$(grep -i -m1 -E '^(lang|language|locale)=' "/usr/local/directadmin/data/users/$user/user.conf" | cut -d= -f2-)"
lang="$(normalize_lang "$cfg_lang")"
fi
label="Zmiana katalogu domeny"
if ! is_enabled_for_user "$user"; then
printf '[]\n'
exit 0
fi
printf '[{"name":"%s","icon":"","entries":[{"name":"%s","icon":"","href":"/CMD_PLUGINS/docroot-changer","newTab":false,"updates":0}]}]\n' "$label" "$label"
+1
View File
@@ -0,0 +1 @@
<a href="/CMD_PLUGINS/docroot-changer">Zmiana katalogu domeny</a>
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none">
<path d="M3.5 6.5A2.5 2.5 0 0 1 6 4h4.1c.8 0 1.4.3 1.9.9l1 1.1c.2.2.4.3.7.3H18A2.5 2.5 0 0 1 20.5 8.8v1H3.5v-3.3Z" fill="#F8B63C"/>
<path d="M2.8 10.2h18.4a1.5 1.5 0 0 1 1.5 1.8l-1.3 6.6a2.5 2.5 0 0 1-2.5 2H5.1a2.5 2.5 0 0 1-2.5-2L1.3 12a1.5 1.5 0 0 1 1.5-1.8Z" fill="#FFD36A"/>
<path d="M6.5 8.1h11.7A2.3 2.3 0 0 1 20.5 10v.2H3.5V10c0-1 .8-1.9 2-1.9Z" fill="#E8A531"/>
</svg>

After

Width:  |  Height:  |  Size: 479 B

+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 4167 4167" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.01421,0,0,1.01421,-112.877,259.062)">
<g id="Layer1">
<g>
<g transform="matrix(5.81705,0,0,5.81705,-791.069,-955.669)">
<path d="M852.334,296.058L852.334,590.728C852.334,615.583 832.155,635.762 807.3,635.762L209.256,635.762C184.401,635.762 164.223,615.583 164.223,590.728L164.223,230.464C164.223,205.609 184.401,185.43 209.256,185.43L380.147,185.43L431.235,251.025L807.3,251.025C832.155,251.025 852.334,271.204 852.334,296.058Z" style="fill:rgb(90,93,157);"/>
</g>
<g transform="matrix(5.81705,0,0,5.81705,-791.069,-223.484)">
<path d="M852.334,230.464L852.334,590.728C852.334,615.583 832.155,635.762 807.3,635.762L209.256,635.762C184.401,635.762 164.223,615.583 164.223,590.728L164.223,230.464C164.223,205.609 184.401,185.43 209.256,185.43L807.3,185.43C832.155,185.43 852.334,205.609 852.334,230.464Z" style="fill:rgb(97,189,246);"/>
</g>
<g transform="matrix(3.23929,0,0,6.02396,1410.65,-2184.76)">
<path d="M265.446,830.527L265.446,768.549L553.27,768.549L553.27,710.997L717.924,799.538L553.27,888.078L553.27,830.527L265.446,830.527Z" style="fill:rgb(90,93,157);"/>
</g>
<g transform="matrix(10.3353,0,0,10.3353,-1432.04,-2942.16)">
<g transform="matrix(189.707,0,0,189.707,329.704,555.54)">
</g>
<text x="251.557px" y="555.54px" style="font-family:'Inter-Black', 'Inter';font-weight:900;font-size:189.707px;fill:white;">/</text>
</g>
<g transform="matrix(1.14154,0,0,1.14154,-655.393,-214.085)">
<path d="M2752.93,2182.83C2919.69,2252.68 2997.15,2448.31 2925.8,2619.42C2854.45,2790.53 2661.14,2872.74 2494.38,2802.89C2327.62,2733.05 2250.16,2537.42 2321.51,2366.31C2392.86,2195.2 2586.17,2112.98 2752.93,2182.83Z" style="fill:rgb(90,93,157);"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -eu
PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="$PLUGIN_DIR/scripts/install.sh"
if [ ! -x "$SCRIPT" ]; then
chmod 755 "$SCRIPT" 2>/dev/null || true
fi
exec "$SCRIPT"
+99
View File
@@ -0,0 +1,99 @@
<?php
return [
'Zmiana Docroot' => 'Docroot Changer',
'Zmiana katalogu domeny' => 'Change domain directory',
'Docroot Changer' => 'Docroot Changer',
'Domena' => 'Domain',
'Aktualny Docroot' => 'Current Docroot',
'Tryb' => 'Mode',
'Domyślny' => 'Default',
'Niestandardowy' => 'Custom',
'Akcje' => 'Actions',
'Edytuj' => 'Edit',
'Zmień katalog domeny' => 'Change domain directory',
'Ustaw domyślną' => 'Set default',
'Brak domen do wyświetlenia.' => 'No domains to display.',
'Wykryty webserwer:' => 'Detected web server:',
'Sprawdź ładowanie elementów statycznych, np. grafik lub stylów CSS po zmianie DocRoot Domeny' => 'Check loading of static assets (for example images or CSS) after changing the domain DocRoot',
'Dozwolona baza ścieżek:' => 'Allowed path base:',
'Zapisz' => 'Save',
'Anuluj' => 'Cancel',
'Potwierdzenie' => 'Confirmation',
'Przywróć domyślną' => 'Restore default',
'Przywróć poprzedni Docroot' => 'Restore previous Docroot',
'Obecne ustawienie' => 'Current setting',
'Standardowy DocRoot' => 'Standard DocRoot',
'Niestandardowy DocRoot' => 'Custom DocRoot',
'Przeglądaj' => 'Browse',
'Wybierz katalog' => 'Select directory',
'Wybierz' => 'Select',
'W górę' => 'Up',
'Nowy katalog' => 'New directory',
'Utwórz' => 'Create',
'Brak katalogów' => 'No directories',
'Nie udało się wczytać katalogów.' => 'Failed to load directories.',
'Nie udało się utworzyć katalogu.' => 'Failed to create directory.',
'Nieprawidłowa nazwa katalogu.' => 'Invalid directory name.',
'Katalog już istnieje.' => 'Directory already exists.',
'Nie można utworzyć katalogu poza dozwoloną ścieżką.' => 'Cannot create directory outside the allowed path.',
'Nie można utworzyć katalogu bezpośrednio w ' => 'Cannot create a directory directly in ',
'Tworzenie katalogów jest wyłączone.' => 'Directory creation is disabled.',
'Podaj nazwę katalogu.' => 'Provide a directory name.',
'Ścieżka musi zaczynać się od:' => 'Path must start with:',
'Ścieżka nie zaczyna się od:' => 'Path does not start with:',
'ścieżka zawiera nieprawidłowe znaki - proszę wprowadzić poprawną ścieżkę lub skorzystać z opcji __ICON__ w celu wyboru poprawnie ścieżki.' => 'Path contains invalid characters - please enter a valid path or use the __ICON__ option to choose a valid path.',
'Ścieżka zawiera niedozwolony fragment "..".' => 'Path contains forbidden ".." segment.',
'Docroot musi wskazywać istniejący katalog.' => 'Docroot must point to an existing directory.',
'Docroot musi być katalogiem.' => 'Docroot must be a directory.',
'Brak dostępu do katalogu docroot.' => 'No access to the docroot directory.',
'Nieprawidłowy token CSRF.' => 'Invalid CSRF token.',
'Metoda niedozwolona.' => 'Method Not Allowed.',
'Brak dostępu do pluginu. Włącz opcję docroot_changer w pakiecie lub dla użytkownika.' => 'No access to the plugin. Enable docroot_changer in package or for the user.',
'Brak dostępu' => 'Access denied',
'Nieprawidłowa domena.' => 'Invalid domain.',
'Nie znaleziono domeny.' => 'Domain not found.',
'Wskazany katalog nie należy do użytkownika docelowego.' => 'The selected directory does not belong to the target user.',
'Nie udało się ustawić właściciela katalogu na użytkownika DirectAdmin.' => 'Failed to set directory owner to the DirectAdmin user.',
'Nie udało się zapisać poprzedniego docroot.' => 'Failed to save previous docroot.',
'Nie znaleziono poprzedniego docroot dla domeny.' => 'No previous docroot found for this domain.',
'Zapisano docroot dla domeny.' => 'Docroot saved for domain.',
'Przywrócono domyślny docroot.' => 'Default docroot restored.',
'Przywrócono poprzedni docroot.' => 'Previous docroot restored.',
'Błąd API DirectAdmin:' => 'DirectAdmin API error:',
'Brak odpowiedzi z API DirectAdmin.' => 'No response from DirectAdmin API.',
'Brak aktywnej sesji DirectAdmin do wywołania API.' => 'No active DirectAdmin session for API call.',
'Nieobsługiwany webserwer, używam trybu apache.' => 'Unsupported web server, using apache mode.',
'Kliknij nazwę domeny, aby zmienić Docroot.' => 'Click a domain name to change Docroot.',
'Docroot dla domeny:' => 'Docroot for domain:',
'Wpisz ścieżkę ręcznie lub użyj pickera katalogów.' => 'Enter a path manually or use the directory picker.',
'Aby zmienić DocRoot dla domeny __DOMAIN__ Wpisz ścieżkę do katalogu docelowego, lub użyj przycisku __ICON__ aby wskazać go w drzewie katalogów.' => 'To change DocRoot for domain __DOMAIN__, enter the target directory path, or use the __ICON__ button to select it in the directory tree.',
'Czy na pewno chcesz zmienić katalog domeny __DOMAIN__ na niestandardowy?' => 'Are you sure you want to set a custom domain directory for __DOMAIN__?',
'Czy na pewno chcesz przywrócić domyślny katalog domeny __DOMAIN__' => 'Are you sure you want to restore the default domain directory for __DOMAIN__?',
'Obecny katalog domeny:' => 'Current domain directory:',
'Nowy katalog domeny:' => 'New domain directory:',
'Domyślny katalog domeny:' => 'Default domain directory:',
'Wskazany katalog ma właściciela __OWNER__:__GROUP__. Czy kontynuować? W przypadku kontynuacji wskazany katalog __PATH__ będzie miał zmienione uprawnienia na __USER__:__USER__. Zmiana dotyczy wyłącznie tego katalogu (bez podkatalogów).' => 'The selected directory owner is __OWNER__:__GROUP__. Continue? If you continue, __PATH__ will be changed to __USER__:__USER__. This change applies only to this directory (without subdirectories).',
'Zmiana katalogu domeny __DOMAIN__ wykonana pomyślnie będzie ona widoczna w ciągu kilku minut' => 'Domain directory change for __DOMAIN__ completed successfully; it will be visible within a few minutes',
'Zamknij' => 'Close',
'Kod błędu:' => 'Error code:',
'Nie udało się wysłać żądania.' => 'Failed to send request.',
'Nie udało się przetworzyć odpowiedzi.' => 'Failed to process response.',
'Przywrócić domyślny docroot dla tej domeny?' => 'Restore default docroot for this domain?',
'Przywrócić poprzedni docroot dla tej domeny?' => 'Restore previous docroot for this domain?',
'Niezgodna konfiguracja Docroot między Apache i Nginx (pokazano pierwszą wykrytą).' => 'Docroot mismatch between Apache and Nginx (showing first detected).',
'Tak' => 'Yes',
'Nie' => 'No',
'Ścieżka musi znajdować się w katalogu:' => 'Path must be inside:',
'Ścieżka musi znajdować się w katalogu użytkownika:' => 'Path must be inside user directory:',
'Nie udało się usunąć pliku konfiguracji webserwera: ' => 'Failed to remove web server configuration file: ',
'Nie udało się odczytać pliku konfiguracji webserwera: ' => 'Failed to read web server configuration file: ',
'Nie udało się zapisać pliku konfiguracji webserwera: ' => 'Failed to write web server configuration file: ',
'Nie udało się zainicjować przebudowy konfiguracji przez dataskq.' => 'Failed to queue configuration rebuild via dataskq.',
'Nieprawidłowa nazwa użytkownika dla zadania rewrite httpd.' => 'Invalid username for httpd rewrite task.',
'Nie udało się dodać zadania rewrite httpd do task.queue.' => 'Failed to append httpd rewrite task to task.queue.',
'Nie udało się zainicjować przebudowy konfiguracji httpd przez task.queue/dataskq.' => 'Failed to trigger httpd configuration rebuild via task.queue/dataskq.',
'Nie udało się zastosować zmiany docroot.' => 'Failed to apply docroot change.',
'wprowadzona ścieżka jest dowiązaniem symbolicznym do: __TARGET__, wprowadź ścieżkę do realnego katalogu' => 'The entered path is a symbolic link to: __TARGET__, enter a path to the real directory',
'Wskazany katalog jest pusty' => 'The selected directory is empty',
'pusty' => 'empty',
];
+7
View File
@@ -0,0 +1,7 @@
<?php
$en = include __DIR__ . '/en.php';
$pl = [];
foreach ($en as $key => $_value) {
$pl[$key] = $key;
}
return $pl;
+7
View File
@@ -0,0 +1,7 @@
[PHP]
short_open_tag=On
variables_order=GPCS
error_reporting=E_ALL & ~E_NOTICE
display_errors=Off
log_errors=On
output_buffering=4096
+17
View File
@@ -0,0 +1,17 @@
# Ustawienia pluginu docroot-changer
# CUSTOM_PACKAGE_ITEM_NAME - nazwa custom package item w DirectAdmin.
CUSTOM_PACKAGE_ITEM_NAME=docroot_changer
# CUSTOM_PACKAGE_ITEM_DEFAULT - domyślna wartość custom package item.
CUSTOM_PACKAGE_ITEM_DEFAULT=no
# ENFORCE_DOMAINS_BASE - ścieżka bazowa dozwolona dla docroot.
ENFORCE_DOMAINS_BASE=/home/DA_USER/domains
# ALLOW_PICKER_CREATE_DIR - pozwala tworzyć katalogi z poziomu pickera (yes/no).
ALLOW_PICKER_CREATE_DIR=yes
# ALLOW_DOCROOT_OUTSIDE_DOMAINS_DIR - pozwala ustawić DocRoot poza /home/DA_USER/domains.
# Gdy yes, dozwolony zakres to tylko /home/DA_USER (nigdy powyżej katalogu użytkownika).
ALLOW_DOCROOT_OUTSIDE_DOMAINS_DIR=no
# DISABLE_CONFIRMATIONS - wyłącza popupy potwierdzeń (true/false).
DISABLE_CONFIRMATIONS=false
# FIX_OWNERSHIP - sprawdza owner katalogu i w razie potrzeby pozwala poprawić owner na DA_USER:DA_USER.
# Ustaw false, aby pominąć sprawdzanie ownera, ostrzeżenia i zmianę ownera.
FIX_OWNERSHIP=true
+6
View File
@@ -0,0 +1,6 @@
name=Docroot-Changer
id=docroot-changer
type=user
author=HITME.PL
version=2.1.2
user_run_as=root
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
if [ "$(id -u)" -eq 0 ]; then
chown -R diradmin:diradmin "$PLUGIN_DIR" || true
else
echo "docroot-changer: warning: not running as root, cannot chown to diradmin" >&2
fi
chmod 755 "$PLUGIN_DIR" "$PLUGIN_DIR/hooks" "$PLUGIN_DIR/user" "$PLUGIN_DIR/scripts" "$PLUGIN_DIR/images" || true
chmod 755 "$PLUGIN_DIR/user/index.html" "$PLUGIN_DIR/install.sh" "$PLUGIN_DIR/uninstall.sh" "$PLUGIN_DIR/scripts/install.sh" "$PLUGIN_DIR/scripts/uninstall.sh" "$PLUGIN_DIR/hooks/menu_user.sh" || true
chmod 644 "$PLUGIN_DIR/plugin.conf" "$PLUGIN_DIR/plugin-settings.conf" "$PLUGIN_DIR/hooks/user_txt.html" "$PLUGIN_DIR/user/evolution.css" "$PLUGIN_DIR/images/user_icon.svg" "$PLUGIN_DIR/images/folder.svg" || true
DATA_DIR="$PLUGIN_DIR/data"
mkdir -p "$DATA_DIR/csrf"
chmod 700 "$DATA_DIR" "$DATA_DIR/csrf" || true
if [ "$(id -u)" -eq 0 ]; then
chown -R diradmin:diradmin "$DATA_DIR" || true
fi
CPI_FILE="/usr/local/directadmin/data/admin/custom_package_items.conf"
ITEM_LINE='docroot_changer=checkbox=no&desc=Allow+Docroot+Changer+plugin'
if [ "$(id -u)" -eq 0 ]; then
mkdir -p "$(dirname "$CPI_FILE")"
touch "$CPI_FILE"
if grep -q '^docroot_changer=' "$CPI_FILE"; then
sed -i.bak '/^docroot_changer=/d' "$CPI_FILE"
fi
printf '%s\n' "$ITEM_LINE" >> "$CPI_FILE"
chmod 644 "$CPI_FILE" || true
chown diradmin:diradmin "$CPI_FILE" || true
else
echo "docroot-changer: warning: not running as root, cannot modify custom_package_items.conf" >&2
fi
echo "docroot-changer: install complete"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
set -euo pipefail
PLUGIN_DIR="$(cd "$(dirname "$0")/.." && pwd)"
if [ "$(basename "$PLUGIN_DIR")" != "docroot-changer" ]; then
echo "docroot-changer: safety check failed (basename is not docroot-changer): $PLUGIN_DIR" >&2
exit 1
fi
if [ ! -f "$PLUGIN_DIR/plugin.conf" ]; then
echo "docroot-changer: safety check failed (plugin.conf missing): $PLUGIN_DIR" >&2
exit 1
fi
CPI_FILE="/usr/local/directadmin/data/admin/custom_package_items.conf"
if [ "$(id -u)" -eq 0 ] && [ -f "$CPI_FILE" ]; then
sed -i.bak '/^docroot_changer=/d' "$CPI_FILE" || true
fi
(sleep 3; rm -rf "$PLUGIN_DIR") >/dev/null 2>&1 &
echo "docroot-changer: uninstall complete"
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -eu
PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT="$PLUGIN_DIR/scripts/uninstall.sh"
if [ ! -x "$SCRIPT" ]; then
chmod 755 "$SCRIPT" 2>/dev/null || true
fi
exec "$SCRIPT"
+347
View File
@@ -0,0 +1,347 @@
:root {
--br-bg: #f5f8ff;
--br-card: #ffffff;
--br-text: #111827;
--br-muted: #64748b;
--br-border: #e2e8f0;
--br-accent: #2563eb;
--br-accent-dark: #1d4ed8;
--br-danger: #dc2626;
--br-warn: #f59e0b;
--br-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--br-bg);
color: var(--br-text);
font-family: "IBM Plex Sans", "Segoe UI", Arial, sans-serif;
}
.br-container {
max-width: none;
width: 100%;
margin: 16px auto;
padding: 16px 24px;
}
.br-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.br-title {
font-size: 20px;
font-weight: 700;
letter-spacing: 0.2px;
}
.br-card {
background: var(--br-card);
border: 1px solid var(--br-border);
border-radius: 12px;
padding: 16px;
box-shadow: var(--br-shadow);
margin-bottom: 16px;
}
.br-muted { color: var(--br-muted); }
.br-alert {
border-radius: 10px;
padding: 10px 12px;
margin: 8px 0;
border: 1px solid transparent;
}
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; }
.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; }
.br-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.br-modal {
width: min(560px, 92vw);
background: #ffffff;
border-radius: 12px;
padding: 16px;
border: 2px solid var(--br-border);
box-shadow: var(--br-shadow);
display: flex;
flex-direction: column;
max-height: 90vh;
}
.br-modal-log {
width: 90vw;
background: #ffffff;
}
.br-modal-warning {
background: #f3c8c8;
}
.br-modal-warning .br-btn-ghost {
background: #ffffff;
}
.br-modal-warning .br-btn-ghost:hover {
background: #f8fafc;
}
.br-modal-danger {
border-color: var(--br-danger);
}
.br-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
.br-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.br-modal-title {
font-weight: 700;
font-size: 16px;
}
.br-modal-body {
max-height: 60vh;
overflow: auto;
}
.br-modal-scroll {
overflow: auto;
flex: 1;
min-height: 0;
}
.br-log-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.br-log-header-actions {
display: flex;
gap: 8px;
}
.br-log-meta {
display: grid;
gap: 6px;
margin: 10px 0 12px;
padding: 10px 12px;
border: 1px solid var(--br-border);
border-radius: 10px;
background: #f8fafc;
font-size: 13px;
}
.br-log-actions {
margin: 8px 0 10px;
}
.br-picker-path {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.br-picker-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid var(--br-border);
border-radius: 10px;
max-height: 45vh;
overflow: auto;
}
.br-picker-create {
margin-top: 10px;
display: flex;
gap: 8px;
}
.br-picker-create input {
flex: 1;
padding: 6px 8px;
border: 1px solid var(--br-border);
border-radius: 8px;
}
.br-picker-list li {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--br-border);
}
.br-picker-list li:last-child {
border-bottom: none;
}
.br-picker-list li:hover {
background: #f8fafc;
}
.br-picker-list li.active {
background: #d1fae5;
}
.br-btn-small {
padding: 6px 10px;
font-size: 12px;
}
.br-actions-row {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.br-actions-row form {
margin: 0;
}
.br-icon-btn {
border: 1px solid var(--br-border);
background: #fff;
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.br-icon-btn:hover {
background: #f8fafc;
}
.br-table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--br-border);
border-radius: 10px;
overflow: hidden;
background: #fff;
table-layout: fixed;
}
.br-table th {
text-align: left;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--br-muted);
background: #f1f5f9;
padding: 10px;
}
.br-table td {
padding: 10px;
border-top: 1px solid var(--br-border);
vertical-align: top;
}
.br-table tr:nth-child(even) td { background: #f8fafc; }
.br-col-select { width: 50px; text-align: center; }
.br-col-status { width: 120px; }
.br-col-progress { width: 90px; }
.br-col-mode { width: 180px; }
.br-col-user { width: 140px; }
.br-col-backup { width: 220px; overflow-wrap: anywhere; }
.br-col-path { width: 260px; overflow-wrap: anywhere; }
.br-col-target { width: 220px; overflow-wrap: anywhere; }
.br-col-time { width: 150px; }
.br-col-actions { width: 120px; }
.br-table input[type="checkbox"] {
accent-color: var(--br-accent);
width: 16px;
height: 16px;
}
.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; }
.br-btn {
background: var(--br-accent);
color: #fff;
border: none;
border-radius: 8px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
}
.br-btn:hover { background: var(--br-accent-dark); }
.br-btn-ghost {
background: transparent;
color: var(--br-text);
border: 1px solid var(--br-border);
}
.br-btn-ghost:hover { background: #e2e8f0; }
.br-btn-danger {
background: var(--br-danger);
}
.br-btn-danger:hover { background: #b91c1c; }
.br-link-danger { color: var(--br-danger); text-decoration: none; }
.br-link-danger:hover { text-decoration: underline; }
.br-inline-form { display: inline; }
.br-link-button {
background: none;
border: none;
padding: 0;
margin: 0;
color: inherit;
cursor: pointer;
font: inherit;
}
.br-link-button:hover { text-decoration: underline; }
.br-link-button.br-link-danger { color: var(--br-danger); }
input[type="text"] {
width: 100%;
max-width: 680px;
padding: 8px 10px;
border: 1px solid var(--br-border);
border-radius: 8px;
font-size: 14px;
background: #fff;
}
pre {
background: #0b1020;
color: #e2e8f0;
padding: 12px;
border-radius: 8px;
overflow: auto;
}
a { color: var(--br-accent-dark); text-decoration: none; }
a:hover { text-decoration: underline; }
.br-toast {
position: fixed;
bottom: 18px;
right: 18px;
background: #16a34a;
color: #fff;
padding: 12px 16px;
border-radius: 10px;
box-shadow: 0 12px 28px rgba(16, 24, 40, 0.25);
z-index: 9999;
font-weight: 600;
}
.br-toast-stack {
position: fixed;
top: 18px;
right: 18px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 9999;
}
.br-toast-stack .br-toast {
position: static;
top: auto;
bottom: auto;
}
.br-toast-error {
background: #dc2626;
top: 18px;
bottom: auto;
}
.br-toast-success {
background: #16a34a;
}
.br-status-line {
margin: 6px 0;
font-size: 14px;
}
.br-progress {
height: 8px;
background: #e5e7eb;
border-radius: 999px;
overflow: hidden;
margin: 6px 0 12px;
}
.br-progress-bar {
height: 100%;
background: var(--br-accent);
transition: width 0.3s ease;
}
+347
View File
@@ -0,0 +1,347 @@
:root {
--br-bg: #f5f8ff;
--br-card: #ffffff;
--br-text: #111827;
--br-muted: #64748b;
--br-border: #e2e8f0;
--br-accent: #2563eb;
--br-accent-dark: #1d4ed8;
--br-danger: #dc2626;
--br-warn: #f59e0b;
--br-shadow: 0 10px 28px rgba(15, 23, 42, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--br-bg);
color: var(--br-text);
font-family: "IBM Plex Sans", "Segoe UI", Arial, sans-serif;
}
.br-container {
max-width: none;
width: 100%;
margin: 16px auto;
padding: 16px 24px;
}
.br-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.br-title {
font-size: 20px;
font-weight: 700;
letter-spacing: 0.2px;
}
.br-card {
background: var(--br-card);
border: 1px solid var(--br-border);
border-radius: 12px;
padding: 16px;
box-shadow: var(--br-shadow);
margin-bottom: 16px;
}
.br-muted { color: var(--br-muted); }
.br-alert {
border-radius: 10px;
padding: 10px 12px;
margin: 8px 0;
border: 1px solid transparent;
}
.br-alert-error { background: #fff1f2; border-color: #fecdd3; color: #9f1239; }
.br-alert-warn { background: #fffbeb; border-color: #fde68a; color: #92400e; }
.br-alert-success { background: #ecfdf3; border-color: #bbf7d0; color: #166534; }
.br-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.br-modal {
width: min(560px, 92vw);
background: #ffffff;
border-radius: 12px;
padding: 16px;
border: 2px solid var(--br-border);
box-shadow: var(--br-shadow);
display: flex;
flex-direction: column;
max-height: 90vh;
}
.br-modal-log {
width: 90vw;
background: #ffffff;
}
.br-modal-warning {
background: #f3c8c8;
}
.br-modal-warning .br-btn-ghost {
background: #ffffff;
}
.br-modal-warning .br-btn-ghost:hover {
background: #f8fafc;
}
.br-modal-danger {
border-color: var(--br-danger);
}
.br-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
.br-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 10px;
}
.br-modal-title {
font-weight: 700;
font-size: 16px;
}
.br-modal-body {
max-height: 60vh;
overflow: auto;
}
.br-modal-scroll {
overflow: auto;
flex: 1;
min-height: 0;
}
.br-log-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.br-log-header-actions {
display: flex;
gap: 8px;
}
.br-log-meta {
display: grid;
gap: 6px;
margin: 10px 0 12px;
padding: 10px 12px;
border: 1px solid var(--br-border);
border-radius: 10px;
background: #f8fafc;
font-size: 13px;
}
.br-log-actions {
margin: 8px 0 10px;
}
.br-picker-path {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.br-picker-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid var(--br-border);
border-radius: 10px;
max-height: 45vh;
overflow: auto;
}
.br-picker-create {
margin-top: 10px;
display: flex;
gap: 8px;
}
.br-picker-create input {
flex: 1;
padding: 6px 8px;
border: 1px solid var(--br-border);
border-radius: 8px;
}
.br-picker-list li {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--br-border);
}
.br-picker-list li:last-child {
border-bottom: none;
}
.br-picker-list li:hover {
background: #f8fafc;
}
.br-picker-list li.active {
background: #d1fae5;
}
.br-btn-small {
padding: 6px 10px;
font-size: 12px;
}
.br-actions-row {
display: flex;
gap: 8px;
margin-top: 10px;
flex-wrap: wrap;
}
.br-actions-row form {
margin: 0;
}
.br-icon-btn {
border: 1px solid var(--br-border);
background: #fff;
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
font-size: 16px;
line-height: 1;
}
.br-icon-btn:hover {
background: #f8fafc;
}
.br-table {
width: 100%;
border-collapse: collapse;
border: 1px solid var(--br-border);
border-radius: 10px;
overflow: hidden;
background: #fff;
table-layout: fixed;
}
.br-table th {
text-align: left;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--br-muted);
background: #f1f5f9;
padding: 10px;
}
.br-table td {
padding: 10px;
border-top: 1px solid var(--br-border);
vertical-align: top;
}
.br-table tr:nth-child(even) td { background: #f8fafc; }
.br-col-select { width: 50px; text-align: center; }
.br-col-status { width: 120px; }
.br-col-progress { width: 90px; }
.br-col-mode { width: 180px; }
.br-col-user { width: 140px; }
.br-col-backup { width: 220px; overflow-wrap: anywhere; }
.br-col-path { width: 260px; overflow-wrap: anywhere; }
.br-col-target { width: 220px; overflow-wrap: anywhere; }
.br-col-time { width: 150px; }
.br-col-actions { width: 120px; }
.br-table input[type="checkbox"] {
accent-color: var(--br-accent);
width: 16px;
height: 16px;
}
.br-actions { display: flex; gap: 8px; align-items: center; margin: 10px 0; }
.br-btn {
background: var(--br-accent);
color: #fff;
border: none;
border-radius: 8px;
padding: 8px 14px;
cursor: pointer;
font-weight: 600;
}
.br-btn:hover { background: var(--br-accent-dark); }
.br-btn-ghost {
background: transparent;
color: var(--br-text);
border: 1px solid var(--br-border);
}
.br-btn-ghost:hover { background: #e2e8f0; }
.br-btn-danger {
background: var(--br-danger);
}
.br-btn-danger:hover { background: #b91c1c; }
.br-link-danger { color: var(--br-danger); text-decoration: none; }
.br-link-danger:hover { text-decoration: underline; }
.br-inline-form { display: inline; }
.br-link-button {
background: none;
border: none;
padding: 0;
margin: 0;
color: inherit;
cursor: pointer;
font: inherit;
}
.br-link-button:hover { text-decoration: underline; }
.br-link-button.br-link-danger { color: var(--br-danger); }
input[type="text"] {
width: 100%;
max-width: 680px;
padding: 8px 10px;
border: 1px solid var(--br-border);
border-radius: 8px;
font-size: 14px;
background: #fff;
}
pre {
background: #0b1020;
color: #e2e8f0;
padding: 12px;
border-radius: 8px;
overflow: auto;
}
a { color: var(--br-accent-dark); text-decoration: none; }
a:hover { text-decoration: underline; }
.br-toast {
position: fixed;
bottom: 18px;
right: 18px;
background: #16a34a;
color: #fff;
padding: 12px 16px;
border-radius: 10px;
box-shadow: 0 12px 28px rgba(16, 24, 40, 0.25);
z-index: 9999;
font-weight: 600;
}
.br-toast-stack {
position: fixed;
top: 18px;
right: 18px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 9999;
}
.br-toast-stack .br-toast {
position: static;
top: auto;
bottom: auto;
}
.br-toast-error {
background: #dc2626;
top: 18px;
bottom: auto;
}
.br-toast-success {
background: #16a34a;
}
.br-status-line {
margin: 6px 0;
font-size: 14px;
}
.br-progress {
height: 8px;
background: #e5e7eb;
border-radius: 999px;
overflow: hidden;
margin: 6px 0 12px;
}
.br-progress-bar {
height: 100%;
background: var(--br-accent);
transition: width 0.3s ease;
}
+2069
View File
File diff suppressed because it is too large Load Diff