driggsby 0.3.0 → 0.3.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.
@@ -23,33 +23,40 @@ export function hostPageHtml(slug, appOrigin, background) {
23
23
  <meta name="viewport" content="width=device-width, initial-scale=1">
24
24
  <title>${slug} — driggsby dev</title>
25
25
  <style>
26
+ /* The strip above the frame is the console's title row, in its own
27
+ measured values: 46px tall on the black ground, 14px ink, a 14%
28
+ white hairline under it. */
26
29
  * { box-sizing: border-box; }
30
+ html { color-scheme: dark; }
27
31
  body {
28
32
  margin: 0;
29
33
  display: flex;
30
34
  flex-direction: column;
31
35
  height: 100vh;
32
- background: #ffffff;
33
- color: #1f2430;
34
- font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
36
+ background: #000000;
37
+ color: #fafafa;
38
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
35
39
  font-size: 14px;
40
+ line-height: 21px;
41
+ -webkit-font-smoothing: antialiased;
36
42
  }
37
43
  header {
38
44
  display: flex;
39
- align-items: baseline;
45
+ align-items: center;
40
46
  justify-content: space-between;
41
- padding: 10px 16px;
42
- border-bottom: 1px solid #e6e8ec;
47
+ height: 46px;
48
+ padding: 0 16px;
49
+ border-bottom: 1px solid rgba(255, 255, 255, 0.14);
43
50
  }
44
- header .slug { font-weight: 600; }
45
- header .mode { color: #6d7280; font-size: 13px; }
51
+ header .slug { font-weight: 500; }
52
+ header .mode { color: #a1a1a1; font-size: 13px; }
46
53
  iframe {
47
54
  flex: 1;
48
55
  width: 100%;
49
56
  border: none;
50
57
  /* The app's declared background from driggsby.json, painted while
51
58
  the app loads — the same surface the production host shows. */
52
- background: ${background ?? "#ffffff"};
59
+ background: ${background ?? "#000000"};
53
60
  }
54
61
  </style>
55
62
  </head>
@@ -9,7 +9,8 @@ import { join } from "node:path";
9
9
  import { slugProblem } from "@driggsby/deploy";
10
10
  import { CliError } from "../cli-error.js";
11
11
  import { quotedForTerminal, wrapProse } from "../terminal-text.js";
12
- import { APP_JS, STYLES_CSS, driggsbyJsonText, indexHtml } from "./templates.js";
12
+ import { STYLES_CSS } from "./template-styles.js";
13
+ import { APP_JS, driggsbyJsonText, indexHtml } from "./templates.js";
13
14
  // Argv and prompt answers echo back inside quotes with this cap; a slug can
14
15
  // never be longer than 63 characters, so the cap only trims hostile input.
15
16
  const MAX_SLUG_ECHO_CHARS = 80;
@@ -0,0 +1,241 @@
1
+ // The scaffold's styles.css. A deployed app lives inside Driggsby's dark
2
+ // chrome, so the page is dark by default and its values are Driggsby's
3
+ // own: the console's ground, card rung, hairline, ink ramp, radii, and
4
+ // type sizes, written out as plain colors because the app is its own
5
+ // origin and cannot read the console's stylesheet. --bg-app must stay
6
+ // equal to driggsby.json's "background" (init-command.test.ts pins the
7
+ // two together): Driggsby paints that color while the app loads.
8
+ export const STYLES_CSS = `/* Driggsby's console palette, so this page reads as part of the app it
9
+ opens inside. Change these together if you re-theme; keep --bg-app equal
10
+ to the "background" in driggsby.json. */
11
+ :root {
12
+ color-scheme: dark;
13
+ --bg-app: #000000;
14
+ --bg-card: #0a0a0a;
15
+ --bg-mute: #262626;
16
+ --hairline: rgba(255, 255, 255, 0.14);
17
+ --text-primary: #fafafa;
18
+ --text-secondary: #c4c4c4;
19
+ --text-tertiary: #a1a1a1;
20
+ --radius-card: 8px;
21
+ --radius-bar: 4px;
22
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ body {
30
+ margin: 0;
31
+ background: var(--bg-app);
32
+ color: var(--text-primary);
33
+ font-family: var(--font-sans);
34
+ font-size: 14px;
35
+ line-height: 21px;
36
+ -webkit-font-smoothing: antialiased;
37
+ }
38
+
39
+ main {
40
+ max-width: 800px;
41
+ margin: 0 auto;
42
+ padding: 24px 24px 64px;
43
+ }
44
+
45
+ h1 {
46
+ margin: 0;
47
+ font-size: 24px;
48
+ font-weight: 600;
49
+ line-height: 36px;
50
+ letter-spacing: -0.02em;
51
+ }
52
+
53
+ h2 {
54
+ margin: 24px 0 12px;
55
+ font-size: 14px;
56
+ font-weight: 500;
57
+ line-height: 21px;
58
+ letter-spacing: -0.02em;
59
+ }
60
+
61
+ /* One card, four cells; the 1px gap over the card's own hairline color
62
+ draws the divider between cells however the grid wraps. */
63
+ .stat-grid {
64
+ display: grid;
65
+ grid-template-columns: repeat(4, minmax(0, 1fr));
66
+ gap: 1px;
67
+ margin-top: 24px;
68
+ border: 1px solid var(--hairline);
69
+ border-radius: var(--radius-card);
70
+ background: var(--hairline);
71
+ overflow: hidden;
72
+ }
73
+
74
+ @media (max-width: 640px) {
75
+ .stat-grid {
76
+ grid-template-columns: repeat(2, minmax(0, 1fr));
77
+ }
78
+ }
79
+
80
+ @media (max-width: 400px) {
81
+ .stat-grid {
82
+ grid-template-columns: minmax(0, 1fr);
83
+ }
84
+ }
85
+
86
+ .stat {
87
+ display: flex;
88
+ flex-direction: column;
89
+ padding: 16px;
90
+ background: var(--bg-card);
91
+ }
92
+
93
+ .stat-label {
94
+ font-size: 13px;
95
+ line-height: 20px;
96
+ color: var(--text-tertiary);
97
+ }
98
+
99
+ .stat-value {
100
+ margin-top: 4px;
101
+ font-size: 20px;
102
+ font-weight: 600;
103
+ line-height: 28px;
104
+ letter-spacing: -0.02em;
105
+ font-variant-numeric: tabular-nums;
106
+ /* A currency string has no natural break, and a clipped balance is a
107
+ wrong number. If a value ever outgrows its cell, it wraps. */
108
+ overflow-wrap: anywhere;
109
+ }
110
+
111
+ #accounts {
112
+ border: 1px solid var(--hairline);
113
+ border-radius: var(--radius-card);
114
+ background: var(--bg-card);
115
+ overflow: hidden;
116
+ }
117
+
118
+ .account-row {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 14px;
122
+ padding: 14px 16px;
123
+ }
124
+
125
+ .account-row + .account-row {
126
+ border-top: 1px solid var(--hairline);
127
+ }
128
+
129
+ /* A list with nothing in it says so, in the row's own inset. */
130
+ .empty-row {
131
+ padding: 14px 16px;
132
+ color: var(--text-secondary);
133
+ }
134
+
135
+ /* The institution's initial on a round pill, as the console draws it. */
136
+ .account-glyph {
137
+ flex: none;
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ width: 32px;
142
+ height: 32px;
143
+ border-radius: 50%;
144
+ background: var(--bg-mute);
145
+ color: var(--text-secondary);
146
+ font-size: 16px;
147
+ font-weight: 600;
148
+ }
149
+
150
+ .account-names {
151
+ display: flex;
152
+ flex-direction: column;
153
+ flex: 1;
154
+ min-width: 0;
155
+ }
156
+
157
+ .account-name {
158
+ font-weight: 500;
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ white-space: nowrap;
162
+ }
163
+
164
+ .account-institution {
165
+ /* Holds its line even when empty, so every row measures the same as
166
+ the skeleton row it replaces. */
167
+ min-height: 21px;
168
+ color: var(--text-secondary);
169
+ overflow: hidden;
170
+ text-overflow: ellipsis;
171
+ white-space: nowrap;
172
+ }
173
+
174
+ .account-balance {
175
+ text-align: right;
176
+ font-variant-numeric: tabular-nums;
177
+ }
178
+
179
+ /* The skeleton ships in the HTML so the first paint is the final layout
180
+ as muted bars; the first data render replaces it in a dissolve (see the
181
+ "Settling in" section of app.js). Each bar's margins pad it to exactly
182
+ the height of the text it stands in for, and the columns holding
183
+ stacked bars are flex so those margins never collapse — the skeleton
184
+ page and the data page measure identical, and nothing shifts when data
185
+ lands. */
186
+ .skeleton {
187
+ border-radius: var(--radius-bar);
188
+ background: var(--bg-mute);
189
+ animation: skeleton-pulse 1.6s ease-in-out infinite;
190
+ }
191
+
192
+ .skeleton-label {
193
+ width: 64px;
194
+ height: 12px;
195
+ margin: 4px 0;
196
+ }
197
+
198
+ .skeleton-value {
199
+ width: 96px;
200
+ height: 20px;
201
+ margin: 8px 0 4px;
202
+ }
203
+
204
+ .skeleton-glyph {
205
+ flex: none;
206
+ width: 32px;
207
+ height: 32px;
208
+ border-radius: 50%;
209
+ }
210
+
211
+ .skeleton-name {
212
+ width: 150px;
213
+ height: 14px;
214
+ margin: 3.5px 0;
215
+ }
216
+
217
+ .skeleton-institution {
218
+ width: 110px;
219
+ height: 12px;
220
+ margin: 4.5px 0;
221
+ }
222
+
223
+ .skeleton-balance {
224
+ width: 72px;
225
+ height: 14px;
226
+ margin: 3.5px 0;
227
+ }
228
+
229
+ @keyframes skeleton-pulse {
230
+ 0%, 100% { opacity: 1; }
231
+ 50% { opacity: 0.55; }
232
+ }
233
+
234
+ /* The skeleton-to-data dissolve itself is app.js's "Settling in" View
235
+ Transition; it needs no CSS here and already sits out reduced motion. */
236
+ @media (prefers-reduced-motion: reduce) {
237
+ .skeleton {
238
+ animation: none;
239
+ }
240
+ }
241
+ `;
@@ -6,8 +6,8 @@
6
6
  // tool result shapes only in structure, so the scaffold's render functions
7
7
  // work unchanged when live data replaces them. The HTML ships a skeleton
8
8
  // of the final layout so the first paint — in every context, before any
9
- // script runs — is muted bars that the first render replaces, fading in
10
- // at the exact same size so nothing shifts.
9
+ // script runs — is muted bars that the first render replaces at the exact
10
+ // same size, dissolving one into the other, so nothing shifts or pops.
11
11
  export function indexHtml(slug) {
12
12
  return `<!doctype html>
13
13
  <html lang="en">
@@ -51,6 +51,7 @@ export function indexHtml(slug) {
51
51
  <h2>Accounts</h2>
52
52
  <div id="accounts" aria-busy="true">
53
53
  <div class="account-row">
54
+ <div class="skeleton skeleton-glyph"></div>
54
55
  <div class="account-names">
55
56
  <div class="skeleton skeleton-name"></div>
56
57
  <div class="skeleton skeleton-institution"></div>
@@ -58,6 +59,7 @@ export function indexHtml(slug) {
58
59
  <div class="skeleton skeleton-balance"></div>
59
60
  </div>
60
61
  <div class="account-row">
62
+ <div class="skeleton skeleton-glyph"></div>
61
63
  <div class="account-names">
62
64
  <div class="skeleton skeleton-name"></div>
63
65
  <div class="skeleton skeleton-institution"></div>
@@ -65,6 +67,7 @@ export function indexHtml(slug) {
65
67
  <div class="skeleton skeleton-balance"></div>
66
68
  </div>
67
69
  <div class="account-row">
70
+ <div class="skeleton skeleton-glyph"></div>
68
71
  <div class="account-names">
69
72
  <div class="skeleton skeleton-name"></div>
70
73
  <div class="skeleton skeleton-institution"></div>
@@ -162,10 +165,22 @@ function formatMoney(value) {
162
165
  if (!value || typeof value.amount !== "string") return "—";
163
166
  const number = Number(value.amount);
164
167
  if (!Number.isFinite(number)) return "—";
165
- return new Intl.NumberFormat(undefined, {
166
- style: "currency",
167
- currency: value.currency_code || "USD"
168
- }).format(number);
168
+ const currency = value.currency_code === undefined ? "USD" : value.currency_code;
169
+ if (typeof currency !== "string") return "—";
170
+ try {
171
+ return new Intl.NumberFormat(undefined, {
172
+ style: "currency",
173
+ currency: currency
174
+ }).format(number);
175
+ } catch (error) {
176
+ // An unknown currency code renders as absent, never as a guess in
177
+ // the wrong currency — and never takes the rest of the page down.
178
+ return "—";
179
+ }
180
+ }
181
+
182
+ function asText(value) {
183
+ return typeof value === "string" ? value : "";
169
184
  }
170
185
 
171
186
  function element(tag, className, text) {
@@ -175,19 +190,6 @@ function element(tag, className, text) {
175
190
  return node;
176
191
  }
177
192
 
178
- // The page ships with a skeleton of this exact layout in its HTML — muted
179
- // bars where the numbers will be — so the very first paint is already the
180
- // final shape. The first render into a container replaces its skeleton and
181
- // fades in, so data arriving reads as the page settling, never as content
182
- // popping. classList.add is idempotent and re-adding a class never restarts
183
- // its animation, so later live updates repaint in place without re-fading.
184
- // aria-busy ships in the HTML so assistive tech hears "loading" until the
185
- // first real content lands.
186
- function revealOnce(container) {
187
- container.classList.add("fade-in");
188
- container.removeAttribute("aria-busy");
189
- }
190
-
191
193
  function renderOverview(result) {
192
194
  const rollups = (result && result.summary_rollups) || {};
193
195
  const container = document.getElementById("overview");
@@ -206,29 +208,142 @@ function renderOverview(result) {
206
208
  );
207
209
  container.append(stat);
208
210
  }
209
- revealOnce(container);
211
+ // aria-busy ships in the HTML so assistive tech hears "loading" until
212
+ // the first content lands; every render clears it (harmless once gone).
213
+ container.removeAttribute("aria-busy");
210
214
  }
211
215
 
212
216
  function renderAccounts(result) {
213
- const accounts = (result && result.linked_accounts) || [];
217
+ const list = result && Array.isArray(result.linked_accounts) ? result.linked_accounts : [];
218
+ const accounts = list.filter((account) => account && typeof account === "object");
214
219
  const container = document.getElementById("accounts");
215
220
  container.replaceChildren();
221
+ if (accounts.length === 0) {
222
+ container.append(element("div", "empty-row", "No linked accounts yet."));
223
+ container.removeAttribute("aria-busy");
224
+ return;
225
+ }
216
226
  for (const account of accounts) {
217
227
  const row = element("div", "account-row");
218
228
  const names = element("div", "account-names");
219
- const mask = account.account_mask_last4;
229
+ // Only strings paint; a malformed field renders as absent, never as
230
+ // "[object Object]".
231
+ const displayName = asText(account.account_display_name);
232
+ const institution = asText(account.institution_name);
233
+ const mask = asText(account.account_mask_last4);
234
+ // The institution's initial on a round pill, the way Driggsby's own
235
+ // account rows draw it. Array.from keeps an emoji-leading name from
236
+ // breaking the render (it splits by whole character, not UTF-16 half).
237
+ const initialSource = (institution || displayName).trim();
238
+ const initial = (Array.from(initialSource)[0] || "?").toUpperCase();
239
+ const institutionLine = [institution, mask ? "····" + mask : ""].filter(Boolean).join(" ");
220
240
  names.append(
221
- element("div", "account-name", account.account_display_name || "Account"),
222
- element(
223
- "div",
224
- "account-institution",
225
- (account.institution_name || "") + (mask ? " ····" + mask : "")
226
- )
241
+ element("div", "account-name", displayName || "Account"),
242
+ element("div", "account-institution", institutionLine)
243
+ );
244
+ const glyph = element("div", "account-glyph", initial);
245
+ // Decorative: the institution's name is read out on the next line.
246
+ glyph.setAttribute("aria-hidden", "true");
247
+ row.append(
248
+ glyph,
249
+ names,
250
+ element("div", "account-balance", formatMoney(account.current_balance))
227
251
  );
228
- row.append(names, element("div", "account-balance", formatMoney(account.current_balance)));
229
252
  container.append(row);
230
253
  }
231
- revealOnce(container);
254
+ container.removeAttribute("aria-busy");
255
+ }
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Settling in. The first data a section shows replaces its skeleton in a
259
+ // dissolve: the browser snapshots the page, the paint lands, and old
260
+ // crossfades into new (a View Transition — pixels that did not change
261
+ // don't visibly change, so what reads as animating is the bars becoming
262
+ // numbers). The skeleton and the data measure identical (see styles.css),
263
+ // so the dissolve reads as the page coming into focus — never a blink, a
264
+ // shift, or a pop. Where the browser has no View Transitions, or the
265
+ // person prefers reduced motion, the swap is simply instant.
266
+ //
267
+ // The beat below covers sections whose data lands together (the common
268
+ // case). A section whose first data trails in later gets its own beat and
269
+ // its own dissolve; starting it skips whatever remained of the first one's
270
+ // animation — the paints always land, only the crossfade is cut short.
271
+ // ---------------------------------------------------------------------------
272
+
273
+ // How long a section's first paint may wait so sections whose data lands
274
+ // in the same beat dissolve in together instead of one by one.
275
+ const SETTLE_TOGETHER_MS = 50;
276
+
277
+ let pendingFirstPaints = [];
278
+ let settleFlushScheduled = false;
279
+
280
+ function settle(paint) {
281
+ const reducedMotion =
282
+ typeof window.matchMedia === "function" &&
283
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
284
+ if (typeof document.startViewTransition !== "function" || reducedMotion) {
285
+ paint();
286
+ return;
287
+ }
288
+ pendingFirstPaints.push(paint);
289
+ if (settleFlushScheduled) return; // this paint rides the beat already in flight
290
+ settleFlushScheduled = true;
291
+ setTimeout(() => {
292
+ // Collected inside the flush, which runs inside the transition
293
+ // callback, so a first result that lands between the beat ending and
294
+ // the snapshot being taken joins this dissolve instead of cutting it
295
+ // short with a second one.
296
+ const flush = () => {
297
+ const paints = pendingFirstPaints;
298
+ pendingFirstPaints = [];
299
+ settleFlushScheduled = false;
300
+ for (const apply of paints) {
301
+ // One section's render must never stop another's: the scaffold
302
+ // is meant to be edited, and a throw mid-edit stays that
303
+ // section's problem — it heals on its own next result.
304
+ try {
305
+ apply();
306
+ } catch (error) {
307
+ console.error(error);
308
+ }
309
+ }
310
+ };
311
+ try {
312
+ const transition = document.startViewTransition(flush);
313
+ // A skipped transition (a hidden tab, a second dissolve starting)
314
+ // still runs flush; only the animation is dropped. Without this,
315
+ // the skip surfaces as an uncaught rejection in the console.
316
+ transition.ready.catch(() => {});
317
+ } catch (error) {
318
+ // If starting the transition itself threw, nothing will call
319
+ // flush; run it plain so the paints land and the beat resets.
320
+ console.error(error);
321
+ flush();
322
+ }
323
+ }, SETTLE_TOGETHER_MS);
324
+ }
325
+
326
+ // Wraps a render function for its watch: the first result goes through
327
+ // settle() above, and only the latest result paints if more arrive while
328
+ // that dissolve is still pending. Every later result repaints in place
329
+ // with no animation at all.
330
+ function settledRenderer(render) {
331
+ let hasSettled = false;
332
+ let firstPaintQueued = false;
333
+ let latestResult;
334
+ return (result) => {
335
+ if (hasSettled) {
336
+ render(result);
337
+ return;
338
+ }
339
+ latestResult = result;
340
+ if (firstPaintQueued) return;
341
+ firstPaintQueued = true;
342
+ settle(() => {
343
+ hasSettled = true;
344
+ render(latestResult);
345
+ });
346
+ };
232
347
  }
233
348
 
234
349
  // ---------------------------------------------------------------------------
@@ -239,7 +354,7 @@ function renderAccounts(result) {
239
354
  // shows its own notice explaining that the page runs with real data only
240
355
  // inside Driggsby. Embedded, sample numbers never paint at all — the
241
356
  // shipped skeleton holds the layout until the first real results replace
242
- // it and fade in.
357
+ // it in a dissolve (see "Settling in" above).
243
358
  // ---------------------------------------------------------------------------
244
359
 
245
360
  const standalone = window.parent === window;
@@ -250,156 +365,14 @@ if (standalone) {
250
365
  }
251
366
 
252
367
  if (window.driggsby) {
253
- driggsby.watch("get_overview", {}, (result) => {
254
- renderOverview(result);
255
- });
256
- driggsby.watch("list_accounts", {}, (result) => {
257
- renderAccounts(result);
258
- });
259
- }
260
- `;
261
- export const STYLES_CSS = `:root {
262
- --background: #ffffff;
263
- --text: #1f2430;
264
- --text-muted: #6d7280;
265
- --hairline: #e6e8ec;
266
- }
267
-
268
- * {
269
- box-sizing: border-box;
270
- }
271
-
272
- body {
273
- margin: 0;
274
- background: var(--background);
275
- color: var(--text);
276
- font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
277
- font-size: 15px;
278
- line-height: 1.5;
279
- }
280
-
281
- main {
282
- max-width: 720px;
283
- margin: 0 auto;
284
- padding: 48px 24px 64px;
285
- }
286
-
287
- h1 {
288
- margin: 0;
289
- font-size: 22px;
290
- font-weight: 600;
291
- }
292
-
293
- h2 {
294
- margin: 40px 0 4px;
295
- font-size: 15px;
296
- font-weight: 600;
297
- }
298
-
299
- /* The skeleton ships in the HTML so the first paint is the final layout as
300
- muted bars; the first render replaces it and fades in (see app.js). The
301
- bar margins make each skeleton block exactly the height of the text it
302
- stands in for, so nothing shifts when data lands. */
303
- .skeleton {
304
- border-radius: 4px;
305
- background: var(--hairline);
306
- animation: skeleton-pulse 1.6s ease-in-out infinite;
307
- }
308
-
309
- .skeleton-label {
310
- width: 64px;
311
- height: 12px;
312
- margin: 3.75px 0 0;
313
- }
314
-
315
- .skeleton-value {
316
- width: 96px;
317
- height: 20px;
318
- margin: 8.75px 0 5px;
319
- }
320
-
321
- .skeleton-name {
322
- width: 150px;
323
- height: 14px;
324
- margin: 4px 0 8px;
325
- }
326
-
327
- .skeleton-institution {
328
- width: 110px;
329
- height: 12px;
330
- margin: 4px 0;
331
- }
332
-
333
- .skeleton-balance {
334
- width: 72px;
335
- height: 14px;
336
- }
337
-
338
- @keyframes skeleton-pulse {
339
- 0%, 100% { opacity: 1; }
340
- 50% { opacity: 0.55; }
341
- }
342
-
343
- .fade-in {
344
- animation: fade-in 200ms ease-out;
345
- }
346
-
347
- @keyframes fade-in {
348
- from { opacity: 0; }
349
- to { opacity: 1; }
350
- }
351
-
352
- @media (prefers-reduced-motion: reduce) {
353
- .skeleton,
354
- .fade-in {
355
- animation: none;
356
- }
357
- }
358
-
359
- .stat-grid {
360
- display: grid;
361
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
362
- gap: 20px;
363
- margin-top: 32px;
364
- }
365
-
366
- .stat-label {
367
- font-size: 13px;
368
- color: var(--text-muted);
369
- }
370
-
371
- .stat-value {
372
- font-size: 20px;
373
- font-weight: 600;
374
- font-variant-numeric: tabular-nums;
375
- }
376
-
377
- .account-row {
378
- display: flex;
379
- align-items: center;
380
- justify-content: space-between;
381
- gap: 16px;
382
- padding: 12px 0;
383
- border-top: 1px solid var(--hairline);
384
- }
385
-
386
- .account-row:first-child {
387
- border-top: none;
388
- }
389
-
390
- .account-institution {
391
- font-size: 13px;
392
- color: var(--text-muted);
393
- }
394
-
395
- .account-balance {
396
- font-variant-numeric: tabular-nums;
368
+ driggsby.watch("get_overview", {}, settledRenderer(renderOverview));
369
+ driggsby.watch("list_accounts", {}, settledRenderer(renderAccounts));
397
370
  }
398
371
  `;
399
372
  // "background" is the template's own page background (styles.css
400
- // --background): Driggsby paints it while the app loads, so the loading
373
+ // --bg-app): Driggsby paints it while the app loads, so the loading
401
374
  // surface matches the app from the first frame. An agent that re-themes
402
375
  // the app should keep this equal to the page's real background color.
403
376
  export function driggsbyJsonText(slug) {
404
- return `{\n "slug": ${JSON.stringify(slug)},\n "serve": ".",\n "background": "#ffffff"\n}\n`;
377
+ return `{\n "slug": ${JSON.stringify(slug)},\n "serve": ".",\n "background": "#000000"\n}\n`;
405
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driggsby",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Set up and build on Driggsby, the MCP server for personal finance.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -28,7 +28,7 @@
28
28
  "node": ">=18"
29
29
  },
30
30
  "dependencies": {
31
- "@driggsby/deploy": "0.3.0",
32
- "@driggsby/sdk": "0.3.0"
31
+ "@driggsby/deploy": "0.3.2",
32
+ "@driggsby/sdk": "0.3.2"
33
33
  }
34
34
  }