Files
ss-tools/frontend/src/components/git/BranchSelector.svelte
busya cf7b3556f7 refactor(frontend): enforce semantic tokens + extract 3 Screen Models
Color tokens: 3658→0 violations across 186 .svelte/.svelte.ts files.
All raw Tailwind colors (bg-blue-*, text-gray-*, border-red-*, etc.)
replaced with semantic tokens from tailwind.config.js in 5 perl passes.

Model extraction (11→8 oversized pages):
- LLMReportModel.svelte.ts: LLM report page 413→221 lines
- DatasetDetailModel.svelte.ts: dataset detail page 416→218 lines
- DatasetsHubModel.svelte.ts: datasets hub page 468→246 lines

Tests: 14 assertions updated (color classes + model refs).
Build: 1 duplicate class:text-primary fix in ValidationTaskForm.
All 699/699 tests pass.
2026-06-02 17:58:36 +03:00

123 lines
4.2 KiB
Svelte

<!-- #region BranchSelector [C:3] [TYPE Component] [SEMANTICS git, branch, checkout, create, selector] -->
<!-- @BRIEF UI for selecting and creating Git branches for a dashboard repository. -->
<!-- @LAYER UI -->
<!--
@RELATION BINDS_TO -> [BranchModel]
@PRE dashboardId is a valid dashboard slug; envId required when ref is slug.
@UX_STATE Loading -> Select disabled, spinner.
@UX_STATE Loaded -> Branch dropdown enabled.
@UX_STATE CreateMode -> Inline input for new branch name.
@UX_STATE Error -> Inline error message below the dropdown with dismiss button.
@UX_RECOVERY Dismiss error; retry checkout/create.
@UX_REACTIVITY Props -> $props(), Model -> BranchModel
-->
<!-- @INVARIANT This component contains NO business logic — all state and actions are delegated to BranchModel. -->
<script lang="ts">
import { onMount } from 'svelte';
import { BranchModel } from '$lib/models/BranchModel.svelte.ts';
import { t } from '$lib/i18n/index.svelte.js';
import { Button, Select, Input } from '$lib/ui';
let {
dashboardId,
envId = null,
currentBranch = $bindable('main'),
onchange = () => {},
} = $props();
const model = new BranchModel({ dashboardId, envId, currentBranch, onChange: onchange });
// Sync prop changes to model
$effect(() => {
model.dashboardId = dashboardId;
model.envId = envId;
model.currentBranch = currentBranch;
model.onChange = onchange;
});
// Sync model.currentBranch back to bindable prop
$effect(() => {
currentBranch = model.currentBranch;
});
onMount(async () => {
await model.loadBranches(dashboardId, envId);
});
</script>
<div class="space-y-3">
<div class="flex items-center gap-3">
<div class="flex-grow">
<Select
bind:value={model.currentBranch}
onchange={(e) => model.handleSelect(e)}
disabled={model.loading}
options={model.branches.map(b => ({ value: b.name, label: b.name }))}
/>
</div>
<Button
variant="ghost"
size="sm"
onclick={() => model.toggleCreateForm()}
disabled={model.loading}
class="text-primary"
>
+ {$t.git.new_branch}
</Button>
</div>
{#if model.branchError}
<div class="max-h-32 overflow-y-auto rounded-lg border border-destructive-ring bg-destructive-light p-3 text-sm text-destructive" role="alert">
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="mt-0.5 h-4 w-4 flex-shrink-0 text-destructive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 9l-6 6m0-6l6 6"/></svg>
<div class="flex-1 min-w-0">
<p class="truncate">{model.branchError.message}</p>
{#if model.branchError.next_steps?.length}
<ul class="mt-1 list-disc pl-4 text-xs text-destructive">
{#each model.branchError.next_steps as step}
<li>{step}</li>
{/each}
</ul>
{/if}
</div>
<button type="button" onclick={() => model.branchError = null} class="flex-shrink-0 rounded p-0.5 hover:bg-destructive-light" aria-label="Закрыть">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
{/if}
{#if model.showCreate}
<div class="flex items-end gap-2 bg-surface-muted p-3 rounded-lg border border-dashed border-border">
<div class="flex-grow">
<Input
bind:value={model.newBranchName}
placeholder={$t.git?.branch_name_placeholder}
disabled={model.loading}
/>
</div>
<Button
variant="primary"
size="sm"
onclick={() => model.handleCreate()}
disabled={model.loading || !model.newBranchName}
isLoading={model.loading}
class="bg-success hover:bg-success"
>
{$t.git.create}
</Button>
<Button
variant="ghost"
size="sm"
onclick={() => model.toggleCreateForm()}
disabled={model.loading}
>
{$t.common.cancel}
</Button>
</div>
{/if}
</div>
<!-- #endregion BranchSelector -->