Files
FabledScribe/frontend/src/components/MilestoneSelector.vue
T
bvandeusenandClaude Fable 5 a72605de8f
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / integration (push) Successful in 34s
CI & Build / Python tests (push) Failing after 54s
CI & Build / Build & push image (push) Skipped
refactor(frontend): the last pay-down, part 1 — three dead views deleted, editor rules shared, .page-container + .fs-input canon, rules-shared.css; a one-declaration CSS body is not a shape (#2903, milestone 299 step 5)
The derive queue said the biggest duplicate families were whole views:
TaskViewerView, UserManagementView and LogsView were imported nowhere — left
behind when tasks moved to the editor and users/logs became SettingsView
tabs. Deleted (rule 22). Note/TaskEditorView carried six identical scoped
rules -> editor-shared.css (the .tag-suggest-row gap the scoped copies
actually rendered wins). Three views wrapped the page under three names ->
.page-container in components.css. Three scoped input recipes -> the design
system fs-input recipe (snippet #2336), verbatim, in components.css; width/
box-sizing stay with the caller. The three rules panes share .pane and the
pane heading via rules-shared.css (the auth-shared pattern, #2852).

Ledger: a single-declaration CSS rule keeps its selector in its fingerprint,
so `color: var(--fs-text-tertiary)` under five different names is no longer
a five-file "identical body" family — the first pay-down found that most of
the 148 dup families were exactly this, and nobody would consolidate them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 14:55:28 -04:00

74 lines
1.6 KiB
Vue

<script setup lang="ts">
import { ref, watch } from "vue";
import { apiGet } from "@/api/client";
interface Milestone {
id: number;
title: string;
status: string;
}
const props = defineProps<{
projectId: number | null;
modelValue: number | null;
}>();
const emit = defineEmits<{
(e: "update:modelValue", val: number | null): void;
}>();
const milestones = ref<Milestone[]>([]);
const loading = ref(false);
watch(
() => props.projectId,
async (pid) => {
milestones.value = [];
if (!pid) {
emit("update:modelValue", null);
return;
}
loading.value = true;
try {
const data = await apiGet<{ milestones: Milestone[] }>(
`/api/projects/${pid}/milestones`
);
milestones.value = data.milestones.filter((m) => m.status === "active");
} catch {
milestones.value = [];
} finally {
loading.value = false;
}
},
{ immediate: true }
);
function onChange(e: Event) {
const val = (e.target as HTMLSelectElement).value;
emit("update:modelValue", val ? Number(val) : null);
}
</script>
<template>
<select
class="fs-input milestone-select"
:value="modelValue ?? ''"
:disabled="!projectId || loading"
@change="onChange"
>
<option value="">{{ projectId ? "No milestone" : "(Select project first)" }}</option>
<option v-for="m in milestones" :key="m.id" :value="m.id">
{{ m.title }}
</option>
</select>
</template>
<style scoped>
/* The input itself is the .fs-input canon (components.css); only the
layout remainder lives here. */
.milestone-select {
box-sizing: border-box;
width: 100%;
}
</style>