Initial scaffold: single-month budget MVP #2

Merged
claude-code merged 6 commits from feat/1-scaffold into main 2026-04-17 11:31:12 -06:00
4 changed files with 153 additions and 132 deletions
Showing only changes of commit c6cb037f4f - Show all commits

View file

@ -1,11 +1,11 @@
:root { :root {
--bg: #f7f6f2; --bg: #f7f6f2;
--card: #ffffff;
--ink: #1f1f1f; --ink: #1f1f1f;
--muted: #6b6b6b; --muted: #6b6b6b;
--accent: #2f6b4f; --accent: #2f6b4f;
--danger: #a03030; --danger: #a03030;
--border: #d8d6cf; --rule: #d8d6cf;
--row: #fafaf7;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
@ -19,48 +19,49 @@ body {
} }
header { header {
padding: 1.5rem 2rem 0.5rem; padding: 1.25rem 2rem 0.25rem;
border-bottom: 1px solid var(--rule);
background: #fff;
} }
header h1 { header h1 {
margin: 0; margin: 0;
font-size: 1.6rem; font-size: 1.4rem;
} }
.subtitle { .subtitle {
margin: 0; margin: 0;
color: var(--muted); color: var(--muted);
font-size: 0.9rem;
} }
main { main {
padding: 1rem 2rem 2rem; padding: 1rem 0 3rem;
} }
.grid { .budget {
display: grid; max-width: 720px;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); margin: 0 auto;
gap: 1rem; padding: 0 1rem;
} }
.card { .section {
background: var(--card); margin-top: 1.5rem;
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
} }
.card-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
padding: 0.25rem 0.5rem;
border-bottom: 2px solid var(--ink);
} }
.card-header h2 { .section-header h2 {
margin: 0; margin: 0;
font-size: 1.1rem; font-size: 1.05rem;
font-weight: 600;
letter-spacing: 0.01em;
} }
.total { .total {
@ -69,92 +70,89 @@ main {
color: var(--accent); color: var(--accent);
} }
ul.entries { .total.empty {
list-style: none; color: var(--muted);
padding: 0; font-weight: 500;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
} }
.entry { table.entries {
display: grid; width: 100%;
grid-template-columns: 1fr auto auto; border-collapse: collapse;
align-items: center;
gap: 0.5rem;
padding: 0.35rem 0.5rem;
border-radius: 4px;
background: #fafaf7;
}
.entry-amount {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
}
table.entries td {
padding: 0.35rem 0.5rem;
border-bottom: 1px solid var(--rule);
vertical-align: middle;
}
tr.entry td.entry-name { width: auto; }
tr.entry td.entry-amount {
width: 9rem;
text-align: right;
color: var(--muted); color: var(--muted);
} }
tr.entry td.entry-actions {
width: 2.25rem;
text-align: right;
}
.empty { tr.empty td {
color: var(--muted); color: var(--muted);
font-style: italic; font-style: italic;
} }
tr.add-row td {
padding: 0.35rem 0.5rem;
background: transparent;
border-bottom: none;
}
.muted { color: var(--muted); font-style: italic; }
button.delete { button.delete {
background: transparent; background: transparent;
border: none; border: none;
color: var(--danger); color: var(--danger);
font-size: 1rem; font-size: 1rem;
cursor: pointer; cursor: pointer;
padding: 0 0.35rem; padding: 0 0.25rem;
line-height: 1;
} }
.add-form, .add-form {
.target-form {
display: grid; display: grid;
grid-template-columns: 1fr 7rem auto; grid-template-columns: 1fr 9rem auto;
gap: 0.5rem; gap: 0.5rem;
} }
.target-form { .target-form {
display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
align-items: center; gap: 0.5rem;
}
.target-form label {
grid-column: 1 / -1;
font-size: 0.85rem;
color: var(--muted);
}
.target-current {
margin: 0;
padding: 0.5rem 0.75rem;
background: #f0ece0;
border-radius: 4px;
display: flex;
justify-content: space-between;
}
.target-current.empty {
color: var(--muted);
font-style: italic;
} }
input[type=text], input[type=text],
input[type=number], input[type=number],
select { select {
padding: 0.35rem 0.5rem; padding: 0.35rem 0.5rem;
border: 1px solid var(--border); border: 1px solid var(--rule);
border-radius: 4px; border-radius: 3px;
font: inherit; font: inherit;
background: #fff; background: #fff;
} }
button[type=submit] { button[type=submit] {
padding: 0.35rem 0.75rem; padding: 0.35rem 0.9rem;
background: var(--accent); background: var(--accent);
color: #fff; color: #fff;
border: none; border: none;
border-radius: 4px; border-radius: 3px;
cursor: pointer; cursor: pointer;
font: inherit; font: inherit;
} }
.target-section .section-header {
border-bottom-style: dashed;
}

View file

@ -1,9 +1,13 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="grid"> <div class="budget">
{% for section in sections %} {% for section in sections %}
{% include "partials/section.html" %} {% if section.section.value == 'debt_minimum' %}
{% include "partials/section.html" %}
{% include "partials/target_card.html" %}
{% else %}
{% include "partials/section.html" %}
{% endif %}
{% endfor %} {% endfor %}
{% include "partials/target_card.html" %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,37 +1,45 @@
<section class="card" id="section-{{ section.section.value }}"> <section class="section" id="section-{{ section.section.value }}">
<header class="card-header"> <div class="section-header">
<h2>{{ section.label }}</h2> <h2>{{ section.label }}</h2>
<span class="total" data-testid="total-{{ section.section.value }}"> <span class="total" data-testid="total-{{ section.section.value }}">
${{ '%.2f' | format(section.total) }} ${{ '%.2f' | format(section.total) }}
</span> </span>
</header> </div>
<ul class="entries"> <table class="entries">
{% for entry in section.entries %} <tbody>
<li class="entry"> {% for entry in section.entries %}
<span class="entry-name">{{ entry.name }}</span> <tr class="entry">
<span class="entry-amount">${{ '%.2f' | format(entry.amount) }}</span> <td class="entry-name">{{ entry.name }}</td>
<button <td class="entry-amount">${{ '%.2f' | format(entry.amount) }}</td>
class="delete" <td class="entry-actions">
type="button" <button
hx-delete="/entries/{{ entry.id }}" class="delete"
hx-target="#section-{{ section.section.value }}" type="button"
hx-swap="outerHTML" hx-delete="/entries/{{ entry.id }}"
aria-label="Delete {{ entry.name }}" hx-target="#section-{{ section.section.value }}"
>&times;</button> hx-swap="outerHTML"
</li> aria-label="Delete {{ entry.name }}"
{% else %} >&times;</button>
<li class="empty">No entries yet.</li> </td>
{% endfor %} </tr>
</ul> {% else %}
<form <tr class="empty"><td colspan="3">No entries yet.</td></tr>
class="add-form" {% endfor %}
hx-post="/sections/{{ section.section.value }}/entries" <tr class="add-row">
hx-target="#section-{{ section.section.value }}" <td colspan="3">
hx-swap="outerHTML" <form
hx-on::after-request="if(event.detail.successful) this.reset()" class="add-form"
> hx-post="/sections/{{ section.section.value }}/entries"
<input type="text" name="name" placeholder="Name" required> hx-target="#section-{{ section.section.value }}"
<input type="number" name="amount" step="0.01" min="0" placeholder="0.00" required> hx-swap="outerHTML"
<button type="submit">Add</button> hx-on::after-request="if(event.detail.successful) this.reset()"
</form> >
<input type="text" name="name" placeholder="Name" required>
<input type="number" name="amount" step="0.01" min="0" placeholder="0.00" required>
<button type="submit">Add</button>
</form>
</td>
</tr>
</tbody>
</table>
</section> </section>

View file

@ -1,31 +1,42 @@
<section class="card target-card" id="section-debt_target" hx-swap-oob="outerHTML"> <section class="section target-section" id="section-debt_target" hx-swap-oob="outerHTML">
<header class="card-header"> <div class="section-header">
<h2>Primary Debt Target</h2> <h2>Primary Debt Target</h2>
</header> {% if target.entry %}
{% if target.entry %} <span class="total">${{ '%.2f' | format(target.entry.amount) }}</span>
<p class="target-current"> {% else %}
<span class="entry-name">{{ target.entry.name }}</span> <span class="total empty">$0.00</span>
<span class="entry-amount">${{ '%.2f' | format(target.entry.amount) }}</span> {% endif %}
</p> </div>
{% else %} <table class="entries">
<p class="target-current empty">No target selected.</p> <tbody>
{% endif %} <tr class="entry">
<form <td class="entry-name">
class="target-form" {% if target.entry %}{{ target.entry.name }}{% else %}<span class="muted">No target selected.</span>{% endif %}
hx-post="/debt-target" </td>
hx-target="#section-debt_target" <td class="entry-amount"></td>
hx-swap="outerHTML" <td class="entry-actions"></td>
> </tr>
<label for="debt-target-select">Choose from Debt Minimums</label> <tr class="add-row">
<select id="debt-target-select" name="debt_minimum_id"> <td colspan="3">
<option value="">(none)</option> <form
{% for dm in debt_minimums %} class="target-form"
<option hx-post="/debt-target"
value="{{ dm.id }}" hx-target="#section-debt_target"
{% if target.debt_minimum_id == dm.id %}selected{% endif %} hx-swap="outerHTML"
>{{ dm.name }}: ${{ '%.2f' | format(dm.amount) }}</option> >
{% endfor %} <select name="debt_minimum_id">
</select> <option value="">(none)</option>
<button type="submit">Set</button> {% for dm in debt_minimums %}
</form> <option
value="{{ dm.id }}"
{% if target.debt_minimum_id == dm.id %}selected{% endif %}
>{{ dm.name }}: ${{ '%.2f' | format(dm.amount) }}</option>
{% endfor %}
</select>
<button type="submit">Set</button>
</form>
</td>
</tr>
</tbody>
</table>
</section> </section>