spendwise 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,10 @@
18
18
  <meta name="apple-mobile-web-app-title" content="Spend Wise">
19
19
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
20
20
  <link rel="stylesheet" href="css/app.css">
21
+ <!-- sets html.dark before the body paints (no light flash). Kept after the
22
+ stylesheet link so it sits after the <base href="/renderer/"> the web
23
+ server injects there - otherwise js/theme.js would 404 in web mode. -->
24
+ <script src="js/theme.js"></script>
21
25
 
22
26
  <script src="vendor/apexcharts.min.js"></script>
23
27
  <script src="../shared/engine.js"></script>
@@ -55,26 +59,26 @@
55
59
  <!-- lock screen: casual privacy for shared computers (web logs in at the server) -->
56
60
  <template x-if="$store.ui.locked">
57
61
  <div class="fixed inset-0 z-[100] bg-zinc-100 flex items-center justify-center" x-data="{ pw: '', busy: false, bad: false }">
58
- <form class="bg-white border border-zinc-200 rounded-xl shadow-lg p-8 w-80 text-center" :class="bad && 'animate-shake'"
62
+ <form class="bg-white dark:bg-zinc-100 border border-zinc-200 rounded-xl shadow-lg p-8 w-80 text-center" :class="bad && 'animate-shake'"
59
63
  @submit.prevent="busy = true; bad = false; window.api.authVerify({ password: pw }).then((r) => {
60
64
  busy = false;
61
65
  if (r.ok && r.valid) { $store.ui.locked = false; } else { bad = true; pw = ''; }
62
66
  })">
63
67
  <img src="assets/icon.ico" alt="" class="h-10 w-10 mx-auto mb-2" draggable="false">
64
- <h2 class="font-black font-brand text-lg text-emerald-700 mb-4 select-none">Spend Wise</h2>
68
+ <h2 class="font-black font-brand text-lg text-emerald-700 dark:text-[#c7e0da] mb-4 select-none">Spend Wise</h2>
65
69
  <input type="password" x-model="pw" placeholder="App password" x-init="$nextTick(() => $el.focus())"
66
70
  class="w-full border border-zinc-300 rounded-md px-3 py-2 text-sm text-center focus:outline-none focus:ring-1 focus:ring-emerald-500">
67
71
  <button type="submit" :disabled="busy || !pw"
68
- class="mt-3 w-full bg-emerald-600 hover:bg-emerald-500 text-white rounded-md py-2 text-sm font-bold cursor-pointer transition-colors disabled:bg-zinc-200 disabled:text-zinc-400">Unlock</button>
69
- <p x-show="bad" class="text-xs text-red-600 mt-2">Wrong password</p>
72
+ class="mt-3 w-full hover:bg-emerald-500 text-white dark:text-[#1b1e25] rounded-md py-2 text-sm font-bold cursor-pointer transition-colors disabled:bg-zinc-200 disabled:text-zinc-400">Unlock</button>
73
+ <p x-show="bad" class="text-xs text-red-600 dark:text-red-400 mt-2">Wrong password</p>
70
74
  </form>
71
75
  </div>
72
76
  </template>
73
77
 
74
78
  <!-- ================================================== header -->
75
- <header class="bg-white border-b border-zinc-200 sticky top-0 z-30">
79
+ <header class="bg-white dark:bg-zinc-100 border-b border-zinc-200 sticky top-0 z-30">
76
80
  <div class="max-w-screen-2xl mx-auto px-3 sm:px-6 py-3 flex items-center justify-between">
77
- <div class="flex items-center gap-2.5 select-none shrink-0">
81
+ <div class="flex items-center gap-2.5 select-none shrink-0" :class="$store.ui.view !== 'month' && 'cursor-pointer'" @click="switchView('month')">
78
82
  <!-- the piggy's visual mass sits ~10% low in its square (light
79
83
  slot/ears on top, heavy body below), so a geometrically
80
84
  centred icon reads low. -2.5px puts its centre of mass on
@@ -84,7 +88,7 @@
84
88
  20px), so items-center centres the BOX and leaves the visible
85
89
  letters riding high. Same 0.15em correction as the group
86
90
  titles - measured, and scales with font-size. -->
87
- <h1 class="font-black font-brand tracking-tight text-2xl leading-none text-emerald-700 hidden sm:block relative top-[0.125em]">Spend Wise</h1>
91
+ <h1 id="wordmark" class="font-black font-brand tracking-tight text-2xl leading-none text-emerald-700 dark:text-[#c7e0da] hidden sm:block relative top-[0.125em]"><span>S</span><span>p</span><span>e</span><span>n</span><span>d</span> <span>W</span><span>i</span><span>s</span><span>e</span></h1>
88
92
  </div>
89
93
  <div class="flex items-center gap-2 sm:gap-4">
90
94
  <!-- month picker (month view only): status tag + selector.
@@ -94,19 +98,19 @@
94
98
  <template x-if="$store.ui.view === 'month' && $store.data.db && $store.ui.currentKey && $store.data.db.months[$store.ui.currentKey]">
95
99
  <div class="flex items-center gap-2">
96
100
  <span x-show="JSON.stringify($store.data.db.months[$store.ui.currentKey]) !== $store.ui.savedSnapshot"
97
- class="text-amber-500 select-none" title="Unsaved changes">●</span>
101
+ class="text-amber-500 dark:text-amber-400 select-none" title="Unsaved changes">●</span>
98
102
  <!-- on phones the open/closed pill lives in the This Month panel instead
99
103
  (which sits inside main, so it goes with it while switching) -->
100
104
  <span x-show="!$store.ui.switching"
101
105
  class="hidden sm:inline-block text-[10px] uppercase tracking-widest font-bold px-2.5 py-1 rounded-full select-none"
102
106
  :class="$store.data.db.months[$store.ui.currentKey].status === 'closed'
103
- ? ($store.ui.unlockedKeys.includes($store.ui.currentKey) ? 'bg-amber-100 text-amber-700' : 'bg-zinc-200 text-zinc-500')
104
- : 'bg-emerald-100 text-emerald-700'"
107
+ ? ($store.ui.unlockedKeys.includes($store.ui.currentKey) ? 'bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400' : 'bg-zinc-200 text-zinc-500')
108
+ : 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400'"
105
109
  x-text="$store.data.db.months[$store.ui.currentKey].status === 'closed'
106
110
  ? ($store.ui.unlockedKeys.includes($store.ui.currentKey) ? 'closed · editing' : 'closed') : 'open'"></span>
107
111
  <select :value="$store.ui.currentKey"
108
112
  @change="$store.ui.monthGoto && $store.ui.monthGoto($event.target.value).then(() => { $el.value = $store.ui.currentKey })"
109
- class="border border-zinc-300 bg-white rounded-md px-2.5 py-1 text-sm font-semibold cursor-pointer focus:outline-none focus:ring-1 focus:ring-emerald-500">
113
+ class="border border-zinc-300 bg-white dark:bg-zinc-100 rounded-md px-2.5 py-1 text-sm dark:text-[#c7e0da] font-semibold cursor-pointer focus:outline-none focus:ring-1 focus:ring-emerald-500">
110
114
  <template x-for="k in FinEngine.monthKeys($store.data.db).slice().reverse()" :key="k">
111
115
  <option :value="k" :selected="k === $store.ui.currentKey" x-text="FinEngine.keyLabel(k)"></option>
112
116
  </template>
@@ -116,21 +120,21 @@
116
120
  <nav class="flex gap-1 items-center">
117
121
  <button type="button" @click="switchView('month')"
118
122
  class="px-2.5 sm:px-4 py-1.5 rounded-md text-sm font-semibold transition-colors cursor-pointer"
119
- :class="$store.ui.view === 'month' ? 'bg-emerald-600 text-white' : 'text-zinc-500 hover:bg-zinc-100'">Month</button>
123
+ :class="$store.ui.view === 'month' ? 'bg-emerald-600 dark:bg-emerald-400/70 text-white dark:text-[#1b1e25]' : 'text-zinc-500 hover:bg-zinc-100'">Month</button>
120
124
  <button type="button" @click="switchView('insights')" title="Insights"
121
125
  class="px-2.5 sm:px-4 py-1.5 rounded-md text-sm font-semibold transition-colors cursor-pointer flex items-center"
122
- :class="$store.ui.view === 'insights' ? 'bg-emerald-600 text-white' : 'text-zinc-500 hover:bg-zinc-100'">
126
+ :class="$store.ui.view === 'insights' ? 'bg-emerald-600 dark:bg-emerald-400/70 text-white dark:text-[#1b1e25]' : 'text-zinc-500 hover:bg-zinc-100'">
123
127
  <span class="hidden sm:inline">Insights</span>
124
128
  <svg class="w-4 h-4 sm:hidden"><use href="#i-bulb"/></svg>
125
129
  </button>
126
130
  <button type="button" @click="switchView('settings')"
127
131
  :title="$store.data.update.available ? 'Settings — version ' + $store.data.update.latest + ' is available' : 'Settings'"
128
132
  class="relative p-1.5 rounded-md transition-colors cursor-pointer"
129
- :class="$store.ui.view === 'settings' ? 'bg-emerald-600 text-white' : 'text-zinc-500 hover:bg-zinc-100'">
133
+ :class="$store.ui.view === 'settings' ? 'bg-emerald-600 dark:bg-emerald-400/70 text-white dark:text-[#1b1e25]' : 'text-zinc-500 hover:bg-zinc-100'">
130
134
  <!-- the launch toast fades; this doesn't, so the nudge survives
131
135
  without ever blocking the books -->
132
136
  <span x-show="$store.data.update.available" aria-hidden="true"
133
- class="absolute top-0.5 right-0.5 w-2 h-2 rounded-full bg-emerald-500 ring-2 ring-white"></span>
137
+ class="absolute top-0.5 right-0.5 w-2 h-2 rounded-full bg-emerald-500 ring-2 ring-white dark:ring-zinc-100"></span>
134
138
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5" aria-hidden="true">
135
139
  <path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
136
140
  </svg>
@@ -143,7 +147,7 @@
143
147
  <!-- offline (web clients only): every save needs the server, so the app
144
148
  goes read-only rather than letting edits pile up with nowhere to go -->
145
149
  <div x-show="$store.ui.offline" x-cloak
