langcoach 0.3.0 → 0.4.1

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.
@@ -1,39 +1,68 @@
1
1
  :root {
2
- --bg: #f7f9fc;
2
+ /* Notebook tokens: exact copy of NOTEBOOK_TOKENS (@langcoach/core theme.ts); test/theme-tokens.test.ts keeps them in sync. */
3
+ --lc-paper: #faf7f2;
4
+ --lc-rule: #efe7da;
5
+ --lc-border: #eadfcd;
6
+ --lc-shadow: 0 3px 0 #eadfcd;
7
+ --lc-streak-tile: #fff1e0;
8
+ --lc-words-tile: #eef6e8;
9
+ --lc-sticker: #fde68a;
10
+ --lc-sticker-rotate: -6deg;
11
+ --lc-xp-from: #fb923c;
12
+ --lc-xp-to: #facc15;
13
+ --lc-learned-bg: #dcfce7;
14
+ --lc-learned-fg: #166534;
15
+ --lc-weak-bg: #ffe4e6;
16
+ --lc-weak-fg: #9f1239;
17
+ --lc-radius: 14px;
18
+ --lc-serif: Georgia, "Times New Roman", serif;
19
+ --lc-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
20
+
21
+ --bg: var(--lc-paper);
3
22
  --surface: #ffffff;
4
- --surface-muted: #eef2f8;
5
- --border: #d8e0ed;
6
- --text: #161d2b;
7
- --text-muted: #556178;
8
- --primary: #1a56db;
9
- --primary-hover: #1545b5;
10
- --primary-soft: #e7effd;
11
- --success: #0f7b4c;
12
- --success-soft: #e3f6ed;
23
+ --surface-muted: #f6f0e6;
24
+ --border: var(--lc-border);
25
+ --text: #292524;
26
+ --text-muted: #6b5d4f;
27
+ --primary: #c2410c;
28
+ --primary-hover: #9a3412;
29
+ --primary-soft: var(--lc-streak-tile);
30
+ --on-primary: #ffffff;
31
+ --success: var(--lc-learned-fg);
32
+ --success-soft: var(--lc-learned-bg);
13
33
  --warning: #b45309;
14
- --danger: #c81e1e;
15
- --danger-soft: #fde8e8;
16
- --radius: 12px;
17
- --shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
34
+ --danger: var(--lc-weak-fg);
35
+ --danger-soft: var(--lc-weak-bg);
36
+ --radius: var(--lc-radius);
37
+ --shadow: var(--lc-shadow);
38
+ --sticker-edge: #e5c76b;
39
+ --sticker-ink: #292524;
18
40
  }
19
41
 
42
+ /* Warm dark variant of the same notebook: ink-stained paper, same roles. */
20
43
  @media (prefers-color-scheme: dark) {
21
44
  :root {
22
- --bg: #11151f;
23
- --surface: #1b2232;
24
- --surface-muted: #242d42;
25
- --border: #323e59;
26
- --text: #eef2f9;
27
- --text-muted: #9da9c2;
28
- --primary: #5b8def;
29
- --primary-hover: #7aa4f5;
30
- --primary-soft: #223254;
31
- --success: #34d399;
32
- --success-soft: #14382c;
45
+ --lc-paper: #1c1814;
46
+ --lc-rule: #29221b;
47
+ --lc-border: #3d3329;
48
+ --lc-shadow: 0 3px 0 #0f0c09;
49
+ --lc-streak-tile: #33241a;
50
+ --lc-words-tile: #1f2a1d;
51
+ --lc-sticker: #fcd34d;
52
+ --lc-learned-bg: #173322;
53
+ --lc-learned-fg: #86efac;
54
+ --lc-weak-bg: #3b1a22;
55
+ --lc-weak-fg: #fda4af;
56
+
57
+ --surface: #26201a;
58
+ --surface-muted: #2f281f;
59
+ --text: #f3ece2;
60
+ --text-muted: #bcae9b;
61
+ --primary: #fb923c;
62
+ --primary-hover: #fdba74;
63
+ --on-primary: #1c1814;
33
64
  --warning: #fbbf24;
34
- --danger: #f87171;
35
- --danger-soft: #3b1c22;
36
- --shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
65
+ --sticker-edge: #b45309;
37
66
  }
38
67
  }
