@tiagojct/subsub 0.6.1 → 0.8.0

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/web/app.js CHANGED
@@ -67,6 +67,24 @@ marked.use({
67
67
  image(token) {
68
68
  return escapeHtml(token.text || token.href || "");
69
69
  },
70
+ table(token) {
71
+ let headerHtml = "";
72
+ for (let r = 0; r < token.header.length; r++) {
73
+ headerHtml += this.tablecell(token.header[r]);
74
+ }
75
+ let t = this.tablerow({ text: headerHtml });
76
+ let s = "";
77
+ for (let r = 0; r < token.rows.length; r++) {
78
+ let row = token.rows[r];
79
+ let n = "";
80
+ for (let o = 0; o < row.length; o++) {
81
+ n += this.tablecell(row[o]);
82
+ }
83
+ s += this.tablerow({ text: n });
84
+ }
85
+ if (s) s = `<tbody>${s}</tbody>`;
86
+ return `<div class="md-table-wrap"><table><thead>${t}</thead>${s}</table></div>`;
87
+ },
70
88
  },
71
89
  });
72
90
  function md(text) {
@@ -77,9 +95,28 @@ function md(text) {
77
95
  }
78
96
  }
79
97
 
80
- // ---------------------------------------------------------------- theme & language
98
+ // ---------------------------------------------------------------- theme, layout & language
81
99
 
82
100
  let currentTheme = localStorage.getItem("subsub-theme") || "auto";
101
+ let currentLayout = localStorage.getItem("subsub-layout") || "standard";
102
+
103
+ function updateLayoutUI() {
104
+ const isWide = currentLayout === "wide";
105
+ document.body.classList.toggle("layout-full", isWide);
106
+ const textEl = $("width-btn-text");
107
+ if (textEl) textEl.textContent = t("layoutWide");
108
+ const btn = $("width-btn");
109
+ if (btn) {
110
+ btn.title = t("toggleWidth");
111
+ btn.setAttribute("aria-pressed", String(isWide));
112
+ }
113
+ }
114
+
115
+ function toggleLayout() {
116
+ currentLayout = currentLayout === "wide" ? "standard" : "wide";
117
+ localStorage.setItem("subsub-layout", currentLayout);
118
+ updateLayoutUI();
119
+ }
83
120
 
84
121
  function themeLabel(theme) {
85
122
  if (theme === "light") return t("themeLight");
@@ -87,16 +124,23 @@ function themeLabel(theme) {
87
124
  return t("themeAuto");
88
125
  }
89
126
 
127
+ /** Stroke icons for the theme button: fixed markup, no user text. */
128
+ const THEME_ICONS = {
129
+ light: '<circle cx="12" cy="12" r="4"/><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.3 5.3l1.4 1.4M17.3 17.3l1.4 1.4M5.3 18.7l1.4-1.4M17.3 6.7l1.4-1.4"/>',
130
+ dark: '<path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z"/>',
131
+ auto: '<rect x="3" y="4.5" width="18" height="12" rx="1.5"/><path d="M8.5 20h7M12 16.5V20"/>',
132
+ };
133
+
90
134
  function updateThemeUI() {
91
- const icon = currentTheme === "light" ? "☀️" : currentTheme === "dark" ? "🌙" : "💻";
92
- const label = `${icon} ${themeLabel(currentTheme)}`;
135
+ const label = `${t("theme")}: ${themeLabel(currentTheme)}`;
93
136
  const btn = $("theme-btn");
94
137
  if (btn) {
95
- btn.textContent = label;
96
- btn.title = t("themeToggle");
138
+ btn.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${THEME_ICONS[currentTheme] ?? THEME_ICONS.auto}</svg>`;
139
+ btn.append(el("span", { class: "visually-hidden", text: label }));
140
+ btn.title = `${label} (${t("themeToggle")})`;
97
141
  }
98
142
  const sideBtn = $("theme-btn-side");
99
- if (sideBtn) sideBtn.textContent = label;
143
+ if (sideBtn) sideBtn.textContent = themeLabel(currentTheme);
100
144
  }
101
145
 
102
146
  function setTheme(theme) {
@@ -119,7 +163,10 @@ function toggleTheme() {
119
163
  function applyLang() {
120
164
  document.documentElement.lang = lang === "pt" ? "pt-PT" : "en";
121
165
  for (const e of document.querySelectorAll("[data-i18n]")) e.textContent = t(e.dataset.i18n);
166
+ for (const e of document.querySelectorAll("[data-i18n-placeholder]")) e.placeholder = t(e.dataset.i18nPlaceholder);
167
+ for (const e of document.querySelectorAll("[data-i18n-title]")) e.title = t(e.dataset.i18nTitle);
122
168
  updateThemeUI();
169
+ updateLayoutUI();
123
170
  }
124
171
 
125
172
  function mode() {
@@ -130,6 +177,8 @@ function renderHeader() {
130
177
  document.body.dataset.mode = mode();
131
178
  for (const b of document.querySelectorAll(".modes button")) b.setAttribute("aria-pressed", String(b.dataset.mode === mode()));
132
179
  if (state?.profile) $("profile").value = $("profile-side").value = state.profile;
180
+ $("starbuck-toggle").checked = Boolean(state?.starbuck);
181
+ $("starbuck-label").title = t("starbuckHint");
133
182
  const m = state?.model ?? session?.model;
134
183
  $("model-btn").textContent = $("model-btn-side").textContent = m ? m.id : t("noModel");
135
184
  const lib = $("library");
@@ -143,16 +192,22 @@ function renderHeader() {
143
192
  txt.textContent = t("zoteroDown");
144
193
  lib.classList.add("down");
145
194
  } else {
146
- const parts = [t("items", { n: L.items ?? "?" })];
147
- if (L.toReview) parts.push(t("toReview", { n: L.toReview }));
195
+ const num = (n) => (typeof n === "number" ? n.toLocaleString(lang === "pt" ? "pt-PT" : "en-GB") : (n ?? "?"));
196
+ const parts = [t("items", { n: num(L.items) })];
197
+ if (L.toReview) parts.push(t("toReview", { n: num(L.toReview) }));
148
198
  txt.textContent = `Zotero: ${parts.join(", ")}`;
149
199
  lib.classList.add("reachable");
150
200
  }
151
201
  if (state?.down?.length) {
152
- txt.textContent += ` | ${t("serversDown", { x: state.down.join(", ") })}`;
202
+ txt.textContent += ` · ${t("serversDown", { x: state.down.join(", ") })}`;
153
203
  lib.classList.add("down");
154
204
  }
205
+ lib.title = txt.textContent;
155
206
  $("input").placeholder = mode() === "librarian" ? t("placeholderLibrarian") : t("placeholderResearcher");
207
+ if (log.querySelector(".empty")) {
208
+ clearEmpty();
209
+ renderEmpty();
210
+ }
156
211
  renderActions();
157
212
  renderBanner();
158
213
  updateThemeUI();
@@ -166,7 +221,9 @@ function hasCommand(name) {
166
221
  function renderActions() {
167
222
  const box = $("actions");
168
223
  box.replaceChildren();
169
- const list = [...ACTIONS[mode()]].filter((a) => hasCommand(a.cmd));
224
+ const list = [...ACTIONS[mode()]].filter(
225
+ (a) => hasCommand(a.cmd) && (!a.needs || state?.[a.needs]) && (!a.profiles || a.profiles.includes(state?.profile ?? "scholar")),
226
+ );
170
227
  for (const a of list) box.append(actionButton(a));
171
228
  box.append(el("hr"));
172
229
  for (const a of ACTIONS.both.filter((x) => hasCommand(x.cmd))) box.append(actionButton(a));
@@ -194,7 +251,7 @@ function renderBanner() {
194
251
  b.replaceChildren();
195
252
  const m = state?.model ?? session?.model;
196
253
  if (!m && session) {
197
- b.append(el("span", { text: t("connectModel") }), el("button", { type: "button", class: "primary small", text: t("connect"), onclick: openModels }));
254
+ b.append(el("span", { text: t("connectModel") }), el("button", { type: "button", class: "primary small", text: t("connect"), onclick: showModels }));
198
255
  b.hidden = false;
199
256
  } else b.hidden = true;
200
257
  }
@@ -317,14 +374,16 @@ function renderEmpty() {
317
374
  el(
318
375
  "div",
319
376
  { class: "empty" },
377
+ el("p", { class: "kicker", text: t(mode()) }),
320
378
  el("h1", { text: t("emptyTitle") }),
379
+ el("p", { class: "lede", text: t(mode() === "librarian" ? "emptyLibrarian" : "emptyResearcher") }),
321
380
  el(
322
381
  "div",
323
382
  { class: "chips" },
324
383
  s.map((x) =>
325
384
  el("button", {
326
385
  type: "button",
327
- text: x.trim(),
386
+ text: x.endsWith(" ") ? `${x.trim()}…` : x,
328
387
  onclick: () => {
329
388
  const input = $("input");
330
389
  input.value = x;
@@ -733,6 +792,7 @@ function nextDialog() {
733
792
  const d = dialogs[0];
734
793
  shownDialog = d;
735
794
  const dlg = $("dialog");
795
+ $("dialog-kicker").textContent = d.method === "confirm" ? t("previewKicker") : "";
736
796
  $("dialog-title").textContent = dialogTitle(d);
737
797
  const hint = $("dialog-hint");
738
798
  hint.textContent = d.method === "confirm" ? t("approveHint") : "";
@@ -790,6 +850,11 @@ function nextDialog() {
790
850
 
791
851
  // ---------------------------------------------------------------- models and keys
792
852
 
853
+ /** Open the model dialog; a failure becomes a note, not an unhandled rejection. */
854
+ function showModels() {
855
+ openModels().catch((err) => note(err.message, "warning"));
856
+ }
857
+
793
858
  async function openModels() {
794
859
  const dlg = $("model-dialog");
795
860
  const ul = $("models");
@@ -973,11 +1038,24 @@ for (const b of document.querySelectorAll(".modes button")) {
973
1038
  });
974
1039
  }
975
1040
  $("profile").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
1041
+ $("starbuck-toggle").addEventListener("change", async (e) => {
1042
+ const on = e.target.checked;
1043
+ e.target.disabled = true;
1044
+ try {
1045
+ await api("/api/addons", { starbuck: on });
1046
+ note(t(on ? "starbuckOn" : "starbuckOff"), "info");
1047
+ } catch (err) {
1048
+ e.target.checked = !on;
1049
+ note(err.message, "warning");
1050
+ } finally {
1051
+ e.target.disabled = false;
1052
+ }
1053
+ });
976
1054
  $("profile-side").addEventListener("change", (e) => sendMessage(`/profile ${e.target.value}`));
977
- $("model-btn").addEventListener("click", openModels);
1055
+ $("model-btn").addEventListener("click", showModels);
978
1056
  $("model-btn-side").addEventListener("click", () => {
979
1057
  closeSide();
980
- openModels();
1058
+ showModels();
981
1059
  });
982
1060
  $("model-close").addEventListener("click", () => $("model-dialog").close());
983
1061
  $("main").addEventListener("click", () => {
@@ -985,10 +1063,19 @@ $("main").addEventListener("click", () => {
985
1063
  });
986
1064
  $("menu-btn").addEventListener("click", (e) => {
987
1065
  e.stopPropagation();
988
- const side = $("side");
989
- side.classList.toggle("open");
990
- $("menu-btn").setAttribute("aria-expanded", String(side.classList.contains("open")));
1066
+ if (window.innerWidth > 832) {
1067
+ const app = document.querySelector(".app");
1068
+ app.classList.toggle("side-collapsed");
1069
+ const collapsed = app.classList.contains("side-collapsed");
1070
+ $("menu-btn").setAttribute("aria-expanded", String(!collapsed));
1071
+ localStorage.setItem("subsub-side-collapsed", String(collapsed));
1072
+ } else {
1073
+ const side = $("side");
1074
+ side.classList.toggle("open");
1075
+ $("menu-btn").setAttribute("aria-expanded", String(side.classList.contains("open")));
1076
+ }
991
1077
  });
1078
+ $("width-btn")?.addEventListener("click", toggleLayout);
992
1079
  $("theme-btn")?.addEventListener("click", toggleTheme);
993
1080
  $("theme-btn-side")?.addEventListener("click", () => {
994
1081
  closeSide();
@@ -1029,5 +1116,10 @@ function connect() {
1029
1116
  }
1030
1117
 
1031
1118
  setTheme(currentTheme);
1119
+ updateLayoutUI();
1120
+ if (localStorage.getItem("subsub-side-collapsed") === "true" && window.innerWidth > 832) {
1121
+ document.querySelector(".app")?.classList.add("side-collapsed");
1122
+ $("menu-btn")?.setAttribute("aria-expanded", "false");
1123
+ }
1032
1124
  applyLang();
1033
1125
  connect();
package/web/i18n.js CHANGED
@@ -68,13 +68,24 @@ export const STRINGS = {
68
68
  themeLight: "Light",
69
69
  themeDark: "Dark",
70
70
  themeToggle: "Toggle theme",
71
+ layoutWide: "Wide",
72
+ layoutFocus: "Focus",
73
+ toggleWidth: "Toggle wide layout",
71
74
  copy: "Copy",
72
- copied: "Copied!",
75
+ copied: "Copied",
73
76
  copyNote: "Copy text",
74
- searchConversations: "Search conversations...",
77
+ searchConversations: "Search conversations",
75
78
  export: "Export",
76
79
  exportTitle: "Export conversation",
77
80
  confirmShortcutHint: "Y to approve, N to decline",
81
+ starbuck: "Reference checks (Starbuck)",
82
+ starbuckHint: "Checks that cited works exist, match their citation and were not retracted. Sub-Sub restarts when you change this.",
83
+ starbuckOn: "Reference checks are on. Use /verify, or ask the researcher to check a manuscript's references.",
84
+ starbuckOff: "Reference checks are off.",
85
+ composerHint: "Enter sends. Shift+Enter starts a new line. / shows the commands.",
86
+ emptyResearcher: "The researcher reads your library and searches PubMed, Europe PMC and OpenAlex. It cannot change your library.",
87
+ emptyLibrarian: "The librarian changes your library. You see a preview of each change, and nothing changes until you approve it.",
88
+ previewKicker: "Preview",
78
89
  },
79
90
  pt: {
80
91
  researcher: "Investigador",
@@ -142,13 +153,24 @@ export const STRINGS = {
142
153
  themeLight: "Claro",
143
154
  themeDark: "Escuro",
144
155
  themeToggle: "Mudar tema",
156
+ layoutWide: "Largo",
157
+ layoutFocus: "Foco",
158
+ toggleWidth: "Mudar largura do ecrã",
145
159
  copy: "Copiar",
146
- copied: "Copiado!",
160
+ copied: "Copiado",
147
161
  copyNote: "Copiar texto",
148
- searchConversations: "Procurar conversas...",
162
+ searchConversations: "Procurar conversas",
149
163
  export: "Exportar",
150
164
  exportTitle: "Exportar conversa",
151
165
  confirmShortcutHint: "Y para aprovar, N para recusar",
166
+ starbuck: "Verificação de referências (Starbuck)",
167
+ starbuckHint: "Verifica se as obras citadas existem, correspondem à citação e não foram retratadas. O Sub-Sub reinicia quando muda esta opção.",
168
+ starbuckOn: "A verificação de referências está ligada. Use /verify, ou peça ao investigador que verifique as referências de um manuscrito.",
169
+ starbuckOff: "A verificação de referências está desligada.",
170
+ composerHint: "Enter envia. Shift+Enter muda de linha. / mostra os comandos.",
171
+ emptyResearcher: "O investigador lê a sua biblioteca e pesquisa na PubMed, Europe PMC e OpenAlex. Não pode alterar a biblioteca.",
172
+ emptyLibrarian: "O bibliotecário altera a sua biblioteca. Vê uma pré-visualização de cada alteração, e nada muda até a aprovar.",
173
+ previewKicker: "Pré-visualização",
152
174
  },
153
175
  };
154
176
 
@@ -156,11 +178,17 @@ export const STRINGS = {
156
178
  export const ACTIONS = {
157
179
  researcher: [
158
180
  { cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
181
+ { cmd: "lit", fill: true, profiles: ["scholar", "author", "editor"], en: "Literature review", pt: "Revisão da literatura" },
182
+ { cmd: "compare", fill: true, profiles: ["scholar", "author", "editor"], en: "Compare sources", pt: "Comparar fontes" },
159
183
  { cmd: "lit-note", fill: true, en: "Literature note", pt: "Nota de leitura" },
160
184
  { cmd: "synthesis", fill: true, en: "Synthesis", pt: "Síntese" },
161
185
  { cmd: "gaps", fill: true, en: "What is missing", pt: "O que falta" },
162
186
  { cmd: "manuscript", fill: true, en: "Check a manuscript", pt: "Verificar um manuscrito" },
187
+ { cmd: "verify", fill: true, needs: "starbuck", en: "Check references", pt: "Verificar referências" },
188
+ { cmd: "review", fill: true, profiles: ["author", "editor"], en: "Review a manuscript", pt: "Rever um manuscrito" },
163
189
  { cmd: "alert", send: true, en: "New publications", pt: "Novas publicações" },
190
+ { cmd: "request-copy", fill: true, en: "Ask an author for a copy", pt: "Pedir uma cópia ao autor" },
191
+ { cmd: "digest", send: true, profiles: ["scholar", "author", "editor"], en: "Digest of the week", pt: "Resumo da semana" },
164
192
  ],
165
193
  librarian: [
166
194
  { cmd: "subsub", send: true, en: "Library overview", pt: "Resumo da biblioteca" },
@@ -228,6 +256,14 @@ export const TOOLS = {
228
256
  queue_imports: "queue imports",
229
257
  attach_note: "add a linked note",
230
258
  check_manuscript: "check a manuscript",
259
+ verify_check_manuscript: "check the references",
260
+ search_europepmc: "search Europe PMC",
261
+ search_multi: "search PubMed, Europe PMC and OpenAlex",
262
+ read_oa_fulltext: "read the open-access full text",
263
+ find_contact: "find the author's address",
264
+ verify_check_references: "check references",
265
+ verify_prepare_claims: "prepare the claim check",
266
+ verify_record_claims: "record the claim check",
231
267
  export_bibliography: "export a bibliography",
232
268
  read: "read a file",
233
269
  write: "write a file",
@@ -276,6 +312,14 @@ export const TOOLS = {
276
312
  queue_imports: "pôr importações na fila",
277
313
  attach_note: "acrescentar uma nota ligada",
278
314
  check_manuscript: "verificar um manuscrito",
315
+ verify_check_manuscript: "verificar as referências",
316
+ search_europepmc: "pesquisar na Europe PMC",
317
+ search_multi: "pesquisar na PubMed, Europe PMC e OpenAlex",
318
+ read_oa_fulltext: "ler o texto integral em acesso aberto",
319
+ find_contact: "procurar o endereço do autor",
320
+ verify_check_references: "verificar referências",
321
+ verify_prepare_claims: "preparar a verificação das afirmações",
322
+ verify_record_claims: "registar a verificação das afirmações",
279
323
  export_bibliography: "exportar uma bibliografia",
280
324
  read: "ler um ficheiro",
281
325
  write: "escrever um ficheiro",
package/web/icon.svg CHANGED
@@ -1 +1,6 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#12161b"/><text x="16" y="21.5" text-anchor="middle" font-family="IBM Plex Mono, ui-monospace, monospace" font-size="14" font-weight="600" fill="#e0832a">SS</text></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="7" fill="#1d4f7c"/>
3
+ <rect x="5.5" y="8" width="21" height="16" rx="1.5" fill="#f7f6f2"/>
4
+ <path d="M9 12.5h8M9 16h14" stroke="#1d4f7c" stroke-width="1.6" stroke-linecap="round"/>
5
+ <circle cx="16" cy="20.3" r="1.5" fill="#1d4f7c"/>
6
+ </svg>
package/web/index.html CHANGED
@@ -6,22 +6,22 @@
6
6
  <meta name="color-scheme" content="light dark">
7
7
  <title>Sub-Sub</title>
8
8
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
9
- <link rel="stylesheet" href="/glauca.css">
9
+ <link rel="preload" href="/fonts/IBMPlexSans.woff2" as="font" type="font/woff2" crossorigin>
10
10
  <link rel="stylesheet" href="/app.css">
11
11
  <script type="module" src="/app.js"></script>
12
12
  </head>
13
13
  <body data-mode="researcher">
14
14
  <div class="app">
15
15
  <header class="top">
16
- <button type="button" class="ghost menu-btn" id="menu-btn" aria-controls="side" aria-expanded="false" data-i18n="menu">Menu</button>
17
- <div class="brand"><span class="mark" aria-hidden="true">SS</span> Sub-Sub</div>
16
+ <button type="button" class="icon-btn" id="menu-btn" aria-controls="side" aria-expanded="true" data-i18n-title="menu" title="Menu"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M9.5 4.5v15"/></svg><span class="visually-hidden" data-i18n="menu">Menu</span></button>
17
+ <div class="brand"><svg class="mark" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="1.5"/><path d="M6.5 9h7M6.5 12h11"/><circle cx="12" cy="16" r="1.1"/></svg><span>Sub<span class="hyphen">-</span>Sub</span></div>
18
18
  <div class="modes" role="group" aria-label="Mode">
19
19
  <button type="button" data-mode="researcher" aria-pressed="true" data-i18n="researcher">Researcher</button>
20
20
  <button type="button" data-mode="librarian" aria-pressed="false" data-i18n="librarian">Librarian</button>
21
21
  </div>
22
22
  <div class="status">
23
23
  <span class="library" id="library"><span class="library-dot" aria-hidden="true"></span><span class="library-text"></span></span>
24
- <label class="profile"><span class="visually-hidden" data-i18n="profile">Profile</span>
24
+ <label class="profile wide-only"><span class="visually-hidden" data-i18n="profile">Profile</span>
25
25
  <select id="profile">
26
26
  <option value="reader">Reader</option>
27
27
  <option value="scholar">Scholar</option>
@@ -29,13 +29,14 @@
29
29
  <option value="editor">Editor</option>
30
30
  </select>
31
31
  </label>
32
- <button type="button" class="ghost model-btn" id="model-btn"></button>
33
- <button type="button" class="ghost theme-btn" id="theme-btn" aria-label="Toggle theme" title="Toggle theme"></button>
32
+ <button type="button" class="chip-btn model-btn wide-only" id="model-btn"></button>
33
+ <button type="button" class="icon-btn wide-only" id="width-btn" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h16M4 12l3.5-3.5M4 12l3.5 3.5M20 12l-3.5-3.5M20 12l-3.5 3.5"/></svg><span class="visually-hidden" id="width-btn-text">Wide</span></button>
34
+ <button type="button" class="icon-btn" id="theme-btn"></button>
34
35
  </div>
35
36
  </header>
36
37
 
37
38
  <aside class="side" id="side">
38
- <button type="button" class="primary block" id="new-btn" data-i18n="newConversation">New conversation</button>
39
+ <button type="button" class="new-btn" id="new-btn"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span data-i18n="newConversation">New conversation</span></button>
39
40
  <section class="phone-only settings">
40
41
  <label><span data-i18n="profile">Profile</span>
41
42
  <select id="profile-side">
@@ -49,7 +50,7 @@
49
50
  <button type="button" class="secondary model-btn" id="model-btn-side"></button>
50
51
  </label>
51
52
  <label><span data-i18n="theme">Theme</span>
52
- <button type="button" class="secondary theme-btn" id="theme-btn-side"></button>
53
+ <button type="button" class="secondary" id="theme-btn-side"></button>
53
54
  </label>
54
55
  </section>
55
56
  <section>
@@ -59,16 +60,17 @@
59
60
  <section class="grow">
60
61
  <div class="side-subhead">
61
62
  <h2 data-i18n="conversations">Conversations</h2>
62
- <button type="button" class="ghost small" id="export-btn" title="Export conversation as Markdown" data-i18n="export">Export</button>
63
- </div>
64
- <div class="search-wrap">
65
- <input type="search" id="session-search" placeholder="Search conversations..." autocomplete="off">
63
+ <button type="button" class="link-btn" id="export-btn" data-i18n="export" data-i18n-title="exportTitle">Export</button>
66
64
  </div>
65
+ <input type="search" id="session-search" class="search" data-i18n-placeholder="searchConversations" placeholder="Search conversations" autocomplete="off">
67
66
  <ul class="sessions" id="sessions"></ul>
68
67
  </section>
69
68
  <footer class="side-foot">
70
- <button type="button" class="ghost small" id="quit-btn" data-i18n="quit">Close Sub-Sub</button>
71
- <span class="version" id="version"></span>
69
+ <label class="switch" id="starbuck-label"><input type="checkbox" role="switch" id="starbuck-toggle"><span class="track" aria-hidden="true"></span><span data-i18n="starbuck">Reference checks (Starbuck)</span></label>
70
+ <div class="side-foot-row">
71
+ <button type="button" class="link-btn" id="quit-btn" data-i18n="quit">Close Sub-Sub</button>
72
+ <span class="version" id="version"></span>
73
+ </div>
72
74
  </footer>
73
75
  </aside>
74
76
 
@@ -77,10 +79,15 @@
77
79
  <div class="log" id="log" aria-live="polite"></div>
78
80
  <form class="composer" id="composer">
79
81
  <div class="suggest" id="suggest" role="listbox" hidden></div>
80
- <textarea id="input" rows="2" autocomplete="off" spellcheck="true"></textarea>
81
- <div class="composer-buttons">
82
- <button type="button" class="secondary" id="stop" hidden data-i18n="stop">Stop</button>
83
- <button type="submit" class="primary" id="send" data-i18n="send">Send</button>
82
+ <div class="composer-box">
83
+ <textarea id="input" rows="1" autocomplete="off" spellcheck="true"></textarea>
84
+ <div class="composer-row">
85
+ <span class="composer-hint" data-i18n="composerHint">Enter to send, Shift+Enter for a new line, / for commands</span>
86
+ <div class="composer-buttons">
87
+ <button type="button" class="secondary" id="stop" hidden data-i18n="stop">Stop</button>
88
+ <button type="submit" class="primary" id="send" data-i18n="send">Send</button>
89
+ </div>
90
+ </div>
84
91
  </div>
85
92
  </form>
86
93
  </main>
@@ -88,6 +95,7 @@
88
95
 
89
96
  <dialog id="dialog" class="modal">
90
97
  <form method="dialog" id="dialog-form">
98
+ <p class="modal-kicker" id="dialog-kicker"></p>
91
99
  <h2 id="dialog-title"></h2>
92
100
  <p class="hint" id="dialog-hint"></p>
93
101
  <pre class="preview" id="dialog-message"></pre>
@@ -99,7 +107,7 @@
99
107
  <dialog id="model-dialog" class="modal wide">
100
108
  <div class="modal-head">
101
109
  <h2 data-i18n="modelTitle">Model</h2>
102
- <button type="button" class="ghost small" id="model-close" data-i18n="close">Close</button>
110
+ <button type="button" class="link-btn" id="model-close" data-i18n="close">Close</button>
103
111
  </div>
104
112
  <h3 data-i18n="chooseModel">Choose a model</h3>
105
113
  <label class="check"><input type="checkbox" id="model-both" checked> <span data-i18n="forBoth">Use it in both modes</span></label>
package/web/glauca.css DELETED
@@ -1,106 +0,0 @@
1
- /* Generated from glauca.json. Edit the json, then `make generate`. */
2
- :root {
3
- --gl-font-serif: "IBM Plex Serif", "IBM Plex Serif fallback", Georgia, serif;
4
- --gl-font-sans: "IBM Plex Sans", "IBM Plex Sans fallback", system-ui, sans-serif;
5
- --gl-font-mono: "IBM Plex Mono", "IBM Plex Mono fallback", ui-monospace, monospace;
6
- --gl-font-reading: "IBM Plex Sans", "IBM Plex Sans fallback", system-ui, sans-serif;
7
- --gl-text-xs: 0.64rem;
8
- --gl-text-sm: 0.8rem;
9
- --gl-text-base: 1rem;
10
- --gl-text-lg: 1.25rem;
11
- --gl-text-xl: 1.563rem;
12
- --gl-text-2xl: 1.953rem;
13
- --gl-text-3xl: 2.441rem;
14
- --gl-text-4xl: 3.052rem;
15
- --gl-text-display: clamp(2.5rem, 6vw, 4rem);
16
- --gl-text-poster: clamp(3rem, 10vw, 6.5rem);
17
- --gl-leading-tight: 1.08;
18
- --gl-leading-snug: 1.3;
19
- --gl-leading-normal: 1.55;
20
- --gl-leading-relaxed: 1.7;
21
- --gl-weight-regular: 400;
22
- --gl-weight-medium: 500;
23
- --gl-weight-semibold: 600;
24
- --gl-weight-bold: 700;
25
- --gl-space-0: 0;
26
- --gl-space-1: 0.25rem;
27
- --gl-space-2: 0.5rem;
28
- --gl-space-3: 0.75rem;
29
- --gl-space-4: 1rem;
30
- --gl-space-5: 1.25rem;
31
- --gl-space-6: 1.5rem;
32
- --gl-space-8: 2rem;
33
- --gl-space-10: 2.5rem;
34
- --gl-space-12: 3rem;
35
- --gl-space-16: 4rem;
36
- --gl-space-20: 5rem;
37
- --gl-space-24: 6rem;
38
- --gl-radius-none: 0;
39
- --gl-radius-sm: 2px;
40
- --gl-radius-md: 4px;
41
- --gl-radius-lg: 6px;
42
- --gl-radius-pill: 999px;
43
- --gl-border-width: 1px;
44
- }
45
-
46
- /* Pruina is the light default; Profundum is dark. */
47
- :root,
48
- [data-mode="light"],
49
- [data-theme="light"],
50
- html[data-theme="light"] {
51
- --gl-bg: #f0f4f6;
52
- --gl-surface: #f7fafb;
53
- --gl-surface-raised: #ffffff;
54
- --gl-text: #16222a;
55
- --gl-text-muted: #55646d;
56
- --gl-border: #cdd7dc;
57
- --gl-accent: #0b62cf;
58
- --gl-accent-bright: #007aff;
59
- --gl-accent-deep: #084b96;
60
- --gl-on-accent: #f2f7fb;
61
- --gl-tint-deep: #16303f;
62
- --gl-tint: #35576b;
63
- --gl-tint-bright: #3e6d84;
64
- --gl-tint-pale: #b7cdd6;
65
- --gl-on-tint: #f2f7fb;
66
- }
67
-
68
- [data-mode="dark"],
69
- [data-theme="dark"],
70
- html[data-theme="dark"] {
71
- --gl-bg: #10161c;
72
- --gl-surface: #171f26;
73
- --gl-surface-raised: #1f2932;
74
- --gl-text: #e8eef2;
75
- --gl-text-muted: #8c8c8c;
76
- --gl-border: #2a3540;
77
- --gl-accent: #3d97ff;
78
- --gl-accent-bright: #6cb2ff;
79
- --gl-accent-deep: #007aff;
80
- --gl-on-accent: #0b1218;
81
- --gl-tint-deep: #142430;
82
- --gl-tint: #2b4356;
83
- --gl-tint-bright: #4d7391;
84
- --gl-tint-pale: #93b7c9;
85
- --gl-on-tint: #0d1720;
86
- }
87
-
88
- @media (prefers-color-scheme: dark) {
89
- :root:not([data-theme="light"]) {
90
- --gl-bg: #10161c;
91
- --gl-surface: #171f26;
92
- --gl-surface-raised: #1f2932;
93
- --gl-text: #e8eef2;
94
- --gl-text-muted: #8c8c8c;
95
- --gl-border: #2a3540;
96
- --gl-accent: #3d97ff;
97
- --gl-accent-bright: #6cb2ff;
98
- --gl-accent-deep: #007aff;
99
- --gl-on-accent: #0b1218;
100
- --gl-tint-deep: #142430;
101
- --gl-tint: #2b4356;
102
- --gl-tint-bright: #4d7391;
103
- --gl-tint-pale: #93b7c9;
104
- --gl-on-tint: #0d1720;
105
- }
106
- }