146
- class="bg-amber-100 border-b border-amber-300 text-amber-900 text-xs text-center py-2 px-4 sticky top-14 z-20 select-none">
150
+ class="bg-amber-100 dark:bg-amber-500/15 border-b border-amber-300 dark:border-amber-500/30 text-amber-900 dark:text-amber-300 text-xs text-center py-2 px-4 sticky top-14 z-20 select-none">
147
151
  <span class="font-bold">Offline</span> - can’t reach the server. Editing is paused until the connection
148
152
  returns; anything you already typed is still here.
149
153
  </div>
@@ -169,9 +173,9 @@
169
173
  per-child opt-out inside a disabled fieldset -->
170
174
  <fieldset class="flex-1 w-full min-w-0 border-0 m-0 p-0 order-2 xl:order-none">
171
175
  <template x-for="group in month.groups" :key="group.groupId">
172
- <section class="mb-5 bg-white rounded-lg border border-zinc-200 shadow-sm overflow-hidden">
176
+ <section class="mb-5 bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm overflow-hidden">
173
177
  <header @click="toggleCollapsed(group.groupId)"
174
- class="flex justify-between items-center px-4 py-2.5 bg-emerald-600 text-white cursor-pointer select-none">
178
+ class="flex justify-between items-center px-4 py-2.5 bg-emerald-600 dark:bg-emerald-400/70 text-white dark:text-[#1b1e25] cursor-pointer select-none">
175
179
  <!-- the amount is the preferred item: it never wraps or shrinks;
176
180
  the title takes whatever room is left (min-w-0 lets it).
177
181
  top-[0.15em]: Roca's line box is taller above the caps than
@@ -190,7 +194,7 @@
190
194
  </header>
191
195
  <div x-show="!collapsed[group.groupId]" x-collapse.duration.200ms>
192
196
  <div class="p-2.5 sm:p-4">
193
- <p class="text-xs text-zinc-400 mb-3 grayscale" x-text="groupHint(group.kind)"></p>
197
+ <p class="text-xs text-zinc-400 mb-3" x-html="groupHint(group.kind)"></p>
194
198
 
195
199
  <!-- column headers for envelope/goal groups -->
196
200
  <div x-show="(group.kind === 'envelope' || group.kind === 'goal') && group.fields.length"
@@ -235,12 +239,12 @@
235
239
  ? (field.pinned ? 'Was carried into the next month' : 'Was not carried forward')
236
240
  : (field.pinned ? 'Carries into future months' : 'This month only - click to pin')"
237
241
  class="max-sm:order-1 w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer transition-colors"
238
- :class="field.pinned ? 'bg-emerald-100 text-emerald-700' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-pin"/></svg></button>
242
+ :class="field.pinned ? 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-pin"/></svg></button>
239
243
  </template>
240
244
 
241
245
  <div class="relative flex-1 min-w-28 max-sm:order-2">
242
246
  <input type="text" x-model="field.label" placeholder="Label" :disabled="frozen"
243
- class="w-full border rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1"
247
+ class="w-full border rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white dark:text-[#c7e0da] dark:focus:bg-zinc-100 focus:outline-none focus:ring-1"
244
248
  :class="group.kind === 'envelope' && overBudget(field)
245
249
  ? 'border-red-400 focus:ring-red-400'
246
250
  : 'border-zinc-200 focus:ring-emerald-500'">
@@ -280,17 +284,17 @@
280
284
  :title="group.kind === 'expense' && field.budgetId && !FinEngine.num(field.value)
281
285
  ? 'Linked to an envelope but $0 - it draws nothing until an amount is entered'
282
286
  : (group.kind === 'envelope' || group.kind === 'goal' ? 'Monthly allotment' : 'Amount')"
283
- class="w-24 max-sm:w-20 border border-zinc-200 rounded pl-5 pr-1 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500"
284
- :class="group.kind === 'expense' && field.budgetId && !FinEngine.num(field.value) && 'ring-1 ring-amber-400 bg-amber-50'">
287
+ class="w-24 max-sm:w-20 border border-zinc-200 rounded pl-5 pr-1 py-1.5 text-sm dark:text-[#c7e0da] text-right bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500"
288
+ :class="group.kind === 'expense' && field.budgetId && !FinEngine.num(field.value) && 'ring-1 ring-amber-400 bg-amber-50 dark:bg-amber-500/10'">
285
289
  </div>
286
290
  </template>
287
291
  <template x-if="isAuto(field) && !isSched(group, field)">
288
- <span class="w-24 max-sm:w-20 max-sm:order-3 shrink-0 text-right text-sm tabular-nums font-semibold text-violet-600 pr-1 select-none"
292
+ <span class="w-24 max-sm:w-20 max-sm:order-3 shrink-0 text-right text-sm tabular-nums font-semibold text-violet-600 dark:text-violet-400 pr-1 select-none"
289
293
  :title="'Auto-funded: ' + field.auto.pct + '% of the selected groups’ net'"
290
294
  x-text="fmt(rateOf(field))"></span>
291
295
  </template>
292
296
  <template x-if="isSched(group, field)">
293
- <span class="w-24 max-sm:w-20 max-sm:order-3 shrink-0 text-right text-sm tabular-nums font-semibold text-violet-600 pr-1 select-none"
297
+ <span class="w-24 max-sm:w-20 max-sm:order-3 shrink-0 text-right text-sm tabular-nums font-semibold text-violet-600 dark:text-violet-400 pr-1 select-none"
294
298
  :title="'Scheduled deposit: ' + fmt(field.target) + ' due ' + FinEngine.keyLabel(field.dueKey) + ' - headroom spread over the months remaining'"
295
299
  x-text="fmt(field.value)"></span>
296
300
  </template>
@@ -303,7 +307,7 @@
303
307
  <div class="max-sm:order-6 max-sm:flex max-sm:items-center max-sm:gap-1.5 shrink-0 sm:contents">
304
308
  <span class="sm:hidden shrink-0 text-[10px] uppercase tracking-widest text-zinc-350 select-none">spent</span>
305
309
  <span class="w-24 max-sm:w-auto shrink-0 text-right text-xs tabular-nums select-none"
306
- :class="effSpent(field) > 0 ? (overBudget(field) ? 'text-red-600 font-semibold' : 'text-emerald-600') : 'text-zinc-300'"
310
+ :class="effSpent(field) > 0 ? (overBudget(field) ? 'text-red-600 dark:text-red-400 font-semibold' : 'text-emerald-600 dark:text-emerald-400') : 'text-zinc-300'"
307
311
  :title="'Spent from this envelope - expense rows assigned to it via the “from” dropdown' + (FinEngine.num(field.spent) > 0 ? ' (includes ' + fmt(field.spent) + ' carried from the legacy import)' : '')"
308
312
  x-text="fmt(effSpent(field))"></span>
309
313
  </div>
@@ -311,8 +315,8 @@
311
315
  <template x-if="group.kind === 'envelope' || group.kind === 'goal'">
312
316
  <div class="max-sm:order-6 max-sm:flex max-sm:items-center max-sm:gap-1.5 shrink-0 sm:contents">
313
317
  <span class="sm:hidden shrink-0 text-[10px] uppercase tracking-widest text-zinc-350 select-none">available</span>
314
- <span class="w-24 max-sm:w-auto shrink-0 text-right text-xs tabular-nums font-semibold select-none"
315
- :class="overBudget(field) ? 'text-red-600' : (progress(field).reached ? 'text-amber-600' : 'text-zinc-600')"
318
+ <span class="w-24 max-sm:w-auto shrink-0 text-right text-xs dark:text-[#c7e0da] tabular-nums font-semibold select-none"
319
+ :class="overBudget(field) ? 'text-red-600 dark:text-red-400' : (progress(field).reached ? 'text-amber-600 dark:text-amber-400' : 'text-zinc-600')"
316
320
  :title="progress(field).reached ? 'At the cap - holds here until spent' : 'Available in this envelope right now'"
317
321
  x-text="fmt(field.avail)"></span>
318
322
  </div>
@@ -326,7 +330,7 @@
326
330
  <div class="relative" title="Goal target">
327
331
  <span class="absolute left-2 top-1/2 -translate-y-1/2 text-zinc-400 text-xs select-none">$</span>
328
332
  <input type="number" x-model.number="field.target" placeholder="target" :disabled="frozen"
329
- class="w-24 max-sm:w-20 border border-zinc-200 rounded pl-5 pr-1 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500">
333
+ class="w-24 max-sm:w-20 border border-zinc-200 rounded pl-5 pr-1 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 dark:text-[#c7e0da] focus:outline-none focus:ring-1 focus:ring-emerald-500">
330
334
  </div>
331
335
  </div>
332
336
  </template>
@@ -344,7 +348,7 @@
344
348
  :disabled="frozen" title="Draw this expense from an envelope"
345
349
  x-effect="budgetOptions; field.budgetId; $nextTick(() => { $el.value = field.budgetId || ''; })"
346
350
  class="w-[calc(100%-24px)] sm:w-[198px] shrink-0 text-xs border border-zinc-200 rounded py-2 px-1 bg-zinc-50 cursor-pointer focus:outline-none focus:ring-1 focus:ring-emerald-500"
347
- :class="field.budgetId && 'text-emerald-700 font-semibold'">
351
+ :class="field.budgetId && 'text-emerald-700 dark:text-emerald-400/70 font-semibold'">
348
352
  <template x-for="opt in budgetOptions" :key="opt.id">
349
353
  <!-- x-effect, not :value - Alpine drops the value
350
354
  ATTRIBUTE for an empty string -->
@@ -359,7 +363,7 @@
359
363
  <button type="button" @click="field.accounted = !field.accounted" :disabled="frozen"
360
364
  :title="field.accounted ? 'Marked as paid / received' : 'Mark as paid / received'"
361
365
  class="w-7 h-7 shrink-0 max-sm:order-4 rounded flex items-center justify-center cursor-pointer transition-colors"
362
- :class="field.accounted ? 'bg-emerald-500 text-white' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-check"/></svg></button>
366
+ :class="field.accounted ? 'bg-emerald-500 dark:bg-emerald-400/70 text-white dark:text-[#1b1e25]' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-check"/></svg></button>
363
367
  </template>
