daily-focus 2.0.0-dev.14 → 2.0.0-dev.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daily-focus",
3
- "version": "2.0.0-dev.14",
3
+ "version": "2.0.0-dev.16",
4
4
  "description": "An interactive dashboard of what you should focus on today at work",
5
5
  "keywords": [
6
6
  "dashboard",
@@ -28,6 +28,7 @@
28
28
  "public/index.html",
29
29
  "public/style.css",
30
30
  "public/app.js",
31
+ "public/fonts/",
31
32
  "prompts/",
32
33
  "schema/",
33
34
  "tools/dfcal/build.sh",
@@ -66,5 +67,8 @@
66
67
  "happy-dom": "20.14.5",
67
68
  "preact": "10.29.8",
68
69
  "typescript": "7.0.2"
70
+ },
71
+ "allowScripts": {
72
+ "esbuild": true
69
73
  }
70
74
  }
@@ -340,7 +340,7 @@ Then the general editorial bar:
340
340
  {
341
341
  "version": 1,
342
342
  "generatedAt": "<ISO 8601 timestamp, with offset>",
343
- "generatedBy": "<name yourself — your model or agent id>",
343
+ "generatedBy": "<your model or agent id, such as codex, and nothing else>",
344
344
  "date": "<today, YYYY-MM-DD>",
345
345
  "headline": "<one or two sentences, or omit>",
346
346
  "items": [ … ]
@@ -418,7 +418,9 @@ Check all of these, and fix anything that fails:
418
418
  6. Every `url` starts with `http://` or `https://`, and came from a tool rather than
419
419
  being built by you.
420
420
  7. Every `start`/`end` has a timezone offset, unless deliberately all-day.
421
- 8. `generatedAt` is now, and `generatedBy` names whoever ran.
421
+ 8. `generatedAt` is now, and `generatedBy` names whoever ran — your model or agent
422
+ id and nothing else: not a working directory, a path or an account. The dashboard
423
+ prints it after "updated … by" in its header.
422
424
  9. **When `focus.md` sets an objective, at least one item offers a concrete path to
423
425
  it** — a real one you found, or the synthesised one. With no objective, no item has
424
426
  `advancesObjective`.
@@ -512,7 +512,7 @@ Then the general editorial bar:
512
512
  {
513
513
  "version": 1,
514
514
  "generatedAt": "<ISO 8601 timestamp, with offset>",
515
- "generatedBy": "<name yourself — your model or agent id>",
515
+ "generatedBy": "<your model or agent id, such as codex, and nothing else>",
516
516
  "date": "<today, YYYY-MM-DD>",
517
517
  "headline": "<one or two sentences, or omit>",
518
518
  "dayStart": "09:00",
@@ -601,7 +601,9 @@ Check all of these, and fix anything that fails:
601
601
  6. Every `url` starts with `http://` or `https://`.
602
602
  7. Every `start`/`end` has a timezone offset, unless deliberately all-day.
603
603
  8. `generatedAt` is now, and `generatedBy` names whoever ran — your model or agent
604
- id, so the dashboard can show who produced the brief.
604
+ id and nothing else: not a working directory, a path or an account. The dashboard
605
+ prints it after "updated … by" in its header, so `codex` reads well there and
606
+ `codex (/root)` does not.
605
607
  9. **When `focus.md` sets an objective, at least one item offers a concrete path to
606
608
  it** — a real one you found, or the synthesised "no path today" item. A brief with
607
609
  no route to the objective and no acknowledgement of that fact is a failed run.
package/public/app.js CHANGED
@@ -2480,10 +2480,11 @@ function agentRunBanner(run, state2, handlers2) {
2480
2480
  };
2481
2481
  if (run.status === "running") {
2482
2482
  const helping = (run.steps ?? []).filter((step) => step.subagent && step.state === "running").length;
2483
+ const doing = briefLanded(run, state2.brief?.generatedAt) ? "The brief is in. The morning agent is checking it and writing its report" : "The morning agent is writing a new brief";
2483
2484
  return bannerNode(
2484
2485
  "info",
2485
2486
  "i",
2486
- `The morning agent is writing a new brief, started ${relativeTime(run.startedAt, now)}` + (helping ? `, with ${helping} ${helping === 1 ? "subagent" : "subagents"} at work.` : "."),
2487
+ `${doing}, started ${relativeTime(run.startedAt, now)}` + (helping ? `, with ${helping} ${helping === 1 ? "subagent" : "subagents"} at work.` : "."),
2487
2488
  [el("button", { type: "button", class: "button banner__action", onClick: () => handlers2.stopAgent() }, "Stop")],
2488
2489
  strip
2489
2490
  );
@@ -2504,6 +2505,11 @@ function agentRunBanner(run, state2, handlers2) {
2504
2505
  strip
2505
2506
  );
2506
2507
  }
2508
+ function briefLanded(run, generatedAt) {
2509
+ const started = parseDate(run.startedAt)?.getTime();
2510
+ const landed = generatedAt ? parseDate(generatedAt)?.getTime() : void 0;
2511
+ return started !== void 0 && landed !== void 0 && landed >= started;
2512
+ }
2507
2513
  function isHeldOverDayOff(state2) {
2508
2514
  if (state2.brief.generatedAt === null || state2.schedule.runsToday) return false;
2509
2515
  const today = dayKey(new Date(state2.now));
@@ -2640,7 +2646,11 @@ function ItemRow({ item, state: state2, ui: ui2, handlers: handlers2 }) {
2640
2646
  },
2641
2647
  el(
2642
2648
  "span",
2643
- { class: "item__mark" },
2649
+ // A ranked row's number stands in the margin where the source dot would be:
2650
+ // the stylesheet draws it off `data-rank` and hides the dot, since the
2651
+ // source is named in the meta line anyway and the rank is what this row
2652
+ // is for.
2653
+ { class: "item__mark", "data-rank": item.priority !== void 0 && item.status === "open" ? String(item.priority) : void 0 },
2644
2654
  el("span", {
2645
2655
  class: "item__dot",
2646
2656
  style: `background:${sourceColor(item.source)}`,
@@ -2666,9 +2676,6 @@ function renderMeta(item, state2, now) {
2666
2676
  pills.push(el("span", { class: cls }, text));
2667
2677
  said.add(text.trim().toLowerCase());
2668
2678
  };
2669
- if (item.priority !== void 0 && item.status === "open") {
2670
- pill("pill pill--priority", `#${item.priority}`);
2671
- }
2672
2679
  for (const [cls, text] of rowPillTexts(item, state2)) pill(cls, text);
2673
2680
  if (item.advancesObjective) {
2674
2681
  pill("pill pill--objective", "objective");
@@ -3730,7 +3737,7 @@ function briefRefresh(state2, handlers2) {
3730
3737
  const agent = state2.agentRun;
3731
3738
  if (!agent?.enabled) return null;
3732
3739
  const running = agent.last?.status === "running";
3733
- const label = running ? `The morning agent is writing a new brief, started ${formatTime(agent.last?.startedAt)}` : `Run the morning agent now, for a fresh brief${nextRunPhraseFor(agent)}`;
3740
+ const label = running ? agent.last && briefLanded(agent.last, state2.brief.generatedAt) ? `The brief is in; the morning agent is checking it and writing its report, started ${formatTime(agent.last.startedAt)}` : `The morning agent is writing a new brief, started ${formatTime(agent.last?.startedAt)}` : `Run the morning agent now, for a fresh brief${nextRunPhraseFor(agent)}`;
3734
3741
  return el(
3735
3742
  "button",
3736
3743
  {
@@ -3897,8 +3904,8 @@ function RunPanel({ run, state: state2, ui: ui2, handlers: handlers2 }) {
3897
3904
  el(
3898
3905
  "div",
3899
3906
  { class: "flyout__body", ref: body, onScroll },
3900
- runStatus(run, now, handlers2),
3901
- runReport(run, ui2, handlers2),
3907
+ run.status === "running" ? null : runStatus(run, now, handlers2),
3908
+ runReport(run, { landed: briefLanded(run, state2.brief?.generatedAt), now }, ui2, handlers2),
3902
3909
  run.status === "running" ? null : runConversation(run, busy, answering, enabled, ui2, handlers2, askInput),
3903
3910
  runList(run, runs, now, agent, handlers2)
3904
3911
  )
@@ -3935,8 +3942,8 @@ function describeWhen(value, now) {
3935
3942
  const named = day === "today" || day === "tomorrow" || day === "yesterday";
3936
3943
  return `${named ? day : date && date.getFullYear() === now.getFullYear() ? formatShortDay(value) : formatDay(value)} at ${formatTime(value)}`;
3937
3944
  }
3938
- function runReport(run, ui2, handlers2) {
3939
- if (run.steps?.length) return runWork(run, ui2, handlers2);
3945
+ function runReport(run, live, ui2, handlers2) {
3946
+ if (run.steps?.length) return runWork(run, live, ui2, handlers2);
3940
3947
  const messages = run.messages ?? (run.report ? [run.report] : []);
3941
3948
  const running = run.status === "running";
3942
3949
  const earlier = !running && messages.length && messages.at(-1) === run.report ? messages.slice(0, -1) : messages;
@@ -3975,12 +3982,9 @@ function runReport(run, ui2, handlers2) {
3975
3982
  })
3976
3983
  ) : null;
3977
3984
  if (running) {
3978
- return el(
3979
- "section",
3980
- { class: "flyout__section" },
3981
- el("h3", { class: "flyout__label" }, "What it is doing"),
3982
- list ?? el("p", { class: "flyout__empty" }, "Nothing yet.")
3983
- );
3985
+ const latest = earlier.at(-1);
3986
+ const summary = `${latest ? firstLine(latest) : "Starting"} \xB7 ${earlier.length} ${earlier.length === 1 ? "message" : "messages"}`;
3987
+ return liveSection(run, live, summary, [list, el("ol", { class: "assistant__steps agent-run__steps" }, waitingLine(earlier.length > 0))], ui2, handlers2);
3984
3988
  }
3985
3989
  return el(
3986
3990
  "section",
@@ -3995,16 +3999,9 @@ function runReport(run, ui2, handlers2) {
3995
3999
  ) : null
3996
4000
  );
3997
4001
  }
3998
- function runWork(run, ui2, handlers2) {
4002
+ function runWork(run, live, ui2, handlers2) {
3999
4003
  const running = run.status === "running";
4000
- if (running) {
4001
- return el(
4002
- "section",
4003
- { class: "flyout__section" },
4004
- el("h3", { class: "flyout__label" }, "What it is doing"),
4005
- runSteps(run, run.steps, null, ui2, handlers2)
4006
- );
4007
- }
4004
+ if (running) return liveSection(run, live, workSummary(run), runSteps(run, run.steps, null, true, ui2, handlers2), ui2, handlers2);
4008
4005
  const last = run.steps.findLast((step) => step.by === null && step.kind === "message");
4009
4006
  const steps2 = last && sameWords(last.text, run.report) ? run.steps.filter((step) => step !== last) : run.steps;
4010
4007
  const handoffs = steps2.filter((step) => step.subagent).length;
@@ -4021,29 +4018,122 @@ function runWork(run, ui2, handlers2) {
4021
4018
  null,
4022
4019
  `How it got there, in ${steps2.length} ${steps2.length === 1 ? "step" : "steps"}` + (handoffs ? `, with ${handoffs} ${handoffs === 1 ? "subagent" : "subagents"}` : "")
4023
4020
  ),
4024
- runSteps(run, steps2, null, ui2, handlers2)
4021
+ runSteps(run, steps2, null, false, ui2, handlers2)
4025
4022
  ) : null
4026
4023
  );
4027
4024
  }
4025
+ function liveSection(run, live, summary, content, ui2, handlers2) {
4026
+ const phase = live.landed ? "The brief is in; checking it and writing its report" : "Writing a new brief";
4027
+ return el(
4028
+ "section",
4029
+ { class: "flyout__section" },
4030
+ el("h3", { class: "flyout__label" }, `${phase}, started ${relativeTime(run.startedAt, live.now)}`),
4031
+ el(
4032
+ "div",
4033
+ { class: "agent-run__live" },
4034
+ workFold(run, summary, content, ui2, handlers2),
4035
+ el("button", { type: "button", class: "button", onClick: () => handlers2.stopAgent() }, "Stop")
4036
+ )
4037
+ );
4038
+ }
4039
+ function workFold(run, summary, content, ui2, handlers2) {
4040
+ const key = `${run.id}:work`;
4041
+ return el(
4042
+ "details",
4043
+ {
4044
+ class: "agent-run__list agent-run__list--working",
4045
+ open: ui2.messageOpen.value.get(key) ?? false,
4046
+ onToggle: (event) => {
4047
+ const isOpen = event.currentTarget.open;
4048
+ handlers2.toggleMessage(key, isOpen ? true : null);
4049
+ }
4050
+ },
4051
+ el("summary", null, summary),
4052
+ content
4053
+ );
4054
+ }
4055
+ function workSummary(run) {
4056
+ const own = run.steps.filter((step) => step.by === null);
4057
+ const inFlight = own.findLast((step) => step.kind === "tool" && step.state === "running");
4058
+ const latest = own.at(-1);
4059
+ const working = run.steps.filter((step) => step.subagent && step.state === "running");
4060
+ const count = `${run.steps.length} ${run.steps.length === 1 ? "step" : "steps"}`;
4061
+ if (inFlight?.subagent) {
4062
+ return `Waiting on ${working.map((step) => step.detail ?? "a subagent").join(", ") || "a subagent"} \xB7 ${count}`;
4063
+ }
4064
+ const doing = inFlight ? inFlight.text : latest ? latest.kind === "message" ? firstLine(latest.text) : latest.text : "Starting";
4065
+ const helping = working.length;
4066
+ return `${doing} \xB7 ${count}${helping ? `, ${helping} ${helping === 1 ? "subagent" : "subagents"} at work` : ""}`;
4067
+ }
4068
+ function waitingLine(begun) {
4069
+ return el(
4070
+ "li",
4071
+ { class: "assistant__step assistant__step--waiting", "data-state": "running" },
4072
+ el("span", { class: "assistant__step-text" }, begun ? "Thinking\u2026" : "Starting\u2026")
4073
+ );
4074
+ }
4028
4075
  var NESTING = 4;
4029
- function runSteps(run, steps2, by, ui2, handlers2, depth = 0) {
4030
- const own = steps2.filter((step) => step.by === by);
4076
+ var CALLS_FOLD = 2;
4077
+ function runSteps(run, steps2, by, active, ui2, handlers2, depth = 0) {
4078
+ const own = steps2.map((step, index) => ({ step, index })).filter(({ step }) => step.by === by);
4079
+ const inFlight = own.some(({ step }) => step.state === "running");
4080
+ const items = [];
4081
+ let calls = [];
4082
+ const flush = () => {
4083
+ if (calls.length >= CALLS_FOLD) items.push(callsFold(run, by, calls, ui2, handlers2));
4084
+ else for (const placed of calls) items.push(callLine(placed));
4085
+ calls = [];
4086
+ };
4087
+ for (const placed of own) {
4088
+ const { step, index } = placed;
4089
+ const folds = step.subagent && depth < NESTING;
4090
+ if (step.kind === "tool" && !folds && step.state !== "running") {
4091
+ calls.push(placed);
4092
+ continue;
4093
+ }
4094
+ flush();
4095
+ if (step.kind === "message") items.push(el("li", { key: index, class: "agent-run__said" }, renderMarkdownBlocks(step.text)));
4096
+ else if (folds) items.push(runHandoff(run, steps2, step, ui2, handlers2, depth, index));
4097
+ else items.push(callLine(placed));
4098
+ }
4099
+ flush();
4031
4100
  return el(
4032
4101
  "ol",
4033
4102
  { class: "assistant__steps agent-run__steps" },
4034
- own.length ? null : el("li", { class: "assistant__step assistant__step--message" }, "Nothing yet."),
4035
- own.map((step, index) => {
4036
- if (step.kind === "message") {
4037
- return el("li", { key: index, class: "agent-run__said" }, renderMarkdownBlocks(step.text));
4038
- }
4039
- if (step.subagent && depth < NESTING) return runHandoff(run, steps2, step, ui2, handlers2, depth, index);
4040
- return el(
4041
- "li",
4042
- { key: index, class: "assistant__step", "data-state": step.state },
4043
- el("span", { class: "assistant__step-text" }, step.text),
4044
- step.detail ? el("code", { class: "assistant__step-detail", title: step.detail }, step.detail) : null
4045
- );
4046
- })
4103
+ own.length || active ? null : el("li", { class: "assistant__step assistant__step--message" }, "Nothing yet."),
4104
+ items,
4105
+ active && !inFlight ? waitingLine(own.length > 0) : null
4106
+ );
4107
+ }
4108
+ function callLine({ step, index }) {
4109
+ return el(
4110
+ "li",
4111
+ { key: index, class: "assistant__step", "data-state": step.state },
4112
+ el("span", { class: "assistant__step-text" }, step.text),
4113
+ step.detail ? el("code", { class: "assistant__step-detail", title: step.detail }, step.detail) : null
4114
+ );
4115
+ }
4116
+ function callsFold(run, by, calls, ui2, handlers2) {
4117
+ const key = `${run.id}:${by ?? "run"}:calls@${calls[0].index}`;
4118
+ const failed = calls.filter(({ step }) => step.state === "failed").length;
4119
+ const names = [...new Set(calls.map(({ step }) => step.text))];
4120
+ const named = names.length <= 2 ? names.join(", ") : `${names.slice(0, 2).join(", ")} and ${names.length - 2} more`;
4121
+ const summary = `${calls.length} calls${failed ? `, ${failed} failed` : ""} \xB7 ${named}`;
4122
+ return el(
4123
+ "li",
4124
+ { key, class: "assistant__step agent-run__calls", "data-state": failed ? "failed" : "done" },
4125
+ el(
4126
+ "details",
4127
+ {
4128
+ open: ui2.messageOpen.value.get(key) ?? false,
4129
+ onToggle: (event) => {
4130
+ const isOpen = event.currentTarget.open;
4131
+ handlers2.toggleMessage(key, isOpen ? true : null);
4132
+ }
4133
+ },
4134
+ el("summary", null, summary),
4135
+ el("ol", { class: "assistant__steps agent-run__steps" }, calls.map((placed) => callLine(placed)))
4136
+ )
4047
4137
  );
4048
4138
  }
4049
4139
  function runHandoff(run, steps2, step, ui2, handlers2, depth, index) {
@@ -4053,8 +4143,7 @@ function runHandoff(run, steps2, step, ui2, handlers2, depth, index) {
4053
4143
  const calls = own.filter((candidate) => candidate.kind === "tool").length;
4054
4144
  const doing = step.state === "running" ? latest ? latest.kind === "message" ? firstLine(latest.text) : latest.text : "starting" : `${calls} ${calls === 1 ? "step" : "steps"}`;
4055
4145
  const key = `${run.id}:${id}`;
4056
- const fallback = step.state === "running";
4057
- const open = ui2.messageOpen.value.has(key) ? ui2.messageOpen.value.get(key) : fallback;
4146
+ const open = ui2.messageOpen.value.get(key) ?? false;
4058
4147
  return el(
4059
4148
  "li",
4060
4149
  { key: index, class: "assistant__step agent-run__handoff", "data-state": step.state },
@@ -4065,7 +4154,7 @@ function runHandoff(run, steps2, step, ui2, handlers2, depth, index) {
4065
4154
  // As the message folds do: a toggle back to the default is forgotten.
4066
4155
  onToggle: (event) => {
4067
4156
  const isOpen = event.currentTarget.open;
4068
- handlers2.toggleMessage(key, isOpen === fallback ? null : isOpen);
4157
+ handlers2.toggleMessage(key, isOpen ? true : null);
4069
4158
  }
4070
4159
  },
4071
4160
  el(
@@ -4074,7 +4163,7 @@ function runHandoff(run, steps2, step, ui2, handlers2, depth, index) {
4074
4163
  el("span", { class: "assistant__step-text" }, `Subagent: ${step.detail ?? "unnamed"}`),
4075
4164
  el("span", { class: "assistant__step-detail" }, doing)
4076
4165
  ),
4077
- runSteps(run, steps2, id, ui2, handlers2, depth + 1)
4166
+ runSteps(run, steps2, id, step.state === "running", ui2, handlers2, depth + 1)
4078
4167
  )
4079
4168
  );
4080
4169
  }
@@ -4082,14 +4171,6 @@ function sameWords(a4, b4) {
4082
4171
  return a4.replace(/\s+/g, " ").trim() === b4.replace(/\s+/g, " ").trim();
4083
4172
  }
4084
4173
  function runStatus(run, now, handlers2) {
4085
- if (run.status === "running") {
4086
- return el(
4087
- "p",
4088
- { class: "agent-run__status" },
4089
- `Writing a new brief, started ${relativeTime(run.startedAt, now)}. `,
4090
- el("button", { type: "button", class: "button", onClick: () => handlers2.stopAgent() }, "Stop")
4091
- );
4092
- }
4093
4174
  const took = runDuration(run);
4094
4175
  if (run.status === "done") {
4095
4176
  return el("p", { class: "agent-run__status" }, `Finished at ${formatTime(run.endedAt)}${took ? `, in ${took}` : ""}.`);
@@ -4961,6 +5042,11 @@ function SettingsView({ state: state2, ui: ui2, handlers: handlers2 }) {
4961
5042
  }, []);
4962
5043
  const page = ui2.settings.value;
4963
5044
  const draft = ui2.settingsDraft.value;
5045
+ const sections = [
5046
+ { id: "objective", title: "Your objective" },
5047
+ { id: "sources", title: "Where the agent looks" },
5048
+ ...page?.groups ?? []
5049
+ ];
4964
5050
  return el(
4965
5051
  "div",
4966
5052
  { class: "settings" },
@@ -4970,6 +5056,7 @@ function SettingsView({ state: state2, ui: ui2, handlers: handlers2 }) {
4970
5056
  el("h2", { class: "settings__title" }, "Settings"),
4971
5057
  el("button", { type: "button", class: "button", onClick: () => handlers2.closeSettings() }, `Back to ${VIEW_NAME[ui2.settingsReturn.value]}`)
4972
5058
  ),
5059
+ k(SectionIndex, { sections }),
4973
5060
  page?.error ? el("p", { class: "settings__problem", role: "alert" }, page.error) : null,
4974
5061
  objectiveCard(state2, ui2, handlers2),
4975
5062
  sourcesCard(state2, ui2, handlers2),
@@ -4994,6 +5081,29 @@ function SettingsView({ state: state2, ui: ui2, handlers: handlers2 }) {
4994
5081
  saveBar(ui2, handlers2)
4995
5082
  );
4996
5083
  }
5084
+ function SectionIndex({ sections }) {
5085
+ const [current, setCurrent] = d2(sections[0]?.id ?? null);
5086
+ h2(() => {
5087
+ const look = () => {
5088
+ let seen = sections[0]?.id ?? null;
5089
+ for (const section2 of sections) {
5090
+ const card = document.getElementById(`settings-${section2.id}`);
5091
+ if (card && card.getBoundingClientRect().top <= 120) seen = section2.id;
5092
+ }
5093
+ setCurrent(seen);
5094
+ };
5095
+ look();
5096
+ window.addEventListener("scroll", look, { passive: true });
5097
+ return () => window.removeEventListener("scroll", look);
5098
+ }, [sections.map((section2) => section2.id).join(" ")]);
5099
+ return el(
5100
+ "nav",
5101
+ { class: "settings__nav", "aria-label": "Settings sections" },
5102
+ sections.map(
5103
+ (section2) => el("a", { key: section2.id, href: `#settings-${section2.id}`, "aria-current": section2.id === current ? "true" : void 0 }, section2.title)
5104
+ )
5105
+ );
5106
+ }
4997
5107
  function objectiveCard(state2, ui2, handlers2) {
4998
5108
  const focus = state2.focus;
4999
5109
  return el(
@@ -5122,6 +5232,7 @@ function shownValue(setting, draft) {
5122
5232
  if (draft.has(setting.key)) return draft.get(setting.key) ?? "";
5123
5233
  return setting.source === "default" ? "" : setting.value ?? "";
5124
5234
  }
5235
+ var SEGMENTS = 4;
5125
5236
  function flagValue(raw) {
5126
5237
  if (raw === "") return "";
5127
5238
  return ["off", "false", "0", "no"].includes(raw.trim().toLowerCase()) ? "off" : "on";
@@ -5138,7 +5249,30 @@ function settingField(setting, draft, handlers2) {
5138
5249
  if (setting.kind === "choice" || setting.kind === "flag") {
5139
5250
  const choices = setting.kind === "flag" ? [{ value: "on", label: "On" }, { value: "off", label: "Off" }] : setting.choices ?? [];
5140
5251
  const current = setting.kind === "flag" ? flagValue(value) : value;
5141
- input = el(
5252
+ const fallback = choices.find((choice) => choice.label.toLowerCase() === setting.fallback.toLowerCase());
5253
+ input = choices.length <= SEGMENTS && fallback ? el(
5254
+ "div",
5255
+ { class: "segmented", id, role: "radiogroup", "aria-label": setting.label, "aria-disabled": String(locked) },
5256
+ choices.map((choice) => {
5257
+ const isDefault = choice.value === fallback.value;
5258
+ const checked = current === "" ? isDefault : current === choice.value;
5259
+ return el(
5260
+ "button",
5261
+ {
5262
+ type: "button",
5263
+ key: choice.value,
5264
+ role: "radio",
5265
+ class: "segmented__option",
5266
+ "aria-checked": String(checked),
5267
+ disabled: locked,
5268
+ title: isDefault ? "The default" : void 0,
5269
+ onClick: () => handlers2.editSetting(setting.key, isDefault ? null : choice.value)
5270
+ },
5271
+ choice.label,
5272
+ checked && current === "" ? el("small", null, "default") : null
5273
+ );
5274
+ })
5275
+ ) : el(
5142
5276
  "select",
5143
5277
  { class: "field__input", id, disabled: locked, value: current, onChange },
5144
5278
  el("option", { value: "" }, `Default: ${setting.fallback}`),
@@ -5291,7 +5425,10 @@ function FirstBriefCard({ state: state2, handlers: handlers2 }) {
5291
5425
  // client/today.ts
5292
5426
  function TodayView({ state: state2, ui: ui2, handlers: handlers2 }) {
5293
5427
  if (state2.setup?.needed) return [k(FirstBriefCard, { state: state2, handlers: handlers2 })];
5294
- return [renderObjective(state2, handlers2), renderStats(state2), renderHeadline(state2), renderSections(state2, ui2, handlers2)];
5428
+ return [
5429
+ el("div", { class: "today__masthead" }, renderObjective(state2, handlers2), renderStats(state2), renderHeadline(state2)),
5430
+ renderSections(state2, ui2, handlers2)
5431
+ ];
5295
5432
  }
5296
5433
  function renderObjective(state2, handlers2 = null) {
5297
5434
  const focus = state2.focus;
@@ -5413,8 +5550,9 @@ function App({ ui: ui2, handlers: handlers2 }) {
5413
5550
  if (current?.onboarding) return [k(OnboardingView, { state: current, ui: ui2, handlers: handlers2 }), k(Toast, { ui: ui2, handlers: handlers2 })];
5414
5551
  return [
5415
5552
  el("a", { class: "skip-link", href: "#main" }, "Skip to content"),
5416
- k(Header, { state: current, ui: ui2, handlers: handlers2 }),
5417
- k(Tabs, { state: current, ui: ui2, handlers: handlers2 }),
5553
+ // The band: the masthead's navy runs behind both, and on Today on behind
5554
+ // the objective below (see `.band` in the stylesheet).
5555
+ el("div", { class: "band" }, k(Header, { state: current, ui: ui2, handlers: handlers2 }), k(Tabs, { state: current, ui: ui2, handlers: handlers2 })),
5418
5556
  el("div", { id: "banners", class: "banners" }, current ? renderBanners(current, ui2, handlers2) : null),
5419
5557
  el(
5420
5558
  "main",
@@ -0,0 +1,93 @@
1
+ Copyright © 2017 IBM Corp. with Reserved Font Name "Plex"
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+
5
+ This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.