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 +2 -1
- package/SETUP.md +14 -0
- package/dist/{audit-TPBQFSL3.js → audit-DTXVAT5Z.js} +4 -4
- package/dist/{chunk-6VWWJZKJ.js → chunk-BC2W7NND.js} +17 -2
- package/dist/{chunk-G4K24I4M.js → chunk-IFAGJFOT.js} +2 -2
- package/dist/{chunk-EXGTFXNV.js → chunk-K2VT4JBR.js} +4 -0
- package/dist/{chunk-CQ53JYUU.js → chunk-NAD6PRZT.js} +5 -3
- package/dist/{chunk-NDWZUZJE.js → chunk-UAR65OCG.js} +1 -1
- package/dist/cli.js +11 -17
- package/dist/{init-ZJCI2723.js → init-BMFPWMXL.js} +3 -3
- package/dist/sample-QDEXFFRY.js +7 -0
- package/dist/{seed-S5ZU6EIO.js → seed-GCT7Z3IV.js} +5 -5
- package/dist/{server-O5GF3TRI.js → server-ANIS2MXU.js} +10 -6
- package/dist/{service-ILBMIZB7.js → service-NEQVCWI5.js} +3 -3
- package/package.json +5 -1
- package/public/app.js +119 -71
- package/public/style.css +45 -13
- package/dist/sample-M7WNKQOV.js +0 -7
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
|
|
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-
|
|
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-
|
|
20
|
+
} from "./chunk-IFAGJFOT.js";
|
|
21
21
|
import {
|
|
22
22
|
command
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-BC2W7NND.js";
|
|
24
24
|
import {
|
|
25
25
|
workingMsBetween
|
|
26
|
-
} from "./chunk-
|
|
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
|
-
|
|
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-
|
|
4
|
+
} from "./chunk-BC2W7NND.js";
|
|
5
5
|
import {
|
|
6
6
|
isSameLocalDay,
|
|
7
7
|
localDateKey,
|
|
8
8
|
parseISO,
|
|
9
9
|
startOfLocalDay
|
|
10
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
};
|
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
|
-
|
|
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 {
|
|
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-
|
|
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-
|
|
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(
|
|
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-
|
|
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-
|
|
178
|
+
await import("./init-BMFPWMXL.js");
|
|
185
179
|
return;
|
|
186
180
|
case "audit":
|
|
187
|
-
await import("./audit-
|
|
181
|
+
await import("./audit-DTXVAT5Z.js");
|
|
188
182
|
return;
|
|
189
183
|
case "seed":
|
|
190
|
-
await import("./seed-
|
|
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-
|
|
11
|
+
} from "./chunk-IFAGJFOT.js";
|
|
12
12
|
import {
|
|
13
13
|
PACKAGED,
|
|
14
14
|
command
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
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";
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sampleBrief
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UAR65OCG.js";
|
|
4
4
|
import {
|
|
5
5
|
Store
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-NAD6PRZT.js";
|
|
7
7
|
import {
|
|
8
8
|
loadConfig
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IFAGJFOT.js";
|
|
10
10
|
import {
|
|
11
11
|
command
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
33
|
+
} from "./chunk-IFAGJFOT.js";
|
|
34
34
|
import {
|
|
35
35
|
PACKAGED,
|
|
36
36
|
command,
|
|
37
|
+
packageVersion,
|
|
37
38
|
ranDirectly
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-BC2W7NND.js";
|
|
39
40
|
import {
|
|
40
41
|
laterISO,
|
|
41
42
|
parseISO
|
|
42
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
11
|
-
import "./chunk-
|
|
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.
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
2406
|
-
const briefDate = state2.brief.date ??
|
|
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(
|
|
2672
|
-
["In 3 days", addDays(
|
|
2673
|
-
["Next week", addDays(
|
|
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(
|
|
2716
|
+
presets.push(["Next month", addMonths(today, 1), true], ["Next quarter", addMonths(today, 3), true]);
|
|
2677
2717
|
}
|
|
2678
2718
|
let dateInput = null;
|
|
2679
|
-
return
|
|
2680
|
-
|
|
2681
|
-
{ class: "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
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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,
|
|
3346
|
+
ui2.menuFor.value === row.id ? renderSnoozeMenu(row, now, handlers2, PARK_MENU) : null
|
|
3307
3347
|
);
|
|
3308
3348
|
}
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
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: "
|
|
3317
|
-
|
|
3318
|
-
|
|
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
|
|
3340
|
-
|
|
3341
|
-
{ class: "menu menu--status"
|
|
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
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
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
|
|
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
|
|
3506
|
-
return formatDay(state2.brief.date ??
|
|
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
|
|
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
|
-
|
|
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:
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
*
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
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
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
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:
|
|
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;
|