This commit is contained in:
Nick Sweeting
2025-12-28 17:51:54 -08:00
parent 54f91c1339
commit f0aa19fa7d
157 changed files with 6774 additions and 5061 deletions

View File

@@ -150,8 +150,10 @@
<a href="{% url 'admin:core_snapshot_change' obj.pk %}">
<span class="timestamp">{{obj.bookmarked_at}}</span>
</a>
<div style="padding: 4px 0;">
{{ obj.icons|safe }}
</div>
<label>
<span class="num_outputs">📄 &nbsp; {{obj.num_outputs}}</span> &nbsp; &nbsp;
<span>🗄&nbsp; {{ obj.archive_size | file_size }}</span>
<input type="checkbox" name="_selected_action" value="{{obj.pk}}"/>
</label>

View File

@@ -29,7 +29,8 @@
</center>
{% else %}
<div id="in-progress" style="display: none;">
<center><h3>Adding URLs to index and running archive methods...</h3>
<center><h3>Creating crawl and queueing snapshots...</h3>
<p>Your crawl is being created. The orchestrator will process URLs and create snapshots in the background.</p>
<br/>
<div class="loader"></div>
<br/>
@@ -37,16 +38,230 @@
</center>
</div>
<form id="add-form" method="POST" class="p-form">{% csrf_token %}
<h1>Add new URLs to your archive</h1>
<h1>Create a new Crawl</h1>
<div class="crawl-explanation">
<p>
A <strong>Crawl</strong> is a job that processes URLs and creates <strong>Snapshots</strong> (archived copies) for each URL discovered.
The settings below apply to the entire crawl and all snapshots it creates.
</p>
</div>
<br/>
{{ form.as_p }}
<!-- Basic fields -->
<div class="form-section">
<h3>Crawl Settings</h3>
<div class="form-field">
{{ form.url.label_tag }}
{{ form.url }}
<div id="url-counter" class="url-counter">0 URLs detected</div>
{% if form.url.errors %}
<div class="error">{{ form.url.errors }}</div>
{% endif %}
<div class="help-text">
Enter URLs to archive, one per line. Examples:<br/>
<code>https://example.com</code><br/>
<code>https://news.ycombinator.com</code><br/>
<code>https://github.com/ArchiveBox/ArchiveBox</code>
</div>
</div>
<div class="form-field">
{{ form.tag.label_tag }}
{{ form.tag }}
<!-- Tag autocomplete datalist -->
<datalist id="tag-datalist">
{% for tag_name in available_tags %}
<option value="{{ tag_name }}">
{% endfor %}
</datalist>
{% if form.tag.errors %}
<div class="error">{{ form.tag.errors }}</div>
{% endif %}
<div class="help-text">Tags will be applied to all snapshots created by this crawl. Start typing to see existing tags.</div>
</div>
<div class="form-field">
{{ form.depth.label_tag }}
{{ form.depth }}
{% if form.depth.errors %}
<div class="error">{{ form.depth.errors }}</div>
{% endif %}
<div class="help-text">Controls how many links deep the crawl will follow from the starting URLs.</div>
</div>
<div class="form-field">
{{ form.notes.label_tag }}
{{ form.notes }}
{% if form.notes.errors %}
<div class="error">{{ form.notes.errors }}</div>
{% endif %}
<div class="help-text">Optional description for this crawl (visible in the admin interface).</div>
</div>
</div>
<!-- Plugins section -->
<div class="form-section">
<h3>Crawl Plugins</h3>
<p class="section-description">
Select which archiving methods to run for all snapshots in this crawl. If none selected, all available plugins will be used.
<a href="/admin/environment/plugins/" target="_blank">View plugin details →</a>
</p>
<!-- Plugin Presets -->
<div class="plugin-presets">
<span class="preset-label">Quick Select:</span>
<button type="button" class="preset-btn" data-preset="quick-archive">📦 Quick Archive</button>
<button type="button" class="preset-btn" data-preset="full-chrome">🌐 Full Chrome</button>
<button type="button" class="preset-btn" data-preset="text-only">📄 Text Only</button>
<button type="button" class="preset-btn" data-preset="select-all">✓ Select All</button>
<button type="button" class="preset-btn" data-preset="clear-all">✗ Clear All</button>
</div>
<!-- Chrome-dependent plugins with "Select All" -->
<div class="plugin-group">
<div class="plugin-group-header">
<label>Chrome-dependent plugins</label>
<button type="button" class="select-all-btn" data-group="chrome">
Select All Chrome
</button>
</div>
<div class="plugin-checkboxes" id="chrome-plugins">
{{ form.chrome_plugins }}
</div>
</div>
<!-- Archiving plugins -->
<div class="plugin-group">
<div class="plugin-group-header">
<label>Archiving</label>
</div>
<div class="plugin-checkboxes">
{{ form.archiving_plugins }}
</div>
</div>
<!-- Parsing plugins -->
<div class="plugin-group">
<div class="plugin-group-header">
<label>Parsing</label>
</div>
<div class="plugin-checkboxes">
{{ form.parsing_plugins }}
</div>
</div>
<!-- Search plugins -->
<div class="plugin-group">
<div class="plugin-group-header">
<label>Search</label>
</div>
<div class="plugin-checkboxes">
{{ form.search_plugins }}
</div>
</div>
<!-- Binary provider plugins -->
<div class="plugin-group">
<div class="plugin-group-header">
<label>Binary Providers</label>
</div>
<div class="plugin-checkboxes">
{{ form.binary_plugins }}
</div>
</div>
<!-- Extension plugins -->
<div class="plugin-group">
<div class="plugin-group-header">
<label>Browser Extensions</label>
</div>
<div class="plugin-checkboxes">
{{ form.extension_plugins }}
</div>
</div>
</div>
<!-- Advanced options (collapsible) -->
<div class="form-section">
<details class="advanced-section">
<summary><h3>Advanced Crawl Options</h3></summary>
<p class="section-description">Additional settings that control how this crawl processes URLs and creates snapshots.</p>
<div class="form-field">
{{ form.schedule.label_tag }}
{{ form.schedule }}
{% if form.schedule.errors %}
<div class="error">{{ form.schedule.errors }}</div>
{% endif %}
<div class="help-text">
Optional: Schedule this crawl to repeat automatically. Examples:<br/>
<code>daily</code> - Run once per day<br/>
<code>weekly</code> - Run once per week<br/>
<code>0 */6 * * *</code> - Every 6 hours (cron format)<br/>
<code>0 0 * * 0</code> - Every Sunday at midnight (cron format)
</div>
</div>
<div class="form-field">
{{ form.persona.label_tag }}
{{ form.persona }}
{% if form.persona.errors %}
<div class="error">{{ form.persona.errors }}</div>
{% endif %}
<div class="help-text">
Authentication profile to use for all snapshots in this crawl.
<a href="/admin/personas/persona/add/" target="_blank">Create new persona →</a>
</div>
</div>
<div class="form-field checkbox-field">
{{ form.overwrite }}
{{ form.overwrite.label_tag }}
{% if form.overwrite.errors %}
<div class="error">{{ form.overwrite.errors }}</div>
{% endif %}
<div class="help-text">Re-archive URLs even if they already exist</div>
</div>
<div class="form-field checkbox-field">
{{ form.update }}
{{ form.update.label_tag }}
{% if form.update.errors %}
<div class="error">{{ form.update.errors }}</div>
{% endif %}
<div class="help-text">Retry archiving URLs that previously failed</div>
</div>
<div class="form-field checkbox-field">
{{ form.index_only }}
{{ form.index_only.label_tag }}
{% if form.index_only.errors %}
<div class="error">{{ form.index_only.errors }}</div>
{% endif %}
<div class="help-text">Create snapshots but don't run archiving plugins yet (queue for later)</div>
</div>
<div class="form-field">
{{ form.config.label_tag }}
{{ form.config }}
{% if form.config.errors %}
<div class="error">{{ form.config.errors }}</div>
{% endif %}
<div class="help-text">
Override any config option for this crawl (e.g., TIMEOUT, USER_AGENT, CHROME_BINARY, etc.)
</div>
</div>
</details>
</div>
<center>
<button role="submit" id="submit">&nbsp; Add URLs and archive </button>
<button role="submit" id="submit">&nbsp; Create Crawl and Start Archiving </button>
</center>
</form>
<br/><br/><br/>
<center id="delay-warning" style="display: none">
<small>(you will be redirected to your <a href="/">Snapshot list</a> momentarily, its safe to close this page at any time)</small>
<small>(you will be redirected to your new Crawl page momentarily, it's safe to close this page at any time)</small>
</center>
{% if absolute_add_path %}
<!-- <center id="bookmarklet">
@@ -55,6 +270,109 @@
</center> -->
{% endif %}
<script>
// URL Counter - detect URLs in textarea using regex
const urlTextarea = document.querySelector('textarea[name="url"]');
const urlCounter = document.getElementById('url-counter');
function updateURLCount() {
const text = urlTextarea.value;
// Match http(s):// URLs
const urlRegex = /https?:\/\/[^\s]+/gi;
const matches = text.match(urlRegex) || [];
const count = matches.length;
urlCounter.textContent = `${count} URL${count !== 1 ? 's' : ''} detected`;
urlCounter.className = count > 0 ? 'url-counter url-counter-positive' : 'url-counter';
}
urlTextarea.addEventListener('input', updateURLCount);
updateURLCount(); // Initial count
// Plugin Presets
const presetConfigs = {
'quick-archive': ['screenshot', 'dom', 'favicon', 'wget', 'title'],
'full-chrome': ['chrome', 'screenshot', 'pdf', 'dom', 'singlefile', 'consolelog', 'redirects', 'responses', 'ssl', 'headers', 'title', 'accessibility', 'seo'],
'text-only': ['wget', 'readability', 'mercury', 'htmltotext', 'title', 'favicon']
};
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', function() {
const preset = this.dataset.preset;
const allCheckboxes = document.querySelectorAll('.plugin-checkboxes input[type="checkbox"]');
if (preset === 'select-all') {
allCheckboxes.forEach(cb => cb.checked = true);
} else if (preset === 'clear-all') {
allCheckboxes.forEach(cb => cb.checked = false);
} else if (presetConfigs[preset]) {
const pluginsToSelect = presetConfigs[preset];
allCheckboxes.forEach(cb => {
cb.checked = pluginsToSelect.includes(cb.value);
});
}
// Save to localStorage after preset selection
saveFormState();
});
});
// Select All Chrome button handler
document.querySelectorAll('.select-all-btn').forEach(btn => {
btn.addEventListener('click', function() {
const group = this.dataset.group;
const container = document.getElementById(group + '-plugins');
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => {
cb.checked = !allChecked;
});
this.textContent = allChecked ? 'Select All Chrome' : 'Deselect All Chrome';
saveFormState();
});
});
// LocalStorage: Save/Load form state (all fields including URLs for repeat crawls)
const STORAGE_KEY = 'archivebox_add_form_state';
function saveFormState() {
const state = {};
document.querySelectorAll('#add-form input, #add-form textarea, #add-form select').forEach(el => {
if (el.name === 'csrfmiddlewaretoken') return;
if (el.type === 'checkbox' || el.type === 'radio') {
state[el.name + ':' + el.value] = el.checked;
} else {
state[el.name] = el.value;
}
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
function loadFormState() {
try {
const state = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
for (const [key, value] of Object.entries(state)) {
if (key.includes(':')) {
const [name, val] = key.split(':');
const el = document.querySelector(`[name="${name}"][value="${val}"]`);
if (el) el.checked = value;
} else {
const el = document.querySelector(`[name="${key}"]`);
if (el && el.type !== 'checkbox' && el.type !== 'radio') el.value = value;
}
}
updateURLCount(); // Update counter after loading URLs
} catch (e) {}
}
// Auto-save on changes
document.querySelectorAll('#add-form input, #add-form textarea, #add-form select').forEach(el => {
el.addEventListener('change', saveFormState);
});
loadFormState();
// Form submission handler
document.getElementById('add-form').addEventListener('submit', function(event) {
document.getElementById('in-progress').style.display = 'block'
document.getElementById('add-form').style.display = 'none'

View File

@@ -1,4 +1,4 @@
{% load tz core_tags %}
{% load tz core_tags config_tags %}
<!DOCTYPE html>
<html lang="en">
@@ -358,64 +358,26 @@
</div>
</div>
<div class="row header-bottom-frames">
<div class="col-lg-2">
<div class="card selected-card">
<iframe class="card-img-top" src="{{singlefile_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{singlefile_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./singlefile.html</code></p>
</a>
<a href="{{singlefile_path}}" target="preview"><h4 class="card-title">Chrome &gt; SingleFile</h4></a>
</div>
</div>
</div>
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top pdf-frame" src="{{pdf_path}}#toolbar=0" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{pdf_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./output.pdf</code></p>
</a>
<a href="{{pdf_path}}" target="preview" id="pdf-btn"><h4 class="card-title">Chrome &gt; PDF</h4></a>
</div>
</div>
</div>
<div class="col-lg-2">
<div class="card">
<img class="card-img-top" src="{{screenshot_path}}" onerror="this.style.opacity=0.2"/>
<div class="card-body">
<a href="{{screenshot_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./screenshot.png</code></p>
</a>
<a href="{{screenshot_path}}" target="preview"><h4 class="card-title">Chrome &gt; Screenshot</h4></a>
</div>
</div>
</div>
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{archive_url}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{archive_url}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./{{domain}}</code></p>
</a>
<a href="{{archive_url}}" target="preview"><h4 class="card-title">Wget &gt; HTML</h4></a>
{% for result_info in archiveresults %}
{% if result_info.result %}
<div class="col-lg-2">
<div class="card{% if forloop.first %} selected-card{% endif %}">
{% plugin_thumbnail result_info.result %}
<div class="card-body">
<a href="{{ result_info.path }}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>{{ result_info.path }}</code></p>
</a>
<a href="{{ result_info.path }}" target="preview">
<h4 class="card-title">{{ result_info.name|title }}</h4>
</a>
</div>
</div>
</div>
</div>
</div>
{% if SAVE_ARCHIVE_DOT_ORG %}
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{archive_org_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{archive_org_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>🌐 web.archive.org/web/...</code></p>
</a>
<a href="{{archive_org_path}}" target="preview" id="archive_dot_org-btn"><h4 class="card-title">Archive.Org</h4></a>
</div>
</div>
</div>
{% endif %}
{% if PREVIEW_ORIGINALS %}
{% endif %}
{% endfor %}
{% get_config "PREVIEW_ORIGINALS" as preview_originals %}
{% if preview_originals %}
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{url}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy" referrerpolicy="no-referrer"></iframe>
@@ -426,77 +388,10 @@
<a href="{{url}}" target="preview" id="original-btn" referrerpolicy="no-referrer">
<h4 class="card-title">Original</h4>
</a>
</div>
</div>
</div>
</div>
{% endif %}
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{headers_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{headers_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./headers.json</code></p>
</a>
<a href="{{headers_path}}" target="preview"><h4 class="card-title">Headers</h4></a>
</div>
</div>
</div>
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{dom_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{dom_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./output.html</code></p>
</a>
<a href="{{dom_path}}" target="preview"><h4 class="card-title">Chrome &gt; HTML</h4></a>
</div>
</div>
</div>
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{readability_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{readability_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./readability/content.html</code></p>
</a>
<a href="{{readability_path}}" target="preview"><h4 class="card-title">Readability</h4></a>
</div>
</div>
</div>
<br/>
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{mercury_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{mercury_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./mercury/content.html</code></p>
</a>
<a href="{{mercury_path}}" target="preview"><h4 class="card-title">Mercury</h4></a>
</div>
</div>
</div>
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{media_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{media_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./media/*.mp4</code></p>
</a>
<a href="{{media_path}}" target="preview"><h4 class="card-title">Media</h4></a>
</div>
</div>
</div>
<div class="col-lg-2">
<div class="card">
<iframe class="card-img-top" src="{{git_path}}" sandbox="allow-same-origin allow-top-navigation-by-user-activation allow-scripts allow-forms" scrolling="no" loading="lazy"></iframe>
<div class="card-body">
<a href="{{git_path}}" title="Open in new tab..." target="_blank" rel="noopener">
<p class="card-text"><code>./git/*.git</code></p>
</a>
<a href="{{git_path}}" target="preview"><h4 class="card-title">Git</h4></a>
</div>
</div>
</div>
</div>
</div>
</header>

View File

@@ -72,19 +72,339 @@ ul#id_depth {
}
textarea, select {
textarea, select, input[type="text"] {
border-radius: 4px;
border: 2px solid #004882;
box-shadow: 4px 4px 4px rgba(0,0,0,0.02);
box-shadow: 4px 4px 4px rgba(0,0,0,0.02);
width: 100%;
padding: 8px 12px;
font-size: 14px;
}
select option:not(:checked) {
border: 1px dashed rgba(10,200,20,0.12);
}
select option:checked {
border: 1px solid green;
background-color: green;
color: green;
textarea {
min-height: 300px;
}
textarea[rows="3"] {
min-height: 80px;
}
select {
min-height: 40px;
}
/* Crawl explanation box */
.crawl-explanation {
background-color: #e8f4f8;
border-left: 4px solid #004882;
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 4px;
}
.crawl-explanation p {
margin: 0;
line-height: 1.6;
color: #333;
}
/* Form sections */
.form-section {
margin-bottom: 30px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
}
.form-section h3 {
margin-top: 0;
margin-bottom: 15px;
color: #004882;
font-size: 18px;
}
.section-description {
margin: 0 0 15px 0;
color: #666;
font-size: 14px;
line-height: 1.5;
}
.section-description a {
color: #004882;
text-decoration: none;
font-weight: 500;
}
.section-description a:hover {
text-decoration: underline;
}
.help-text code {
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
font-size: 12px;
color: #333;
}
.form-field {
margin-bottom: 20px;
}
.form-field label {
display: block;
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.form-field .help-text {
font-size: 12px;
color: #666;
margin-top: 4px;
font-style: italic;
}
.form-field .error {
color: #ba2121;
font-size: 13px;
margin-top: 4px;
}
/* Checkbox fields (for overwrite, update, index_only) */
.checkbox-field {
display: flex;
align-items: center;
gap: 10px;
}
.checkbox-field input[type="checkbox"] {
width: auto;
margin: 0;
}
.checkbox-field label {
margin: 0;
font-weight: normal;
}
/* URL Counter */
.url-counter {
display: inline-block;
margin-top: 8px;
padding: 4px 10px;
font-size: 13px;
font-weight: 600;
color: #666;
background-color: #f5f5f5;
border-radius: 4px;
border: 1px solid #ddd;
}
.url-counter-positive {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
/* Plugin Presets */
.plugin-presets {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
}
.preset-label {
font-weight: 600;
color: #495057;
margin-right: 8px;
}
.preset-btn {
padding: 6px 14px;
font-size: 13px;
font-weight: 500;
background-color: white;
border: 1px solid #ced4da;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.preset-btn:hover {
background-color: #e9ecef;
border-color: #adb5bd;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.preset-btn:active {
transform: translateY(0);
box-shadow: none;
}
/* Plugin groups */
.plugin-group {
margin-bottom: 20px;
padding: 15px;
background-color: white;
border: 1px solid #ddd;
border-radius: 6px;
}
.plugin-group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #004882;
}
.plugin-group-header label {
font-size: 15px;
font-weight: 700;
color: #004882;
margin: 0;
}
.select-all-btn {
padding: 4px 12px;
font-size: 12px;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.select-all-btn:hover {
background-color: #e0e0e0;
}
.plugin-checkboxes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
}
.plugin-checkboxes ul {
list-style-type: none;
padding: 0;
margin: 0;
display: contents;
}
.plugin-checkboxes li {
display: flex;
align-items: center;
gap: 8px;
padding: 6px;
border-radius: 4px;
transition: background-color 0.2s;
}
.plugin-checkboxes li:hover {
background-color: #f5f5f5;
}
.plugin-checkboxes input[type="checkbox"] {
margin: 0;
width: auto;
}
.plugin-checkboxes label {
margin: 0;
font-size: 14px;
font-weight: normal;
cursor: pointer;
}
/* Advanced section (collapsible) */
.advanced-section {
background-color: white;
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
}
.advanced-section summary {
cursor: pointer;
user-select: none;
list-style: none;
}
.advanced-section summary::-webkit-details-marker {
display: none;
}
.advanced-section summary h3 {
display: inline-block;
margin: 0;
color: #004882;
}
.advanced-section summary h3:before {
content: '▶ ';
display: inline-block;
transition: transform 0.2s;
}
.advanced-section[open] summary h3:before {
transform: rotate(90deg);
}
.advanced-section summary:hover {
color: #003060;
}
.advanced-section[open] .form-field {
margin-top: 20px;
}
/* Depth radio buttons */
ul#id_depth li {
margin-bottom: 8px;
}
/* Focus indicators for accessibility */
input:focus, select:focus, textarea:focus, button:focus {
outline: 3px solid #4A90E2;
outline-offset: 2px;
}
/* Responsive layout */
@media (max-width: 768px) {
.plugin-checkboxes {
grid-template-columns: 1fr;
}
.plugin-group-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.plugin-presets {
flex-direction: column;
align-items: stretch;
}
.preset-label {
margin-bottom: 4px;
}
.preset-btn {
width: 100%;
text-align: center;
}
}