From 65d06f1754e831aa71648e45d8f7613040bc055d Mon Sep 17 00:00:00 2001
From: Vantz Stockwell
Date: Sat, 23 May 2026 07:58:27 -0400
Subject: [PATCH] Two-column layout with sticky character summary
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Build Ledger now uses a two-column grid: the builder UI stays on the
left, and a new sticky CharacterSummary panel on the right aggregates the
whole character at a glance — house, level, total XP, skill/intel point
pools (with POI bonus broken out), per-spec levels with cumulative XP and
perks unlocked, current faction tier and progress to next, skill
allocations grouped by class, and the six most recent perk unlocks across
character + specs.
The previous separate "Skill Summary" panel is subsumed by the sidebar.
Wrap width grows from 1240px to 1560px. Below 1180px the layout collapses
to one column and the summary becomes non-sticky.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
character-builder/frontend/src/App.vue | 119 ++---
.../src/components/CharacterSummary.vue | 465 ++++++++++++++++++
character-builder/frontend/src/styles.css | 24 +-
3 files changed, 517 insertions(+), 91 deletions(-)
create mode 100644 character-builder/frontend/src/components/CharacterSummary.vue
diff --git a/character-builder/frontend/src/App.vue b/character-builder/frontend/src/App.vue
index d867c10..ef79cb3 100644
--- a/character-builder/frontend/src/App.vue
+++ b/character-builder/frontend/src/App.vue
@@ -3,6 +3,7 @@ import { computed, onMounted, ref, shallowRef, watch } from 'vue';
import XpProgressCard from './components/XpProgressCard.vue';
import FactionTrack from './components/FactionTrack.vue';
import SkillTree from './components/SkillTree.vue';
+import CharacterSummary from './components/CharacterSummary.vue';
import {
applyBuild,
build,
@@ -130,33 +131,20 @@ const totalSpentAcrossClasses = computed(() =>
Object.values(build.skills).reduce((a, b) => a + (b || 0), 0),
);
-// Per-class summary: { classId: { spent, allocations: [{node, points}] } }
-const summaryByClass = computed(() => {
- const out: Record<
- ClassId,
- { spent: number; allocations: { tag: string; name: string; kind: string; points: number; max: number }[] }
- > = {
- benegesserit: { spent: 0, allocations: [] },
- mentat: { spent: 0, allocations: [] },
- planetologist: { spent: 0, allocations: [] },
- swordmaster: { spent: 0, allocations: [] },
- trooper: { spent: 0, allocations: [] },
+// Per-class spent counts — used by the skill-tree tab chips.
+const spentByClass = computed>(() => {
+ const out: Record = {
+ benegesserit: 0,
+ mentat: 0,
+ planetologist: 0,
+ swordmaster: 0,
+ trooper: 0,
};
for (const c of CLASSES) {
const tree = skillTrees.value[c.id];
if (!tree) continue;
for (const node of tree.nodes) {
- const pts = build.skills[node.tag] || 0;
- if (pts > 0) {
- out[c.id].spent += pts;
- out[c.id].allocations.push({
- tag: node.tag,
- name: node.name,
- kind: node.kind,
- points: pts,
- max: node.maxPoints,
- });
- }
+ out[c.id] += build.skills[node.tag] || 0;
}
}
return out;
@@ -265,6 +253,9 @@ const specMeta: Record = {
+
+
+
@@ -422,7 +413,7 @@ const specMeta: Record = {
>
{{ c.name }}
- {{ summaryByClass[c.id].spent }} pt{{
- summaryByClass[c.id].spent === 1 ? '' : 's'
- }}
+ {{ spentByClass[c.id] }} pt{{ spentByClass[c.id] === 1 ? '' : 's' }}
@@ -446,70 +435,22 @@ const specMeta: Record
= {
/>
-
-
-
-
Skill Summary
-
All allocations across classes
-
-
-
{{ c.name.toUpperCase().slice(0, 3) }}
-
- {{ c.name }}
- {{ summaryByClass[c.id].spent }} pts
-
-
- no points allocated
-
-
- -
-
- {{
- a.kind.slice(0, 3).toUpperCase()
- }}
- {{ a.name }}
-
- {{ a.points }}/{{ a.max }}
-
-
-
-
-
+
+
+