364
368
 
365
369
  <!-- envelope: auto-fund -->
@@ -367,7 +371,7 @@
367
371
  <button type="button" @click="toggleAutoEditor(field)" :disabled="frozen"
368
372
  title="Auto-fund: derive this monthly amount from a percentage of other groups (e.g. 33% of self-employment net)"
369
373
  class="max-sm:hidden w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer transition-colors"
370
- :class="isAuto(field) ? 'bg-violet-100 text-violet-700' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-percent"/></svg></button>
374
+ :class="isAuto(field) ? 'bg-violet-100 dark:bg-violet-500/15 text-violet-700 dark:text-violet-400' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-percent"/></svg></button>
371
375
  </template>
372
376
 
373
377
  <!-- goal: schedule -->
@@ -375,21 +379,21 @@
375
379
  <button type="button" @click="toggleSchedEditor(field)" :disabled="frozen"
376
380
  title="Schedule: set a due date and frequency and the monthly deposit is calculated for you - great for recurring bills"
377
381
  class="max-sm:hidden w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer transition-colors"
378
- :class="field.dueKey ? 'bg-violet-100 text-violet-700' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-clock"/></svg></button>
382
+ :class="field.dueKey ? 'bg-violet-100 dark:bg-violet-500/15 text-violet-700 dark:text-violet-400' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'"><svg class="w-3.5 h-3.5"><use href="#i-clock"/></svg></button>
379
383
  </template>
380
384
 
381
385
  <!-- tags -->
382
386
  <button type="button" @click="toggleTagEditor(field)" title="Tags" :disabled="offline"
383
387
  class="max-sm:hidden w-8 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer transition-colors"
384
- :class="(field.tags && field.tags.length) ? 'bg-emerald-100 text-emerald-700' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'">
388
+ :class="(field.tags && field.tags.length) ? 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400' : 'bg-zinc-100 text-zinc-300 hover:text-zinc-500'">
385
389
  <svg x-show="!(field.tags && field.tags.length > 1)" class="w-3.5 h-3.5"><use href="#i-tag"/></svg>
386
390
  <svg x-show="field.tags && field.tags.length > 1" class="w-4 h-4"><use href="#i-tags"/></svg>
387
391
  </button>
388
392
 
389
393
  <button type="button" @click="removeField(group, field)" :disabled="frozen" title="Remove this field"
390
- class="max-sm:hidden w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer bg-zinc-100 text-zinc-400 hover:bg-red-100 hover:text-red-600 transition-colors"><svg class="w-3.5 h-3.5"><use href="#i-xmark"/></svg></button>
394
+ class="max-sm:hidden w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer bg-zinc-100 text-zinc-400 hover:bg-red-100 dark:hover:bg-red-500/15 hover:text-red-600 dark:hover:text-red-400 transition-colors"><svg class="w-3.5 h-3.5"><use href="#i-xmark"/></svg></button>
391
395
  <button type="button" @click="addField(group, fi)" :disabled="frozen" title="Add a field below"
392
- class="max-sm:hidden w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer bg-zinc-100 text-zinc-400 hover:bg-emerald-100 hover:text-emerald-700 transition-colors"><svg class="w-3.5 h-3.5"><use href="#i-plus"/></svg></button>
396
+ class="max-sm:hidden w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer bg-zinc-100 text-zinc-400 hover:bg-emerald-100 dark:hover:bg-emerald-500/15 hover:text-emerald-700 dark:hover:text-emerald-400 transition-colors"><svg class="w-3.5 h-3.5"><use href="#i-plus"/></svg></button>
393
397
 
394
398
  <!-- phones: row actions menu (everything but the checkmark) -->
395
399
  <!-- .stop is load-bearing: without it the menu's @click.outside
@@ -401,31 +405,31 @@
401
405
  <button type="button" @click.stop="toggleRowMenu(field, $event)" title="Row actions"
402
406
  class="sm:hidden max-sm:order-4 w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer bg-zinc-100 text-zinc-400 active:bg-zinc-200"><svg class="w-3.5 h-3.5"><use href="#i-kebab"/></svg></button>
403
407
  <template x-if="rowMenuFor === field.id">
404
- <div class="fixed z-50 w-56 bg-white border border-zinc-200 rounded-md shadow-lg py-1"
408
+ <div class="fixed z-50 w-56 bg-white dark:bg-zinc-100 border border-zinc-200 rounded-md shadow-lg py-1"
405
409
  :style="{ left: rowMenuPos.x + 'px', top: rowMenuPos.y + 'px' }"
406
410
  @click.outside="rowMenuFor = null">
407
411
  <template x-if="group.kind === 'envelope'">
408
412
  <button type="button" :disabled="frozen" @click="rowMenuFor = null; toggleAutoEditor(field)"
409
413
  class="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2.5 cursor-pointer text-zinc-600 hover:bg-zinc-50 disabled:opacity-40">
410
- <svg class="w-3.5 h-3.5" :class="isAuto(field) && 'text-violet-600'"><use href="#i-percent"/></svg> Auto-fund…
414
+ <svg class="w-3.5 h-3.5" :class="isAuto(field) && 'text-violet-600 dark:text-violet-400'"><use href="#i-percent"/></svg> Auto-fund…
411
415
  </button>
412
416
  </template>
413
417
  <template x-if="group.kind === 'goal'">
414
418
  <button type="button" :disabled="frozen" @click="rowMenuFor = null; toggleSchedEditor(field)"
415
419
  class="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2.5 cursor-pointer text-zinc-600 hover:bg-zinc-50 disabled:opacity-40">
416
- <svg class="w-3.5 h-3.5" :class="field.dueKey && 'text-violet-600'"><use href="#i-clock"/></svg> Schedule…
420
+ <svg class="w-3.5 h-3.5" :class="field.dueKey && 'text-violet-600 dark:text-violet-400'"><use href="#i-clock"/></svg> Schedule…
417
421
  </button>
418
422
  </template>
419
423
  <button type="button" :disabled="offline" @click="rowMenuFor = null; toggleTagEditor(field)"
420
424
  class="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2.5 cursor-pointer text-zinc-600 hover:bg-zinc-50 disabled:opacity-40">
421
- <svg class="w-3.5 h-3.5" :class="field.tags && field.tags.length && 'text-emerald-600'"><use href="#i-tag"/></svg> Tags…
425
+ <svg class="w-3.5 h-3.5" :class="field.tags && field.tags.length && 'text-emerald-600 dark:text-emerald-400'"><use href="#i-tag"/></svg> Tags…
422
426
  </button>
423
427
  <button type="button" :disabled="frozen" @click="rowMenuFor = null; addField(group, fi)"
424
428
  class="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2.5 cursor-pointer text-zinc-600 hover:bg-zinc-50 disabled:opacity-40">
425
429
  <svg class="w-3.5 h-3.5"><use href="#i-plus"/></svg> Add a field below
426
430
  </button>
427
431
  <button type="button" :disabled="frozen" @click="rowMenuFor = null; removeField(group, field)"
428
- class="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2.5 cursor-pointer text-red-600 hover:bg-red-50 disabled:opacity-40">
432
+ class="w-full text-left px-3 py-2.5 text-sm flex items-center gap-2.5 cursor-pointer text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 disabled:opacity-40">
429
433
  <svg class="w-3.5 h-3.5"><use href="#i-xmark"/></svg> Remove field
430
434
  </button>
431
435
  </div>
@@ -436,20 +440,20 @@
436
440
  <div x-show="schedEditorFor === field.id" class="flex flex-wrap items-center gap-2 mt-1.5 pl-9 pr-20 text-[11px] text-zinc-400">
437
441
  <span class="select-none">due</span>
438
442
  <input type="month" x-model="field.dueKey" :disabled="frozen" title="When the full target is needed - the monthly deposit is derived from this."
439
- class="border border-zinc-200 rounded px-1.5 py-0.5 bg-white text-[11px] focus:outline-none focus:ring-1 focus:ring-violet-400">
443
+ class="border border-zinc-200 rounded px-1.5 py-0.5 bg-white dark:bg-zinc-100 dark:text-[#c7e0da] text-[11px] focus:outline-none focus:ring-1 focus:ring-violet-400">
440
444
  <span class="select-none">every</span>
441
445
  <input type="number" min="0" x-model.number="field.freqMonths" placeholder="12" :disabled="frozen"
442
446
  title="Months between due dates (12 = yearly). 0 or blank = one-time - the due date won’t advance."
443
- class="w-12 border border-zinc-200 rounded px-1.5 py-0.5 text-right bg-white text-[11px] focus:outline-none focus:ring-1 focus:ring-violet-400">
447
+ class="w-12 border border-zinc-200 rounded px-1.5 py-0.5 text-right bg-white dark:bg-zinc-100 text-[11px] dark:text-[#c7e0da] focus:outline-none focus:ring-1 focus:ring-violet-400">
444
448
  <span class="select-none">months</span>
445
- <span x-show="isSched(group, field)" class="text-violet-600 font-semibold tabular-nums select-none"
449
+ <span x-show="isSched(group, field)" class="text-violet-600 dark:text-violet-400 font-semibold tabular-nums select-none"
446
450
  x-text="'= ' + fmt(field.value) + ' this month'"></span>
447
451
  <button type="button" x-show="field.dueKey" @click="clearSchedule(field)" :disabled="frozen"
448
- class="text-zinc-400 hover:text-red-600 cursor-pointer">clear</button>
452
+ class="text-zinc-400 hover:text-red-600 dark:hover:text-red-400 cursor-pointer">clear</button>
449
453
  <template x-if="FinEngine.num(field.target) > 0">
450
454
  <div class="flex items-center gap-2 flex-1 min-w-40">
451
455
  <span class="text-[11px] whitespace-nowrap select-none"
452
- :class="progress(field).reached ? 'text-amber-600 font-bold' : 'text-zinc-400'"
456
+ :class="progress(field).reached ? 'text-amber-600 dark:text-amber-400 font-bold' : 'text-zinc-400'"
453
457
  x-text="progress(field).reached
454
458
  ? (contrib(group, field) > 0 ? '● funded' : '● at cap - contributing $0 until spent')
455
459
  : progress(field).pct + '% of ' + fmt(field.target)
@@ -463,7 +467,7 @@
463
467
  <span class="select-none">auto-fund</span>
464
468
  <input type="number" min="0" max="100" step="0.01" :value="field.auto ? field.auto.pct : ''" @input="setAutoPct(field, $event.target.value)" :disabled="frozen"
465
469
  title="Decimals allowed - e.g. 33.33 for one third"
466
- class="w-16 border border-zinc-200 rounded px-1.5 py-0.5 text-right bg-white focus:outline-none focus:ring-1 focus:ring-violet-400">
470
+ class="w-16 border border-zinc-200 rounded px-1.5 py-0.5 text-right dark:text-[#c7e0da] bg-white dark:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-violet-400">
467
471
  <span class="select-none">% of the net of</span>
468
472
  <template x-for="g in sourceGroups" :key="g.groupId">
469
473
  <label class="flex items-center gap-1 cursor-pointer bg-zinc-100 hover:bg-zinc-200 rounded-full px-2.5 py-1 select-none transition-colors">
@@ -473,18 +477,18 @@
473
477
  <span x-text="g.title"></span>
474
478
  </label>
475
479
  </template>
476
- <span x-show="isAuto(field)" class="text-violet-600 font-semibold tabular-nums select-none"
480
+ <span x-show="isAuto(field)" class="text-violet-600 dark:text-violet-400 font-semibold tabular-nums select-none"
477
481
  x-text="'= ' + fmt(rateOf(field)) + ' this month'"></span>
478
482
  <button type="button" x-show="field.auto" @click="clearAuto(field)" :disabled="frozen"
479
- class="text-zinc-400 hover:text-red-600 cursor-pointer">clear</button>
483
+ class="text-zinc-400 hover:text-red-600 dark:hover:text-red-400 cursor-pointer">clear</button>
480
484
  </div>
481
485
 
482
486
  <!-- tag editor -->
483
487
  <div x-show="tagEditorFor === field.id" class="flex flex-wrap items-center gap-1.5 mt-1.5 pl-9">
484
488
  <template x-for="tag in field.tags" :key="tag">
485
- <span class="inline-flex items-center gap-1 bg-emerald-100 text-emerald-700 text-[11px] px-2 py-0.5 rounded-full">
489
+ <span class="inline-flex items-center gap-1 bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 text-[11px] px-2 py-0.5 rounded-full">
486
490
  <span x-text="tag"></span>
487
- <button type="button" @click="removeTag(field, tag)" :disabled="offline" class="cursor-pointer hover:text-emerald-900 flex items-center"><svg class="w-2.5 h-2.5"><use href="#i-xmark"/></svg></button>
491
+ <button type="button" @click="removeTag(field, tag)" :disabled="offline" class="cursor-pointer hover:text-emerald-900 dark:hover:text-emerald-200 flex items-center"><svg class="w-2.5 h-2.5"><use href="#i-xmark"/></svg></button>
488
492
  </span>
489
493
  </template>
490
494
  <input type="text" x-model="newTag" placeholder="add tag ⏎" :disabled="offline"
@@ -492,19 +496,19 @@
492
496
  @focus="openTagSuggest($event)"
493
497
  @input="tagSuggestOpen || openTagSuggest($event)"
494
498
  @blur="tagSuggestOpen = false"
495
- class="w-28 border border-zinc-200 rounded-full px-2.5 py-0.5 text-[11px] bg-white focus:outline-none focus:ring-1 focus:ring-emerald-400">
499
+ class="w-28 border border-zinc-200 rounded-full px-2.5 py-0.5 text-[11px] dark:text-[#c7e0da] bg-white dark:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-400">
496
500
  <!-- suggestion dropdown: x-if (not x-show - a string :style
497
501
  would clobber x-show's display:none, and 100+ hidden
498
502
  copies would render buttons); fixed-positioned because
499
503
  the group card clips overflow; mousedown.prevent keeps
500
504
  the input focused so blur doesn't eat the click -->
501
505
  <template x-if="tagSuggestOpen && tagEditorFor === field.id && tagSuggestions(field).length > 0">
502
- <div class="tag-suggest fixed z-50 w-44 bg-white border border-zinc-200 rounded-md shadow-lg overflow-hidden"
506
+ <div class="tag-suggest fixed z-50 w-44 bg-white dark:bg-zinc-100 border border-zinc-200 rounded-md shadow-lg overflow-hidden"
503
507
  :style="{ left: tagSuggestPos.x + 'px', top: tagSuggestPos.y + 'px' }">
504
508
  <template x-for="(t, ti) in tagSuggestions(field)" :key="t">
505
509
  <button type="button" @mousedown.prevent="pickTag(field, t)"
506
510
  class="w-full text-left px-2.5 py-1.5 text-[11px] cursor-pointer flex items-center gap-1.5"
507
- :class="ti === tagSuggestIndex ? 'bg-emerald-100 text-emerald-800' : 'text-zinc-600 hover:bg-zinc-50'">
511
+ :class="ti === tagSuggestIndex ? 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-800 dark:text-emerald-300' : 'text-zinc-600 hover:bg-zinc-50'">
508
512
  <svg class="w-2.5 h-2.5 shrink-0 text-emerald-500"><use href="#i-tag"/></svg>
509
513
  <span x-text="t"></span>
510
514
  </button>
@@ -517,7 +521,7 @@
517
521
 
518
522
  <div x-show="!group.fields.length" class="text-center py-2">
519
523
  <button type="button" @click="addField(group)" :disabled="frozen"
520
- class="text-sm text-zinc-400 hover:text-emerald-600 cursor-pointer border border-dashed border-zinc-300 hover:border-emerald-400 rounded-md px-4 py-2 transition-colors inline-flex items-center gap-1.5"><svg class="w-3 h-3"><use href="#i-plus"/></svg>add the first field</button>
524
+ class="text-sm text-zinc-400 hover:text-emerald-600 dark:hover:text-emerald-400 cursor-pointer border border-dashed border-zinc-300 hover:border-emerald-400 rounded-md px-4 py-2 transition-colors inline-flex items-center gap-1.5"><svg class="w-3 h-3"><use href="#i-plus"/></svg>add the first field</button>
521
525
  </div>
522
526
  </div>
523
527
  </div>
@@ -530,45 +534,45 @@
530
534
  interleave with the groups by `order`: This Month +
531
535
  action buttons first, groups second, savings last -->
532
536
  <aside class="contents xl:block xl:w-96 xl:shrink-0 xl:space-y-5 xl:sticky xl:top-20">
533
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4 w-full hidden sm:block xl:w-auto order-3 xl:order-none">
537
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4 w-full hidden sm:block xl:w-auto order-3 xl:order-none">
534
538
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-1 select-none">Savings</h3>
535
539
  <!-- phones see charts in Insights; the monthly sparkline is desktop-only -->
536
540
  <div x-ref="savingsChart" class="-mx-2"></div>
537
- <div class="text-4xl font-black font-brand text-center text-zinc-800" x-text="fmt(currentSavings)"></div>
541
+ <div class="text-4xl font-black font-brand text-center text-zinc-800 dark:text-[#c7e0da]" x-text="fmt(currentSavings)"></div>
538
542
  <div class="text-center text-xs text-zinc-400 mb-3" x-text="fmt(withBudgets) + ' incl. unspent envelopes'"></div>
539
543
  <div class="flex items-center justify-between border-t border-zinc-100 pt-3">
540
544
  <span class="text-xs text-zinc-500 select-none">Savings at month start</span>
541
- <span class="text-sm font-semibold tabular-nums text-zinc-700 select-none"
545
+ <span class="text-sm font-semibold tabular-nums text-zinc-700 dark:text-[#c7e0da] select-none"
542
546
  title="Derived from the previous month’s close-out. The very first month starts from “Initial savings” in Settings."
543
547
  x-text="fmt(month.startingSavings)"></span>
544
548
  </div>
545
549
  </section>
546
550
 
547
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4 w-full xl:w-auto order-1 xl:order-none">
551
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4 w-full xl:w-auto order-1 xl:order-none">
548
552
  <div class="flex items-center justify-between mb-2">
549
553
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 select-none">This Month</h3>
550
554
  <span class="sm:hidden text-[10px] uppercase tracking-widest font-bold px-2.5 py-1 rounded-full select-none"
551
- :class="isClosed ? (unlocked ? 'bg-amber-100 text-amber-700' : 'bg-zinc-200 text-zinc-500') : 'bg-emerald-100 text-emerald-700'"
555
+ :class="isClosed ? (unlocked ? 'bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400' : 'bg-zinc-200 text-zinc-500') : 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400'"
552
556
  x-text="isClosed ? (unlocked ? 'closed · editing' : 'closed') : 'open'"></span>
553
557
  </div>
554
- <div class="text-4xl font-black font-brand text-center" :class="net >= 0 ? 'text-emerald-600' : 'text-red-600'" x-text="fmtSigned(net)"></div>
558
+ <div class="text-4xl font-black font-brand text-center" :class="net >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'" x-text="fmtSigned(net)"></div>
555
559
  </section>
556
560
 
557
561
  <div class="space-y-2 w-full xl:w-auto order-1 xl:order-none">
558
562
  <template x-if="!isClosed">
559
563
  <div class="space-y-2">
560
564
  <button type="button" @click="save()" :disabled="offline" title="Save (Ctrl+S) · Undo (Ctrl+Z) · Redo (Ctrl+Y)"
561
- class="w-full bg-emerald-600 hover:bg-emerald-500 border border-emerald-600 hover:border-emerald-500 text-white font-bold uppercase tracking-widest text-sm rounded-md px-5 py-3.5 cursor-pointer transition-colors disabled:bg-zinc-200 disabled:text-zinc-400"
565
+ class="w-full bg-emerald-600 dark:bg-emerald-400/70 dark:text-[#1b1e25] hover:bg-emerald-500 border border-emerald-600 hover:border-emerald-500 text-white font-bold uppercase tracking-widest text-sm rounded-md px-5 py-3.5 cursor-pointer transition-colors disabled:bg-zinc-200 disabled:text-zinc-400"
562
566
  x-text="isDirty ? 'Update This Month ●' : 'Update This Month'"></button>
563
567
  <button type="button" x-show="isLatest" @click="closeOut()" :disabled="offline"
