Initial scaffold: single-month budget MVP #2
4 changed files with 153 additions and 132 deletions
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
{% if section.section.value == 'debt_minimum' %}
|
||||||
{% include "partials/section.html" %}
|
{% include "partials/section.html" %}
|
||||||
{% endfor %}
|
|
||||||
{% include "partials/target_card.html" %}
|
{% include "partials/target_card.html" %}
|
||||||
|
{% else %}
|
||||||
|
{% include "partials/section.html" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
<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">
|
||||||
|
<tbody>
|
||||||
{% for entry in section.entries %}
|
{% for entry in section.entries %}
|
||||||
<li class="entry">
|
<tr class="entry">
|
||||||
<span class="entry-name">{{ entry.name }}</span>
|
<td class="entry-name">{{ entry.name }}</td>
|
||||||
<span class="entry-amount">${{ '%.2f' | format(entry.amount) }}</span>
|
<td class="entry-amount">${{ '%.2f' | format(entry.amount) }}</td>
|
||||||
|
<td class="entry-actions">
|
||||||
<button
|
<button
|
||||||
class="delete"
|
class="delete"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -18,11 +20,13 @@
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
aria-label="Delete {{ entry.name }}"
|
aria-label="Delete {{ entry.name }}"
|
||||||
>×</button>
|
>×</button>
|
||||||
</li>
|
</td>
|
||||||
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="empty">No entries yet.</li>
|
<tr class="empty"><td colspan="3">No entries yet.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
<tr class="add-row">
|
||||||
|
<td colspan="3">
|
||||||
<form
|
<form
|
||||||
class="add-form"
|
class="add-form"
|
||||||
hx-post="/sections/{{ section.section.value }}/entries"
|
hx-post="/sections/{{ section.section.value }}/entries"
|
||||||
|
|
@ -34,4 +38,8 @@
|
||||||
<input type="number" name="amount" step="0.01" min="0" placeholder="0.00" required>
|
<input type="number" name="amount" step="0.01" min="0" placeholder="0.00" required>
|
||||||
<button type="submit">Add</button>
|
<button type="submit">Add</button>
|
||||||
</form>
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
<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 %}
|
||||||
<p class="target-current">
|
<span class="total">${{ '%.2f' | format(target.entry.amount) }}</span>
|
||||||
<span class="entry-name">{{ target.entry.name }}</span>
|
|
||||||
<span class="entry-amount">${{ '%.2f' | format(target.entry.amount) }}</span>
|
|
||||||
</p>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="target-current empty">No target selected.</p>
|
<span class="total empty">$0.00</span>
|
||||||
{% endif %}
|
{% 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
|
<form
|
||||||
class="target-form"
|
class="target-form"
|
||||||
hx-post="/debt-target"
|
hx-post="/debt-target"
|
||||||
hx-target="#section-debt_target"
|
hx-target="#section-debt_target"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
>
|
>
|
||||||
<label for="debt-target-select">Choose from Debt Minimums</label>
|
<select name="debt_minimum_id">
|
||||||
<select id="debt-target-select" name="debt_minimum_id">
|
|
||||||
<option value="">(none)</option>
|
<option value="">(none)</option>
|
||||||
{% for dm in debt_minimums %}
|
{% for dm in debt_minimums %}
|
||||||
<option
|
<option
|
||||||
|
|
@ -28,4 +35,8 @@
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">Set</button>
|
<button type="submit">Set</button>
|
||||||
</form>
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue