redesign model and variable selection
This commit is contained in:
@@ -294,6 +294,16 @@
|
||||
"compare_selection_pending": "Auswahl geändert",
|
||||
"compare_apply_selection": "Anwenden & Diagramme neu laden",
|
||||
"compare_customize_models": "Modelle anpassen",
|
||||
"compare_edit_models": "Modelle bearbeiten",
|
||||
"compare_edit_variables": "Variablen bearbeiten",
|
||||
"compare_edit_models_description": "Modelle suchen oder ganze Anbietergruppen auswählen.",
|
||||
"compare_edit_variables_description": "Variablen suchen oder ganze Themengruppen auswählen.",
|
||||
"compare_search_models": "Modelle suchen",
|
||||
"compare_search_variables": "Variablen suchen",
|
||||
"compare_only_selected": "Nur ausgewählte",
|
||||
"compare_select_group": "Alle auswählen",
|
||||
"compare_clear_group": "Alle abwählen",
|
||||
"compare_no_matching_options": "Keine passenden Optionen.",
|
||||
"compare_no_models_selected": "Keine Modelle ausgewählt.",
|
||||
"compare_remove_model": "{model} entfernen",
|
||||
"compare_timeline_title": "Zeitleiste des Modellvergleichs",
|
||||
|
||||
@@ -294,6 +294,16 @@
|
||||
"compare_selection_pending": "Selection changed",
|
||||
"compare_apply_selection": "Apply & reload charts",
|
||||
"compare_customize_models": "Customize models",
|
||||
"compare_edit_models": "Edit models",
|
||||
"compare_edit_variables": "Edit variables",
|
||||
"compare_edit_models_description": "Search models or select complete provider families.",
|
||||
"compare_edit_variables_description": "Search variables or select complete thematic groups.",
|
||||
"compare_search_models": "Search models",
|
||||
"compare_search_variables": "Search variables",
|
||||
"compare_only_selected": "Only selected",
|
||||
"compare_select_group": "Select all",
|
||||
"compare_clear_group": "Clear all",
|
||||
"compare_no_matching_options": "No matching options.",
|
||||
"compare_no_models_selected": "No models selected.",
|
||||
"compare_remove_model": "Remove {model}",
|
||||
"compare_timeline_title": "Model Comparison Timeline",
|
||||
|
||||
@@ -294,6 +294,16 @@
|
||||
"compare_selection_pending": "Selección modificada",
|
||||
"compare_apply_selection": "Aplicar y recargar gráficos",
|
||||
"compare_customize_models": "Personalizar modelos",
|
||||
"compare_edit_models": "Editar modelos",
|
||||
"compare_edit_variables": "Editar variables",
|
||||
"compare_edit_models_description": "Busca modelos o selecciona familias completas de proveedores.",
|
||||
"compare_edit_variables_description": "Busca variables o selecciona grupos temáticos completos.",
|
||||
"compare_search_models": "Buscar modelos",
|
||||
"compare_search_variables": "Buscar variables",
|
||||
"compare_only_selected": "Solo seleccionados",
|
||||
"compare_select_group": "Seleccionar todo",
|
||||
"compare_clear_group": "Deseleccionar todo",
|
||||
"compare_no_matching_options": "No hay opciones coincidentes.",
|
||||
"compare_no_models_selected": "No hay modelos seleccionados.",
|
||||
"compare_remove_model": "Quitar {model}",
|
||||
"compare_timeline_title": "Cronología de comparación de modelos",
|
||||
|
||||
@@ -294,6 +294,16 @@
|
||||
"compare_selection_pending": "Sélection modifiée",
|
||||
"compare_apply_selection": "Appliquer et recharger les graphiques",
|
||||
"compare_customize_models": "Personnaliser les modèles",
|
||||
"compare_edit_models": "Modifier les modèles",
|
||||
"compare_edit_variables": "Modifier les variables",
|
||||
"compare_edit_models_description": "Recherchez des modèles ou sélectionnez des fournisseurs entiers.",
|
||||
"compare_edit_variables_description": "Recherchez des variables ou sélectionnez des groupes thématiques entiers.",
|
||||
"compare_search_models": "Rechercher des modèles",
|
||||
"compare_search_variables": "Rechercher des variables",
|
||||
"compare_only_selected": "Sélection uniquement",
|
||||
"compare_select_group": "Tout sélectionner",
|
||||
"compare_clear_group": "Tout désélectionner",
|
||||
"compare_no_matching_options": "Aucune option correspondante.",
|
||||
"compare_no_models_selected": "Aucun modèle sélectionné.",
|
||||
"compare_remove_model": "Supprimer {model}",
|
||||
"compare_timeline_title": "Chronologie de comparaison des modèles",
|
||||
|
||||
@@ -294,6 +294,16 @@
|
||||
"compare_selection_pending": "Selezione modificata",
|
||||
"compare_apply_selection": "Applica e ricarica i grafici",
|
||||
"compare_customize_models": "Personalizza modelli",
|
||||
"compare_edit_models": "Modifica modelli",
|
||||
"compare_edit_variables": "Modifica variabili",
|
||||
"compare_edit_models_description": "Cerca modelli o seleziona intere famiglie di fornitori.",
|
||||
"compare_edit_variables_description": "Cerca variabili o seleziona interi gruppi tematici.",
|
||||
"compare_search_models": "Cerca modelli",
|
||||
"compare_search_variables": "Cerca variabili",
|
||||
"compare_only_selected": "Solo selezionati",
|
||||
"compare_select_group": "Seleziona tutto",
|
||||
"compare_clear_group": "Deseleziona tutto",
|
||||
"compare_no_matching_options": "Nessuna opzione corrispondente.",
|
||||
"compare_no_models_selected": "Nessun modello selezionato.",
|
||||
"compare_remove_model": "Rimuovi {model}",
|
||||
"compare_timeline_title": "Cronologia del confronto tra modelli",
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
type ExportableChart,
|
||||
type ExportLegendItem
|
||||
} from '$lib/components/charts';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
|
||||
@@ -41,6 +40,7 @@
|
||||
|
||||
import { useHeroActions } from '../../hero.svelte';
|
||||
import { defaultParameters, hourly, modelGroups, models as modelOptions } from '../../options';
|
||||
import ComparisonSelectionDrawer from './ComparisonSelectionDrawer.svelte';
|
||||
import ModelPictogramTimeline from './ModelPictogramTimeline.svelte';
|
||||
import {
|
||||
cardinalDirection,
|
||||
@@ -67,6 +67,9 @@
|
||||
group.filter((variable) => variable.value !== 'cloud_cover')
|
||||
);
|
||||
const VARIABLE_IDS = new Set(COMPARISON_HOURLY.flat().map((variable) => variable.value));
|
||||
const VARIABLE_ORDER = new Map(
|
||||
COMPARISON_HOURLY.flat().map((variable, index) => [variable.value, index])
|
||||
);
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
const FETCH_DEBOUNCE_MS = 300;
|
||||
const STANDARD_COMPARE_VARIABLES = [
|
||||
@@ -95,8 +98,7 @@
|
||||
let isMobile = $state(false);
|
||||
let comparisonPlotInsetLeft = $derived(isMobile ? 92 : 116);
|
||||
let comparisonPlotInsetRight = $derived(isMobile ? 8 : 20);
|
||||
let modelsOpen = $state(false);
|
||||
let customVariablesOpen = $state(false);
|
||||
let selectionDrawerMode = $state<'models' | 'variables' | null>(null);
|
||||
|
||||
let params = $state({
|
||||
...defaultParameters,
|
||||
@@ -138,6 +140,14 @@
|
||||
);
|
||||
}
|
||||
|
||||
function orderVariables(values: string[]): string[] {
|
||||
return [...values].sort(
|
||||
(left, right) =>
|
||||
(VARIABLE_ORDER.get(left) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(VARIABLE_ORDER.get(right) ?? Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
}
|
||||
|
||||
let variablesDirty = $derived(!hasSameVariables(params.hourly, appliedHourly));
|
||||
let modelsDirty = $derived(!hasSameOrder(params.models, appliedModels));
|
||||
let displayedVariablesStale = $derived(
|
||||
@@ -501,7 +511,19 @@
|
||||
const nextValues = values.includes(value)
|
||||
? values.filter((item) => item !== value)
|
||||
: [...values, value];
|
||||
params[list] = list === 'models' ? orderModels(nextValues) : nextValues;
|
||||
params[list] = list === 'models' ? orderModels(nextValues) : orderVariables(nextValues);
|
||||
}
|
||||
|
||||
function toggleGroupSelection(
|
||||
list: 'models' | 'hourly',
|
||||
values: string[],
|
||||
select: boolean
|
||||
): void {
|
||||
const groupValues = new Set(values);
|
||||
const nextValues = select
|
||||
? [...new Set([...params[list], ...values])]
|
||||
: params[list].filter((value) => !groupValues.has(value));
|
||||
params[list] = list === 'models' ? orderModels(nextValues) : orderVariables(nextValues);
|
||||
}
|
||||
|
||||
function restoreStandardVariables(): void {
|
||||
@@ -535,10 +557,15 @@
|
||||
return model ? [model] : [];
|
||||
})
|
||||
);
|
||||
let modelSelectionGroups = $derived(
|
||||
let modelDrawerGroups = $derived(
|
||||
modelGroups.map((group) => ({
|
||||
...group,
|
||||
selectedCount: group.models.filter((model) => params.models.includes(model.value)).length
|
||||
value: group.value,
|
||||
label: group.label,
|
||||
options: group.models.map((model) => ({
|
||||
value: model.value,
|
||||
label: model.label,
|
||||
metadata: [model.resolution, model.update].filter(Boolean).join(' · ')
|
||||
}))
|
||||
}))
|
||||
);
|
||||
let selectedVariables = $derived(
|
||||
@@ -551,11 +578,14 @@
|
||||
params.hourly.length === STANDARD_COMPARE_VARIABLES.length &&
|
||||
STANDARD_COMPARE_VARIABLES.every((variable) => params.hourly.includes(variable))
|
||||
);
|
||||
let variableSelectionGroups = $derived(
|
||||
let variableDrawerGroups = $derived(
|
||||
COMPARISON_HOURLY.map((variables, index) => ({
|
||||
label: variableGroupLabels[index],
|
||||
variables,
|
||||
selectedCount: variables.filter((variable) => params.hourly.includes(variable.value)).length
|
||||
value: `variables_${index}`,
|
||||
label: variableGroupLabels[index](),
|
||||
options: variables.map((variable) => ({
|
||||
value: variable.value,
|
||||
label: variableLabel(variable.value)
|
||||
}))
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
@@ -812,7 +842,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Compact ordered selection summaries with expandable model/variable pickers. -->
|
||||
<!-- Compact summaries keep the page scannable; full editing happens in the shared drawer. -->
|
||||
<section class="mt-6 border-t border-border/60 pt-5 md:mt-10" aria-labelledby="models">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<h2 id="models" class="text-2xl md:text-3xl">{m.compare_models_heading()}</h2>
|
||||
@@ -870,11 +900,10 @@
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
|
||||
onclick={() => (modelsOpen = !modelsOpen)}
|
||||
aria-expanded={modelsOpen}
|
||||
aria-controls="model-options"
|
||||
onclick={() => (selectionDrawerMode = 'models')}
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
{modelsOpen ? m.action_close() : m.compare_customize_models()}
|
||||
{m.compare_customize_models()}
|
||||
</button>
|
||||
{#if modelsDirty}
|
||||
<button
|
||||
@@ -887,47 +916,6 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if modelsOpen}
|
||||
<div id="model-options" class="mt-3 border-t border-border/60 pt-3">
|
||||
<div class="grid items-start gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each modelSelectionGroups as group (group.value)}
|
||||
<details class="rounded-lg border border-border/60 bg-background/40">
|
||||
<summary
|
||||
class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-sm font-bold marker:content-none"
|
||||
>
|
||||
<span>{group.label}</span>
|
||||
<span class="flex items-center gap-2 text-xs font-normal text-muted-foreground">
|
||||
<span>{group.selectedCount}/{group.models.length}</span>
|
||||
<span aria-hidden="true">⌄</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="border-t border-border/50 p-2">
|
||||
{#each group.models as model (model.value)}
|
||||
<div class="flex min-h-9 items-center rounded-md px-1 hover:bg-muted/60">
|
||||
<Checkbox
|
||||
id="{model.value}_model"
|
||||
value={model.value}
|
||||
checked={params.models.includes(model.value)}
|
||||
onCheckedChange={() => toggleSelection('models', model.value)}
|
||||
/>
|
||||
<Label
|
||||
for="{model.value}_model"
|
||||
class="min-w-0 flex-1 cursor-pointer py-1.5 pl-2 text-xs"
|
||||
>
|
||||
<span class="font-medium">{model.label}</span>
|
||||
<span class="text-muted-foreground">
|
||||
· {model.resolution} · {model.update}
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -999,11 +987,10 @@
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
|
||||
onclick={() => (customVariablesOpen = !customVariablesOpen)}
|
||||
aria-expanded={customVariablesOpen}
|
||||
aria-controls="variable-options"
|
||||
onclick={() => (selectionDrawerMode = 'variables')}
|
||||
aria-haspopup="dialog"
|
||||
>
|
||||
{customVariablesOpen ? m.action_close() : m.compare_customize_variables()}
|
||||
{m.compare_customize_variables()}
|
||||
</button>
|
||||
{#if variablesDirty}
|
||||
<button
|
||||
@@ -1016,42 +1003,32 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if customVariablesOpen}
|
||||
<div id="variable-options" class="border-t border-border/60 p-3 sm:p-4">
|
||||
<div class="grid items-start gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each variableSelectionGroups as variableGroup (variableGroup.label())}
|
||||
<details class="rounded-lg border border-border/60 bg-background/40">
|
||||
<summary
|
||||
class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 px-3 py-2 text-sm font-bold marker:content-none"
|
||||
>
|
||||
<span>{variableGroup.label()}</span>
|
||||
<span class="flex items-center gap-2 text-xs font-normal text-muted-foreground">
|
||||
<span>{variableGroup.selectedCount}/{variableGroup.variables.length}</span>
|
||||
<span aria-hidden="true">⌄</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="border-t border-border/50 p-2">
|
||||
{#each variableGroup.variables as variable (variable.value)}
|
||||
<div class="flex min-h-10 items-center rounded-md px-1 hover:bg-muted/60">
|
||||
<Checkbox
|
||||
id="{variable.value}_hourly"
|
||||
checked={params.hourly.includes(variable.value)}
|
||||
onCheckedChange={() => toggleSelection('hourly', variable.value)}
|
||||
/>
|
||||
<Label
|
||||
for="{variable.value}_hourly"
|
||||
class="min-w-0 flex-1 cursor-pointer py-2 pl-2 text-sm"
|
||||
>
|
||||
{variableLabel(variable.value)}
|
||||
</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if selectionDrawerMode === 'models'}
|
||||
<ComparisonSelectionDrawer
|
||||
open
|
||||
mode="models"
|
||||
groups={modelDrawerGroups}
|
||||
selected={params.models}
|
||||
dirty={comparisonSelectionDirty}
|
||||
onToggle={(value) => toggleSelection('models', value)}
|
||||
onToggleGroup={(values, select) => toggleGroupSelection('models', values, select)}
|
||||
onApply={applyComparisonSelection}
|
||||
onClose={() => (selectionDrawerMode = null)}
|
||||
/>
|
||||
{:else if selectionDrawerMode === 'variables'}
|
||||
<ComparisonSelectionDrawer
|
||||
open
|
||||
mode="variables"
|
||||
groups={variableDrawerGroups}
|
||||
selected={params.hourly}
|
||||
dirty={comparisonSelectionDirty}
|
||||
onToggle={(value) => toggleSelection('hourly', value)}
|
||||
onToggleGroup={(values, select) => toggleGroupSelection('hourly', values, select)}
|
||||
onApply={applyComparisonSelection}
|
||||
onClose={() => (selectionDrawerMode = null)}
|
||||
onRestoreDefaults={restoreStandardVariables}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<script lang="ts">
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
metadata?: string;
|
||||
}
|
||||
|
||||
interface OptionGroup {
|
||||
value: string;
|
||||
label: string;
|
||||
options: Option[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
mode: 'models' | 'variables';
|
||||
groups: OptionGroup[];
|
||||
selected: string[];
|
||||
dirty: boolean;
|
||||
onToggle: (value: string) => void;
|
||||
onToggleGroup: (values: string[], select: boolean) => void;
|
||||
onApply: () => void;
|
||||
onClose: () => void;
|
||||
onRestoreDefaults?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
open,
|
||||
mode,
|
||||
groups,
|
||||
selected,
|
||||
dirty,
|
||||
onToggle,
|
||||
onToggleGroup,
|
||||
onApply,
|
||||
onClose,
|
||||
onRestoreDefaults
|
||||
}: Props = $props();
|
||||
|
||||
let search = $state('');
|
||||
let onlySelected = $state(false);
|
||||
let normalizedSearch = $derived(search.trim().toLocaleLowerCase());
|
||||
|
||||
let visibleGroups = $derived.by(() =>
|
||||
groups.flatMap((group) => {
|
||||
const options = group.options.filter((option) => {
|
||||
if (onlySelected && !selected.includes(option.value)) return false;
|
||||
if (!normalizedSearch) return true;
|
||||
return `${group.label} ${option.label} ${option.value} ${option.metadata ?? ''}`
|
||||
.toLocaleLowerCase()
|
||||
.includes(normalizedSearch);
|
||||
});
|
||||
return options.length > 0 ? [{ ...group, options }] : [];
|
||||
})
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (open) return;
|
||||
search = '';
|
||||
onlySelected = false;
|
||||
});
|
||||
|
||||
function groupSelection(group: OptionGroup): { count: number; all: boolean; partial: boolean } {
|
||||
const count = group.options.filter((option) => selected.includes(option.value)).length;
|
||||
return { count, all: count === group.options.length, partial: count > 0 && count < group.options.length };
|
||||
}
|
||||
|
||||
function applyAndClose(): void {
|
||||
onApply();
|
||||
onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Dialog {open} onOpenChange={(nextOpen) => !nextOpen && onClose()}>
|
||||
<Dialog.DialogContent
|
||||
showCloseButton={false}
|
||||
class="top-0 right-0 bottom-0 left-auto flex h-dvh w-full max-w-none translate-x-0 translate-y-0 flex-col gap-0 rounded-none border-0 p-0 sm:w-[640px] sm:max-w-[90vw] sm:border-l"
|
||||
>
|
||||
<header class="shrink-0 border-b border-border bg-background px-4 py-4 sm:px-5">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<Dialog.DialogTitle class="text-lg font-bold">
|
||||
{mode === 'models' ? m.compare_edit_models() : m.compare_edit_variables()}
|
||||
</Dialog.DialogTitle>
|
||||
<Dialog.DialogDescription class="mt-0.5 text-xs">
|
||||
{mode === 'models'
|
||||
? m.compare_edit_models_description()
|
||||
: m.compare_edit_variables_description()}
|
||||
</Dialog.DialogDescription>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex size-9 shrink-0 items-center justify-center rounded-lg text-xl text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
onclick={onClose}
|
||||
aria-label={m.action_close()}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
type="search"
|
||||
bind:value={search}
|
||||
placeholder={mode === 'models'
|
||||
? m.compare_search_models()
|
||||
: m.compare_search_variables()}
|
||||
aria-label={mode === 'models'
|
||||
? m.compare_search_models()
|
||||
: m.compare_search_variables()}
|
||||
/>
|
||||
<div
|
||||
class="flex min-h-9 shrink-0 cursor-pointer items-center gap-2 rounded-lg border border-border px-3 text-xs font-semibold hover:bg-muted"
|
||||
>
|
||||
<Checkbox id="drawer_{mode}_only_selected" bind:checked={onlySelected} />
|
||||
<Label for="drawer_{mode}_only_selected" class="cursor-pointer text-xs">
|
||||
{m.compare_only_selected()}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-4 sm:px-5">
|
||||
{#if visibleGroups.length > 0}
|
||||
<div class="grid items-start gap-3 sm:grid-cols-2">
|
||||
{#each visibleGroups as visibleGroup (visibleGroup.value)}
|
||||
{@const fullGroup = groups.find((group) => group.value === visibleGroup.value) ?? visibleGroup}
|
||||
{@const selection = groupSelection(fullGroup)}
|
||||
<section class="overflow-hidden rounded-xl border border-border/70 bg-card">
|
||||
<div class="flex min-h-11 items-center gap-2 border-b border-border/60 bg-muted/30 px-3 py-2">
|
||||
<Checkbox
|
||||
id="drawer_{mode}_group_{fullGroup.value}"
|
||||
checked={selection.all}
|
||||
indeterminate={selection.partial}
|
||||
onCheckedChange={() =>
|
||||
onToggleGroup(
|
||||
fullGroup.options.map((option) => option.value),
|
||||
!selection.all
|
||||
)}
|
||||
/>
|
||||
<Label
|
||||
for="drawer_{mode}_group_{fullGroup.value}"
|
||||
class="min-w-0 flex-1 cursor-pointer text-sm font-bold"
|
||||
>
|
||||
{fullGroup.label}
|
||||
</Label>
|
||||
<span class="shrink-0 text-[11px] text-muted-foreground tabular-nums">
|
||||
{selection.count}/{fullGroup.options.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded-md px-2 py-1 text-[11px] font-semibold text-primary hover:bg-primary/10"
|
||||
onclick={() =>
|
||||
onToggleGroup(
|
||||
fullGroup.options.map((option) => option.value),
|
||||
!selection.all
|
||||
)}
|
||||
>
|
||||
{selection.all ? m.compare_clear_group() : m.compare_select_group()}
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
{#each visibleGroup.options as option (option.value)}
|
||||
<div class="flex min-h-10 items-center rounded-lg px-1.5 hover:bg-muted/60">
|
||||
<Checkbox
|
||||
id="drawer_{mode}_{option.value}"
|
||||
checked={selected.includes(option.value)}
|
||||
onCheckedChange={() => onToggle(option.value)}
|
||||
/>
|
||||
<Label
|
||||
for="drawer_{mode}_{option.value}"
|
||||
class="min-w-0 flex-1 cursor-pointer py-2 pl-2"
|
||||
>
|
||||
<span class="block text-sm font-medium">{option.label}</span>
|
||||
{#if option.metadata}
|
||||
<span class="block text-[10px] text-muted-foreground">
|
||||
{option.metadata}
|
||||
</span>
|
||||
{/if}
|
||||
</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
{m.compare_no_matching_options()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer class="shrink-0 border-t border-border bg-background px-4 py-3 sm:px-5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
{#if mode === 'variables' && onRestoreDefaults}
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-9 rounded-lg border border-border px-3 py-1.5 text-xs font-semibold hover:bg-muted"
|
||||
onclick={onRestoreDefaults}
|
||||
>
|
||||
{m.compare_restore_defaults()}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-10 rounded-lg border border-border px-4 py-2 text-sm font-semibold hover:bg-muted"
|
||||
onclick={onClose}
|
||||
>
|
||||
{m.action_close()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="min-h-10 rounded-lg bg-primary px-4 py-2 text-sm font-bold text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
disabled={!dirty}
|
||||
onclick={applyAndClose}
|
||||
>
|
||||
{m.compare_apply_selection()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</Dialog.DialogContent>
|
||||
</Dialog.Dialog>
|
||||
Reference in New Issue
Block a user