daily-focus 0.1.1-dev.9 → 0.2.0-dev.14

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 CHANGED
@@ -41,7 +41,8 @@ daily-focus service
41
41
 
42
42
  Or, on a Mac, let the menu bar app keep it running: it starts the dashboard at login,
43
43
  shows the brief's age at a glance, and tells you when a new brief arrives. It carries
44
- everything it needs, Node.js included. See
44
+ everything it needs, Node.js included, and installs a newer dashboard from its menu
45
+ without a new download of the app. See
45
46
  [SETUP.md](SETUP.md#keep-the-dashboard-running).
46
47
 
47
48
  [SETUP.md](SETUP.md) covers each step, the integrations and every setting. The
package/SETUP.md CHANGED
@@ -324,6 +324,20 @@ Download `Daily-Focus.dmg` from the [latest release](https://github.com/watson/d
324
324
  open it and drag the app to Applications, then open it from there.
325
325
  It has no window and no Dock icon: look for it in the menu bar.
326
326
 
327
+ The dashboard updates without a new disk image. **Updates** in the menu shows two
328
+ versions, the app's and the dashboard's, and which releases to take: **Stable
329
+ Releases**, or **Development Builds**, a build of every change as soon as it is
330
+ merged. A few times a day the app asks npm for the newest release, and when it
331
+ finds a newer dashboard, it posts a notification and the menu offers **Install
332
+ Dashboard**. Installing downloads it from the GitHub release, a few hundred
333
+ kilobytes, and runs it only if it is signed by the same developer as the app
334
+ and nothing in it has changed. The dashboard restarts into it once the morning
335
+ agent and the assistant aren't busy. If it doesn't start, the app goes back to the
336
+ one it had and says so. The app itself changes less often; when a release brings a
337
+ new one, or a dashboard that needs one, the menu links to its disk image instead.
338
+ A dashboard that `daily-focus service` or a terminal runs is updated the way it was
339
+ installed, not by the app.
340
+
327
341
  To build it yourself from a checkout:
328
342
 
329
343
  ```sh
@@ -8,7 +8,7 @@ import {
8
8
  readArchiveIndex,
9
9
  resolveItems,
10
10
  runContractChecks
11
- } from "./chunk-CQ53JYUU.js";
11
+ } from "./chunk-NAD6PRZT.js";
12
12
  import {
13
13
  BRIEF_REFRESH_GRACE_HOURS,
14
14
  describeSchedule,
@@ -17,13 +17,13 @@ import {
17
17
  nextRunDate,
18
18
  resolveSchedule,
19
19
  runsOn
20
- } from "./chunk-G4K24I4M.js";
20
+ } from "./chunk-IFAGJFOT.js";
21
21
  import {
22
22
  command
23
- } from "./chunk-6VWWJZKJ.js";
23
+ } from "./chunk-BC2W7NND.js";
24
24
  import {
25
25
  workingMsBetween
26
- } from "./chunk-EXGTFXNV.js";
26
+ } from "./chunk-K2VT4JBR.js";
27
27
 
28
28
  // scripts/audit.ts
29
29
  import { readFile, readdir } from "node:fs/promises";
@@ -1,6 +1,7 @@
1
1
  // src/install.ts
2
+ import { readFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
- import { resolve, sep } from "node:path";
4
+ import { dirname, join, resolve, sep } from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  var PACKAGED = import.meta.url.endsWith(".js");
6
7
  var ROOT = resolve(import.meta.dirname, "..");
@@ -13,7 +14,20 @@ function ranDirectly(url) {
13
14
  const file = fileURLToPath(url);
14
15
  return file.endsWith(".ts") && process.argv[1] !== void 0 && resolve(process.argv[1]) === file;
15
16
  }
16
- var APP_CALENDAR_APP = ROOT.includes(`.app${sep}Contents${sep}Resources${sep}`) ? resolve(ROOT, "..", "..", "Helpers", "Daily Focus Calendar.app") : null;
17
+ function packageVersion(root = ROOT) {
18
+ try {
19
+ return JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version ?? "unknown";
20
+ } catch {
21
+ return "unknown";
22
+ }
23
+ }
24
+ function appCalendarApp(execPath, root) {
25
+ const contents = `.app${sep}Contents${sep}`;
26
+ if (dirname(execPath).endsWith(`${contents}Helpers`)) return resolve(dirname(execPath), "Daily Focus Calendar.app");
27
+ if (root.includes(`${contents}Resources${sep}`)) return resolve(root, "..", "..", "Helpers", "Daily Focus Calendar.app");
28
+ return null;
29
+ }
30
+ var APP_CALENDAR_APP = appCalendarApp(process.execPath, ROOT);
17
31
  var USER_CALENDAR_APP = resolve(homedir(), "Library", "Application Support", "Daily Focus", "Daily Focus Calendar.app");
18
32
 
19
33
  export {
@@ -22,6 +36,7 @@ export {
22
36
  IN_NPX_CACHE,
23
37
  command,
24
38
  ranDirectly,
39
+ packageVersion,
25
40
  APP_CALENDAR_APP,
26
41
  USER_CALENDAR_APP
27
42
  };
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  APP_CALENDAR_APP,
3
3
  USER_CALENDAR_APP
4
- } from "./chunk-6VWWJZKJ.js";
4
+ } from "./chunk-BC2W7NND.js";
5
5
  import {
6
6
  isSameLocalDay,
7
7
  localDateKey,
8
8
  parseISO,
9
9
  startOfLocalDay
10
- } from "./chunk-EXGTFXNV.js";
10
+ } from "./chunk-K2VT4JBR.js";
11
11
 
12
12
  // src/env.ts
13
13
  import { readFileSync } from "node:fs";
@@ -1,5 +1,8 @@
1
1
  // src/time.ts
2
2
  var MS_PER_DAY = 864e5;
3
+ function localTimeZone() {
4
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
5
+ }
3
6
  function localDateKey(d) {
4
7
  const y = d.getFullYear();
5
8
  const m = String(d.getMonth() + 1).padStart(2, "0");
@@ -64,6 +67,7 @@ function laterISO(a, b) {
64
67
  }
65
68
 
66
69
  export {
70
+ localTimeZone,
67
71
  localDateKey,
68
72
  isDateOnly,
69
73
  parseISO,
@@ -4,18 +4,19 @@ import {
4
4
  nextRunDate,
5
5
  resolveSchedule,
6
6
  runsOn
7
- } from "./chunk-G4K24I4M.js";
7
+ } from "./chunk-IFAGJFOT.js";
8
8
  import {
9
9
  calendarDaysBetween,
10
10
  isDateOnly,
11
11
  isSameLocalDay,
12
12
  isWorkingDay,
13
13
  localDateKey,
14
+ localTimeZone,
14
15
  parseISO,
15
16
  startOfLocalDay,
16
17
  workingDaysSince,
17
18
  workingMsBetween
18
- } from "./chunk-EXGTFXNV.js";
19
+ } from "./chunk-K2VT4JBR.js";
19
20
 
20
21
  // src/fs.ts
21
22
  import { mkdir, rename, writeFile } from "node:fs/promises";
@@ -913,7 +914,8 @@ var Store = class {
913
914
  // Parser complaints and contract failures render the same way: both are
914
915
  // things the agent got wrong that you would not otherwise see.
915
916
  warnings: [...warnings, ...contractIssues],
916
- now: now.toISOString()
917
+ now: now.toISOString(),
918
+ timeZone: localTimeZone()
917
919
  };
918
920
  }
919
921
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  localDateKey
3
- } from "./chunk-EXGTFXNV.js";
3
+ } from "./chunk-K2VT4JBR.js";
4
4
 
5
5
  // src/sample.ts
6
6
  function sampleBrief(now = /* @__PURE__ */ new Date()) {
package/dist/cli.js CHANGED
@@ -2,12 +2,13 @@
2
2
  import {
3
3
  IN_NPX_CACHE,
4
4
  ROOT,
5
- USER_CALENDAR_APP
6
- } from "./chunk-6VWWJZKJ.js";
5
+ USER_CALENDAR_APP,
6
+ packageVersion
7
+ } from "./chunk-BC2W7NND.js";
7
8
 
8
9
  // src/cli.ts
9
10
  import { execFile, spawn } from "node:child_process";
10
- import { readFileSync, rmSync } from "node:fs";
11
+ import { rmSync } from "node:fs";
11
12
  import { cp, mkdir, mkdtemp, writeFile } from "node:fs/promises";
12
13
  import { tmpdir } from "node:os";
13
14
  import { join } from "node:path";
@@ -36,13 +37,6 @@ Options
36
37
  -h, --help print this
37
38
 
38
39
  Everything else is set up in the dashboard itself.`;
39
- function version() {
40
- try {
41
- return JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version ?? "unknown";
42
- } catch {
43
- return "unknown";
44
- }
45
- }
46
40
  function openInBrowser(url) {
47
41
  const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
48
42
  const child = spawn(command, args, { stdio: "ignore", detached: true });
@@ -68,7 +62,7 @@ async function demo(open) {
68
62
  DAILY_FOCUS_JIRA: "off",
69
63
  DAILY_FOCUS_CALENDAR: "off"
70
64
  });
71
- const { sampleBrief } = await import("./sample-M7WNKQOV.js");
65
+ const { sampleBrief } = await import("./sample-QDEXFFRY.js");
72
66
  await writeFile(join(store, "items.json"), `${JSON.stringify(sampleBrief(), null, 2)}
73
67
  `);
74
68
  await start(open, (url) => {
@@ -78,7 +72,7 @@ async function demo(open) {
78
72
  });
79
73
  }
80
74
  async function start(open, onStarted) {
81
- const { runServer } = await import("./server-O5GF3TRI.js");
75
+ const { runServer } = await import("./server-ANIS2MXU.js");
82
76
  await runServer({
83
77
  onStarted: (url) => {
84
78
  onStarted?.(url);
@@ -136,7 +130,7 @@ async function main(argv) {
136
130
  return;
137
131
  }
138
132
  if (values.version) {
139
- console.log(version());
133
+ console.log(packageVersion());
140
134
  return;
141
135
  }
142
136
  if (values.data !== void 0) process.env.DAILY_FOCUS_DATA = values.data;
@@ -174,20 +168,20 @@ async function main(argv) {
174
168
  }
175
169
  return start(open);
176
170
  case "service": {
177
- const { main: service } = await import("./service-ILBMIZB7.js");
171
+ const { main: service } = await import("./service-NEQVCWI5.js");
178
172
  process.exitCode = await service(values.remove ? ["--remove"] : []);
179
173
  return;
180
174
  }
181
175
  // These three are scripts that do their work as they load, and read their
182
176
  // own flags from the command line.
183
177
  case "init":
184
- await import("./init-ZJCI2723.js");
178
+ await import("./init-BMFPWMXL.js");
185
179
  return;
186
180
  case "audit":
187
- await import("./audit-TPBQFSL3.js");
181
+ await import("./audit-DTXVAT5Z.js");
188
182
  return;
189
183
  case "seed":
190
- await import("./seed-S5ZU6EIO.js");
184
+ await import("./seed-GCT7Z3IV.js");
191
185
  return;
192
186
  case "build-calendar":
193
187
  return buildCalendar();
@@ -8,12 +8,12 @@ import {
8
8
  } from "./chunk-QKT2FAB5.js";
9
9
  import {
10
10
  loadConfig
11
- } from "./chunk-G4K24I4M.js";
11
+ } from "./chunk-IFAGJFOT.js";
12
12
  import {
13
13
  PACKAGED,
14
14
  command
15
- } from "./chunk-6VWWJZKJ.js";
16
- import "./chunk-EXGTFXNV.js";
15
+ } from "./chunk-BC2W7NND.js";
16
+ import "./chunk-K2VT4JBR.js";
17
17
 
18
18
  // scripts/init.ts
19
19
  import { mkdir, writeFile } from "node:fs/promises";
@@ -0,0 +1,7 @@
1
+ import {
2
+ sampleBrief
3
+ } from "./chunk-UAR65OCG.js";
4
+ import "./chunk-K2VT4JBR.js";
5
+ export {
6
+ sampleBrief
7
+ };
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  sampleBrief
3
- } from "./chunk-NDWZUZJE.js";
3
+ } from "./chunk-UAR65OCG.js";
4
4
  import {
5
5
  Store
6
- } from "./chunk-CQ53JYUU.js";
6
+ } from "./chunk-NAD6PRZT.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-G4K24I4M.js";
9
+ } from "./chunk-IFAGJFOT.js";
10
10
  import {
11
11
  command
12
- } from "./chunk-6VWWJZKJ.js";
13
- import "./chunk-EXGTFXNV.js";
12
+ } from "./chunk-BC2W7NND.js";
13
+ import "./chunk-K2VT4JBR.js";
14
14
 
