Initial scaffold: single-month budget MVP #2
4 changed files with 153 additions and 132 deletions
|
|
@ -1,11 +1,11 @@
|
|||
:root {
|
||||
--bg: #f7f6f2;
|
||||
--card: #ffffff;
|
||||
--ink: #1f1f1f;
|
||||
--muted: #6b6b6b;
|
||||
--accent: #2f6b4f;
|
||||
--danger: #a03030;
|
||||
--border: #d8d6cf;
|
||||
--rule: #d8d6cf;
|
||||
--row: #fafaf7;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
|
@ -19,48 +19,49 @@ body {
|
|||
}
|
||||
|
||||
header {
|
||||
padding: 1.5rem 2rem 0.5rem;
|
||||
padding: 1.25rem 2rem 0.25rem;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem 2rem 2rem;
|
||||
padding: 1rem 0 3rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
.budget {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
.section {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-bottom: 2px solid var(--ink);
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.total {
|
||||
|
|
@ -69,92 +70,89 @@ main {
|
|||
color: var(--accent);
|
||||
}
|
||||
|
||||
ul.entries {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
.total.empty {
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entry {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: #fafaf7;
|
||||
}
|
||||
|
||||
.entry-amount {
|
||||
table.entries {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
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);
|
||||
}
|
||||
tr.entry td.entry-actions {
|
||||
width: 2.25rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.empty {
|
||||
tr.empty td {
|
||||
color: var(--muted);
|
||||
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 {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--danger);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 0 0.35rem;
|
||||
padding: 0 0.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.add-form,
|
||||
.target-form {
|
||||
.add-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 7rem auto;
|
||||
grid-template-columns: 1fr 9rem auto;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.target-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
input[type=text],
|
||||
input[type=number],
|
||||
select {
|
||||
padding: 0.35rem 0.5rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--rule);
|
||||
border-radius: 3px;
|
||||
font: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
button[type=submit] {
|
||||
padding: 0.35rem 0.75rem;
|
||||
padding: 0.35rem 0.9rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.target-section .section-header {
|
||||
border-bottom-style: dashed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="grid">
|
||||
<div class="budget">
|
||||
{% 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 %}
|
||||
{% include "partials/target_card.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,45 @@
|
|||
<section class="card" id="section-{{ section.section.value }}">
|
||||
<header class="card-header">
|
||||
<section class="section" id="section-{{ section.section.value }}">
|
||||
<div class="section-header">
|
||||
<h2>{{ section.label }}</h2>
|
||||
<span class="total" data-testid="total-{{ section.section.value }}">
|
||||
${{ '%.2f' | format(section.total) }}
|
||||
</span>
|
||||
</header>
|
||||
<ul class="entries">
|
||||
{% for entry in section.entries %}
|
||||
<li class="entry">
|
||||
<span class="entry-name">{{ entry.name }}</span>
|
||||
<span class="entry-amount">${{ '%.2f' | format(entry.amount) }}</span>
|
||||
<button
|
||||
class="delete"
|
||||
type="button"
|
||||
hx-delete="/entries/{{ entry.id }}"
|
||||
hx-target="#section-{{ section.section.value }}"
|
||||
hx-swap="outerHTML"
|
||||
aria-label="Delete {{ entry.name }}"
|
||||
>×</button>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="empty">No entries yet.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form
|
||||
class="add-form"
|
||||
hx-post="/sections/{{ section.section.value }}/entries"
|
||||
hx-target="#section-{{ section.section.value }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if(event.detail.successful) this.reset()"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<table class="entries">
|
||||
<tbody>
|
||||
{% for entry in section.entries %}
|
||||
<tr class="entry">
|
||||
<td class="entry-name">{{ entry.name }}</td>
|
||||
<td class="entry-amount">${{ '%.2f' | format(entry.amount) }}</td>
|
||||
<td class="entry-actions">
|
||||
<button
|
||||
class="delete"
|
||||
type="button"
|
||||
hx-delete="/entries/{{ entry.id }}"
|
||||
hx-target="#section-{{ section.section.value }}"
|
||||
hx-swap="outerHTML"
|
||||
aria-label="Delete {{ entry.name }}"
|
||||
>×</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr class="empty"><td colspan="3">No entries yet.</td></tr>
|
||||
{% endfor %}
|
||||
<tr class="add-row">
|
||||
<td colspan="3">
|
||||
<form
|
||||
class="add-form"
|
||||
hx-post="/sections/{{ section.section.value }}/entries"
|
||||
hx-target="#section-{{ section.section.value }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if(event.detail.successful) this.reset()"
|
||||
>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,42 @@
|
|||
<section class="card target-card" id="section-debt_target" hx-swap-oob="outerHTML">
|
||||
<header class="card-header">
|
||||
<section class="section target-section" id="section-debt_target" hx-swap-oob="outerHTML">
|
||||
<div class="section-header">
|
||||
<h2>Primary Debt Target</h2>
|
||||
</header>
|
||||
{% if target.entry %}
|
||||
<p class="target-current">
|
||||
<span class="entry-name">{{ target.entry.name }}</span>
|
||||
<span class="entry-amount">${{ '%.2f' | format(target.entry.amount) }}</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="target-current empty">No target selected.</p>
|
||||
{% endif %}
|
||||
<form
|
||||
class="target-form"
|
||||
hx-post="/debt-target"
|
||||
hx-target="#section-debt_target"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<label for="debt-target-select">Choose from Debt Minimums</label>
|
||||
<select id="debt-target-select" name="debt_minimum_id">
|
||||
<option value="">(none)</option>
|
||||
{% for dm in debt_minimums %}
|
||||
<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>
|
||||
{% if target.entry %}
|
||||
<span class="total">${{ '%.2f' | format(target.entry.amount) }}</span>
|
||||
{% else %}
|
||||
<span class="total empty">$0.00</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<table class="entries">
|
||||
<tbody>
|
||||
<tr class="entry">
|
||||
<td class="entry-name">
|
||||
{% if target.entry %}{{ target.entry.name }}{% else %}<span class="muted">No target selected.</span>{% endif %}
|
||||
</td>
|
||||
<td class="entry-amount"></td>
|
||||
<td class="entry-actions"></td>
|
||||
</tr>
|
||||
<tr class="add-row">
|
||||
<td colspan="3">
|
||||
<form
|
||||
class="target-form"
|
||||
hx-post="/debt-target"
|
||||
hx-target="#section-debt_target"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<select name="debt_minimum_id">
|
||||
<option value="">(none)</option>
|
||||
{% for dm in debt_minimums %}
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue