124 lines
3.6 KiB
Vue
124 lines
3.6 KiB
Vue
<script setup lang="ts">
|
||
import { ref, watch, onMounted } from "vue";
|
||
import { useRulebooksStore } from "@/stores/rulebooks";
|
||
|
||
const props = defineProps<{ ruleId: number | null; topicId: number | null }>();
|
||
const emit = defineEmits<{ close: [] }>();
|
||
|
||
const store = useRulebooksStore();
|
||
const title = ref("");
|
||
const statement = ref("");
|
||
const why = ref("");
|
||
const howToApply = ref("");
|
||
|
||
const isCreating = ref(props.ruleId === null);
|
||
|
||
async function load() {
|
||
if (props.ruleId !== null) {
|
||
await store.fetchRule(props.ruleId);
|
||
const r = store.currentRule;
|
||
if (r) {
|
||
title.value = r.title;
|
||
statement.value = r.statement;
|
||
why.value = r.why || "";
|
||
howToApply.value = r.how_to_apply || "";
|
||
}
|
||
} else {
|
||
title.value = "";
|
||
statement.value = "";
|
||
why.value = "";
|
||
howToApply.value = "";
|
||
}
|
||
}
|
||
|
||
async function save() {
|
||
if (!title.value.trim() || !statement.value.trim()) {
|
||
emit("close");
|
||
return;
|
||
}
|
||
if (isCreating.value && props.topicId !== null) {
|
||
await store.createRule(props.topicId, {
|
||
title: title.value, statement: statement.value,
|
||
why: why.value, how_to_apply: howToApply.value,
|
||
});
|
||
} else if (props.ruleId !== null) {
|
||
await store.updateRule(props.ruleId, {
|
||
title: title.value, statement: statement.value,
|
||
why: why.value, how_to_apply: howToApply.value,
|
||
});
|
||
}
|
||
emit("close");
|
||
}
|
||
|
||
async function remove() {
|
||
if (props.ruleId === null) return;
|
||
if (!confirm("Delete this rule? This cannot be undone.")) return;
|
||
await store.deleteRule(props.ruleId);
|
||
emit("close");
|
||
}
|
||
|
||
onMounted(load);
|
||
watch(() => props.ruleId, load);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="backdrop" @click="save">
|
||
<aside class="slide-over" @click.stop>
|
||
<header>
|
||
<h2>{{ isCreating ? "New rule" : "Edit rule" }}</h2>
|
||
<button v-if="!isCreating" class="trash" @click="remove" aria-label="Delete">🗑</button>
|
||
<button class="close" @click="save" aria-label="Close">×</button>
|
||
</header>
|
||
<label>
|
||
Title
|
||
<input v-model="title" placeholder="e.g. dev is home" />
|
||
</label>
|
||
<label>
|
||
Statement <span class="required">*</span>
|
||
<textarea v-model="statement" rows="3" placeholder="The actionable instruction (1-2 sentences)." />
|
||
</label>
|
||
<label>
|
||
Why
|
||
<textarea v-model="why" rows="4" placeholder="Rationale — the reason this rule exists." />
|
||
</label>
|
||
<label>
|
||
How to apply
|
||
<textarea v-model="howToApply" rows="4" placeholder="When / where this kicks in." />
|
||
</label>
|
||
</aside>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.backdrop {
|
||
position: fixed; inset: 0;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
z-index: 100;
|
||
}
|
||
.slide-over {
|
||
position: fixed; top: 0; right: 0; bottom: 0;
|
||
width: min(520px, 90vw);
|
||
background: var(--color-surface, #18181b);
|
||
border-left: 2px solid var(--color-primary, #6366f1);
|
||
padding: 1.5rem;
|
||
overflow-y: auto;
|
||
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.3);
|
||
}
|
||
header { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
|
||
header h2 {
|
||
flex: 1; margin: 0;
|
||
font-family: Fraunces, serif; font-style: italic;
|
||
}
|
||
label { display: block; margin-bottom: 1rem; }
|
||
.required { color: var(--color-primary, #6366f1); }
|
||
input, textarea {
|
||
width: 100%; margin-top: 0.25rem;
|
||
background: var(--color-bg, #111113); color: inherit;
|
||
border: 1px solid var(--color-border, #2a2a2e); border-radius: 6px;
|
||
padding: 0.5rem; font: inherit;
|
||
font-family: inherit;
|
||
}
|
||
.trash, .close { background: none; border: none; cursor: pointer; opacity: 0.6; font-size: 1.25em; }
|
||
.trash:hover, .close:hover { opacity: 1; }
|
||
</style>
|