feat(inception): UI — New-project modal step 2, InceptionCard on the project page, Rules tab shows excluded always-on rulebooks; REST exclusion routes (#2883, milestone 297 step 5)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 21s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Failing after 42s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 21s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Failing after 42s
CI & Build / Build & push image (push) Skipped
- components/InceptionCard.vue: the one form, two homes — mode="create" in the New-project modal's second step (emits the choices; the create carries `inception`), mode="decide" on ProjectView for the owner of an undecided project (loads that project's defaults, records the decision). Always-on rulebooks listed checked (uncheck = exclude), others unchecked (check = subscribe), design system select, seed-Systems toggle (disabled once the project has Systems). Tokens only; modal canon (#2855); .btn-* canon. - ProjectView: the card while undecided, one "Inheritance decided <date> via … · …" line after; onDecided refreshes the project. - ProjectRulesTab: "Excluded always-on rulebooks" section with include-back. - api/inception.ts (types, fetchInceptionDefaults, decideInception); api/rulebooks.ts: ApplicableRules.excluded_always_on, exclude/include wrappers; REST POST/DELETE /api/projects/<id>/exclusions/rulebooks/<rb>. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/** Project inception (milestone 297): what a project was decided to inherit. */
|
||||
import { apiGet, apiPost } from "@/api/client";
|
||||
|
||||
export interface InceptionChoices {
|
||||
exclude_always_on_rulebooks: number[];
|
||||
subscribe_rulebooks: number[];
|
||||
design_system_id: number | null;
|
||||
seed_systems: boolean;
|
||||
}
|
||||
|
||||
export interface InceptionRecord {
|
||||
decided_at: string;
|
||||
decided_by: number | null;
|
||||
via: "mcp" | "ui" | "legacy";
|
||||
choices: InceptionChoices;
|
||||
}
|
||||
|
||||
export interface InceptionDefaults {
|
||||
always_on_rulebooks: { id: number; title: string }[];
|
||||
other_rulebooks: { id: number; title: string }[];
|
||||
excluded_always_on: { id: number; title: string }[];
|
||||
subscribed_rulebooks: { id: number; title: string }[];
|
||||
design_system_id: number | null;
|
||||
design_systems: { id: number; title: string }[];
|
||||
systems: number;
|
||||
}
|
||||
|
||||
export interface InceptionDecision {
|
||||
project_id: number;
|
||||
inception: InceptionRecord;
|
||||
effects: { excluded: number[]; subscribed: number[]; design_system_id: number | null; systems_seeded: string[] };
|
||||
}
|
||||
|
||||
export const emptyChoices = (): InceptionChoices => ({
|
||||
exclude_always_on_rulebooks: [], subscribe_rulebooks: [], design_system_id: null, seed_systems: false,
|
||||
});
|
||||
|
||||
export const fetchInceptionDefaults = (projectId: number) =>
|
||||
apiGet<InceptionDefaults>(`/api/projects/${projectId}/inception/defaults`);
|
||||
|
||||
export const decideInception = (projectId: number, choices: InceptionChoices) =>
|
||||
apiPost<InceptionDecision>(`/api/projects/${projectId}/inception`, { choices });
|
||||
@@ -71,6 +71,8 @@ export interface ApplicableRules {
|
||||
}[];
|
||||
truncated: boolean;
|
||||
subscribed_rulebooks: { id: number; title: string }[];
|
||||
/** Always-on rulebooks this project opted out of at inception (milestone 297). */
|
||||
excluded_always_on: { id: number; title: string }[];
|
||||
}
|
||||
|
||||
// ── Rulebooks ───────────────────────────────────────────────────────
|
||||
@@ -181,3 +183,14 @@ export async function suppressTopicForProject(projectId: number, topicId: number
|
||||
export async function unsuppressTopicForProject(projectId: number, topicId: number): Promise<void> {
|
||||
return apiDelete(`/api/projects/${projectId}/suppressions/topics/${topicId}`);
|
||||
}
|
||||
|
||||
// ── Always-on exclusions (milestone 297) ────────────────────────────────────
|
||||
|
||||
export async function excludeAlwaysOnRulebook(projectId: number, rulebookId: number): Promise<void> {
|
||||
await apiPost(`/api/projects/${projectId}/exclusions/rulebooks/${rulebookId}`, {});
|
||||
}
|
||||
|
||||
export async function includeAlwaysOnRulebook(projectId: number, rulebookId: number): Promise<void> {
|
||||
await apiDelete(`/api/projects/${projectId}/exclusions/rulebooks/${rulebookId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* The inception form (milestone 297): "what does this project inherit?"
|
||||
*
|
||||
* Two homes, one component. mode="create" rides the New-project modal's
|
||||
* second step and only emits the choices (the project does not exist yet);
|
||||
* mode="decide" sits on ProjectView for an undecided project, loads that
|
||||
* project's current defaults, and records the decision itself.
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { apiErrorMessage } from "@/api/client";
|
||||
import { fetchDesignSystems } from "@/api/designSystems";
|
||||
import {
|
||||
decideInception, emptyChoices, fetchInceptionDefaults,
|
||||
type InceptionChoices, type InceptionDecision, type InceptionDefaults,
|
||||
} from "@/api/inception";
|
||||
import { listRulebooks } from "@/api/rulebooks";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
mode: "create" | "decide";
|
||||
projectId?: number;
|
||||
choices?: InceptionChoices;
|
||||
}>(), { projectId: 0, choices: undefined });
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:choices": [value: InceptionChoices];
|
||||
decided: [decision: InceptionDecision];
|
||||
}>();
|
||||
|
||||
const local = ref<InceptionChoices>(props.choices ? { ...props.choices } : emptyChoices());
|
||||
const alwaysOn = ref<{ id: number; title: string }[]>([]);
|
||||
const others = ref<{ id: number; title: string }[]>([]);
|
||||
const designSystems = ref<{ id: number; title: string }[]>([]);
|
||||
const systemsCount = ref(0);
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
const error = ref("");
|
||||
|
||||
function emitChoices() {
|
||||
emit("update:choices", { ...local.value });
|
||||
}
|
||||
watch(local, emitChoices, { deep: true });
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
if (props.mode === "decide" && props.projectId) {
|
||||
const d: InceptionDefaults = await fetchInceptionDefaults(props.projectId);
|
||||
alwaysOn.value = d.always_on_rulebooks;
|
||||
others.value = d.other_rulebooks;
|
||||
designSystems.value = d.design_systems;
|
||||
systemsCount.value = d.systems;
|
||||
// Start from what stands today so "record" without changes is a true inherit-all.
|
||||
local.value = {
|
||||
exclude_always_on_rulebooks: d.excluded_always_on.map((r) => r.id),
|
||||
subscribe_rulebooks: d.subscribed_rulebooks.map((r) => r.id),
|
||||
design_system_id: d.design_system_id,
|
||||
seed_systems: false,
|
||||
};
|
||||
} else {
|
||||
const [rulebooks, ds] = await Promise.all([listRulebooks(), fetchDesignSystems()]);
|
||||
alwaysOn.value = rulebooks.filter((r) => r.always_on).map((r) => ({ id: r.id, title: r.title }));
|
||||
others.value = rulebooks.filter((r) => !r.always_on).map((r) => ({ id: r.id, title: r.title }));
|
||||
designSystems.value = ds.design_systems.map((d) => ({ id: d.id, title: d.title }));
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
error.value = apiErrorMessage(e, "Could not load what this project could inherit");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function inherits(id: number): boolean {
|
||||
return !local.value.exclude_always_on_rulebooks.includes(id);
|
||||
}
|
||||
function toggleInherit(id: number) {
|
||||
const list = local.value.exclude_always_on_rulebooks;
|
||||
local.value.exclude_always_on_rulebooks = list.includes(id) ? list.filter((x) => x !== id) : [...list, id];
|
||||
}
|
||||
function subscribed(id: number): boolean {
|
||||
return local.value.subscribe_rulebooks.includes(id);
|
||||
}
|
||||
function toggleSubscribe(id: number) {
|
||||
const list = local.value.subscribe_rulebooks;
|
||||
local.value.subscribe_rulebooks = list.includes(id) ? list.filter((x) => x !== id) : [...list, id];
|
||||
}
|
||||
|
||||
const nothingToDecide = computed(
|
||||
() => !alwaysOn.value.length && !others.value.length && !designSystems.value.length,
|
||||
);
|
||||
|
||||
async function record() {
|
||||
if (!props.projectId) return;
|
||||
saving.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const decision = await decideInception(props.projectId, local.value);
|
||||
emit("decided", decision);
|
||||
} catch (e: unknown) {
|
||||
error.value = apiErrorMessage(e, "Could not record the decision");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="inception" aria-labelledby="inception-title">
|
||||
<h3 id="inception-title" class="inception-title">What does this project inherit?</h3>
|
||||
<p class="inception-lede">
|
||||
A project's inheritance is a decision, not a default. Until it is recorded,
|
||||
every always-on rulebook binds, nothing is subscribed, and there is no design
|
||||
system or Systems.
|
||||
</p>
|
||||
<p v-if="loading" class="inception-muted">Loading…</p>
|
||||
<p v-else-if="error" class="error-msg">{{ error }}</p>
|
||||
<template v-else>
|
||||
<div v-if="alwaysOn.length" class="inception-group">
|
||||
<h4>Always-on rulebooks</h4>
|
||||
<p class="inception-muted">Checked = inherits. Uncheck to exclude a rulebook for this project only.</p>
|
||||
<label v-for="rb in alwaysOn" :key="rb.id" class="inception-choice">
|
||||
<input type="checkbox" :checked="inherits(rb.id)" @change="toggleInherit(rb.id)" />
|
||||
<span>{{ rb.title }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="others.length" class="inception-group">
|
||||
<h4>Subscribe to rulebooks</h4>
|
||||
<label v-for="rb in others" :key="rb.id" class="inception-choice">
|
||||
<input type="checkbox" :checked="subscribed(rb.id)" @change="toggleSubscribe(rb.id)" />
|
||||
<span>{{ rb.title }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="inception-group">
|
||||
<h4>Design system</h4>
|
||||
<select v-model="local.design_system_id" class="inception-select" aria-label="Design system">
|
||||
<option :value="null">None</option>
|
||||
<option v-for="ds in designSystems" :key="ds.id" :value="ds.id">{{ ds.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inception-group">
|
||||
<label class="inception-choice">
|
||||
<input type="checkbox" v-model="local.seed_systems" :disabled="systemsCount > 0" />
|
||||
<span>
|
||||
Seed the standard starter Systems (CI & Release, Auth & Access, Data Model & Storage, …)
|
||||
<em v-if="systemsCount > 0" class="inception-muted"> — this project already has {{ systemsCount }}</em>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="nothingToDecide" class="inception-muted">
|
||||
Nothing to inherit yet on this install — recording still settles the question.
|
||||
</p>
|
||||
<div v-if="mode === 'decide'" class="inception-actions">
|
||||
<button class="btn-primary" :disabled="saving" @click="record">
|
||||
{{ saving ? "Recording…" : "Record decision" }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inception {
|
||||
background: var(--fs-surface-raised);
|
||||
border: 1px solid var(--fs-border-color);
|
||||
border-radius: var(--fs-radius-lg);
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.inception-title { margin: 0 0 0.35rem; font-size: 1.05rem; }
|
||||
.inception-lede { margin: 0 0 1rem; color: var(--fs-text-secondary); font-size: 0.9rem; }
|
||||
.inception-muted { color: var(--fs-text-tertiary); font-size: 0.85rem; margin: 0 0 0.35rem; }
|
||||
.inception-group { margin-bottom: 1rem; }
|
||||
.inception-group h4 { margin: 0 0 0.35rem; font-size: 0.9rem; font-weight: 500; }
|
||||
.inception-choice { display: flex; align-items: flex-start; gap: 0.5rem; font-size: 0.9rem; margin: 0.25rem 0; }
|
||||
.inception-choice input { margin-top: 0.2rem; accent-color: var(--fs-accent); }
|
||||
.inception-select {
|
||||
padding: 0.45rem 0.7rem;
|
||||
border: 1px solid var(--fs-border-color);
|
||||
border-radius: var(--fs-radius-sm);
|
||||
background: var(--fs-surface-page);
|
||||
color: var(--fs-text-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.inception-actions { display: flex; justify-content: flex-end; margin-top: 0.5rem; }
|
||||
.error-msg { color: var(--fs-error); font-size: 0.9rem; }
|
||||
</style>
|
||||
@@ -2,10 +2,18 @@
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
getProjectApplicableRules, subscribeProject, unsubscribeProject,
|
||||
listRulebooks, getRule, createProjectRule, deleteRule,
|
||||
suppressRuleForProject, unsuppressRuleForProject,
|
||||
suppressTopicForProject, unsuppressTopicForProject,
|
||||
getProjectApplicableRules,
|
||||
subscribeProject,
|
||||
unsubscribeProject,
|
||||
listRulebooks,
|
||||
getRule,
|
||||
createProjectRule,
|
||||
deleteRule,
|
||||
suppressRuleForProject,
|
||||
unsuppressRuleForProject,
|
||||
suppressTopicForProject,
|
||||
unsuppressTopicForProject,
|
||||
includeAlwaysOnRulebook,
|
||||
} from "@/api/rulebooks";
|
||||
import type { ApplicableRules, Rulebook } from "@/api/rulebooks";
|
||||
|
||||
@@ -35,6 +43,11 @@ async function subscribe(rulebookId: number) {
|
||||
await load();
|
||||
}
|
||||
|
||||
async function includeBack(rulebookId: number) {
|
||||
await includeAlwaysOnRulebook(props.projectId, rulebookId);
|
||||
await load();
|
||||
}
|
||||
|
||||
async function unsubscribe(rulebookId: number) {
|
||||
if (!confirm("Unsubscribe from this rulebook for this project?")) return;
|
||||
await unsubscribeProject(props.projectId, rulebookId);
|
||||
@@ -172,6 +185,17 @@ watch(() => props.projectId, load);
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="applicable.excluded_always_on?.length" class="excluded">
|
||||
<h3>Excluded always-on rulebooks</h3>
|
||||
<p class="excluded-note">Opted out at inception — these do not bind this project.</p>
|
||||
<div class="chips">
|
||||
<span v-for="rb in applicable.excluded_always_on" :key="rb.id" class="chip chip-excluded">
|
||||
<a @click="openInRulesView(rb.id)">{{ rb.title }}</a>
|
||||
<button class="chip-remove" @click="includeBack(rb.id)" aria-label="Include again" title="Include again">↩</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="project-rules">
|
||||
<div class="section-head">
|
||||
<h3>Project rules</h3>
|
||||
@@ -321,6 +345,9 @@ watch(() => props.projectId, load);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.excluded-note { margin: 0 0 0.5rem; color: var(--fs-text-tertiary); font-size: 0.85rem; }
|
||||
.chip-excluded { opacity: 0.8; text-decoration: line-through; }
|
||||
.chip-excluded .chip-remove { text-decoration: none; }
|
||||
.rules-tab { padding: 1rem; }
|
||||
h3 {
|
||||
font-size: 0.9em; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { apiGet, apiPost } from "@/api/client";
|
||||
import { apiGet, apiPost, apiErrorMessage } from "@/api/client";
|
||||
import { emptyChoices, type InceptionChoices } from "@/api/inception";
|
||||
import InceptionCard from "@/components/InceptionCard.vue";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { milestoneColor } from "@/utils/palette";
|
||||
|
||||
@@ -47,6 +49,9 @@ const newTitle = ref("");
|
||||
const newDescription = ref("");
|
||||
const newGoal = ref("");
|
||||
const creating = ref(false);
|
||||
// Step 2 of the modal (milestone 297): what the new project inherits.
|
||||
const modalStep = ref<1 | 2>(1);
|
||||
const newInception = ref<InceptionChoices>(emptyChoices());
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
if (activeTab.value === "all") return projects.value;
|
||||
@@ -73,6 +78,8 @@ function openNewProjectModal() {
|
||||
newTitle.value = "";
|
||||
newDescription.value = "";
|
||||
newGoal.value = "";
|
||||
modalStep.value = 1;
|
||||
newInception.value = emptyChoices();
|
||||
showNewProjectModal.value = true;
|
||||
}
|
||||
|
||||
@@ -88,13 +95,15 @@ async function createProject() {
|
||||
title: newTitle.value.trim(),
|
||||
description: newDescription.value.trim() || undefined,
|
||||
goal: newGoal.value.trim() || undefined,
|
||||
// The decision rides the create: a project made here is never undecided.
|
||||
inception: newInception.value,
|
||||
});
|
||||
projects.value.unshift(project);
|
||||
showNewProjectModal.value = false;
|
||||
toast.show("Project created");
|
||||
router.push(`/projects/${project.id}`);
|
||||
} catch {
|
||||
toast.show("Failed to create project", "error");
|
||||
} catch (e: unknown) {
|
||||
toast.show(apiErrorMessage(e, "Failed to create project"), "error");
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
@@ -266,8 +275,9 @@ function overallPct(project: Project): { total: number; pct: number } {
|
||||
<teleport to="body">
|
||||
<div v-if="showNewProjectModal" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-card">
|
||||
<h3 class="modal-title">New Project</h3>
|
||||
<div class="modal-field">
|
||||
<h3 class="modal-title">{{ modalStep === 1 ? "New Project" : "New Project — what it inherits" }}</h3>
|
||||
<InceptionCard v-if="modalStep === 2" mode="create" v-model:choices="newInception" />
|
||||
<div v-if="modalStep === 1" class="modal-field">
|
||||
<label>Title <span class="required">*</span></label>
|
||||
<input
|
||||
v-model="newTitle"
|
||||
@@ -275,11 +285,11 @@ function overallPct(project: Project): { total: number; pct: number } {
|
||||
class="modal-input"
|
||||
placeholder="Project title"
|
||||
autofocus
|
||||
@keydown.enter="createProject"
|
||||
@keydown.enter="modalStep = 2"
|
||||
@keydown.escape="closeModal"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<div v-if="modalStep === 1" class="modal-field">
|
||||
<label>Goal</label>
|
||||
<input
|
||||
v-model="newGoal"
|
||||
@@ -289,7 +299,7 @@ function overallPct(project: Project): { total: number; pct: number } {
|
||||
@keydown.escape="closeModal"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-field">
|
||||
<div v-if="modalStep === 1" class="modal-field">
|
||||
<label>Description</label>
|
||||
<textarea
|
||||
v-model="newDescription"
|
||||
@@ -301,7 +311,17 @@ function overallPct(project: Project): { total: number; pct: number } {
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-btn" @click="closeModal">Cancel</button>
|
||||
<button v-if="modalStep === 2" class="modal-btn" @click="modalStep = 1">Back</button>
|
||||
<button
|
||||
v-if="modalStep === 1"
|
||||
class="modal-btn modal-btn-primary"
|
||||
@click="modalStep = 2"
|
||||
:disabled="!newTitle.trim()"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="modal-btn modal-btn-primary"
|
||||
@click="createProject"
|
||||
:disabled="!newTitle.trim() || creating"
|
||||
|
||||
@@ -11,6 +11,9 @@ import ShareDialog from "@/components/ShareDialog.vue";
|
||||
import ProjectDesignTab from "@/components/ProjectDesignTab.vue";
|
||||
import ProjectRulesTab from "@/components/rules/ProjectRulesTab.vue";
|
||||
import SystemsSection from "@/components/SystemsSection.vue";
|
||||
import InceptionCard from "@/components/InceptionCard.vue";
|
||||
import { fmtDate } from "@/utils/dateFormat";
|
||||
import type { InceptionDecision, InceptionRecord } from "@/api/inception";
|
||||
import {
|
||||
fetchDesignSystems,
|
||||
setProjectDesignSystem,
|
||||
@@ -50,6 +53,7 @@ interface Project {
|
||||
color: string | null;
|
||||
design_system_id: number | null;
|
||||
forge_connection_id: number | null;
|
||||
inception?: InceptionRecord | null;
|
||||
permission?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
@@ -75,6 +79,12 @@ interface NoteItem {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToastStore();
|
||||
|
||||
function onInceptionDecided(decision: InceptionDecision) {
|
||||
if (project.value) project.value.inception = decision.inception;
|
||||
toast.show("Inheritance recorded");
|
||||
void loadProject();
|
||||
}
|
||||
const tasksStore = useTasksStore();
|
||||
|
||||
const project = ref<Project | null>(null);
|
||||
@@ -695,6 +705,26 @@ async function confirmDelete() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Inception (milestone 297): the owner of an undecided project is asked
|
||||
what it inherits; once recorded, one line says what was decided. -->
|
||||
<InceptionCard
|
||||
v-if="project.inception == null && isProjectOwner"
|
||||
mode="decide"
|
||||
:project-id="projectId"
|
||||
@decided="onInceptionDecided"
|
||||
/>
|
||||
<p v-else-if="project.inception" class="inception-line">
|
||||
Inheritance decided {{ fmtDate(project.inception.decided_at) }} via {{ project.inception.via }}
|
||||
<template v-if="project.inception.choices.exclude_always_on_rulebooks.length">
|
||||
· excludes {{ project.inception.choices.exclude_always_on_rulebooks.length }} always-on rulebook(s)
|
||||
</template>
|
||||
<template v-if="project.inception.choices.subscribe_rulebooks.length">
|
||||
· subscribes {{ project.inception.choices.subscribe_rulebooks.length }}
|
||||
</template>
|
||||
· design system {{ project.inception.choices.design_system_id ? "#" + project.inception.choices.design_system_id : "none" }}
|
||||
<template v-if="project.inception.choices.seed_systems"> · Systems seeded</template>
|
||||
</p>
|
||||
|
||||
<!-- Summary stat chips -->
|
||||
<div v-if="project.summary" class="summary-stats">
|
||||
<div class="stat-chip stat-todo">
|
||||
@@ -1197,6 +1227,7 @@ async function confirmDelete() {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.inception-line { margin: 0 0 1rem; color: var(--fs-text-secondary); font-size: 0.85rem; }
|
||||
.project-title-input {
|
||||
flex: 1;
|
||||
font-size: 1.75rem;
|
||||
|
||||
Reference in New Issue
Block a user