39
68
 
@@ -609,8 +638,6 @@ button:focus-visible, select:focus-visible, input:focus-visible {
609
638
  padding: 12px 16px;
610
639
  border: none;
611
640
  border-radius: 16px;
612
- background: linear-gradient(135deg, #1a56db, #7c3aed);
613
- color: #fff;
614
641
  text-align: start;
615
642
  box-shadow: 0 4px 0 rgba(0, 0, 0, 0.18);
616
643
  transition: transform 150ms var(--px-ease), box-shadow 150ms var(--px-ease);
@@ -1233,7 +1260,6 @@ button:focus-visible, select:focus-visible, input:focus-visible {
1233
1260
  --accent-violet-soft: #ede9fe;
1234
1261
  --star: #b45309;
1235
1262
  --focus: var(--primary);
1236
- --brand-gradient: linear-gradient(135deg, #1a56db 0%, #7c3aed 100%);
1237
1263
  }
1238
1264
  @media (prefers-color-scheme: dark) {
1239
1265
  :root {
@@ -1244,7 +1270,6 @@ button:focus-visible, select:focus-visible, input:focus-visible {
1244
1270
  --accent-violet: #a78bfa;
1245
1271
  --accent-violet-soft: #2a2148;
1246
1272
  --star: #fbbf24;
1247
- --brand-gradient: linear-gradient(135deg, #6b93f0 0%, #a78bfa 100%);
1248
1273
  }
1249
1274
  }
1250
1275
 
@@ -1264,13 +1289,6 @@ button:focus-visible, select:focus-visible, input:focus-visible {
1264
1289
  .card {
1265
1290
  border-radius: var(--radius-lg);
1266
1291
  }
1267
- .brand {
1268
- background: var(--brand-gradient);
1269
- -webkit-background-clip: text;
1270
- background-clip: text;
1271
- color: transparent;
1272
- font-weight: 800;
1273
- }
1274
1292
  button,
1275
1293
  .tab-btn,
1276
1294
  .chip {
@@ -1563,8 +1581,6 @@ button:disabled {
1563
1581
  border-radius: 20px;
1564
1582
  display: grid;
1565
1583
  place-items: center;
1566
- background: var(--brand-gradient);
1567
- color: #fff;
1568
1584
  font-size: 28px;
1569
1585
  font-weight: 800;
1570
1586
  box-shadow: var(--shadow);
@@ -1727,3 +1743,246 @@ ul.ach-grid {
1727
1743
  animation: none;
1728
1744
  }
1729
1745
  }
1746
+
1747
+ /* ───────────── Notebook look (sticker notebook, shared with the agent dashboard) ───────────── */
1748
+ :root {
1749
+ --radius-lg: var(--lc-radius);
1750
+ --focus: #c2410c;
1751
+ }
1752
+ @media (prefers-color-scheme: dark) {
1753
+ :root {
1754
+ --focus: #fdba74;
1755
+ }
1756
+ }
1757
+
1758
+ body {
1759
+ font-family: var(--lc-sans);
1760
+ background-color: var(--lc-paper);
1761
+ background-image: repeating-linear-gradient(transparent 0 27px, var(--lc-rule) 27px 28px);
1762
+ background-attachment: local;
1763
+ }
1764
+
1765
+ /* Header and tabs sit on the paper like a notebook's top strip. */
1766
+ .panel-header,
1767
+ .tab-bar {
1768
+ background: var(--lc-paper);
1769
+ border-bottom-color: var(--lc-border);
1770
+ }
1771
+ .brand {
1772
+ background: none;
1773
+ color: var(--text);
1774
+ font: italic 400 19px/1.2 var(--lc-serif);
1775
+ letter-spacing: 0;
1776
+ }
1777
+ .pill {
1778
+ background: var(--surface);
1779
+ border: 1px solid var(--lc-border);
1780
+ }
1781
+ /* One-line tabs: they share the width and scroll sideways only when a locale's labels are too long. */
1782
+ .tab-bar {
1783
+ display: flex;
1784
+ gap: 2px;
1785
+ padding: 6px 8px;
1786
+ overflow-x: auto;
1787
+ scrollbar-width: thin;
1788
+ }
1789
+ .tab-btn {
1790
+ flex: 1 0 auto;
1791
+ padding: 8px 6px;
1792
+ white-space: nowrap;
1793
+ overflow-wrap: normal;
1794
+ }
1795
+ .tab-btn[aria-current="page"] {
1796
+ background: var(--lc-sticker);
1797
+ color: var(--sticker-ink);
1798
+ box-shadow: 0 2px 0 var(--sticker-edge);
1799
+ }
1800
+
1801
+ /* Cards: white page cut-outs with a flat paper shadow. */
1802
+ .card,
1803
+ .stat-box,
1804
+ .task-item,
1805
+ .vocab-item,
1806
+ .scenario-card,
1807
+ .ach-item,
1808
+ .goal-option,
1809
+ .px-stat,
1810
+ .px-end-stat {
1811
+ border-color: var(--lc-border);
1812
+ box-shadow: var(--lc-shadow);
1813
+ }
1814
+ .card {
1815
+ border-radius: var(--lc-radius);
1816
+ }
1817
+ .task-item,
1818
+ .vocab-item,
1819
+ .stat-box,
1820
+ .ach-item {
1821
+ border-radius: 10px;
1822
+ }
1823
+ .vocab-item {
1824
+ background: var(--surface);
1825
+ }
1826
+
1827
+ h1,
1828
+ .level-name {
1829
+ font: italic 400 24px/1.2 var(--lc-serif);
1830
+ letter-spacing: 0.01em;
1831
+ }
1832
+ .level-name {
1833
+ font-size: 22px;
1834
+ }
1835
+
1836
+ button.primary,
1837
+ .chip.active,
1838
+ .play-big {
1839
+ color: var(--on-primary);
1840
+ }
1841
+ .chip.active .chip-count {
1842
+ background: color-mix(in srgb, var(--on-primary) 22%, transparent);
1843
+ color: var(--on-primary);
1844
+ }
1845
+ button:focus-visible,
1846
+ select:focus-visible,
1847
+ input:focus-visible,
1848
+ a:focus-visible,
1849
+ .tab-btn:focus-visible,
1850
+ .goal-option:focus-within {
1851
+ outline: 3px solid var(--focus);
1852
+ outline-offset: 2px;
1853
+ }
1854
+ /* Inset so the scrolling tab bar does not clip the ring. */
1855
+ .tab-btn:focus-visible {
1856
+ outline-offset: -3px;
1857
+ }
1858
+
1859
+ /* XP bars: orange to sun-yellow. */
1860
+ .progress-fill {
1861
+ background: linear-gradient(90deg, var(--lc-xp-from), var(--lc-xp-to));
1862
+ border-radius: 999px;
1863
+ }
1864
+ .progress-bar {
1865
+ background: color-mix(in srgb, var(--lc-border) 70%, var(--surface));
1866
+ }
1867
+
1868
+ /* Today: hero page with the streak as an orange tile. */
1869
+ .today-hero {
1870
+ background: var(--surface);
1871
+ align-items: stretch;
1872
+ }
1873
+ .streak-box {
1874
+ margin-top: 10px;
1875
+ padding: 10px 12px;
1876
+ border-radius: var(--lc-radius);
1877
+ background: var(--lc-streak-tile);
1878
+ border: 1px solid var(--lc-border);
1879
+ box-shadow: var(--lc-shadow);
1880
+ }
1881
+ .streak-count {
1882
+ font-size: 26px;
1883
+ font-weight: 800;
1884
+ color: var(--text);
1885
+ }
1886
+ .freeze-pill {
1887
+ background: var(--surface);
1888
+ border: 1px solid var(--lc-border);
1889
+ }
1890
+ .today-goal {
1891
+ align-content: center;
1892
+ }
1893
+ .task-item.done {
1894
+ background: var(--lc-learned-bg);
1895
+ border-color: color-mix(in srgb, var(--lc-learned-fg) 30%, var(--lc-learned-bg));
1896
+ color: var(--lc-learned-fg);
1897
+ }
1898
+ .task-item.done .muted {
1899
+ color: var(--lc-learned-fg);
1900
+ }
1901
+ .practice-cta {
1902
+ background: linear-gradient(135deg, var(--lc-xp-from), var(--lc-xp-to));
1903
+ color: #292524;
1904
+ border-radius: var(--lc-radius);
1905
+ box-shadow: 0 3px 0 #d97706;
1906
+ }
1907
+ .practice-cta:active {
1908
+ box-shadow: 0 1px 0 #d97706;
1909
+ }
1910
+ .practice-cta.fresh {
1911
+ background: var(--lc-words-tile);
1912
+ color: var(--text);
1913
+ border: 1px solid var(--lc-border);
1914
+ box-shadow: var(--lc-shadow);
1915
+ }
1916
+ .scenario-card {
1917
+ border-inline-start-color: var(--lc-xp-from);
1918
+ }
1919
+ .scenario-card.tone-1 { border-inline-start-color: var(--lc-learned-fg); }
1920
+ .scenario-card.tone-2 { border-inline-start-color: var(--lc-xp-to); }
1921
+ .rollover-banner {
1922
+ background: var(--lc-streak-tile);
1923
+ border-color: var(--lc-border);
1924
+ box-shadow: var(--lc-shadow);
1925
+ }
1926
+
1927
+ /* Words: learned / weak chips. */
1928
+ .status-tag.learned,
1929
+ .chip-mastered:not(.active),
1930
+ .btn-know.done,
1931
+ .btn-know:disabled {
1932
+ background: var(--lc-learned-bg);
1933
+ color: var(--lc-learned-fg);
1934
+ border-color: color-mix(in srgb, var(--lc-learned-fg) 25%, var(--lc-learned-bg));
1935
+ }
1936
+ .chip-weak:not(.active) {
1937
+ background: var(--lc-weak-bg);
1938
+ color: var(--lc-weak-fg);
1939
+ border-color: color-mix(in srgb, var(--lc-weak-fg) 25%, var(--lc-weak-bg));
1940
+ }
1941
+ .btn-know {
1942
+ background: var(--surface);
1943
+ color: var(--lc-learned-fg);
1944
+ border: 1px solid var(--lc-border);
1945
+ }
1946
+ .status-tag.new {
1947
+ background: var(--lc-streak-tile);
1948
+ color: var(--primary);
1949
+ border-color: var(--lc-border);
1950
+ }
1951
+ .show-more {
1952
+ background: var(--surface);
1953
+ border: 1px dashed var(--lc-border);
1954
+ }
1955
+ .words-search {
1956
+ border-color: var(--lc-border);
1957
+ }
1958
+ .strength.s3 .strength-bar.on { background: var(--lc-xp-from); }
1959
+
1960
+ /* Progress: the level is a tilted sticker. */
1961
+ .level-badge {
1962
+ background: var(--lc-sticker);
1963
+ color: var(--sticker-ink);
1964
+ border-radius: 10px;
1965
+ transform: rotate(var(--lc-sticker-rotate));
1966
+ box-shadow: 0 3px 0 var(--sticker-edge);
1967
+ }
1968
+ .stat-box:nth-child(1) { background: var(--lc-streak-tile); }
1969
+ .stat-box:nth-child(4) { background: var(--lc-words-tile); }
1970
+ .week-bar {
1971
+ background: var(--lc-streak-tile);
1972
+ border-color: var(--lc-xp-from);
1973
+ }
1974
+ .week-col.met .week-bar {
1975
+ background: linear-gradient(0deg, var(--lc-xp-from), var(--lc-xp-to));
1976
+ border-color: var(--lc-xp-from);
1977
+ }
1978
+ .week-goal-line {
1979
+ border-top-color: var(--text-muted);
1980
+ }
1981
+ .ach-item.unlocked {
1982
+ background: var(--lc-words-tile);
1983
+ border-color: var(--lc-border);
1984
+ }
1985
+ .goal-option.active {
1986
+ border-color: var(--primary);
1987
+ background: var(--lc-streak-tile);
1988
+ }
@@ -1,4 +1,4 @@
1
- import { S as toLocalDate, _ as CorruptDataError, c as version, f as Coach, g as parseState, h as isDue, l as DEFAULT_SETTINGS, p as progressOf, r as translationPair, s as suggestThemes, u as isMessage, v as InvalidDataError, w as emptyState, x as isoWeek, y as NotFoundError } from "./chunks/translate-CM6Phtg_.js";
1
+ import { C as toLocalDate, S as isoWeek, T as emptyState, _ as parseState, b as NotFoundError, c as version, g as isDue, l as DEFAULT_SETTINGS, m as progressOf, p as Coach, r as translationPair, s as suggestThemes, u as isMessage, v as CorruptDataError, y as InvalidDataError } from "./chunks/translate-DvfZknmj.js";
2
2
  //#region src/shared/dev-hosts.ts
3
3
  /**
4
4
  * Local hosts the e2e build (and unit tests) accept in place of labs.google. Production builds define
@@ -874,7 +874,7 @@ function createRouter(deps) {
874
874
  //#endregion
875
875
  //#region src/background/bridge.ts
876
876
  /**
877
- * The local langcoach MCP server (loopback only; see docs/superpowers/plans/2026-11-09-lll-bridge.md).
877
+ * The local langcoach MCP server (loopback only, token-paired; protocol in packages/mcp/src/bridge.ts).
878
878
  * The e2e build talks to a stub on another port, so tests never reach a real agent's data on this machine.
879
879
  */
880
880
  var BRIDGE_ORIGIN = "http://127.0.0.1:47821";
@@ -7499,7 +7499,7 @@ function progressOf(state, now) {
7499
7499
  }
7500
7500
  //#endregion
7501
7501
  //#region ../../packages/core/src/coach.ts
7502
- var LEVELS = [
7502
+ var LEVELS$1 = [
7503
7503
  "A1",
7504
7504
  "A2",
7505
7505
  "B1",
@@ -7602,7 +7602,7 @@ var Coach = class {
7602
7602
  if (input.dailyGoalXp !== void 0 && !DAILY_GOALS.includes(input.dailyGoalXp)) throw new InvalidDataError(`dailyGoalXp must be one of ${DAILY_GOALS.join(", ")}`);
7603
7603
  if (blank(input.nativeLanguage)) throw new InvalidDataError("nativeLanguage must be a non-empty string");
7604
7604
  if (blank(input.targetLanguage)) throw new InvalidDataError("targetLanguage must be a non-empty string");
7605
- if (!LEVELS.includes(input.level)) throw new InvalidDataError(`level must be one of ${LEVELS.join(", ")}`);
7605
+ if (!LEVELS$1.includes(input.level)) throw new InvalidDataError(`level must be one of ${LEVELS$1.join(", ")}`);
7606
7606
  if (typeof input.goal !== "string") throw new InvalidDataError("goal must be a string");
7607
7607
  if (!intIn(input.minutesPerDay, 5, 180)) throw new InvalidDataError("minutesPerDay must be an integer from 5 to 180");
7608
7608
  if (!intIn(input.daysPerWeek, 1, 7)) throw new InvalidDataError("daysPerWeek must be an integer from 1 to 7");
@@ -8143,6 +8143,61 @@ var Coach = class {
8143
8143
  };
8144
8144
  }
8145
8145
  };
8146
+ var LEVELS = [
8147
+ 0,
8148
+ 50,
8149
+ 150,
8150
+ 300,
8151
+ 500,
8152
+ 800,
8153
+ 1200,
8154
+ 1700,
8155
+ 2400,
8156
+ 3300
8157
+ ].map((minXp, i) => ({
8158
+ level: i + 1,
8159
+ minXp,
8160
+ key: `level${i + 1}`
8161
+ }));
8162
+ function levelFor(totalXp) {
8163
+ const xp = Number.isFinite(totalXp) ? Math.max(0, totalXp) : 0;
8164
+ let i = 0;
8165
+ while (i + 1 < LEVELS.length && xp >= LEVELS[i + 1].minXp) i++;
8166
+ const cur = LEVELS[i];
8167
+ const next = LEVELS[i + 1];
8168
+ if (!next) return {
8169
+ ...cur,
8170
+ nextMinXp: null,
8171
+ progress: 1,
8172
+ toNext: 0
8173
+ };
8174
+ const span = next.minXp - cur.minXp;
8175
+ return {
8176
+ ...cur,
8177
+ nextMinXp: next.minXp,
8178
+ progress: Math.min(1, (xp - cur.minXp) / span),
8179
+ toNext: next.minXp - xp
8180
+ };
8181
+ }
8182
+ Object.freeze({
8183
+ paper: "#faf7f2",
8184
+ rule: "#efe7da",
8185
+ border: "#eadfcd",
8186
+ shadow: "0 3px 0 #eadfcd",
8187
+ streakTile: "#fff1e0",
8188
+ wordsTile: "#eef6e8",
8189
+ sticker: "#fde68a",
8190
+ stickerRotate: "-6deg",
8191
+ xpFrom: "#fb923c",
8192
+ xpTo: "#facc15",
8193
+ learnedBg: "#dcfce7",
8194
+ learnedFg: "#166534",
8195
+ weakBg: "#ffe4e6",
8196
+ weakFg: "#9f1239",
8197
+ radius: "14px",
8198
+ serif: "Georgia, \"Times New Roman\", serif",
8199
+ sans: "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif"
8200
+ });
8146
8201
  //#endregion
8147
8202
  //#region src/shared/messages.ts
8148
8203
  var DEFAULT_SETTINGS = {
@@ -8189,7 +8244,7 @@ async function send(msg) {
8189
8244
  }
8190
8245
  //#endregion
8191
8246
  //#region public/manifest.json
8192
- var version = "0.3.0";
8247
+ var version = "0.4.1";
8193
8248
  //#endregion
8194
8249
  //#region src/lib/themes.ts
8195
8250
  var GOALS = [
@@ -9109,4 +9164,4 @@ function createTranslatorCache(api, getGlobal = globalApi) {
9109
9164
  };
9110
9165
  }
9111
9166
  //#endregion
9112
- export { DAILY_GOALS as C, toLocalDate as S, CorruptDataError as _, resolveLanguage as a, addDays as b, version as c, send as d, Coach as f, parseState as g, isDue as h, LLL_LANGUAGES as i, DEFAULT_SETTINGS as l, computeStreak as m, nativeLanguageCode as n, GOALS as o, progressOf as p, translationPair as r, suggestThemes as s, createTranslatorCache as t, isMessage as u, InvalidDataError as v, emptyState as w, isoWeek as x, NotFoundError as y };
9167
+ export { toLocalDate as C, isoWeek as S, emptyState as T, parseState as _, resolveLanguage as a, NotFoundError as b, version as c, send as d, levelFor as f, isDue as g, computeStreak as h, LLL_LANGUAGES as i, DEFAULT_SETTINGS as l, progressOf as m, nativeLanguageCode as n, GOALS as o, Coach as p, translationPair as r, suggestThemes as s, createTranslatorCache as t, isMessage as u, CorruptDataError as v, DAILY_GOALS as w, addDays as x, InvalidDataError as y };
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "__MSG_extName__",
4
4
  "description": "__MSG_extDescription__",
5
- "version": "0.3.0",
5
+ "version": "0.4.1",
6
6
  "default_locale": "en",
7
7
  "minimum_chrome_version": "116",
8
8
  "icons": {
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>LangCoach</title>
7
7
  <script type="module" crossorigin src="/sidepanel.js"></script>
8
- <link rel="modulepreload" crossorigin href="/chunks/translate-CM6Phtg_.js">
9
- <link rel="stylesheet" crossorigin href="/assets/sidepanel-DMjVhmRR.css">
8
+ <link rel="modulepreload" crossorigin href="/chunks/translate-DvfZknmj.js">
9
+ <link rel="stylesheet" crossorigin href="/assets/sidepanel-BBkB7IY_.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="app"></div>