564
- class="w-full bg-white border border-zinc-300 hover:border-emerald-500 hover:text-emerald-700 text-zinc-600 font-bold uppercase tracking-widest text-sm rounded-md px-5 py-3.5 cursor-pointer transition-colors disabled:text-zinc-300 disabled:border-zinc-200">Close Out This Month</button>
568
+ class="w-full bg-white dark:bg-zinc-100 border border-zinc-300 dark:border-[#c7e0da]/20 hover:border-emerald-500 hover:text-emerald-700 dark:hover:text-emerald-400/90 text-zinc-600 dark:text-[#c7e0da]/90 font-bold uppercase tracking-widest text-sm rounded-md px-5 py-3.5 cursor-pointer transition-colors disabled:text-zinc-300 disabled:border-zinc-200">Close Out This Month</button>
565
569
  </div>
566
570
  </template>
567
571
  <template x-if="readonly">
568
572
  <div class="bg-zinc-100 border border-zinc-200 rounded-md p-4 text-center">
569
573
  <p class="text-sm text-zinc-500 mb-3">This month is closed. Editing it will recompute every later month.</p>
570
574
  <button type="button" @click="unlock()" :disabled="offline"
571
- class="bg-white border border-zinc-300 hover:border-amber-500 hover:text-amber-700 text-zinc-600 font-bold uppercase tracking-widest text-xs rounded-md px-4 py-2.5 cursor-pointer transition-colors disabled:text-zinc-300">Edit this month</button>
575
+ class="bg-white dark:bg-zinc-100 border border-zinc-300 hover:border-amber-500 hover:text-amber-700 dark:hover:text-amber-400 text-zinc-600 font-bold uppercase tracking-widest text-xs rounded-md px-4 py-2.5 cursor-pointer transition-colors disabled:text-zinc-300">Edit this month</button>
572
576
  </div>
573
577
  </template>
574
578
  <template x-if="isClosed && unlocked">
@@ -576,7 +580,7 @@
576
580
  <button type="button" @click="save()" :disabled="offline"
577
581
  class="w-full bg-amber-500 hover:bg-amber-400 border border-amber-500 hover:border-amber-400 text-white font-bold uppercase tracking-widest text-sm rounded-md px-5 py-3.5 cursor-pointer transition-colors disabled:bg-zinc-200 disabled:text-zinc-400">Save &amp; Recompute Forward</button>
578
582
  <button type="button" @click="cancelEditing()"
579
- class="w-full bg-white border border-zinc-300 hover:border-zinc-500 hover:text-zinc-700 text-zinc-600 font-bold uppercase tracking-widest text-sm rounded-md px-5 py-3.5 cursor-pointer transition-colors disabled:text-zinc-300 disabled:border-zinc-200">Cancel editing</button>
583
+ class="w-full bg-white dark:bg-zinc-100 border border-zinc-300 hover:border-zinc-500 hover:text-zinc-700 text-zinc-600 font-bold uppercase tracking-widest text-sm rounded-md px-5 py-3.5 cursor-pointer transition-colors disabled:text-zinc-300 disabled:border-zinc-200">Cancel editing</button>
580
584
  </div>
581
585
  </template>
582
586
  <div class="mt-4 uppercase font-bold text-xs tracking-widest text-zinc-400 flex justify-between opacity-60"><span>Save <kbd class="border border-zinc-300 rounded-sm py-px px-1">Ctrl+S</kbd></span><span>Undo <kbd class="border border-zinc-300 rounded-sm py-px px-1">Ctrl+Z</kbd></span><span>Redo <kbd class="border border-zinc-300 rounded-sm py-px px-1">Ctrl+Shift+Z</kbd></span></div>
@@ -598,27 +602,27 @@
598
602
  <div>
599
603
 
600
604
  <div class="flex items-center gap-2 mb-6">
601
- <h2 class="text-lg font-black font-brand text-zinc-700 mr-4 select-none">Insights</h2>
605
+ <h2 class="text-lg font-black font-brand text-zinc-700 dark:text-[#c7e0da] mr-4 leading-[32px] select-none">Insights</h2>
602
606
  <template x-for="r in [['12mo','Last 12 months'],['ytd','This year'],['all','All time']]" :key="r[0]">
603
607
  <button type="button" @click="range = r[0]" x-text="r[1]"
604
- class="px-3 py-1.5 rounded-md text-sm font-semibold cursor-pointer transition-colors"
605
- :class="range === r[0] ? 'bg-emerald-600 text-white' : 'bg-white border border-zinc-200 text-zinc-500 hover:bg-zinc-100'"></button>
608
+ class="px-3 py-1 rounded-md text-sm font-semibold cursor-pointer transition-colors border"
609
+ :class="range === r[0] ? 'bg-emerald-600 border-emerald-600 text-white dark:bg-emerald-400/70 dark:text-[#1b1e25]' : 'bg-white dark:bg-zinc-100 border-zinc-200 text-zinc-500 hover:bg-zinc-100'"></button>
606
610
  </template>
607
611
  </div>
608
612
 
609
613
  <div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
610
- <template x-for="card in [['Avg monthly income', stats().avgIncome, 'text-emerald-600'], ['Avg monthly spend', stats().avgSpend, 'text-red-500'], ['Avg monthly net', stats().avgNet, stats().avgNet >= 0 ? 'text-emerald-600' : 'text-red-500'], ['Months in range', stats().months, 'text-zinc-700']]">
611
- <div class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4">
614
+ <template x-for="card in [['Avg monthly income', stats().avgIncome, 'text-emerald-600 dark:text-emerald-400'], ['Avg monthly spend', stats().avgSpend, 'text-red-500 dark:text-red-400'], ['Avg monthly net', stats().avgNet, stats().avgNet >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'], ['Months in range', stats().months, 'text-zinc-700']]">
615
+ <div class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4">
612
616
  <div class="text-[11px] uppercase tracking-widest font-bold text-zinc-400 select-none" x-text="card[0]"></div>
613
617
  <div class="text-2xl font-black font-brand mt-1" :class="card[2]" x-text="card[0] === 'Months in range' ? card[1] : fmt(card[1])"></div>
614
618
  </div>
615
619
  </template>
616
620
  </div>
617
621
 
618
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4 mb-6">
622
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4 mb-6">
619
623
  <div class="flex items-baseline justify-between mb-3">
620
624
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 select-none">Savings over time &amp; projection</h3>
621
- <span x-show="projected12 !== null" class="text-xs font-semibold text-violet-600 select-none"
625
+ <span x-show="projected12 !== null" class="text-xs font-semibold text-violet-600 dark:text-violet-400 select-none"
622
626
  title="Linear trend of the selected timespan, projected forward"
623
627
  x-text="'≈ ' + fmt(projected12) + ' in 12 months at the current trend'"></span>
624
628
  </div>
@@ -626,16 +630,16 @@
626
630
  </section>
627
631
 
628
632
  <div class="grid lg:grid-cols-2 gap-6 mb-6">
629
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4">
633
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4">
630
634
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-3 select-none">Spend by tag</h3>
631
635
  <!-- charts mount by id, not x-ref: $refs go stale across x-if re-creation -->
632
636
  <div id="insights-chart-donut"></div>
633
637
  <p x-show="!tagRows().length" class="text-center text-zinc-400 text-sm py-10">
634
- No spending in this range yet. Tag fields with the <span class="bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded inline-flex items-center align-middle"><svg class="w-3 h-3"><use href="#i-tag"/></svg></span> button -
638
+ No spending in this range yet. Tag fields with the <span class="bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 px-1.5 py-0.5 rounded inline-flex items-center align-middle"><svg class="w-3 h-3"><use href="#i-tag"/></svg></span> button -
635
639
  tagging a budget also covers the expenses that draw from it.
636
640
  </p>
637
641
  </section>
638
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4">
642
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4">
639
643
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-3 select-none">Monthly spend, top tags</h3>
640
644
  <div id="insights-chart-bar"></div>
641
645
  </section>
@@ -644,7 +648,7 @@
644
648
  <!-- income streams: only rendered content when tagged income
645
649
  exists in range - untagged income gets no insights -->
646
650
  <div id="insights-income-block" x-show="incomeRows().length" class="grid lg:grid-cols-2 gap-6 mb-6">
647
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4">
651
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4">
648
652
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-3 select-none">Income by tag</h3>
649
653
  <div id="insights-chart-income-donut"></div>
650
654
  <table class="w-full text-sm mt-3">
@@ -659,7 +663,7 @@
659
663
  <tbody>
660
664
  <template x-for="row in incomeRows()" :key="row.tag">
661
665
  <tr class="border-t border-zinc-100">
662
- <td class="py-1.5"><span class="bg-emerald-100 text-emerald-700 text-[11px] px-2 py-0.5 rounded-full" x-text="row.tag"></span></td>
666
+ <td class="py-1.5"><span class="bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 text-[11px] px-2 py-0.5 rounded-full" x-text="row.tag"></span></td>
663
667
  <td class="py-1.5 text-right font-semibold" x-text="fmt(row.total)"></td>
664
668
  <td class="py-1.5 text-right text-zinc-500" x-text="fmt(row.avg)"></td>
665
669
  <td class="py-1.5 text-right text-zinc-400" x-text="row.pct + '%'"></td>
@@ -668,14 +672,14 @@
668
672
  </tbody>
669
673
  </table>
670
674
  </section>
671
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4">
675
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4">
672
676
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-3 select-none">Income streams over time</h3>
673
677
  <div id="insights-chart-income-bar"></div>
674
678
  </section>
675
679
  </div>
676
680
 
677
681
  <div class="grid lg:grid-cols-2 gap-6">
678
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4">
682
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4">
679
683
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-3 select-none">Tag breakdown</h3>
680
684
  <table class="w-full text-sm">
681
685
  <thead>
@@ -689,7 +693,7 @@
689
693
  <tbody>
690
694
  <template x-for="row in tagRows()" :key="row.tag">
691
695
  <tr class="border-t border-zinc-100">
692
- <td class="py-1.5"><span class="bg-emerald-100 text-emerald-700 text-[11px] px-2 py-0.5 rounded-full" x-text="row.tag"></span></td>
696
+ <td class="py-1.5"><span class="bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 text-[11px] px-2 py-0.5 rounded-full" x-text="row.tag"></span></td>
693
697
  <td class="py-1.5 text-right font-semibold" x-text="fmt(row.total)"></td>
694
698
  <td class="py-1.5 text-right text-zinc-500" x-text="fmt(row.avg)"></td>
695
699
  <td class="py-1.5 text-right text-zinc-400" x-text="row.pct + '%'"></td>
@@ -700,17 +704,17 @@
700
704
  </table>
701
705
  </section>
702
706
 
703
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-4">
707
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-4">
704
708
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-3 select-none">Envelope health (latest month)</h3>
705
709
  <div class="space-y-3">
706
710
  <template x-for="env in envelopes()" :key="env.label + env.kind">
707
711
  <div>
708
712
  <div class="flex justify-between text-sm mb-1">
709
713
  <span class="font-semibold text-zinc-700" x-text="env.label"></span>
710
- <span :class="env.left < 0 ? 'text-red-600 font-bold' : 'text-zinc-500'"
714
+ <span :class="env.left < 0 ? 'text-red-600 dark:text-red-400 font-bold' : 'text-zinc-500'"
711
715
  x-text="fmt(env.spent) + ' spent of ' + fmt(env.avail)"></span>
712
716
  </div>
713
- <div class="w-full bg-zinc-100 rounded-full h-2 overflow-hidden">
717
+ <div class="w-full bg-zinc-100 dark:bg-zinc-200 rounded-full h-2 overflow-hidden">
714
718
  <div class="h-full rounded-full" :class="env.left < 0 ? 'bg-red-500' : (env.progress && env.progress.reached ? 'bg-amber-400' : 'bg-emerald-500')"
715
719
  :style="'width:' + Math.min(100, env.avail > 0 ? Math.round(env.spent / env.avail * 100) : (env.spent > 0 ? 100 : 0)) + '%'"></div>
716
720
  </div>
@@ -729,11 +733,11 @@
729
733
  <div x-data="settingsView(true)" x-show="$store.ui.view === 'settings'" @keydown.window="hotkeys($event)">
730
734
  <template x-if="$store.ui.view === 'settings'">
731
735
  <div>
732
- <h2 class="text-lg font-black font-brand text-zinc-700 mb-6 select-none">Settings</h2>
736
+ <h2 class="text-lg font-black font-brand text-zinc-700 dark:text-[#c7e0da] mb-6 leading-[32px] select-none">Settings</h2>
733
737
 
734
738
  <div class="grid lg:grid-cols-2 gap-6 items-start">
735
739
  <!-- groups -->
736
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-5">
740
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-5">
737
741
  <template x-if="draft">
738
742
  <div class="flex items-center justify-between gap-4 mb-5 pb-5 border-b border-zinc-100">
739
743
  <div>
@@ -744,7 +748,7 @@
744
748
  <div class="relative shrink-0">
745
749
  <span class="absolute left-2 top-1/2 -translate-y-1/2 text-zinc-400 text-xs select-none">$</span>
746
750
  <input type="number" x-model.number="draft.initialSavings"
747
- class="w-32 border border-zinc-200 rounded pl-5 pr-1 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500"
751
+ class="w-32 border border-zinc-200 rounded pl-5 pr-1 py-1.5 text-sm dark:text-[#c7e0da] text-right bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500"
748
752
  :class="initialUnlocked && 'ring-1 ring-amber-400'">
749
753
  </div>
750
754
  </template>
@@ -753,7 +757,7 @@
753
757
  <span class="text-sm font-semibold tabular-nums text-zinc-700 select-none"
754
758
  title="Locked once you have more than one month of history" x-text="fmt(draft.initialSavings)"></span>
755
759
  <button type="button" @click="unlockInitial()"
756
- class="text-xs border border-zinc-300 rounded px-2.5 py-1 text-zinc-500 hover:border-amber-500 hover:text-amber-700 cursor-pointer transition-colors">edit…</button>
760
+ class="text-xs border border-zinc-300 rounded px-2.5 py-1 text-zinc-500 hover:border-amber-500 hover:text-amber-700 dark:hover:text-amber-400 cursor-pointer transition-colors">edit…</button>
757
761
  </div>
758
762
  </template>
759
763
  </div>
@@ -768,14 +772,14 @@
768
772
  <div class="border border-zinc-200 rounded-md p-3 mb-2.5 bg-zinc-50">
769
773
  <div class="flex items-center gap-2">
770
774
  <div class="flex flex-col shrink-0">
771
- <button type="button" @click="move(gi, -1)" class="text-zinc-300 hover:text-zinc-600 cursor-pointer leading-none text-xs" :class="gi === 0 && 'opacity-0 pointer-events-none'">▲</button>
772
- <button type="button" @click="move(gi, 1)" class="text-zinc-300 hover:text-zinc-600 cursor-pointer leading-none text-xs" :class="gi === draft.groups.length - 1 && 'opacity-0 pointer-events-none'">▼</button>
775
+ <button type="button" @click="move(gi, -1)" class="text-zinc-300 hover:text-zinc-600 cursor-pointer leading-none text-xs mb-1" :class="gi === 0 && 'opacity-0 pointer-events-none'">▲</button>
776
+ <button type="button" @click="move(gi, 1)" class="text-zinc-300 hover:text-zinc-600 cursor-pointer leading-none text-xs mt-1" :class="gi === draft.groups.length - 1 && 'opacity-0 pointer-events-none'">▼</button>
773
777
  </div>
774
778
  <input type="text" x-model="group.title"
775
- class="flex-1 min-w-0 border border-zinc-200 rounded px-2.5 py-1.5 text-sm font-semibold bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500">
776
- <span class="text-xs text-zinc-400 shrink-0 select-none" x-text="kindLabel(group.kind)"></span>
779
+ class="flex-1 min-w-0 max-w-[calc(100%-177px)] border border-zinc-200 rounded px-2.5 py-1.5 text-sm dark:text-[#c7e0da] font-semibold bg-white dark:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500">
780
+ <span class="text-xs text-zinc-400 shrink-0 min-w-28 select-none" x-text="kindLabel(group.kind)"></span>
777
781
  <button type="button" @click="removeGroup(group)" title="Remove group"
778
- class="w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer bg-white border border-zinc-200 text-zinc-400 hover:bg-red-100 hover:text-red-600 transition-colors"><svg class="w-3.5 h-3.5"><use href="#i-xmark"/></svg></button>
782
+ class="w-7 h-7 shrink-0 rounded flex items-center justify-center cursor-pointer bg-white dark:bg-zinc-100 border border-zinc-200 text-zinc-400 hover:bg-red-100 dark:hover:bg-red-500/15 hover:text-red-600 dark:hover:text-red-400 transition-colors"><svg class="w-3.5 h-3.5"><use href="#i-xmark"/></svg></button>
779
783
  </div>
780
784
  <label x-show="group.kind === 'envelope'" class="flex items-center gap-2 mt-2.5 ml-6 cursor-pointer select-none">
781
785
  <input type="checkbox" x-model="group.strictOverspend" class="accent-emerald-600">
@@ -787,13 +791,13 @@
787
791
  <div class="flex flex-wrap gap-2 mt-4">
788
792
  <template x-for="k in [['income','+ Income'],['envelope','+ Envelope budgets'],['goal','+ Goal budgets'],['expense','+ Expenses']]" :key="k[0]">
789
793
  <button type="button" @click="addGroup(k[0])" x-text="k[1]"
790
- class="text-xs border border-dashed border-zinc-300 hover:border-emerald-400 hover:text-emerald-700 text-zinc-500 rounded-md px-3 py-1.5 cursor-pointer transition-colors"></button>
794
+ class="text-xs border border-dashed border-zinc-300 hover:border-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-400 text-zinc-500 rounded-md px-3 py-1.5 cursor-pointer transition-colors"></button>
791
795
  </template>
792
796
  </div>
793
797
 
794
798
  <div class="mt-5 flex items-center gap-3">
795
799
  <button type="button" @click="save()" :disabled="!dirty || $store.ui.offline"
796
- class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold uppercase tracking-widest text-xs rounded-md px-5 py-2.5 cursor-pointer transition-colors disabled:bg-zinc-200 disabled:text-zinc-400 disabled:cursor-default">Save settings</button>
800
+ class="hover:bg-emerald-500 text-white dark:bg-emerald-400/70 dark:text-[#1b1e25] font-bold uppercase tracking-widest text-xs rounded-md px-5 py-2.5 cursor-pointer transition-colors disabled:bg-zinc-200 dark:disabled:bg-zinc-200 disabled:text-zinc-400 disabled:cursor-default">Save settings</button>
797
801
  <button type="button" x-show="dirty" @click="reset()"
798
802
  class="text-xs text-zinc-400 hover:text-zinc-600 cursor-pointer">discard changes</button>
799
803
  </div>
@@ -807,8 +811,22 @@
807
811
  flex gap (not space-y) so the gap collapses when the access
808
812
  pane is display:none in web mode. -->
809
813
  <div class="flex flex-col gap-6">
814
+ <!-- appearance: per-device theme (localStorage, via window.Theme) -->
815
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-5" x-data="{ theme: window.Theme.get() }">
816
+ <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-1 select-none">Appearance</h3>
817
+ <p class="text-xs text-zinc-400 mb-4 leading-relaxed">Pick a colour theme for this device. <span class="font-semibold">System</span> follows your device’s light or dark setting.</p>
818
+ <div class="inline-flex rounded-md border border-zinc-300 overflow-hidden select-none">
819
+ <template x-for="opt in [['system','System'],['light','Light'],['dark','Dark']]" :key="opt[0]">
820
+ <button type="button" @click="theme = opt[0]; window.Theme.set(opt[0])"
821
+ class="px-4 py-1.5 text-xs font-semibold cursor-pointer transition-colors border-l border-zinc-300 first-of-type:border-l-0"
822
+ :class="theme === opt[0] ? 'bg-emerald-600 dark:bg-emerald-400/70 text-white dark:text-[#1b1e25]' : 'text-zinc-500 hover:bg-zinc-100'"
823
+ x-text="opt[1]"></button>
824
+ </template>
825
+ </div>
826
+ </section>
827
+
810
828
  <!-- data management -->
811
- <section class="bg-white rounded-lg border border-zinc-200 shadow-sm p-5">
829
+ <section class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-5">
812
830
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-4 select-none">Data</h3>
813
831
 
814
832
  <div class="text-xs text-zinc-500 mb-4 break-all">
@@ -818,10 +836,10 @@
818
836
  </div>
819
837
 
820
838
  <div class="flex flex-wrap gap-2">
821
- <button type="button" @click="changeLocation()" class="text-xs border border-zinc-300 hover:bg-zinc-100 text-zinc-600 rounded-md px-3 py-2 cursor-pointer transition-colors">Move / share database…</button>
822
- <button type="button" @click="exportDb()" class="text-xs border border-zinc-300 hover:bg-zinc-100 text-zinc-600 rounded-md px-3 py-2 cursor-pointer transition-colors">Export a copy…</button>
839
+ <button x-show="!isWeb" type="button" @click="changeLocation()" class="text-xs border border-zinc-300 hover:bg-zinc-100 text-zinc-600 rounded-md px-3 py-2 cursor-pointer transition-colors">Move / share database…</button>
840
+ <button x-show="!isWeb" type="button" @click="exportDb()" class="text-xs border border-zinc-300 hover:bg-zinc-100 text-zinc-600 rounded-md px-3 py-2 cursor-pointer transition-colors">Export a copy…</button>
823
841
  <button type="button" @click="loadBackups()" class="text-xs border border-zinc-300 hover:bg-zinc-100 text-zinc-600 rounded-md px-3 py-2 cursor-pointer transition-colors">Backups…</button>
824
- <button type="button" @click="importLegacy()" class="text-xs border border-amber-300 text-amber-700 hover:bg-amber-50 rounded-md px-3 py-2 cursor-pointer transition-colors">Import legacy data…</button>
842
+ <button type="button" @click="importLegacy()" class="text-xs border border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-400 hover:bg-amber-50 dark:hover:bg-amber-500/10 rounded-md px-3 py-2 cursor-pointer transition-colors">Import legacy data…</button>
825
843
  </div>
826
844
 
827
845
  <p class="text-[11px] text-zinc-400 mt-4 leading-relaxed">
@@ -847,12 +865,12 @@
847
865
  </div>
848
866
 
849
867
  <!-- an update exists -->
850
- <div x-show="$store.data.update.available" class="mt-3 flex flex-wrap items-center gap-2 rounded-md bg-emerald-50 border border-emerald-200 px-3 py-2">
851
- <span class="text-xs text-emerald-800 select-none">
868
+ <div x-show="$store.data.update.available" class="mt-3 flex flex-wrap items-center gap-2 rounded-md bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/30 px-3 py-2">
869
+ <span class="text-xs text-emerald-800 dark:text-emerald-300 select-none">
852
870
  Version <span class="font-semibold" x-text="$store.data.update.latest"></span> is available.
853
871
  </span>
854
872
  <button type="button" @click="openReleases()"
855
- class="text-xs font-semibold bg-emerald-600 hover:bg-emerald-700 text-white rounded px-2.5 py-1 cursor-pointer transition-colors">Release notes</button>
873
+ class="text-xs font-semibold bg-emerald-600 hover:bg-emerald-700 text-white dark:text-[#1b1e25] rounded px-2.5 py-1 cursor-pointer transition-colors">Release notes</button>
856
874
  </div>
857
875
 
858
876
  <!-- up to date / offline / off: quiet one-liners -->
@@ -865,14 +883,14 @@
865
883
  </p>
866
884
  </div>
867
885
 
868
- <div class="text-[11px] text-zinc-400 mt-4 pt-4 border-t border-zinc-100 select-none"
886
+ <div class="text-[11px] text-zinc-400 mt-4 pt-4.5 border-t border-zinc-100 dark:border-[#c7e0da]/10 select-none"
869
887
  title="Application version - if this is older than your latest install, the window is running stale code">
870
- Spend Wise · v<span class="font-semibold" x-text="$store.data.version || 'dev'"></span> · by Michael Niles
888
+ <span class="font-brand text-sm inline-block -mb-1">Spend Wise</span> v<span class="font-semibold" x-text="$store.data.version || 'dev'"></span> · by Michael Niles
871
889
  </div>
872
890
  </section>
873
891
 
874
892
  <!-- privacy & access -->
875
- <section x-show="!isWeb" class="bg-white rounded-lg border border-zinc-200 shadow-sm p-5">
893
+ <section x-show="!isWeb" class="bg-white dark:bg-zinc-100 rounded-lg border border-zinc-200 shadow-sm p-5">
876
894
  <h3 class="uppercase font-bold tracking-widest text-xs text-zinc-400 mb-1 select-none">App password</h3>
877
895
  <p class="text-xs text-zinc-400 mb-4 leading-relaxed">
878
896
  Locks the app on launch - casual privacy on a shared computer - and is the login for web
@@ -883,23 +901,23 @@
883
901
  <label x-show="hasPassword" class="block">
884
902
  <span class="text-[10px] uppercase tracking-widest font-bold text-zinc-400 block mb-1 select-none">Current</span>
885
903
  <input type="password" x-model="pwCurrent" autocomplete="current-password"
886
- class="w-36 border border-zinc-200 rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500">
904
+ class="w-36 border border-zinc-200 rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500">
887
905
  </label>
888
906
  <label class="block">
889
907
  <span class="text-[10px] uppercase tracking-widest font-bold text-zinc-400 block mb-1 select-none" x-text="hasPassword ? 'New' : 'Password'"></span>
890
908
  <input type="password" x-model="pwNew" autocomplete="new-password"
891
- class="w-36 border border-zinc-200 rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500">
909
+ class="w-36 border border-zinc-200 rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500">
892
910
  </label>
893
911
  <label class="block">
894
912
  <span class="text-[10px] uppercase tracking-widest font-bold text-zinc-400 block mb-1 select-none">Confirm</span>
895
913
  <input type="password" x-model="pwConfirm" autocomplete="new-password" @keydown.enter="setPassword()"
896
- class="w-36 border border-zinc-200 rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500">
914
+ class="w-36 border border-zinc-200 rounded px-2.5 py-1.5 text-sm bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500">
897
915
  </label>
898
916
  <button type="button" @click="setPassword()"
899
917
  class="text-xs border border-zinc-300 hover:bg-zinc-100 text-zinc-600 rounded-md px-3 py-2 cursor-pointer transition-colors"
900
918
  x-text="hasPassword ? 'Change password' : 'Set password'"></button>
901
919
  <button type="button" x-show="hasPassword" @click="removePassword()"
902
- class="text-xs border border-red-200 text-red-600 hover:bg-red-50 rounded-md px-3 py-2 cursor-pointer transition-colors">Remove…</button>
920
+ class="text-xs border border-red-200 dark:border-red-500/30 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-md px-3 py-2 cursor-pointer transition-colors">Remove…</button>
903
921
  </div>
904
922
 
905
923
  <template x-if="web.loaded">
@@ -914,14 +932,14 @@
914
932
  <div class="flex flex-wrap items-center gap-3">
915
933
  <button type="button" @click="toggleWeb()"
916
934
  class="text-xs font-bold rounded-md px-4 py-2 cursor-pointer transition-colors"
917
- :class="web.enabled ? 'bg-emerald-600 hover:bg-emerald-500 text-white' : 'border border-zinc-300 hover:bg-zinc-100 text-zinc-600'"
935
+ :class="web.enabled ? 'hover:bg-emerald-500 text-white dark:text-[#1b1e25]' : 'border border-zinc-300 hover:bg-zinc-100 text-zinc-600'"
918
936
  x-text="web.enabled ? 'Web access: on' : 'Turn on web access'"></button>
919
937
  <label class="flex items-center gap-1.5 text-xs text-zinc-500">
920
938
  port
921
939
  <input type="number" min="1024" max="65535" x-model.number="web.port" @change="applyWebPort()"
922
- class="w-20 border border-zinc-200 rounded px-2 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500">
940
+ class="w-20 border border-zinc-200 rounded px-2 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500">
923
941
  </label>
924
- <span x-show="web.enabled && web.running" class="text-[10px] uppercase tracking-widest font-bold px-2.5 py-1 rounded-full bg-emerald-100 text-emerald-700 select-none">serving</span>
942
+ <span x-show="web.enabled && web.running" class="text-[10px] uppercase tracking-widest font-bold px-2.5 py-1 rounded-full bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400 select-none">serving</span>
925
943
  </div>
926
944
  <div x-show="web.running" class="mt-3 text-xs text-zinc-500 space-y-0.5">
927
945
  <template x-for="url in web.urls" :key="url">
@@ -941,11 +959,11 @@
941
959
  <label class="flex items-center gap-1.5 text-xs text-zinc-500 pl-3">
942
960
  trusted proxies in front
943
961
  <input type="number" min="0" max="10" x-model.number="web.trustProxy" @change="applyWebProxy()"
944
- class="w-16 border border-zinc-200 rounded px-2 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white focus:outline-none focus:ring-1 focus:ring-emerald-500">
962
+ class="w-16 border border-zinc-200 rounded px-2 py-1.5 text-sm text-right bg-zinc-50 focus:bg-white dark:focus:bg-zinc-100 focus:outline-none focus:ring-1 focus:ring-emerald-500">
945
963
  </label>
946
964
  <label class="flex items-center gap-2 text-xs text-zinc-500 pl-3 cursor-pointer">
947
965
  <input type="checkbox" x-model="web.secureCookie" @change="applyWebProxy()"
948
- class="rounded border-zinc-300 text-emerald-600 focus:ring-emerald-500">
966
+ class="rounded border-zinc-300 text-emerald-600 dark:text-emerald-400 focus:ring-emerald-500">
949
967
  force Secure cookies (auto-detected from the proxy when hops &gt; 0)
950
968
  </label>
951
969
  </div>
@@ -986,7 +1004,7 @@
986
1004
  </div>
987
1005
  <div class="mt-5 text-right">
988
1006
  <button type="button" @click="$store.ui.modals.recompute = false"
989
- class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold uppercase tracking-widest text-xs rounded-md px-5 py-2.5 cursor-pointer transition-colors">Got it</button>
1007
+ class="hover:bg-emerald-500 text-white dark:text-[#1b1e25] font-bold uppercase tracking-widest text-xs rounded-md px-5 py-2.5 cursor-pointer transition-colors">Got it</button>
990
1008
  </div>
991
1009
  </x-modal>
992
1010
 
@@ -1000,7 +1018,7 @@
1000
1018
  <div class="flex items-center justify-between gap-3 border border-zinc-200 rounded-md px-3 py-2 bg-zinc-50">
1001
1019
  <span class="text-sm font-semibold text-zinc-700 truncate" x-text="g.title"></span>
1002
1020
  <select x-model="g.kind"
1003
- class="shrink-0 text-xs border border-zinc-200 rounded py-1.5 px-1 bg-white cursor-pointer focus:outline-none focus:ring-1 focus:ring-emerald-500">
1021
+ class="shrink-0 text-xs border border-zinc-200 rounded py-1.5 px-1 bg-white dark:bg-zinc-100 cursor-pointer focus:outline-none focus:ring-1 focus:ring-emerald-500">
1004
1022
  <option value="income">Income (+)</option>
1005
1023
  <option value="expense">Expenses (−)</option>
1006
1024
  <option value="envelope">Envelope budgets (−)</option>
@@ -1013,7 +1031,7 @@
1013
1031
  <button type="button" @click="$store.ui.modals.importMap = false"
1014
1032
  class="bg-zinc-100 hover:bg-zinc-200 text-zinc-600 font-bold uppercase tracking-widest text-xs rounded-md px-4 py-2.5 cursor-pointer transition-colors">Cancel</button>
1015
1033
  <button type="button" @click="runImport()"
1016
- class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold uppercase tracking-widest text-xs rounded-md px-4 py-2.5 cursor-pointer transition-colors">Import</button>
1034
+ class="hover:bg-emerald-500 text-white dark:text-[#1b1e25] font-bold uppercase tracking-widest text-xs rounded-md px-4 py-2.5 cursor-pointer transition-colors">Import</button>
1017
1035
  </div>
1018
1036
  </div>
1019
1037
  </x-modal>
@@ -1027,13 +1045,13 @@
1027
1045
  <span x-text="FinEngine.keyLabel($store.ui.importSummary.firstMonth)"></span> through
1028
1046
  <span x-text="FinEngine.keyLabel($store.ui.importSummary.lastMonth)"></span>.</p>
1029
1047
  <p>Groups: <span x-text="$store.ui.importSummary.groups.join(', ')"></span></p>
1030
- <p x-show="$store.ui.importSummary.warnings.length" class="text-amber-600 text-xs"
1048
+ <p x-show="$store.ui.importSummary.warnings.length" class="text-amber-600 dark:text-amber-400 text-xs"
1031
1049
  x-text="$store.ui.importSummary.warnings.join('; ')"></p>
1032
1050
  </div>
1033
1051
  </template>
1034
1052
  <div class="mt-5 text-right">
1035
1053
  <button type="button" @click="$store.ui.modals.importSummary = false"
1036
- class="bg-emerald-600 hover:bg-emerald-500 text-white font-bold uppercase tracking-widest text-xs rounded-md px-5 py-2.5 cursor-pointer transition-colors">Nice</button>
1054
+ class="hover:bg-emerald-500 text-white dark:text-[#1b1e25] font-bold uppercase tracking-widest text-xs rounded-md px-5 py-2.5 cursor-pointer transition-colors">Nice</button>
1037
1055
  </div>
1038
1056
  </x-modal>
1039
1057
 
@@ -1050,13 +1068,13 @@
1050
1068
  <div class="text-[11px] text-zinc-400" x-text="new Date(b.mtime).toLocaleString() + ' · ' + Math.round(b.size / 1024) + ' KB'"></div>
1051
1069
  </div>
1052
1070
  <button type="button" @click="restoreBackup(b)"
1053
- class="text-[11px] shrink-0 border border-zinc-300 hover:border-amber-500 hover:text-amber-700 text-zinc-500 rounded px-2.5 py-1.5 cursor-pointer transition-colors">Restore</button>
1071
+ class="text-[11px] shrink-0 border border-zinc-300 hover:border-amber-500 hover:text-amber-700 dark:hover:text-amber-400 text-zinc-500 rounded px-2.5 py-1.5 cursor-pointer transition-colors">Restore</button>
1054
1072
  </div>
1055
1073
  </template>
1056
1074
  <p x-show="!backups.length" class="text-center text-zinc-400 text-sm py-6">No backups yet.</p>
1057
1075
  </div>
1058
- <div class="mt-4 flex justify-between">
1059
- <button type="button" @click="openBackupsFolder()" class="text-xs text-zinc-400 hover:text-zinc-600 cursor-pointer">open folder</button>
1076
+ <div class="mt-4 flex" :class="isWeb ? 'justify-end' : 'justify-between'">
1077
+ <button type="button" x-show="!isWeb" @click="openBackupsFolder()" class="text-xs text-zinc-400 hover:text-zinc-600 cursor-pointer">open folder</button>
1060
1078
  <button type="button" @click="$store.ui.modals.backups = false"
1061
1079
  class="bg-zinc-200 hover:bg-zinc-300 text-zinc-600 font-bold uppercase tracking-widest text-xs rounded-md px-5 py-2.5 cursor-pointer transition-colors">Close</button>
1062
1080
  </div>
@@ -1067,10 +1085,10 @@
1067
1085
  <div x-show="$store.ui.confirm.open" x-transition.opacity.duration.150ms
1068
1086
  class="fixed inset-0 z-50 flex items-center justify-center"
1069
1087
  @keydown.escape.window="$store.ui.confirm.resolve && $store.ui.confirm.resolve(false)">
1070
- <div class="absolute inset-0 bg-zinc-900/50 backdrop-blur-[2px] cursor-pointer"
1088
+ <div class="absolute inset-0 bg-zinc-900/50 dark:bg-black/60 backdrop-blur-[2px] cursor-pointer"
1071
1089
  @click="$store.ui.confirm.resolve && $store.ui.confirm.resolve(false)"></div>
1072
- <div class="relative bg-white rounded-xl shadow-2xl p-7 mx-4 w-full max-w-lg">
1073
- <h2 class="text-base font-black font-brand text-zinc-800 mb-2" x-text="$store.ui.confirm.title"></h2>
1090
+ <div class="relative bg-white dark:bg-zinc-100 rounded-xl shadow-2xl p-7 mx-4 w-full max-w-lg">
1091
+ <h2 class="text-xl font-black font-brand text-zinc-800 mb-2" x-text="$store.ui.confirm.title"></h2>
1074
1092
  <p class="text-sm text-zinc-500 leading-relaxed" x-text="$store.ui.confirm.body"></p>
1075
1093
  <div class="mt-6 flex justify-end gap-2 flex-wrap">
1076
1094
  <button type="button" @click="$store.ui.confirm.resolve && $store.ui.confirm.resolve(false)"
@@ -1082,7 +1100,7 @@
1082
1100
  x-text="$store.ui.confirm.altText"></button>
1083
1101
  <button type="button" @click="$store.ui.confirm.resolve && $store.ui.confirm.resolve(true)"
1084
1102
  class="text-white font-bold uppercase tracking-widest text-xs rounded-md px-4 py-2.5 cursor-pointer transition-colors"
1085
- :class="$store.ui.confirm.danger && !$store.ui.confirm.altText ? 'bg-red-600 hover:bg-red-500' : 'bg-emerald-600 hover:bg-emerald-500'"
1103
+ :class="$store.ui.confirm.danger && !$store.ui.confirm.altText ? 'bg-red-600 hover:bg-red-500' : 'bg-emerald-600 hover:bg-emerald-500 dark:bg-emerald-400/70 dark:text-[#1b1e25]'"
1086
1104
  x-text="$store.ui.confirm.confirmText"></button>
1087
1105
  </div>
1088
1106
  </div>
@@ -1092,7 +1110,7 @@
1092
1110
  <div x-show="$store.ui.toast.show" @click="$store.ui.toast.show = false"
1093
1111
  x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0 translate-y-3" x-transition:enter-end="opacity-100 translate-y-0"
1094
1112
  x-transition:leave="transition ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
1095
- class="fixed bottom-7 left-1/2 -translate-x-1/2 z-[60] bg-white rounded-lg shadow-xl border border-zinc-200 flex items-stretch overflow-hidden cursor-pointer max-w-md w-full mx-4">
1113
+ class="fixed bottom-7 left-1/2 -translate-x-1/2 z-[60] bg-white dark:bg-zinc-100 rounded-lg shadow-xl border border-zinc-200 flex items-stretch overflow-hidden cursor-pointer max-w-md w-full mx-4">
1096
1114
  <div class="w-1.5 shrink-0"
1097
1115
  :class="{ 'bg-sky-500': $store.ui.toast.type === 'info', 'bg-emerald-500': $store.ui.toast.type === 'success', 'bg-red-500': $store.ui.toast.type === 'error' }"></div>
1098
1116
  <div class="px-4 py-3 text-sm text-zinc-700" x-text="$store.ui.toast.message"></div>
@@ -1113,8 +1131,8 @@
1113
1131
  x-show="$store.ui.modals[toggle]" x-transition.opacity.duration.150ms
1114
1132
  class="fixed inset-0 z-40 flex items-center justify-center"
1115
1133
  @keydown.escape.window="$store.ui.modals[toggle] = false">
1116
- <div class="absolute inset-0 bg-zinc-900/50 backdrop-blur-[2px] cursor-pointer" @click="$store.ui.modals[toggle] = false"></div>
1117
- <div class="relative bg-white rounded-xl shadow-2xl max-h-[88vh] overflow-y-auto p-8 mx-4 w-full" :class="boolAttr(wide) ? 'max-w-2xl' : 'max-w-xl'">
1134
+ <div class="absolute inset-0 bg-zinc-900/50 dark:bg-black/60 backdrop-blur-[2px] cursor-pointer" @click="$store.ui.modals[toggle] = false"></div>
1135
+ <div class="relative bg-white dark:bg-zinc-100 rounded-xl shadow-2xl max-h-[88vh] overflow-y-auto p-8 mx-4 w-full" :class="boolAttr(wide) ? 'max-w-2xl' : 'max-w-xl'">
1118
1136
  <slot></slot>
1119
1137
  </div>
1120
1138
  </div>