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.
@@ -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
- ui.currentKey = open.length ? open[open.length - 1] : keys[keys.length - 1];
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. 📌 Pin recurring sources so they carry forward.',
239
- envelope: 'Little savings accounts: “monthly” is added at each close-out. Spending flows in automatically from expense rows assigned to the envelope. Use to auto-fund from a percentage of other groups.',
240
- goal: 'Envelope budgets with a cap - ideal for recurring bills. Use 🕓 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. 📌 Pin recurring ones. Assign one to an envelope and it spends from that envelope’s balance instead.',
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