15
15
  // scripts/seed.ts
16
16
  import { writeFile } from "node:fs/promises";
@@ -16,7 +16,7 @@ import {
16
16
  stopSession,
17
17
  tempPathFor,
18
18
  writeJsonAtomic
19
- } from "./chunk-CQ53JYUU.js";
19
+ } from "./chunk-NAD6PRZT.js";
20
20
  import {
21
21
  CLI_NAMES,
22
22
  LAUNCH_SETTINGS,
@@ -30,16 +30,17 @@ import {
30
30
  readDotEnv,
31
31
  resolveSchedule,
32
32
  scheduledRunDue
33
- } from "./chunk-G4K24I4M.js";
33
+ } from "./chunk-IFAGJFOT.js";
34
34
  import {
35
35
  PACKAGED,
36
36
  command,
37
+ packageVersion,
37
38
  ranDirectly
38
- } from "./chunk-6VWWJZKJ.js";
39
+ } from "./chunk-BC2W7NND.js";
39
40
  import {
40
41
  laterISO,
41
42
  parseISO
42
- } from "./chunk-EXGTFXNV.js";
43
+ } from "./chunk-K2VT4JBR.js";
43
44
 
44
45
  // src/server.ts
45
46
  import { createServer } from "node:http";
@@ -4006,6 +4007,7 @@ async function hydratePath(env = process.env, platform2 = process.platform, exec
4006
4007
 
4007
4008
  // src/server.ts
4008
4009
  var PUBLIC_DIR = resolve(fileURLToPath(new URL("../public", import.meta.url)));
4010
+ var VERSION = packageVersion();
4009
4011
  var MIME = {
4010
4012
  ".html": "text/html; charset=utf-8",
4011
4013
  ".js": "text/javascript; charset=utf-8",
@@ -4102,13 +4104,15 @@ function configure(sources) {
4102
4104
  return { config: loadConfig(layerEnv(sources, {})), warnings };
4103
4105
  }
4104
4106
  }
4105
- function statusOf(state, config) {
4107
+ function statusOf(state, config, busy) {
4106
4108
  const last = state.agentRun.last;
4107
4109
  return {
4110
+ version: VERSION,
4108
4111
  dataDir: config.dataDir,
4109
4112
  profile: config.profile,
4110
4113
  setupNeeded: state.setup.needed,
4111
4114
  restartPending: state.restart !== null,
4115
+ busy,
4112
4116
  brief: {
4113
4117
  generatedAt: state.brief.generatedAt,
4114
4118
  ageHours: state.brief.ageHours,
@@ -4692,7 +4696,7 @@ ${entry.source}`)).map(({ title, source }) => ({ title, source })).sort((a, b) =
4692
4696
  return;
4693
4697
  }
4694
4698
  if (path === "/api/status" && req.method === "GET") {
4695
- sendJSON(res, 200, statusOf(await buildState(), config));
4699
+ sendJSON(res, 200, statusOf(await buildState(), config, agent.busy || assistant.busy));
4696
4700
  return;
4697
4701
  }
4698
4702
  if (path === "/api/health" && req.method === "GET") {
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  loadConfig
3
- } from "./chunk-G4K24I4M.js";
3
+ } from "./chunk-IFAGJFOT.js";
4
4
  import {
5
5
  IN_NPX_CACHE,
6
6
  PACKAGED,
7
7
  ROOT,
8
8
  command,
9
9
  ranDirectly
10
- } from "./chunk-6VWWJZKJ.js";
11
- import "./chunk-EXGTFXNV.js";
10
+ } from "./chunk-BC2W7NND.js";
11
+ import "./chunk-K2VT4JBR.js";
12
12
 
13
13
  // scripts/service.ts
14
14
  import { execFile } from "node:child_process";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daily-focus",
3
- "version": "0.1.1-dev.9",
3
+ "version": "0.2.0-dev.14",
4
4
  "description": "An interactive dashboard of what you should focus on today at work",
5
5
  "keywords": [
6
6
  "dashboard",
@@ -38,6 +38,10 @@
38
38
  "engines": {
39
39
  "node": ">=22.18"
40
40
  },
41
+ "daily-focus": {
42
+ "appInterface": 1,
43
+ "appSource": "0f59ad2ffabffbfd1054778996c4ee337199c92c"
44
+ },
41
45
  "scripts": {
42
46
  "start": "node src/server.ts",
43
47
  "prestart": "node scripts/build.ts",
package/public/app.js CHANGED
@@ -1075,6 +1075,7 @@ function postListCalendars() {
1075
1075
  // client/state.ts
1076
1076
  var VIEWS = ["today", "board", "tickets", "settings"];
1077
1077
  var RUN_PREFIX = "run:";
1078
+ var PANEL_PREFIX = "panel:";
1078
1079
  var VIEW_KEY = "daily-focus:view";
1079
1080
  var UNATTENDED_SEEN_KEY = "daily-focus:unattended-seen";
1080
1081
  var AGENT_RUN_SEEN_KEY = "daily-focus:agent-run-seen";
@@ -1320,12 +1321,49 @@ function formatWeekday(value) {
1320
1321
  const d4 = parseDate(value);
1321
1322
  return d4 ? weekdayFmt.format(d4) : "";
1322
1323
  }
1323
- function localDateKey(d4) {
1324
+ var dayKeyFmt = null;
1325
+ var calendarZone;
1326
+ function setCalendarZone(timeZone) {
1327
+ if (timeZone === calendarZone) return;
1328
+ calendarZone = timeZone;
1329
+ try {
1330
+ dayKeyFmt = timeZone ? new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }) : null;
1331
+ } catch {
1332
+ dayKeyFmt = null;
1333
+ }
1334
+ }
1335
+ function dayKey(d4) {
1336
+ if (dayKeyFmt) {
1337
+ const parts = dayKeyFmt.formatToParts(d4);
1338
+ const part = (type) => parts.find((p5) => p5.type === type)?.value ?? "";
1339
+ return `${part("year")}-${part("month")}-${part("day")}`;
1340
+ }
1324
1341
  const m4 = String(d4.getMonth() + 1).padStart(2, "0");
1325
1342
  const day = String(d4.getDate()).padStart(2, "0");
1326
1343
  return `${d4.getFullYear()}-${m4}-${day}`;
1327
1344
  }
1345
+ function splitKey(key) {
1346
+ const [y4, m4, d4] = key.split("-").map(Number);
1347
+ return [y4, m4 - 1, d4];
1348
+ }
1349
+ function keyAt(ms) {
1350
+ return new Date(ms).toISOString().slice(0, 10);
1351
+ }
1352
+ function addDays(key, days) {
1353
+ const [y4, m4, d4] = splitKey(key);
1354
+ return keyAt(Date.UTC(y4, m4, d4 + days));
1355
+ }
1356
+ function addMonths(key, months) {
1357
+ const [y4, m4, d4] = splitKey(key);
1358
+ const lastDay = new Date(Date.UTC(y4, m4 + months + 1, 0)).getUTCDate();
1359
+ return keyAt(Date.UTC(y4, m4 + months, Math.min(d4, lastDay)));
1360
+ }
1328
1361
  function daysFromToday(value, now = /* @__PURE__ */ new Date()) {
1362
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
1363
+ const [y4, m4, d4] = splitKey(value);
1364
+ const [ty, tm, td] = splitKey(dayKey(now));
1365
+ return Math.round((Date.UTC(y4, m4, d4) - Date.UTC(ty, tm, td)) / 864e5);
1366
+ }
1329
1367
  const target = parseDate(value);
1330
1368
  if (!target) return null;
1331
1369
  const a4 = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
@@ -2342,7 +2380,7 @@ function renderBanners(state2, ui2, handlers2) {
2342
2380
  )
2343
2381
  );
2344
2382
  }
2345
- if (!running && agent?.last && agent.last.status !== "done" && agent.last.id !== ui2.agentReportSeen.value && localDateKey(parseDate(agent.last.startedAt) ?? /* @__PURE__ */ new Date(0)) === localDateKey(new Date(state2.now))) {
2383
+ if (!running && agent?.last && agent.last.status !== "done" && agent.last.id !== ui2.agentReportSeen.value && dayKey(parseDate(agent.last.startedAt) ?? /* @__PURE__ */ new Date(0)) === dayKey(new Date(state2.now))) {
2346
2384
  banners.push(agentRunBanner(agent.last, state2, handlers2));
2347
2385
  }
2348
2386
  for (const warning of state2.setupWarnings ?? []) {
@@ -2402,8 +2440,8 @@ function agentRunBanner(run, state2, handlers2) {
2402
2440
  }
2403
2441
  function isHeldOverDayOff(state2) {
2404
2442
  if (state2.brief.generatedAt === null || state2.schedule.runsToday) return false;
2405
- const today = localDateKey(new Date(state2.now));
2406
- const briefDate = state2.brief.date ?? localDateKey(parseDate(state2.brief.generatedAt) ?? new Date(state2.now));
2443
+ const today = dayKey(new Date(state2.now));
2444
+ const briefDate = state2.brief.date ?? dayKey(parseDate(state2.brief.generatedAt) ?? new Date(state2.now));
2407
2445
  return briefDate !== today;
2408
2446
  }
2409
2447
  function nextRunPhraseFor(agent) {
@@ -2653,6 +2691,7 @@ function renderActions(item, state2, ui2, handlers2) {
2653
2691
  {
2654
2692
  type: "button",
2655
2693
  class: "button",
2694
+ "aria-haspopup": "menu",
2656
2695
  "aria-expanded": String(ui2.menuFor.value === item.id),
2657
2696
  onClick: () => handlers2.toggleMenu(item.id)
2658
2697
  },
@@ -2667,18 +2706,19 @@ function renderActions(item, state2, ui2, handlers2) {
2667
2706
  return el("div", { class: "item__actions" }, buttons);
2668
2707
  }
2669
2708
  function renderSnoozeMenu(item, now, handlers2, { indefinite = true, longRange = false } = {}) {
2709
+ const today = dayKey(now);
2670
2710
  const presets = [
2671
- ["Tomorrow", addDays(now, 1), false],
2672
- ["In 3 days", addDays(now, 3), false],
2673
- ["Next week", addDays(now, 7), false]
2711
+ ["Tomorrow", addDays(today, 1), false],
2712
+ ["In 3 days", addDays(today, 3), false],
2713
+ ["Next week", addDays(today, 7), false]
2674
2714
  ];
2675
2715
  if (longRange) {
2676
- presets.push(["Next month", addMonths(now, 1), true], ["Next quarter", addMonths(now, 3), true]);
2716
+ presets.push(["Next month", addMonths(today, 1), true], ["Next quarter", addMonths(today, 3), true]);
2677
2717
  }
2678
2718
  let dateInput = null;
2679
- return el(
2680
- "div",
2681
- { class: "menu", role: "menu" },
2719
+ return k(
2720
+ Menu,
2721
+ { class: "menu" },
2682
2722
  presets.map(
2683
2723
  ([label, until, dated]) => el(
2684
2724
  "button",
@@ -2727,14 +2767,13 @@ function renderSnoozeMenu(item, now, handlers2, { indefinite = true, longRange =
2727
2767
  )
2728
2768
  );
2729
2769
  }
2730
- function addDays(from, days) {
2731
- return localDateKey(new Date(from.getFullYear(), from.getMonth(), from.getDate() + days));
2732
- }
2733
- function addMonths(from, months) {
2734
- const target = new Date(from.getFullYear(), from.getMonth() + months, 1);
2735
- const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
2736
- target.setDate(Math.min(from.getDate(), lastDay));
2737
- return localDateKey(target);
2770
+ function Menu({ class: className, children }) {
2771
+ const ref = A2(null);
2772
+ _2(() => {
2773
+ const node = ref.current;
2774
+ if (node?.showPopover && !node.matches(":popover-open")) node.showPopover();
2775
+ });
2776
+ return el("div", { class: className, role: "menu", popover: "manual", ref }, children);
2738
2777
  }
2739
2778
 
2740
2779
  // client/refresh.ts
@@ -3069,6 +3108,7 @@ function renderPullActions(row, ui2, handlers2) {
3069
3108
  type: "button",
3070
3109
  class: "button",
3071
3110
  title: "Park this until a date",
3111
+ "aria-haspopup": "menu",
3072
3112
  "aria-expanded": String(ui2.menuFor.value === row.id),
3073
3113
  onClick: () => handlers2.toggleMenu(row.id)
3074
3114
  },
@@ -3303,31 +3343,41 @@ function TicketRowView({ item: row, state: state2, ui: ui2, handlers: handlers2
3303
3343
  renderTicketMeta(row, state2, handlers2)
3304
3344
  ),
3305
3345
  renderTicketActions(row, ui2, handlers2),
3306
- ui2.menuFor.value === row.id ? renderSnoozeMenu(row, now, handlers2, { indefinite: false, longRange: true }) : null
3346
+ ui2.menuFor.value === row.id ? renderSnoozeMenu(row, now, handlers2, PARK_MENU) : null
3307
3347
  );
3308
3348
  }
3309
- function renderTicketStatus(row, state2, ui2, handlers2) {
3310
- const offered = ticketStatusOptions(row, state2);
3311
- if (offered.length === 0) {
3312
- return el("div", { class: "item__meta item__meta--status" }, el("span", { class: "pill pill--status" }, row.workflowStatus || "\u2014"));
3313
- }
3349
+ var PARK_MENU = { indefinite: false, longRange: true };
3350
+ function renderTicketControls(row, state2, ui2, handlers2) {
3351
+ const menuKey = `${PANEL_PREFIX}${row.id}`;
3314
3352
  return el(
3315
3353
  "div",
3316
- { class: "item__meta item__meta--status" },
3317
- el(
3318
- "button",
3319
- {
3320
- type: "button",
3321
- class: "pill pill--status pill--button",
3322
- title: `Move ${row.key} to another status`,
3323
- "aria-expanded": String(ui2.statusFor.value === row.id),
3324
- onClick: () => handlers2.toggleStatus(row.id)
3325
- },
3326
- row.workflowStatus || "\u2014"
3327
- ),
3328
- ui2.statusFor.value === row.id ? renderStatusMenu(row, offered, ui2, handlers2) : null
3354
+ { class: "flyout__actions" },
3355
+ statusControl(row, state2, ui2, handlers2, menuKey),
3356
+ flagged(row) ? parkButton(row, ui2, handlers2, menuKey) : null,
3357
+ flagged(row) && ui2.menuFor.value === menuKey ? renderSnoozeMenu(row, new Date(state2.now), handlers2, PARK_MENU) : null
3329
3358
  );
3330
3359
  }
3360
+ function renderTicketStatus(row, state2, ui2, handlers2) {
3361
+ return el("div", { class: "item__meta item__meta--status" }, statusControl(row, state2, ui2, handlers2, row.id));
3362
+ }
3363
+ function statusControl(row, state2, ui2, handlers2, menuKey) {
3364
+ const offered = ticketStatusOptions(row, state2);
3365
+ if (offered.length === 0) return [el("span", { class: "pill pill--status" }, row.workflowStatus || "\u2014")];
3366
+ const open = ui2.statusFor.value === menuKey;
3367
+ const button = el(
3368
+ "button",
3369
+ {
3370
+ type: "button",
3371
+ class: "pill pill--status pill--button",
3372
+ title: `Move ${row.key} to another status`,
3373
+ "aria-haspopup": "menu",
3374
+ "aria-expanded": String(open),
3375
+ onClick: () => handlers2.toggleStatus(menuKey)
3376
+ },
3377
+ row.workflowStatus || "\u2014"
3378
+ );
3379
+ return open ? [button, renderStatusMenu(row, offered, ui2, handlers2)] : [button];
3380
+ }
3331
3381
  function ticketStatusOptions(row, state2) {
3332
3382
  const project = /^([A-Za-z][A-Za-z0-9_]*)-\d+$/.exec(row.key.trim())?.[1]?.toUpperCase();
3333
3383
  const all = project && state2.tickets?.statuses?.[project] || [];
@@ -3336,9 +3386,9 @@ function ticketStatusOptions(row, state2) {
3336
3386
  }
3337
3387
  function renderStatusMenu(row, offered, ui2, handlers2) {
3338
3388
  const busy = ui2.pending.value.has(row.id);
3339
- return el(
3340
- "div",
3341
- { class: "menu menu--status", role: "menu" },
3389
+ return k(
3390
+ Menu,
3391
+ { class: "menu menu--status" },
3342
3392
  offered.map(
3343
3393
  (status) => el(
3344
3394
  "button",
@@ -3380,25 +3430,24 @@ function outOfSyncFlag(row, state2, handlers2) {
3380
3430
  }
3381
3431
  function renderTicketActions(row, ui2, handlers2) {
3382
3432
  if (!flagged(row)) return null;
3383
- const buttons = [];
3384
- if (row.status === "open") {
3385
- buttons.push(
3386
- el(
3387
- "button",
3388
- {
3389
- type: "button",
3390
- class: "button",
3391
- title: "Park this until a date",
3392
- "aria-expanded": String(ui2.menuFor.value === row.id),
3393
- onClick: () => handlers2.toggleMenu(row.id)
3394
- },
3395
- "Park"
3396
- )
3397
- );
3398
- } else {
3399
- buttons.push(el("button", { type: "button", class: "button", onClick: () => handlers2.unpark(row.id) }, "Unpark"));
3433
+ return el("div", { class: "item__actions" }, parkButton(row, ui2, handlers2, row.id));
3434
+ }
3435
+ function parkButton(row, ui2, handlers2, menuKey) {
3436
+ if (row.status !== "open") {
3437
+ return el("button", { type: "button", class: "button", onClick: () => handlers2.unpark(row.id) }, "Unpark");
3400
3438
  }
3401
- return buttons.length > 0 ? el("div", { class: "item__actions" }, buttons) : null;
3439
+ return el(
3440
+ "button",
3441
+ {
3442
+ type: "button",
3443
+ class: "button",
3444
+ title: "Park this until a date",
3445
+ "aria-haspopup": "menu",
3446
+ "aria-expanded": String(ui2.menuFor.value === menuKey),
3447
+ onClick: () => handlers2.toggleMenu(menuKey)
3448
+ },
3449
+ "Park"
3450
+ );
3402
3451
  }
3403
3452
 
3404
3453
  // client/chrome.ts
@@ -3502,8 +3551,8 @@ function Header({ state: state2, ui: ui2, handlers: handlers2 }) {
3502
3551
  );
3503
3552
  }
3504
3553
  function headerDate(state2) {
3505
- const now = new Date(state2.now);
3506
- return formatDay(state2.brief.date ?? state2.now) || formatDay(now.toISOString());
3554
+ const today = dayKey(new Date(state2.now));
3555
+ return formatDay(state2.brief.date ?? today) || formatDay(today);
3507
3556
  }
3508
3557
  function freshness(state2, handlers2) {
3509
3558
  let text;
@@ -3960,7 +4009,7 @@ function RowPanel({ row, state: state2, ui: ui2, handlers: handlers2 }) {
3960
4009
  const enabled = Boolean(state2.assistant?.enabled);
3961
4010
  const entry = state2.assistant?.items?.[row.id] ?? { running: false, sessionId: null, turns: [] };
3962
4011
  return [
3963
- flyoutHeader(facts, handlers2),
4012
+ flyoutHeader(facts, rowControls(row, state2, ui2, handlers2), handlers2),
3964
4013
  el(
3965
4014
  "div",
3966
4015
  { class: "flyout__body", ref: body, onScroll },
@@ -3995,7 +4044,6 @@ function describeRow(row) {
3995
4044
  url: row.url ?? null,
3996
4045
  source: "jira",
3997
4046
  color: typeColor(row.issueType),
3998
- status: row.workflowStatus || null,
3999
4047
  people: [],
4000
4048
  detail: null
4001
4049
  };
@@ -4007,7 +4055,6 @@ function describeRow(row) {
4007
4055
  url: row.url ?? null,
4008
4056
  source: "github",
4009
4057
  color: sourceColor("github"),
4010
- status: null,
4011
4058
  people: [],
4012
4059
  detail: null
4013
4060
  };
@@ -4018,12 +4065,14 @@ function describeRow(row) {
4018
4065
  url: row.url ?? null,
4019
4066
  source: row.source,
4020
4067
  color: sourceColor(row.source),
4021
- status: null,
4022
4068
  people: row.people ?? [],
4023
4069
  detail: row.detail ?? null
4024
4070
  };
4025
4071
  }
4026
- function flyoutHeader(facts, handlers2) {
4072
+ function rowControls(row, state2, ui2, handlers2) {
4073
+ return "key" in row && "summary" in row ? renderTicketControls(row, state2, ui2, handlers2) : null;
4074
+ }
4075
+ function flyoutHeader(facts, controls, handlers2) {
4027
4076
  const title = facts.ref ? [el("span", { class: "item__ref" }, facts.ref), " ", facts.title] : [facts.title];
4028
4077
  return el(
4029
4078
  "div",
@@ -4033,7 +4082,6 @@ function flyoutHeader(facts, handlers2) {
4033
4082
  { class: "flyout__where" },
4034
4083
  el("span", { class: "item__dot", style: `background:${facts.color}`, "aria-hidden": "true" }),
4035
4084
  el("span", { class: "item__source" }, SOURCE_LABEL[facts.source] ?? "Other"),
4036
- facts.status ? el("span", null, `\xB7 ${facts.status}`) : null,
4037
4085
  facts.people.length ? el("span", null, `\xB7 ${facts.people.join(", ")}`) : null
4038
4086
  ),
4039
4087
  el(
@@ -4051,7 +4099,8 @@ function flyoutHeader(facts, handlers2) {
4051
4099
  "p",
4052
4100
  { class: "flyout__title" },
4053
4101
  facts.url ? el("a", { href: facts.url, target: "_blank", rel: "noopener noreferrer" }, ...title) : title
4054
- )
4102
+ ),
4103
+ controls
4055
4104
  );
4056
4105
  }
4057
4106
  function NoteForm({
@@ -4924,9 +4973,7 @@ function selectedItem() {
4924
4973
  return list.find((entry) => entry.id === id) ?? null;
4925
4974
  }
4926
4975
  function tomorrow() {
4927
- const d4 = /* @__PURE__ */ new Date();
4928
- d4.setDate(d4.getDate() + 1);
4929
- return localDateKey(d4);
4976
+ return addDays(dayKey(/* @__PURE__ */ new Date()), 1);
4930
4977
  }
4931
4978
  function installKeyboard(handlers2) {
4932
4979
  document.addEventListener("keydown", (event) => {
@@ -5050,6 +5097,7 @@ function installKeyboard(handlers2) {
5050
5097
 
5051
5098
  // client/main.ts
5052
5099
  ui.theme.value = stampedTheme();
5100
+ j3(() => setCalendarZone(state.value?.timeZone));
5053
5101
  var root = document.getElementById("app");
5054
5102
  if (!root) throw new Error("index.html has no #app to render into");
5055
5103
  R(k(App, { ui, handlers }), root);
package/public/style.css CHANGED
@@ -1152,14 +1152,29 @@ body[data-session-active='true'] #today .item:not([data-running='true']):focus-w
1152
1152
 
1153
1153
  /* snooze menu */
1154
1154
 
1155
+ /*
1156
+ * A popover in the top layer (`Menu` in client/items.ts says why), hung from the
1157
+ * button that opened it. Only one menu is ever open, so only one button is
1158
+ * expanded and the one anchor name is never ambiguous. `inset` and `margin`
1159
+ * undo the popover's own centring on the viewport.
1160
+ */
1161
+ [aria-haspopup='menu'][aria-expanded='true'] {
1162
+ anchor-name: --menu;
1163
+ }
1164
+
1155
1165
  .menu {
1156
- position: absolute;
1157
- top: 34px;
1158
- right: 10px;
1159
- z-index: 5;
1166
+ position: fixed;
1167
+ position-anchor: --menu;
1168
+ inset: auto;
1169
+ top: anchor(bottom);
1170
+ right: anchor(right);
1171
+ margin: 4px 0;
1172
+ /* Above the button instead, when the window has no room left below it. */
1173
+ position-try-fallbacks: flip-block;
1160
1174
  /* Wide enough that the date field below still shows its own placeholder. */
1161
1175
  min-width: 204px;
1162
1176
  padding: 5px;
1177
+ color: var(--ink-2);
1163
1178
  background: var(--surface);
1164
1179
  border: 1px solid var(--border);
1165
1180
  border-radius: var(--radius-sm);
@@ -1636,10 +1651,11 @@ body[data-view='settings'] .layout {
1636
1651
  * line up with the other tabs' however wide the display is. The 460px floor is
1637
1652
  * what drops it to one column on a narrow window rather than squeezing two.
1638
1653
  *
1639
- * The abspos worry that picked the grid does not apply: `.item` is
1640
- * `position: relative`, so `.item__actions` and the snooze `.menu` resolve
1641
- * against the card and travel with it into its column. Only a child resolving
1642
- * against the multicol container itself would land beside the wrong row.
1654
+ * An absolutely positioned child is still fragmented by the columns, and counts
1655
+ * toward balancing them. `.item__actions` stays inside its card, so it travels
1656
+ * with the card into its column and nothing notices. Anything that hangs below a
1657
+ * card must not be in here at all: the row menus once were, and were split
1658
+ * across both columns and pushed the next section down. They are popovers now.
1643
1659
  */
1644
1660
  #tickets .list {
1645
1661
  display: block;
@@ -1836,14 +1852,13 @@ body[data-view='settings'] .layout {
1836
1852
  }
1837
1853
 
1838
1854
  /*
1839
- * Anchored to the gutter rather than the card's right edge, unlike the park
1840
- * menu: it belongs to the pill that opened it, and the two menus must not be
1841
- * able to appear in the same place.
1855
+ * Hung from the pill's left edge, where the park menu hangs from its button's
1856
+ * right: the pill sits in the card's left gutter, so each menu opens toward the
1857
+ * middle of its own card.
1842
1858
  */
1843
1859
  .menu--status {
1844
- top: 26px;
1845
1860
  right: auto;
1846
- left: 0;
1861
+ left: anchor(left);
1847
1862
  min-width: 9rem;
1848
1863
  }
1849
1864
 
@@ -2073,6 +2088,23 @@ body[data-flyout='open'] .layout__side {
2073
2088
  text-decoration-color: var(--accent);
2074
2089
  }
2075
2090
 
2091
+ /*
2092
+ * A ticket's status and park, laid out as on its card: the status at the left,
2093
+ * the park at the right, so each menu opens into the panel rather than out over
2094
+ * the list. Under the close button too, so the park lines up with it. A child
2095
+ * combinator, because the park menu's own Set button is a `.button` in here.
2096
+ */
2097
+ .flyout__actions {
2098
+ grid-column: 1 / -1;
2099
+ display: flex;
2100
+ align-items: center;
2101
+ gap: 8px;
2102
+ margin-top: 4px;
2103
+ }
2104
+ .flyout__actions > .button {
2105
+ margin-left: auto;
2106
+ }
2107
+
2076
2108
  .flyout__body {
2077
2109
  flex: 1;
2078
2110
  min-height: 0;
@@ -1,7 +0,0 @@
1
- import {
2
- sampleBrief
3
- } from "./chunk-NDWZUZJE.js";
4
- import "./chunk-EXGTFXNV.js";
5
- export {
6
- sampleBrief
7
- };