CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 45s
#2444. Each needed reading rather than a batch fix, and the split was 2 real losses, 4 false reports, 5 wrappers that are bare on purpose. REAL: .system-card was a flex row, and every child still says so — .system-swatch and .system-actions are flex-shrink: 0, .system-body and .system-form--inline are flex: 1. align-items: flex-start is why the swatch carries margin-top: 0.3rem: nudged onto the first line of text. .systems-list no rule AT ALL, so the systems list rendered with browser bullets and indent. Invisible to the check — see below. .graph-embed the panel is a flex column whose header is flex-shrink: 0, so this is the item that takes the remaining height. Without it the `height: 100%` on the line below resolves against auto and does nothing, which left the comment above it specifying a rule that could not work. FALSE REPORTS, and the checker was wrong rather than the code: `.pane.empty` and `td.num` are base rules for the element that carries those classes — the check read any compound with more than a lone class as a modifier. It now records a compound's whole class SET and clears an element carrying all of them, which is exact: recording the classes individually would have cleared `.pane` everywhere on the strength of a rule that only applies alongside `.empty`. Four reports gone, and a check with false reports is one that gets skimmed. BARE ON PURPOSE — .rb, .topic-group, .new-topic, .sub-list, .dash-head, and both .detail-row rows. Each namespaces descendant rules and assumes nothing about layout, which is the tell that separates them from a deleted base. All seven now carry a comment saying so, so the next reader doesn't re-litigate them and a NEW entry in the report means something actually changed. Also recorded in the script: it cannot see a class with no rule anywhere, since that is indistinguishable from a semantic-only hook. `.systems-list` was found by reading the file beside a class that WAS half-styled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
160 lines
5.5 KiB
Vue
160 lines
5.5 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, watch } from "vue";
|
|
import { useRulebooksStore } from "@/stores/rulebooks";
|
|
import { apiGet } from "@/api/client";
|
|
import {
|
|
subscribeProject, unsubscribeProject, getProjectApplicableRules,
|
|
} from "@/api/rulebooks";
|
|
import type { RulebookTopic } from "@/api/rulebooks";
|
|
|
|
const props = defineProps<{
|
|
rulebookId: number;
|
|
topics: RulebookTopic[];
|
|
selectedTopicId: number | null;
|
|
}>();
|
|
const emit = defineEmits<{ "select-topic": [id: number] }>();
|
|
|
|
const store = useRulebooksStore();
|
|
const isCreating = ref(false);
|
|
const newTitle = ref("");
|
|
|
|
const currentRulebook = computed(() =>
|
|
store.rulebooks.find((rb) => rb.id === props.rulebookId),
|
|
);
|
|
|
|
interface ProjectLite { id: number; title: string }
|
|
const projects = ref<ProjectLite[]>([]);
|
|
// Map<project_id, Set<rulebook_id>>
|
|
const subscribedRulebookIds = ref<Map<number, Set<number>>>(new Map());
|
|
|
|
async function loadProjects() {
|
|
const data = await apiGet<{ projects: ProjectLite[] }>("/api/projects");
|
|
projects.value = data.projects;
|
|
for (const p of projects.value) {
|
|
const result = await getProjectApplicableRules(p.id);
|
|
subscribedRulebookIds.value.set(
|
|
p.id,
|
|
new Set(result.subscribed_rulebooks.map((rb) => rb.id)),
|
|
);
|
|
}
|
|
}
|
|
|
|
function isSubscribed(projectId: number): boolean {
|
|
return subscribedRulebookIds.value.get(projectId)?.has(props.rulebookId) ?? false;
|
|
}
|
|
|
|
async function toggleSubscription(projectId: number, checked: boolean) {
|
|
if (checked) {
|
|
await subscribeProject(projectId, props.rulebookId);
|
|
const set = subscribedRulebookIds.value.get(projectId) || new Set<number>();
|
|
set.add(props.rulebookId);
|
|
subscribedRulebookIds.value.set(projectId, set);
|
|
} else {
|
|
await unsubscribeProject(projectId, props.rulebookId);
|
|
subscribedRulebookIds.value.get(projectId)?.delete(props.rulebookId);
|
|
}
|
|
// trigger reactivity on Map mutation
|
|
subscribedRulebookIds.value = new Map(subscribedRulebookIds.value);
|
|
}
|
|
|
|
async function submitNew() {
|
|
const title = newTitle.value.trim();
|
|
if (!title) return;
|
|
const topic = await store.createTopic(props.rulebookId, { title });
|
|
newTitle.value = "";
|
|
isCreating.value = false;
|
|
emit("select-topic", topic.id);
|
|
}
|
|
|
|
onMounted(loadProjects);
|
|
watch(() => props.rulebookId, () => {/* re-render of isSubscribed from existing map */});
|
|
</script>
|
|
|
|
<template>
|
|
<section class="pane">
|
|
<header>
|
|
<h2>Topics</h2>
|
|
<label v-if="currentRulebook" class="always-on-toggle" title="When on, rules from this rulebook load at session start regardless of project context">
|
|
<input
|
|
type="checkbox"
|
|
:checked="currentRulebook.always_on"
|
|
@change="store.toggleAlwaysOn(currentRulebook.id)"
|
|
/>
|
|
<span>Always on</span>
|
|
</label>
|
|
</header>
|
|
<ul>
|
|
<li
|
|
v-for="t in topics"
|
|
:key="t.id"
|
|
:class="{ active: t.id === selectedTopicId }"
|
|
@click="emit('select-topic', t.id)"
|
|
>
|
|
{{ t.title }}
|
|
</li>
|
|
</ul>
|
|
<div class="new-topic">
|
|
<button v-if="!isCreating" @click="isCreating = true">+ New topic</button>
|
|
<form v-else @submit.prevent="submitNew">
|
|
<input v-model="newTitle" autofocus placeholder="Topic title (e.g. git-workflow)" />
|
|
<div class="form-buttons">
|
|
<button type="submit">Create</button>
|
|
<button type="button" @click="isCreating = false">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="subscriptions">
|
|
<h3>Subscribers</h3>
|
|
<ul class="sub-list">
|
|
<li v-for="p in projects" :key="p.id">
|
|
<label>
|
|
<input
|
|
type="checkbox"
|
|
:checked="isSubscribed(p.id)"
|
|
@change="toggleSubscription(p.id, ($event.target as HTMLInputElement).checked)"
|
|
/>
|
|
{{ p.title }}
|
|
</label>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pane { background: var(--color-surface); padding: 1rem; overflow-y: auto; }
|
|
header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
header h2 { font-family: Fraunces, serif; font-style: italic; margin: 0 0 0.5rem 0; }
|
|
.always-on-toggle {
|
|
display: flex; align-items: center; gap: 0.4rem;
|
|
font-size: 0.85rem; opacity: 0.85; cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.always-on-toggle input { cursor: pointer; }
|
|
ul { list-style: none; padding: 0; margin: 1rem 0; }
|
|
li { padding: 0.5rem; cursor: pointer; border-radius: 6px; }
|
|
li.active { background: var(--color-primary-bg); }
|
|
li:hover { background: var(--color-hover); }
|
|
/* `.new-topic` and `.sub-list` are deliberately bare (#2444). The first wraps a
|
|
button-or-form whose children style themselves; the second is a `<ul>`, and
|
|
the bare `ul` rule above already gives it list-style, padding and margin —
|
|
a base a class-name check cannot see, since it comes from an element
|
|
selector. Both namespace descendant rules and assume nothing about layout. */
|
|
.new-topic input {
|
|
width: 100%; margin-bottom: 0.5rem;
|
|
background: var(--color-bg); color: inherit;
|
|
border: 1px solid var(--color-border); border-radius: 6px;
|
|
padding: 0.5rem;
|
|
}
|
|
.form-buttons { display: flex; gap: 0.5rem; }
|
|
.subscriptions {
|
|
margin-top: 2rem;
|
|
border-top: 1px solid var(--color-border);
|
|
padding-top: 1rem;
|
|
}
|
|
.subscriptions h3 { font-size: 0.9em; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.sub-list li { cursor: default; }
|
|
.sub-list label { display: flex; gap: 0.5rem; align-items: center; cursor: pointer; }
|
|
button { cursor: pointer; }
|
|
</style>
|