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.
123 lines
4.2 KiB
Svelte
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 -->
|