Two-column layout with sticky character summary
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) <noreply@anthropic.com>
This commit is contained in:
parent
6c7b4b8133
commit
65d06f1754
3 changed files with 517 additions and 91 deletions
|
|
@ -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<Record<ClassId, number>>(() => {
|
||||
const out: Record<ClassId, number> = {
|
||||
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<SpecId, { name: string; sym: string }> = {
|
|||
</p>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<div class="builder">
|
||||
|
||||
<!-- HOUSE -->
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
|
|
@ -422,7 +413,7 @@ const specMeta: Record<SpecId, { name: string; sym: string }> = {
|
|||
>
|
||||
{{ c.name }}
|
||||
<span
|
||||
v-if="summaryByClass[c.id].spent > 0"
|
||||
v-if="spentByClass[c.id] > 0"
|
||||
style="
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
|
|
@ -430,9 +421,7 @@ const specMeta: Record<SpecId, { name: string; sym: string }> = {
|
|||
font-size: 10px;
|
||||
"
|
||||
>
|
||||
{{ summaryByClass[c.id].spent }} pt{{
|
||||
summaryByClass[c.id].spent === 1 ? '' : 's'
|
||||
}}
|
||||
{{ spentByClass[c.id] }} pt{{ spentByClass[c.id] === 1 ? '' : 's' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -446,70 +435,22 @@ const specMeta: Record<SpecId, { name: string; sym: string }> = {
|
|||
/>
|
||||
</section>
|
||||
|
||||
<!-- SKILL SUMMARY -->
|
||||
<section class="panel">
|
||||
<div class="panel-head">
|
||||
<h2>Skill Summary</h2>
|
||||
<div class="sub">All allocations across classes</div>
|
||||
</div>
|
||||
<div class="cards">
|
||||
<div v-for="c in CLASSES" :key="c.id" class="card">
|
||||
<div class="sym">{{ c.name.toUpperCase().slice(0, 3) }}</div>
|
||||
<h3>
|
||||
{{ c.name }}
|
||||
<span
|
||||
style="
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--ink-muted);
|
||||
letter-spacing: 0.12em;
|
||||
margin-left: 8px;
|
||||
"
|
||||
>{{ summaryByClass[c.id].spent }} pts</span
|
||||
>
|
||||
</h3>
|
||||
<div
|
||||
v-if="summaryByClass[c.id].allocations.length === 0"
|
||||
style="color: var(--ink-muted); font-size: 13px; padding: 8px 0"
|
||||
>
|
||||
no points allocated
|
||||
</div>
|
||||
<ul
|
||||
v-else
|
||||
style="
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
"
|
||||
>
|
||||
<li
|
||||
v-for="a in summaryByClass[c.id].allocations"
|
||||
:key="a.tag"
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background: var(--bg);
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--line-soft);
|
||||
"
|
||||
>
|
||||
<span>
|
||||
<span style="color: var(--ink-muted); margin-right: 6px">{{
|
||||
a.kind.slice(0, 3).toUpperCase()
|
||||
}}</span>
|
||||
{{ a.name }}
|
||||
</span>
|
||||
<span style="color: var(--sand)">{{ a.points }}/{{ a.max }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CharacterSummary
|
||||
:build="build"
|
||||
:char-xp="charXp"
|
||||
:spec-tables="specTables"
|
||||
:faction-table="currentFaction || null"
|
||||
:skill-trees="skillTrees"
|
||||
:classes="CLASSES"
|
||||
:specs="SPECS"
|
||||
:spec-meta="specMeta"
|
||||
:total-skill-points="totalSkillPointsAtLevel"
|
||||
:total-intel-level="totalIntelAtLevel"
|
||||
:total-spent-across-classes="totalSpentAcrossClasses"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<footer class="footer-bar">
|
||||
<div>Unofficial fan-made tool · not affiliated with the publisher</div>
|
||||
|
|
|
|||
465
character-builder/frontend/src/components/CharacterSummary.vue
Normal file
465
character-builder/frontend/src/components/CharacterSummary.vue
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type {
|
||||
BuildState,
|
||||
ClassId,
|
||||
FactionTable,
|
||||
Perk,
|
||||
SkillTree,
|
||||
SpecId,
|
||||
XpTable,
|
||||
} from '../types';
|
||||
|
||||
const props = defineProps<{
|
||||
build: BuildState;
|
||||
charXp: XpTable | null;
|
||||
specTables: Record<SpecId, XpTable | null>;
|
||||
factionTable: FactionTable | null;
|
||||
skillTrees: Record<ClassId, SkillTree | null>;
|
||||
classes: { id: ClassId; name: string }[];
|
||||
specs: SpecId[];
|
||||
specMeta: Record<SpecId, { name: string; sym: string }>;
|
||||
totalSkillPoints: number;
|
||||
totalIntelLevel: number;
|
||||
totalSpentAcrossClasses: number;
|
||||
}>();
|
||||
|
||||
function fmt(n: number) {
|
||||
return Number(n || 0).toLocaleString('en-US');
|
||||
}
|
||||
|
||||
const houseName = computed(() =>
|
||||
props.build.house === 'atreides' ? 'House Atreides' : 'House Harkonnen',
|
||||
);
|
||||
|
||||
const currentLevelXp = computed(() => {
|
||||
if (!props.charXp) return 0;
|
||||
const idx = props.build.character.level - 1;
|
||||
if (idx < 0) return 0;
|
||||
return props.charXp.rows[idx]?.totalXp || 0;
|
||||
});
|
||||
|
||||
const totalIntel = computed(
|
||||
() => props.totalIntelLevel + (props.build.character.bonusIntel || 0),
|
||||
);
|
||||
|
||||
const currentTier = computed(() => {
|
||||
if (!props.factionTable) return null;
|
||||
return props.factionTable.tiers[props.build.faction.tier] || null;
|
||||
});
|
||||
const nextTier = computed(() => {
|
||||
if (!props.factionTable) return null;
|
||||
return props.factionTable.tiers[props.build.faction.tier + 1] || null;
|
||||
});
|
||||
|
||||
interface SpecSummary {
|
||||
id: SpecId;
|
||||
name: string;
|
||||
sym: string;
|
||||
level: number;
|
||||
totalXp: number;
|
||||
nextLevelXp: number;
|
||||
perksUnlocked: number;
|
||||
perksTotal: number;
|
||||
}
|
||||
const specSummaries = computed<SpecSummary[]>(() =>
|
||||
props.specs.map((id) => {
|
||||
const t = props.specTables[id];
|
||||
const cur = props.build.specs[id];
|
||||
const totalXp = t && cur.level > 0 ? t.rows[cur.level - 1]?.totalXp || 0 : 0;
|
||||
const nextLevelXp = t ? t.rows[cur.level]?.xpRequired || 0 : 0;
|
||||
let perksU = 0;
|
||||
let perksT = 0;
|
||||
if (t) {
|
||||
for (const r of t.rows) {
|
||||
if (r.perks) {
|
||||
perksT += r.perks.length;
|
||||
if (r.level <= cur.level) perksU += r.perks.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
name: props.specMeta[id].name,
|
||||
sym: props.specMeta[id].sym,
|
||||
level: cur.level,
|
||||
totalXp,
|
||||
nextLevelXp,
|
||||
perksUnlocked: perksU,
|
||||
perksTotal: perksT,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
interface ClassSkillSummary {
|
||||
id: ClassId;
|
||||
name: string;
|
||||
spent: number;
|
||||
allocations: { tag: string; name: string; kind: string; points: number; max: number }[];
|
||||
}
|
||||
const skillsByClass = computed<ClassSkillSummary[]>(() => {
|
||||
const out: ClassSkillSummary[] = [];
|
||||
for (const c of props.classes) {
|
||||
const tree = props.skillTrees[c.id];
|
||||
const allocations: ClassSkillSummary['allocations'] = [];
|
||||
let spent = 0;
|
||||
if (tree) {
|
||||
for (const node of tree.nodes) {
|
||||
const pts = props.build.skills[node.tag] || 0;
|
||||
if (pts > 0) {
|
||||
spent += pts;
|
||||
allocations.push({
|
||||
tag: node.tag,
|
||||
name: node.name,
|
||||
kind: node.kind,
|
||||
points: pts,
|
||||
max: node.maxPoints,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spent > 0 || tree)
|
||||
out.push({ id: c.id, name: c.name, spent, allocations });
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
interface RecentPerk extends Perk {
|
||||
source: string;
|
||||
level: number;
|
||||
}
|
||||
const recentPerks = computed<RecentPerk[]>(() => {
|
||||
// Show last few perks unlocked across character + all specs
|
||||
const all: RecentPerk[] = [];
|
||||
if (props.charXp) {
|
||||
for (const r of props.charXp.rows) {
|
||||
if (r.level <= props.build.character.level && r.perks) {
|
||||
for (const p of r.perks) {
|
||||
all.push({ ...p, source: 'Character', level: r.level });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id of props.specs) {
|
||||
const t = props.specTables[id];
|
||||
if (!t) continue;
|
||||
const lvl = props.build.specs[id].level;
|
||||
for (const r of t.rows) {
|
||||
if (r.level <= lvl && r.perks) {
|
||||
for (const p of r.perks) {
|
||||
all.push({ ...p, source: props.specMeta[id].name, level: r.level });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// newest unlocks first by level
|
||||
all.sort((a, b) => b.level - a.level || a.source.localeCompare(b.source));
|
||||
return all.slice(0, 6);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="summary">
|
||||
<div class="summary-head">
|
||||
<div class="sym">CHARACTER SUMMARY</div>
|
||||
<h2>{{ houseName }}</h2>
|
||||
</div>
|
||||
|
||||
<!-- Identity -->
|
||||
<div class="block">
|
||||
<div class="kv">
|
||||
<span class="k">Level</span>
|
||||
<span class="v big">{{ build.character.level }}</span>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<span class="k">Total XP</span>
|
||||
<span class="v">{{ fmt(currentLevelXp + build.character.xpInto) }}</span>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<span class="k">Currently Viewing</span>
|
||||
<span class="v">
|
||||
{{ classes.find((c) => c.id === build.classId)?.name || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Points pool -->
|
||||
<div class="block">
|
||||
<div class="block-head">Points</div>
|
||||
<div class="kv">
|
||||
<span class="k">Skill Points</span>
|
||||
<span class="v">
|
||||
<span
|
||||
:class="{
|
||||
over: totalSpentAcrossClasses > totalSkillPoints,
|
||||
good: totalSpentAcrossClasses > 0 && totalSpentAcrossClasses <= totalSkillPoints,
|
||||
}"
|
||||
>
|
||||
{{ totalSpentAcrossClasses }}
|
||||
</span>
|
||||
/ {{ totalSkillPoints }} spent
|
||||
</span>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<span class="k">Intel Points</span>
|
||||
<span class="v">
|
||||
{{ fmt(totalIntel) }}
|
||||
<span v-if="build.character.bonusIntel" class="muted">
|
||||
({{ fmt(totalIntelLevel) }} + {{ fmt(build.character.bonusIntel) }} POI)
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Specializations -->
|
||||
<div class="block">
|
||||
<div class="block-head">Specializations</div>
|
||||
<ul class="spec-list">
|
||||
<li v-for="s in specSummaries" :key="s.id">
|
||||
<span class="spec-sym">{{ s.sym }}</span>
|
||||
<span class="spec-name">{{ s.name }}</span>
|
||||
<span class="spec-lvl">L{{ s.level }}</span>
|
||||
<span class="spec-xp">{{ fmt(s.totalXp) }} XP</span>
|
||||
<span
|
||||
v-if="s.perksTotal"
|
||||
class="spec-perks"
|
||||
:title="`${s.perksUnlocked} of ${s.perksTotal} perks unlocked`"
|
||||
>
|
||||
{{ s.perksUnlocked }}/{{ s.perksTotal }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Faction -->
|
||||
<div class="block">
|
||||
<div class="block-head">Faction Standing</div>
|
||||
<div class="kv">
|
||||
<span class="k">{{ houseName }}</span>
|
||||
<span class="v">
|
||||
<strong>{{ currentTier?.name || '—' }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="kv" v-if="nextTier">
|
||||
<span class="k">Next</span>
|
||||
<span class="v">
|
||||
{{ fmt(build.faction.standingInto) }} /
|
||||
{{ fmt(nextTier.standingRequired) }} → {{ nextTier.name }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills by class -->
|
||||
<div class="block">
|
||||
<div class="block-head">Skill Allocations</div>
|
||||
<div v-if="totalSpentAcrossClasses === 0" class="empty">
|
||||
No skill points allocated.
|
||||
</div>
|
||||
<div v-else class="class-blocks">
|
||||
<div
|
||||
v-for="c in skillsByClass.filter((s) => s.allocations.length > 0)"
|
||||
:key="c.id"
|
||||
class="class-block"
|
||||
>
|
||||
<div class="class-row">
|
||||
<span class="cls-name">{{ c.name }}</span>
|
||||
<span class="cls-spent">{{ c.spent }} pts</span>
|
||||
</div>
|
||||
<ul class="skill-list">
|
||||
<li v-for="a in c.allocations" :key="a.tag">
|
||||
<span class="kind">{{ a.kind.slice(0, 3).toUpperCase() }}</span>
|
||||
<span class="skill-name">{{ a.name }}</span>
|
||||
<span class="skill-pts">{{ a.points }}/{{ a.max }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent unlocks -->
|
||||
<div class="block" v-if="recentPerks.length">
|
||||
<div class="block-head">Latest Unlocks</div>
|
||||
<ul class="recent">
|
||||
<li v-for="p in recentPerks" :key="`${p.source}-${p.level}-${p.name}`">
|
||||
<span class="recent-src">{{ p.source }} L{{ p.level }}</span>
|
||||
<span class="recent-name">{{ p.name }}</span>
|
||||
<span v-if="p.effect" class="recent-eff">{{ p.effect }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.summary {
|
||||
background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 4px;
|
||||
padding: 20px 22px;
|
||||
position: sticky;
|
||||
top: 76px;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.summary::-webkit-scrollbar { width: 8px; }
|
||||
.summary::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
|
||||
|
||||
.summary-head {
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px dashed var(--line-soft);
|
||||
}
|
||||
.summary-head .sym {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--ink-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.summary-head h2 {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
color: var(--sand);
|
||||
}
|
||||
|
||||
.block { margin-bottom: 18px; }
|
||||
.block:last-child { margin-bottom: 0; }
|
||||
.block-head {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--ink-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.kv {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 4px 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px dotted rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.kv:last-child { border-bottom: none; }
|
||||
.kv .k { color: var(--ink-muted); }
|
||||
.kv .v { color: var(--ink); }
|
||||
.kv .v.big {
|
||||
font-family: 'Cormorant Garamond', serif;
|
||||
font-size: 22px;
|
||||
color: var(--sand);
|
||||
}
|
||||
.kv .v .muted { color: var(--ink-muted); margin-left: 6px; font-size: 11px; }
|
||||
.kv .v .good { color: var(--sand-2); }
|
||||
.kv .v .over { color: var(--ember); }
|
||||
|
||||
.spec-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.spec-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 1fr 36px auto 50px;
|
||||
gap: 8px;
|
||||
align-items: baseline;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11.5px;
|
||||
padding: 4px 6px;
|
||||
background: var(--bg);
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--line-soft);
|
||||
}
|
||||
.spec-sym { color: var(--ink-muted); letter-spacing: 0.16em; font-size: 10px; }
|
||||
.spec-name { color: var(--ink-dim); }
|
||||
.spec-lvl { color: var(--sand); text-align: right; }
|
||||
.spec-xp { color: var(--ink-muted); text-align: right; font-size: 10.5px; }
|
||||
.spec-perks {
|
||||
color: var(--sand-2);
|
||||
text-align: right;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.empty {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--ink-muted);
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.class-blocks {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.class-block {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 3px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
.class-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
margin-bottom: 6px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px dashed var(--line-soft);
|
||||
}
|
||||
.cls-name { color: var(--ink); letter-spacing: 0.1em; }
|
||||
.cls-spent { color: var(--sand); }
|
||||
|
||||
.skill-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
.skill-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr auto;
|
||||
gap: 6px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.kind { color: var(--ink-muted); font-size: 9.5px; letter-spacing: 0.1em; align-self: center; }
|
||||
.skill-name { color: var(--ink-dim); }
|
||||
.skill-pts { color: var(--sand); }
|
||||
|
||||
.recent {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.recent li {
|
||||
display: grid;
|
||||
grid-template-columns: 90px 1fr;
|
||||
gap: 6px;
|
||||
align-items: baseline;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 11px;
|
||||
padding: 4px 6px;
|
||||
background: var(--bg);
|
||||
border-left: 2px solid var(--sand-2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.recent-src { color: var(--ink-muted); font-size: 10px; letter-spacing: 0.1em; }
|
||||
.recent-name { color: var(--ink); }
|
||||
.recent-eff {
|
||||
grid-column: 2;
|
||||
color: var(--sand-2);
|
||||
font-size: 10.5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -92,10 +92,30 @@ body {
|
|||
a { color: var(--sand); }
|
||||
|
||||
.wrap {
|
||||
max-width: 1240px;
|
||||
max-width: 1560px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 28px 96px;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 400px;
|
||||
gap: 28px;
|
||||
align-items: start;
|
||||
}
|
||||
.builder {
|
||||
min-width: 0; /* prevents grid from blowing out on long content */
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.summary {
|
||||
position: static !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
}
|
||||
header.hero {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
|
@ -430,7 +450,7 @@ button:disabled {
|
|||
margin: -48px 0 32px;
|
||||
}
|
||||
.share-inner {
|
||||
max-width: 1240px;
|
||||
max-width: 1560px;
|
||||
margin: 0 auto;
|
||||
padding: 0 28px;
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue