spendwise 1.0.0 → 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.
- package/README.md +7 -14
- package/package.json +1 -1
- package/src/main/webserver.js +40 -7
- package/src/renderer/css/app.css +1 -1
- package/src/renderer/index.html +146 -128
- package/src/renderer/js/app.js +21 -1
- package/src/renderer/js/charts.js +15 -0
- package/src/renderer/js/theme.js +44 -0
- package/src/renderer/js/views/month.js +4 -4
package/src/renderer/js/app.js
CHANGED
|
@@ -39,6 +39,16 @@ document.addEventListener('alpine:init', () => {
|
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
+
// Last screen (view + month) survives reload. localStorage, not the database:
|
|
43
|
+
// like the theme, it is per-device state - the same db.json is shared between
|
|
44
|
+
// the desktop app, the web server and other machines, and one screen's
|
|
45
|
+
// position must not follow the data.
|
|
46
|
+
const SCREEN_KEY = 'spendwise-last-screen';
|
|
47
|
+
|
|
48
|
+
function loadScreen () {
|
|
49
|
+
try { return JSON.parse(localStorage.getItem(SCREEN_KEY)) || {}; } catch (e) { return {}; }
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
function appRoot () {
|
|
43
53
|
return {
|
|
44
54
|
async init () {
|
|
@@ -53,10 +63,20 @@ function appRoot () {
|
|
|
53
63
|
const ui = Alpine.store('ui');
|
|
54
64
|
const keys = FinEngine.monthKeys(res.data);
|
|
55
65
|
const open = keys.filter((k) => res.data.months[k].status === 'open');
|
|
56
|
-
|
|
66
|
+
const saved = loadScreen(); // restore last screen, but only what still exists
|
|
67
|
+
ui.currentKey = res.data.months[saved.key] ? saved.key
|
|
68
|
+
: open.length ? open[open.length - 1] : keys[keys.length - 1];
|
|
69
|
+
if (['month', 'insights', 'settings'].includes(saved.view)) ui.view = saved.view;
|
|
57
70
|
ui.savedSnapshot = JSON.stringify(res.data.months[ui.currentKey]);
|
|
58
71
|
// desktop privacy lock; web clients authenticated at the server
|
|
59
72
|
ui.locked = !!(res.data.settings && res.data.settings.auth) && !window.IS_WEB;
|
|
73
|
+
|
|
74
|
+
// reactive persistence: fires on any view/month change, whatever
|
|
75
|
+
// its origin (nav, header picker, close-out advancing the month)
|
|
76
|
+
Alpine.effect(() => {
|
|
77
|
+
const snap = JSON.stringify({ view: ui.view, key: ui.currentKey });
|
|
78
|
+
try { localStorage.setItem(SCREEN_KEY, snap); } catch (e) { /* private mode - session only */ }
|
|
79
|
+
});
|
|
60
80
|
} catch (e) {
|
|
61
81
|
showToast('Failed to load data: ' + e.message, 'error', 12000);
|
|
62
82
|
}
|
|
@@ -103,8 +103,23 @@ const Charts = {
|
|
|
103
103
|
return this._render(id, el, options);
|
|
104
104
|
},
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Fold the current theme into a chart's options at render. theme.mode
|
|
108
|
+
* carries the tooltip/data-label palette and a transparent background lets
|
|
109
|
+
* the card show through; the axis-label and grid COLOURS are handled in CSS
|
|
110
|
+
* (input.css) instead, keyed on html.dark - that repaints instantly when the
|
|
111
|
+
* theme flips under an already-rendered chart, which updateOptions() can't.
|
|
112
|
+
*/
|
|
113
|
+
_applyTheme (options) {
|
|
114
|
+
const dark = !!(window.Theme && window.Theme.resolved() === 'dark');
|
|
115
|
+
options.theme = { ...(options.theme || {}), mode: dark ? 'dark' : 'light' };
|
|
116
|
+
options.chart = { ...(options.chart || {}), background: 'transparent' };
|
|
117
|
+
return options;
|
|
118
|
+
},
|
|
119
|
+
|
|
106
120
|
_render (id, el, options) {
|
|
107
121
|
if (!el) return null;
|
|
122
|
+
this._applyTheme(options);
|
|
108
123
|
// destroy + clear + create: updateOptions() does not reliably apply
|
|
109
124
|
// changed label sets (donut legends stick), so a fresh instance it is
|
|
110
125
|
this.destroy(id);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Appearance: sets `html.dark` from the saved preference so the dark palette
|
|
3
|
+
* in app.css applies. Loaded synchronously at the top of <head> (the app's CSP
|
|
4
|
+
* forbids inline scripts) so the class is on the element before first paint -
|
|
5
|
+
* no light-to-dark flash.
|
|
6
|
+
*
|
|
7
|
+
* The preference lives in localStorage, not the database: it is a per-device
|
|
8
|
+
* choice, and the same db.json is shared between the desktop app, the web
|
|
9
|
+
* server, and other machines - one person's dark mode must not follow the data.
|
|
10
|
+
*
|
|
11
|
+
* spendwise-theme = 'system' | 'light' | 'dark' (default 'system')
|
|
12
|
+
*
|
|
13
|
+
* window.Theme is the small API the Settings control drives.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
(function () {
|
|
18
|
+
var KEY = 'spendwise-theme';
|
|
19
|
+
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
20
|
+
|
|
21
|
+
function pref () {
|
|
22
|
+
try { return localStorage.getItem(KEY) || 'system'; } catch (e) { return 'system'; }
|
|
23
|
+
}
|
|
24
|
+
function isDark (p) {
|
|
25
|
+
return p === 'dark' || (p === 'system' && mq.matches);
|
|
26
|
+
}
|
|
27
|
+
function apply () {
|
|
28
|
+
document.documentElement.classList.toggle('dark', isDark(pref()));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
window.Theme = {
|
|
32
|
+
get: pref,
|
|
33
|
+
set: function (p) {
|
|
34
|
+
try { localStorage.setItem(KEY, p); } catch (e) { /* private mode - session only */ }
|
|
35
|
+
apply();
|
|
36
|
+
},
|
|
37
|
+
resolved: function () { return isDark(pref()) ? 'dark' : 'light'; },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// follow the OS while on 'system'
|
|
41
|
+
mq.addEventListener('change', function () { if (pref() === 'system') apply(); });
|
|
42
|
+
|
|
43
|
+
apply();
|
|
44
|
+
}());
|
|
@@ -235,10 +235,10 @@ function monthView () {
|
|
|
235
235
|
|
|
236
236
|
groupHint (kind) {
|
|
237
237
|
return {
|
|
238
|
-
income: 'All income for the month.
|
|
239
|
-
envelope: 'Little savings accounts:
|
|
240
|
-
goal: 'Envelope budgets with a cap - ideal for recurring bills. Use
|
|
241
|
-
expense: 'Monthly or one-time expenses.
|
|
238
|
+
income: 'All income for the month. <kbd class="border border-zinc-300 rounded-sm py-0.5 px-0.75"><svg class="w-3.5 h-3.5 inline-block -mt-1"><use href="#i-pin"/></svg></kbd> Pin recurring sources so they carry forward.',
|
|
239
|
+
envelope: 'Little savings accounts: Spending flows in automatically from expense rows assigned to the envelope. Use <kbd class="border border-zinc-300 rounded-sm py-0.5 px-0.75"><svg class="w-3.5 h-3.5 inline-block -mt-1"><use href="#i-percent"/></svg></kbd> to auto-fund from a percentage of other groups.',
|
|
240
|
+
goal: 'Envelope budgets with a cap - ideal for recurring bills. Use <kbd class="border border-zinc-300 rounded-sm py-0.5 px-0.75"><svg class="w-3.5 h-3.5 inline-block -mt-1"><use href="#i-clock"/></svg></kbd> to set a due date and the monthly deposit is calculated for you and re-spread each cycle.',
|
|
241
|
+
expense: 'Monthly or one-time expenses. <kbd class="border border-zinc-300 rounded-sm py-0.5 px-0.75"><svg class="w-3.5 h-3.5 inline-block -mt-1"><use href="#i-pin"/></svg></kbd> Pin recurring ones. Assign one to an envelope and it spends from that envelope’s balance instead.',
|
|
242
242
|
}[kind] || '';
|
|
243
243
|
},
|
|
244
244
|
|