|
|
|
@@ -12,7 +12,7 @@
|
|
|
|
|
EditorMixUpdateInput,
|
|
|
|
|
RawMaterial
|
|
|
|
|
} from '$lib/types';
|
|
|
|
|
import { ChevronLeft, ChevronRight, FlaskConical, History, ListFilter, Plus, Save, Search, TriangleAlert, X } from 'lucide-svelte';
|
|
|
|
|
import { CheckCircle2, ChevronLeft, ChevronRight, EyeOff, FlaskConical, History, ListChecks, ListFilter, Plus, Save, Search, Trash2, TriangleAlert, X } from 'lucide-svelte';
|
|
|
|
|
import { fade } from 'svelte/transition';
|
|
|
|
|
|
|
|
|
|
let { data } = $props();
|
|
|
|
@@ -386,6 +386,91 @@
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Bulk select: tick multiple mixes, then delete or mark them inactive ──
|
|
|
|
|
let selectMode = $state(false);
|
|
|
|
|
let selectedIds = $state<Set<number>>(new Set());
|
|
|
|
|
// Which bulk action is awaiting confirmation (null = no modal open).
|
|
|
|
|
let bulkAction = $state<'inactive' | 'delete' | null>(null);
|
|
|
|
|
let bulkRunning = $state(false);
|
|
|
|
|
// Summary shown after a bulk run finishes (null = closed).
|
|
|
|
|
let bulkResult = $state<
|
|
|
|
|
{ action: 'inactive' | 'delete'; succeeded: number; failures: { name: string; reason: string }[] } | null
|
|
|
|
|
>(null);
|
|
|
|
|
|
|
|
|
|
// Select-all operates on the rows currently on screen; the selection itself
|
|
|
|
|
// persists across pages so a multi-page selection is possible.
|
|
|
|
|
const pageRowIds = $derived(table.rows.map((row) => row.id));
|
|
|
|
|
const selectedOnPage = $derived(pageRowIds.filter((id) => selectedIds.has(id)).length);
|
|
|
|
|
const allPageSelected = $derived(pageRowIds.length > 0 && selectedOnPage === pageRowIds.length);
|
|
|
|
|
const somePageSelected = $derived(selectedOnPage > 0 && !allPageSelected);
|
|
|
|
|
|
|
|
|
|
function toggleSelectMode() {
|
|
|
|
|
selectMode = !selectMode;
|
|
|
|
|
if (!selectMode) selectedIds = new Set();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleRowSelected(id: number) {
|
|
|
|
|
const next = new Set(selectedIds);
|
|
|
|
|
if (next.has(id)) next.delete(id);
|
|
|
|
|
else next.add(id);
|
|
|
|
|
selectedIds = next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleSelectAll() {
|
|
|
|
|
const next = new Set(selectedIds);
|
|
|
|
|
if (allPageSelected) {
|
|
|
|
|
for (const id of pageRowIds) next.delete(id);
|
|
|
|
|
} else {
|
|
|
|
|
for (const id of pageRowIds) next.add(id);
|
|
|
|
|
}
|
|
|
|
|
selectedIds = next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function requestBulk(action: 'inactive' | 'delete') {
|
|
|
|
|
if (selectedIds.size === 0) return;
|
|
|
|
|
bulkAction = action;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runBulk() {
|
|
|
|
|
const action = bulkAction;
|
|
|
|
|
if (!action) return;
|
|
|
|
|
bulkRunning = true;
|
|
|
|
|
|
|
|
|
|
const targets = rows.filter((row) => selectedIds.has(row.id));
|
|
|
|
|
const failures: { name: string; reason: string }[] = [];
|
|
|
|
|
const succeededIds = new Set<number>();
|
|
|
|
|
|
|
|
|
|
for (const row of targets) {
|
|
|
|
|
try {
|
|
|
|
|
if (action === 'delete') {
|
|
|
|
|
await api.deleteEditorMix(row.id);
|
|
|
|
|
} else {
|
|
|
|
|
applyMixUpdate(await api.updateEditorMix(row.id, { visible: false }));
|
|
|
|
|
}
|
|
|
|
|
succeededIds.add(row.id);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
failures.push({ name: row.name, reason: error instanceof Error ? error.message : 'Could not be updated' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (action === 'delete') {
|
|
|
|
|
rows = rows.filter((row) => !succeededIds.has(row.id));
|
|
|
|
|
// Close the ingredient panel if its mix was just deleted.
|
|
|
|
|
if (expandedMixId !== null && succeededIds.has(expandedMixId)) {
|
|
|
|
|
expandedMixId = null;
|
|
|
|
|
activeFormula = null;
|
|
|
|
|
ingredientDrafts = [];
|
|
|
|
|
ingredientBaseline = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep only failures selected so the user can see/retry them.
|
|
|
|
|
selectedIds = new Set([...selectedIds].filter((id) => !succeededIds.has(id)));
|
|
|
|
|
bulkResult = { action, succeeded: succeededIds.size, failures };
|
|
|
|
|
bulkAction = null;
|
|
|
|
|
bulkRunning = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Jump back to the first page whenever the filtered set changes.
|
|
|
|
|
$effect(() => {
|
|
|
|
|
query;
|
|
|
|
@@ -475,6 +560,11 @@
|
|
|
|
|
</div>
|
|
|
|
|
</dl>
|
|
|
|
|
|
|
|
|
|
<button class="clear-button select-toggle" class:active={selectMode} type="button" onclick={toggleSelectMode}>
|
|
|
|
|
<ListChecks size={16} strokeWidth={2.2} />
|
|
|
|
|
{selectMode ? 'Done' : 'Select'}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button class="apply-button new-mix-button" type="button" onclick={openCreateMix} disabled={creatingMix}>
|
|
|
|
|
<Plus size={16} strokeWidth={2.4} />
|
|
|
|
|
New mix
|
|
|
|
@@ -543,16 +633,58 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="log">
|
|
|
|
|
{#if selectMode}
|
|
|
|
|
<div class="bulk-bar" transition:fade={{ duration: 120 }} aria-label="Bulk actions">
|
|
|
|
|
<div class="bulk-count">
|
|
|
|
|
<span class="bulk-badge">{selectedIds.size}</span>
|
|
|
|
|
<span>selected</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="bulk-actions">
|
|
|
|
|
<button class="clear-button" type="button" disabled={selectedIds.size === 0} onclick={() => requestBulk('inactive')}>
|
|
|
|
|
<EyeOff size={16} strokeWidth={2.2} />
|
|
|
|
|
Mark inactive
|
|
|
|
|
</button>
|
|
|
|
|
<button class="danger-button" type="button" disabled={selectedIds.size === 0} onclick={() => requestBulk('delete')}>
|
|
|
|
|
<Trash2 size={16} strokeWidth={2.2} />
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<div class="log" class:select-mode={selectMode}>
|
|
|
|
|
<div class="log-head">
|
|
|
|
|
{#if selectMode}
|
|
|
|
|
<label class="select-cell">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={allPageSelected}
|
|
|
|
|
indeterminate={somePageSelected}
|
|
|
|
|
onchange={toggleSelectAll}
|
|
|
|
|
aria-label="Select all mixes on this page"
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
{/if}
|
|
|
|
|
<SortHeader label="Client" column="client_name" controller={table} />
|
|
|
|
|
<SortHeader label="Mix" column="name" controller={table} />
|
|
|
|
|
<SortHeader label="Status" column="visible" controller={table} />
|
|
|
|
|
<span>Actions</span>
|
|
|
|
|
<span class="actions-head">Actions</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#each table.rows as row (row.id)}
|
|
|
|
|
<div class="row" class:edited={rowDirty(row)}>
|
|
|
|
|
<div class="row" class:edited={rowDirty(row)} class:selected={selectMode && selectedIds.has(row.id)}>
|
|
|
|
|
{#if selectMode}
|
|
|
|
|
<label class="select-cell">
|
|
|
|
|
<span class="cell-label">Select</span>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectedIds.has(row.id)}
|
|
|
|
|
onchange={() => toggleRowSelected(row.id)}
|
|
|
|
|
aria-label={`Select ${row.name}`}
|
|
|
|
|
/>
|
|
|
|
|
</label>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<div class="client-cell">
|
|
|
|
|
<span class="cell-label">Client</span>
|
|
|
|
|
<span class="readonly-value">{row.client_name}</span>
|
|
|
|
@@ -698,6 +830,99 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if bulkAction}
|
|
|
|
|
<div class="modal-backdrop" role="presentation" onclick={() => { if (!bulkRunning) bulkAction = null; }}>
|
|
|
|
|
<div
|
|
|
|
|
class="modal-card"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-labelledby="bulk-title"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
onclick={(event) => event.stopPropagation()}
|
|
|
|
|
onkeydown={(event) => { if (event.key === 'Escape' && !bulkRunning) bulkAction = null; }}
|
|
|
|
|
>
|
|
|
|
|
<div class="modal-icon" class:danger={bulkAction === 'delete'}>
|
|
|
|
|
{#if bulkAction === 'delete'}
|
|
|
|
|
<Trash2 size={22} strokeWidth={2.2} />
|
|
|
|
|
{:else}
|
|
|
|
|
<EyeOff size={22} strokeWidth={2.2} />
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<h2 id="bulk-title" class="modal-title">
|
|
|
|
|
{bulkAction === 'delete' ? 'Delete' : 'Mark inactive'}
|
|
|
|
|
{selectedIds.size}
|
|
|
|
|
{selectedIds.size === 1 ? 'mix' : 'mixes'}?
|
|
|
|
|
</h2>
|
|
|
|
|
<p class="modal-text">
|
|
|
|
|
{#if bulkAction === 'delete'}
|
|
|
|
|
This permanently removes the selected mixes and their formulas. Any mix that still has linked
|
|
|
|
|
products can't be deleted and will be skipped.
|
|
|
|
|
{:else}
|
|
|
|
|
The selected mixes will be hidden from the active list. You can re-activate them anytime from the
|
|
|
|
|
Inactive filter.
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
<button type="button" class="modal-cancel" disabled={bulkRunning} onclick={() => (bulkAction = null)}>Cancel</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="modal-confirm"
|
|
|
|
|
class:neutral={bulkAction === 'inactive'}
|
|
|
|
|
disabled={bulkRunning}
|
|
|
|
|
onclick={runBulk}
|
|
|
|
|
>
|
|
|
|
|
{bulkRunning ? 'Working…' : bulkAction === 'delete' ? 'Delete mixes' : 'Mark inactive'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if bulkResult}
|
|
|
|
|
<div class="modal-backdrop" role="presentation" onclick={() => (bulkResult = null)}>
|
|
|
|
|
<div
|
|
|
|
|
class="modal-card"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-labelledby="bulk-result-title"
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
onclick={(event) => event.stopPropagation()}
|
|
|
|
|
onkeydown={(event) => { if (event.key === 'Escape') bulkResult = null; }}
|
|
|
|
|
>
|
|
|
|
|
<div class="modal-icon" class:danger={bulkResult.failures.length > 0} class:ok={bulkResult.failures.length === 0}>
|
|
|
|
|
{#if bulkResult.failures.length === 0}
|
|
|
|
|
<CheckCircle2 size={22} strokeWidth={2.2} />
|
|
|
|
|
{:else}
|
|
|
|
|
<TriangleAlert size={22} strokeWidth={2.2} />
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<h2 id="bulk-result-title" class="modal-title">
|
|
|
|
|
{bulkResult.action === 'delete' ? 'Delete complete' : 'Update complete'}
|
|
|
|
|
</h2>
|
|
|
|
|
<p class="modal-text">
|
|
|
|
|
{bulkResult.succeeded}
|
|
|
|
|
{bulkResult.succeeded === 1 ? 'mix' : 'mixes'}
|
|
|
|
|
{bulkResult.action === 'delete' ? 'deleted' : 'marked inactive'}{bulkResult.failures.length
|
|
|
|
|
? `, ${bulkResult.failures.length} skipped`
|
|
|
|
|
: ''}.
|
|
|
|
|
</p>
|
|
|
|
|
{#if bulkResult.failures.length}
|
|
|
|
|
<ul class="result-failures">
|
|
|
|
|
{#each bulkResult.failures as failure}
|
|
|
|
|
<li>
|
|
|
|
|
<strong>{failure.name}</strong>
|
|
|
|
|
<span>{failure.reason}</span>
|
|
|
|
|
</li>
|
|
|
|
|
{/each}
|
|
|
|
|
</ul>
|
|
|
|
|
{/if}
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
<button type="button" class="modal-confirm neutral" onclick={() => (bulkResult = null)}>Done</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</AppSecondaryRailLayout>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
@@ -842,6 +1067,81 @@
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.select-toggle.active {
|
|
|
|
|
color: var(--color-brand);
|
|
|
|
|
border-color: var(--color-brand);
|
|
|
|
|
background: var(--color-brand-tint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Bulk action bar: appears above the table while in select mode. */
|
|
|
|
|
.bulk-bar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
padding: 0.6rem 0.85rem;
|
|
|
|
|
border: 1px solid color-mix(in srgb, var(--color-brand) 30%, var(--color-border));
|
|
|
|
|
border-radius: 0.7rem;
|
|
|
|
|
background: var(--color-brand-tint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bulk-count {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
font-size: 0.88rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bulk-badge {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-width: 1.6rem;
|
|
|
|
|
height: 1.6rem;
|
|
|
|
|
padding: 0 0.45rem;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
background: var(--color-brand);
|
|
|
|
|
color: var(--color-on-brand);
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.bulk-actions {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.danger-button {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: 0.45rem;
|
|
|
|
|
min-height: 34px;
|
|
|
|
|
border-radius: 0.45rem;
|
|
|
|
|
padding: 0.45rem 0.65rem;
|
|
|
|
|
font-size: 0.86rem;
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
color: var(--color-on-brand, #fff);
|
|
|
|
|
background: var(--color-error);
|
|
|
|
|
border: 1px solid var(--color-error);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.danger-button:hover {
|
|
|
|
|
background: color-mix(in srgb, var(--color-error) 88%, black);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.danger-button:disabled {
|
|
|
|
|
opacity: 0.55;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.facts {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 1.35rem;
|
|
|
|
@@ -1140,24 +1440,47 @@
|
|
|
|
|
border-color: var(--color-brand);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* The table is one grid that owns the column tracks; the header and every row
|
|
|
|
|
are subgrids that share those exact tracks. This is what keeps the headers
|
|
|
|
|
lined up with the fields below — separate grids would each size their own
|
|
|
|
|
`auto` Actions column from their own content and drift apart. */
|
|
|
|
|
.log {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns:
|
|
|
|
|
minmax(150px, 1fr)
|
|
|
|
|
minmax(200px, 1.6fr)
|
|
|
|
|
minmax(96px, 0.5fr)
|
|
|
|
|
minmax(198px, auto);
|
|
|
|
|
column-gap: 0.55rem;
|
|
|
|
|
row-gap: 0;
|
|
|
|
|
background: var(--color-bg-surface);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: 0.9rem;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* A leading checkbox column appears in select mode; subgrid rows pick it up
|
|
|
|
|
automatically, so header and rows stay aligned. */
|
|
|
|
|
.log.select-mode {
|
|
|
|
|
grid-template-columns:
|
|
|
|
|
2rem
|
|
|
|
|
minmax(150px, 1fr)
|
|
|
|
|
minmax(200px, 1.6fr)
|
|
|
|
|
minmax(96px, 0.5fr)
|
|
|
|
|
minmax(198px, auto);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.log-head,
|
|
|
|
|
.row,
|
|
|
|
|
.ingredient-panel,
|
|
|
|
|
.empty {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.log-head,
|
|
|
|
|
.row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns:
|
|
|
|
|
minmax(170px, 1fr)
|
|
|
|
|
minmax(220px, 1.6fr)
|
|
|
|
|
minmax(110px, 0.5fr)
|
|
|
|
|
minmax(198px, auto);
|
|
|
|
|
gap: 0.55rem;
|
|
|
|
|
grid-template-columns: subgrid;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1175,6 +1498,17 @@
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Line the header labels and read-only cell text up with the input text,
|
|
|
|
|
which sits one input-padding (0.5rem) in from the cell's left edge. The
|
|
|
|
|
Actions header instead hugs the right, above the right-aligned buttons. */
|
|
|
|
|
.log-head :global(.sort-header) {
|
|
|
|
|
padding-left: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.actions-head {
|
|
|
|
|
text-align: right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.row {
|
|
|
|
|
padding: 0.58rem 0.85rem;
|
|
|
|
|
border-bottom: 1px solid var(--color-divider);
|
|
|
|
@@ -1189,10 +1523,31 @@
|
|
|
|
|
background: var(--color-brand-tint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.row.selected {
|
|
|
|
|
background: color-mix(in srgb, var(--color-brand) 9%, var(--color-bg-surface));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Leading checkbox cell (header select-all + per-row select). */
|
|
|
|
|
.select-cell {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-height: 34px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.select-cell input {
|
|
|
|
|
width: 1.05rem;
|
|
|
|
|
min-height: 1.05rem;
|
|
|
|
|
margin: 0;
|
|
|
|
|
accent-color: var(--color-brand);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.readonly-value {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
min-height: 34px;
|
|
|
|
|
padding-left: 0.5rem;
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
font-size: 0.88rem;
|
|
|
|
|
font-weight: 650;
|
|
|
|
@@ -1221,7 +1576,7 @@
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
gap: 0.3rem;
|
|
|
|
|
min-height: 34px;
|
|
|
|
|
padding: 0.25rem 0;
|
|
|
|
|
padding: 0.25rem 0.5rem;
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: 0.42rem;
|
|
|
|
|
background: transparent;
|
|
|
|
@@ -1381,11 +1736,21 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1180px) {
|
|
|
|
|
/* Drop the shared grid and stack each row as its own card. */
|
|
|
|
|
.log,
|
|
|
|
|
.log.select-mode {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
grid-template-columns: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.log-head {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.row {
|
|
|
|
|
.row,
|
|
|
|
|
.log.select-mode .row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1393,6 +1758,12 @@
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Stacked card layout: show the checkbox inline with its label. */
|
|
|
|
|
.select-cell {
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
gap: 0.5rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.row-actions {
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
}
|
|
|
|
@@ -1479,6 +1850,16 @@
|
|
|
|
|
color: var(--color-warning-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-icon.danger {
|
|
|
|
|
background: var(--color-error-tint, color-mix(in srgb, var(--color-error) 14%, transparent));
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-icon.ok {
|
|
|
|
|
background: color-mix(in srgb, var(--color-success) 14%, transparent);
|
|
|
|
|
color: var(--color-success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-title,
|
|
|
|
|
.modal-text {
|
|
|
|
|
margin: 0;
|
|
|
|
@@ -1536,9 +1917,58 @@
|
|
|
|
|
background: color-mix(in srgb, var(--color-error) 85%, black);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Non-destructive confirm (mark inactive / acknowledge result). */
|
|
|
|
|
.modal-confirm.neutral {
|
|
|
|
|
background: var(--color-brand);
|
|
|
|
|
border-color: var(--color-brand);
|
|
|
|
|
color: var(--color-on-brand);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-confirm.neutral:hover {
|
|
|
|
|
background: color-mix(in srgb, var(--color-brand) 88%, black);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-cancel:disabled,
|
|
|
|
|
.modal-confirm:disabled {
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.modal-cancel:focus-visible,
|
|
|
|
|
.modal-confirm:focus-visible {
|
|
|
|
|
outline: 3px solid var(--color-brand);
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Per-mix reasons shown in the result modal when some rows were skipped. */
|
|
|
|
|
.result-failures {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 0.4rem;
|
|
|
|
|
max-height: 11rem;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
list-style: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-failures li {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 0.1rem;
|
|
|
|
|
padding: 0.5rem 0.65rem;
|
|
|
|
|
border: 1px solid color-mix(in srgb, var(--color-error) 28%, var(--color-border));
|
|
|
|
|
border-radius: 0.55rem;
|
|
|
|
|
background: color-mix(in srgb, var(--color-error) 7%, var(--color-bg-surface));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-failures strong {
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.result-failures span {
|
|
|
|
|
font-size: 0.82rem;
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|