dsh-plugin-lookatstudy 0.25.2 → 0.25.4
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/lib/client.js +292 -6
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +16 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -391,7 +391,16 @@ window.__ModuleLoader__.load({
|
|
|
391
391
|
.lks-ui .lks14-emptycard-title{font-size:1rem;font-weight:800;color:var(--ink-strong)}
|
|
392
392
|
.lks-ui .lks14-emptycard-hint{font-size:.825rem;color:var(--ink-muted);line-height:1.6}
|
|
393
393
|
/* B5: notebook tab capsule + 960px reading column */
|
|
394
|
-
.lks-ui .lks14-notebody{margin:0
|
|
394
|
+
.lks-ui .lks14-notebody{margin:0;width:100%;padding:12px 20px 24px;box-sizing:border-box}
|
|
395
|
+
/* 0.25.3 (owner): the view tabs pin to the note column's top — full-bleed
|
|
396
|
+
negative margins cancel the notebody padding so the bar covers the whole
|
|
397
|
+
column width while content scrolls underneath */
|
|
398
|
+
.lks-ui .lks14-viewtabs{position:sticky;top:0;z-index:20;margin:0 -20px 12px;padding:8px 20px 6px;background:var(--surface-2);border-bottom:1px solid var(--border-faint)}
|
|
399
|
+
.lks-ui .lks14-viewtab{display:inline-flex;align-items:center;justify-content:center;gap:5px;flex:1 1 0}
|
|
400
|
+
/* 0.25.3 karaoke: the speaking sentence highlight (CSS Custom Highlight API
|
|
401
|
+
registration; the span class is the no-Highlight-API fallback) */
|
|
402
|
+
::highlight(lks-reading){background:rgb(var(--brand-rgb)/0.28);color:var(--ink-strong)}
|
|
403
|
+
.lks-ui .lks14-reading-mark{background:rgb(var(--brand-rgb)/0.28);color:var(--ink-strong);border-radius:3px}
|
|
395
404
|
/* #10-followup: the reading column is height:auto (the note col scrolls), so
|
|
396
405
|
.lks14-board's height:100% resolved against a content-sized parent and the
|
|
397
406
|
stage collapsed to a ~90px sliver — the never-upscale contain fit then
|
|
@@ -1418,6 +1427,38 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
1418
1427
|
fill: "currentColor"
|
|
1419
1428
|
}));
|
|
1420
1429
|
}
|
|
1430
|
+
/** IconBookOutline16 — plugin-original learning-domain companion (teach tab). */
|
|
1431
|
+
function IconBookOutline16({ size = 16, className }) {
|
|
1432
|
+
return (0, react.createElement)("svg", {
|
|
1433
|
+
width: size,
|
|
1434
|
+
height: size,
|
|
1435
|
+
className,
|
|
1436
|
+
viewBox: "0 0 16 16",
|
|
1437
|
+
fill: "none",
|
|
1438
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
1439
|
+
}, (0, react.createElement)("path", {
|
|
1440
|
+
fillRule: "evenodd",
|
|
1441
|
+
clipRule: "evenodd",
|
|
1442
|
+
d: "M2 2.5C2 1.67157 2.67157 1 3.5 1H12.5C13.3284 1 14 1.67157 14 2.5V12.5C14 13.3284 13.3284 14 12.5 14H3.5C2.67157 14 2 13.3284 2 12.5V2.5ZM3.5 2C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H4V3H3.5ZM5 3V13H12.5C12.7761 13 13 12.7761 13 12.5V2.5C13 2.22386 12.7761 2 12.5 2H5ZM6 4.5H11.5V5.5H6V4.5ZM6 7H11.5V8H6V7ZM6 9.5H10V10.5H6V9.5Z",
|
|
1443
|
+
fill: "currentColor"
|
|
1444
|
+
}));
|
|
1445
|
+
}
|
|
1446
|
+
/** IconPenOutline16 — plugin-original learning-domain companion (notes tab). */
|
|
1447
|
+
function IconPenOutline16({ size = 16, className }) {
|
|
1448
|
+
return (0, react.createElement)("svg", {
|
|
1449
|
+
width: size,
|
|
1450
|
+
height: size,
|
|
1451
|
+
className,
|
|
1452
|
+
viewBox: "0 0 16 16",
|
|
1453
|
+
fill: "none",
|
|
1454
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
1455
|
+
}, (0, react.createElement)("path", {
|
|
1456
|
+
fillRule: "evenodd",
|
|
1457
|
+
clipRule: "evenodd",
|
|
1458
|
+
d: "M11.4503 1.65669C12.0849 1.02209 13.1145 1.02209 13.7491 1.65669L14.3433 2.25091C14.9779 2.88551 14.9779 3.91507 14.3433 4.54967L6.13604 12.7569L2.33156 13.7212C1.92166 13.8248 1.54569 13.4488 1.64934 13.0389L2.61359 9.23442L10.8208 1.02719L11.4503 1.65669ZM11.5317 2.66034L3.61621 10.5758L3.04325 12.8379L4.3433 12.5094L12.2987 4.55397L11.5317 2.78695V2.66034Z",
|
|
1459
|
+
fill: "currentColor"
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1421
1462
|
/** IconGoalOutline16 */
|
|
1422
1463
|
function IconGoalOutline16({ size = 16, className }) {
|
|
1423
1464
|
return (0, react.createElement)("svg", {
|
|
@@ -4506,6 +4547,21 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4506
4547
|
s = s.replace(/`[^`\n]*`/g, "");
|
|
4507
4548
|
s = s.replace(/!\[[^\]]*\]\([^)\n]*\)/g, "");
|
|
4508
4549
|
s = s.replace(/\[([^\]]+)\]\([^)\n]*\)/g, "$1");
|
|
4550
|
+
s = s.replace(/\[\]\([^)\n]*\)/g, "");
|
|
4551
|
+
s = s.replace(/<br\s*\/?>/gi, "\n");
|
|
4552
|
+
s = s.replace(/<https?:\/\/[^>\s]+>/g, "");
|
|
4553
|
+
s = s.replace(/https?:\/\/[^\s)\]]+/g, "");
|
|
4554
|
+
s = s.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, "\n");
|
|
4555
|
+
s = s.replace(/<[^>\n]+>/g, "");
|
|
4556
|
+
s = s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/�?39;/g, "'").replace(/ /g, " ").replace(/&#(\d+);/g, (_m, d) => {
|
|
4557
|
+
try {
|
|
4558
|
+
return String.fromCodePoint(Number(d));
|
|
4559
|
+
} catch {
|
|
4560
|
+
return "";
|
|
4561
|
+
}
|
|
4562
|
+
}).replace(/&[a-z]+;/gi, "");
|
|
4563
|
+
s = s.replace(/\[([^\]]+)\]\[[^\]]*\]/g, "$1");
|
|
4564
|
+
s = s.replace(/\[\^[^\]]*\]/g, "");
|
|
4509
4565
|
s = s.replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, "");
|
|
4510
4566
|
s = s.replace(/^[ \t]*(?:[-*+]|\d+\.)[ \t]+/gm, "");
|
|
4511
4567
|
s = s.replace(/^[ \t]*>[ \t]?/gm, "");
|
|
@@ -4605,6 +4661,203 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
4605
4661
|
};
|
|
4606
4662
|
}
|
|
4607
4663
|
//#endregion
|
|
4664
|
+
//#region src/client/reading-mark.ts
|
|
4665
|
+
/**
|
|
4666
|
+
* Reading karaoke — the sentence highlight + follow for the teach prose's
|
|
4667
|
+
* read-aloud. Ported subset of upstream LookatStudy src/renderer/lib/
|
|
4668
|
+
* highlightText.ts (MIT License, https://github.com/Kaiji-Z/LookatStudy):
|
|
4669
|
+
* the v9 canonical whole-sentence alignment (punctuation/full-width
|
|
4670
|
+
* differences collapse; edge extension swallows opening quotes and sentence
|
|
4671
|
+
* punctuation so the highlight always covers the visible sentence), the v8
|
|
4672
|
+
* CSS Custom Highlight API registration (zero DOM mutation — span wrapping
|
|
4673
|
+
* fights React's reconciliation on every sentence tick; the span stays only
|
|
4674
|
+
* as a fallback), and the v0.18.1 line-box centering follow (a 13k-char
|
|
4675
|
+
* paragraph renders 3000px tall — element-box scrollIntoView pins at the
|
|
4676
|
+
* paragraph head while the highlight walks on; the sentence's own first-line
|
|
4677
|
+
* rect is what must stay in view). Not ported: companion avoidance, played
|
|
4678
|
+
* -prefix splitting, display-group spans (companion is retired in this
|
|
4679
|
+
* plugin; the controller's sentence index is already authoritative).
|
|
4680
|
+
* @module dsh-plugin-lookatstudy/client/reading-mark
|
|
4681
|
+
*/
|
|
4682
|
+
const WORD_CHAR_RE = /[0-9A-Za-z\u00C0-\u024F\u0370-\u03FF\u0400-\u04FF\u4E00-\u9FFF\u3040-\u30FF\uAC00-\uD7AF]/;
|
|
4683
|
+
const OPEN_EDGE = "「『(([【<《\"'“‘";
|
|
4684
|
+
const CLOSE_EDGE = "。』」))]】>》\"'”’!!??…,,、;;::";
|
|
4685
|
+
/** One char → canonical form (full-width→half-width, lowercase) or null when
|
|
4686
|
+
* the char is punctuation/space/symbol (all filtered). Pure (upstream). */
|
|
4687
|
+
function canonChar(ch) {
|
|
4688
|
+
const c = ch.codePointAt(0);
|
|
4689
|
+
let s = ch;
|
|
4690
|
+
if (c >= 65281 && c <= 65374) s = String.fromCharCode(c - 65248);
|
|
4691
|
+
return WORD_CHAR_RE.test(s) ? s.toLowerCase() : null;
|
|
4692
|
+
}
|
|
4693
|
+
/** Text → canonical string + canonical→original offset map. Pure (upstream). */
|
|
4694
|
+
function canonicalSpeechIndex(text) {
|
|
4695
|
+
let canon = "";
|
|
4696
|
+
const map = [];
|
|
4697
|
+
for (let i = 0; i < text.length; i++) {
|
|
4698
|
+
const c = canonChar(text[i]);
|
|
4699
|
+
if (c !== null) {
|
|
4700
|
+
canon += c;
|
|
4701
|
+
map.push(i);
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
return {
|
|
4705
|
+
canon,
|
|
4706
|
+
map
|
|
4707
|
+
};
|
|
4708
|
+
}
|
|
4709
|
+
/**
|
|
4710
|
+
* Align one speech sentence against the prose's full text (pure). Whole
|
|
4711
|
+
* canonical indexOf first (the common case — punctuation/width differences
|
|
4712
|
+
* vanish); on failure a token-interval match lets displayed-but-unspoken
|
|
4713
|
+
* runs (inline code, table pipes) sit inside the match as gaps; the matched
|
|
4714
|
+
* span then extends over opening quotes before and sentence punctuation
|
|
4715
|
+
* after, so the highlight covers the complete visible sentence.
|
|
4716
|
+
* @returns [start, end) offsets into `text`, or null.
|
|
4717
|
+
*/
|
|
4718
|
+
function matchSentenceAligned(text, sentence, from = 0) {
|
|
4719
|
+
const { canon, map } = canonicalSpeechIndex(text);
|
|
4720
|
+
const sen = canonicalSpeechIndex(sentence).canon;
|
|
4721
|
+
if (sen === "" || canon === "") return null;
|
|
4722
|
+
let hit = canon.indexOf(sen, Math.min(from, canon.length));
|
|
4723
|
+
if (hit === -1) hit = canon.indexOf(sen);
|
|
4724
|
+
let start;
|
|
4725
|
+
let end;
|
|
4726
|
+
if (hit !== -1) {
|
|
4727
|
+
start = map[hit];
|
|
4728
|
+
end = (map[hit + sen.length - 1] ?? map[map.length - 1]) + 1;
|
|
4729
|
+
} else {
|
|
4730
|
+
const head = sen.slice(0, Math.min(2, sen.length));
|
|
4731
|
+
const tail = sen.slice(-Math.min(2, sen.length));
|
|
4732
|
+
const h = canon.indexOf(head, Math.min(from, canon.length));
|
|
4733
|
+
if (h === -1) return null;
|
|
4734
|
+
const t = tail === "" ? h + head.length - 1 : canon.lastIndexOf(tail, Math.min(canon.length - 1, h + sen.length + 128));
|
|
4735
|
+
if (t < h + head.length - 1) return null;
|
|
4736
|
+
start = map[h];
|
|
4737
|
+
end = (map[t + tail.length - 1] ?? map[map.length - 1]) + 1;
|
|
4738
|
+
}
|
|
4739
|
+
while (start > 0 && OPEN_EDGE.includes(text[start - 1])) start--;
|
|
4740
|
+
while (end < text.length && CLOSE_EDGE.includes(text[end])) end++;
|
|
4741
|
+
return [start, end];
|
|
4742
|
+
}
|
|
4743
|
+
function buildTextModel(container) {
|
|
4744
|
+
const nodes = [];
|
|
4745
|
+
let text = "";
|
|
4746
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { acceptNode(node) {
|
|
4747
|
+
let p = node.parentNode;
|
|
4748
|
+
while (p !== null && p !== container) {
|
|
4749
|
+
if (p instanceof HTMLElement && (p.tagName === "SCRIPT" || p.tagName === "STYLE" || p.classList.contains("lks14-reading-mark"))) return NodeFilter.FILTER_REJECT;
|
|
4750
|
+
p = p.parentNode;
|
|
4751
|
+
}
|
|
4752
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
4753
|
+
} });
|
|
4754
|
+
for (let n = walker.nextNode(); n !== null; n = walker.nextNode()) {
|
|
4755
|
+
const t = n;
|
|
4756
|
+
nodes.push({
|
|
4757
|
+
node: t,
|
|
4758
|
+
start: text.length
|
|
4759
|
+
});
|
|
4760
|
+
text += t.data;
|
|
4761
|
+
}
|
|
4762
|
+
return {
|
|
4763
|
+
text,
|
|
4764
|
+
nodes
|
|
4765
|
+
};
|
|
4766
|
+
}
|
|
4767
|
+
function offsetsToRange(model, start, end) {
|
|
4768
|
+
const range = document.createRange();
|
|
4769
|
+
let setStart = false;
|
|
4770
|
+
let setEnd = false;
|
|
4771
|
+
for (const { node, start: ns } of model.nodes) {
|
|
4772
|
+
const len = node.data.length;
|
|
4773
|
+
if (!setStart && start >= ns && start <= ns + len) {
|
|
4774
|
+
range.setStart(node, start - ns);
|
|
4775
|
+
setStart = true;
|
|
4776
|
+
}
|
|
4777
|
+
if (!setEnd && end >= ns && end <= ns + len) {
|
|
4778
|
+
range.setEnd(node, end - ns);
|
|
4779
|
+
setEnd = true;
|
|
4780
|
+
break;
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
return setStart && setEnd ? range : null;
|
|
4784
|
+
}
|
|
4785
|
+
const READING_HIGHLIGHT_NAME = "lks-reading";
|
|
4786
|
+
const READING_MARK_CLASS = "lks14-reading-mark";
|
|
4787
|
+
const readingCursors = /* @__PURE__ */ new WeakMap();
|
|
4788
|
+
/** Drop the registry highlight outright (unmount/stop; DOM may be gone). */
|
|
4789
|
+
function clearReadingHighlightRegistry() {
|
|
4790
|
+
const cssLike = CSS;
|
|
4791
|
+
try {
|
|
4792
|
+
cssLike.highlights?.delete(READING_HIGHLIGHT_NAME);
|
|
4793
|
+
} catch {}
|
|
4794
|
+
}
|
|
4795
|
+
/** Remove the reading mark (registry + fallback spans) from one container. */
|
|
4796
|
+
function clearReadingMark(container) {
|
|
4797
|
+
clearReadingHighlightRegistry();
|
|
4798
|
+
container.querySelectorAll(`.${READING_MARK_CLASS}`).forEach((m) => {
|
|
4799
|
+
const parent = m.parentNode;
|
|
4800
|
+
if (parent !== null) {
|
|
4801
|
+
while (m.firstChild !== null) parent.insertBefore(m.firstChild, m);
|
|
4802
|
+
parent.removeChild(m);
|
|
4803
|
+
}
|
|
4804
|
+
});
|
|
4805
|
+
container.normalize();
|
|
4806
|
+
}
|
|
4807
|
+
/** A fresh read restarts the monotonic cursor. */
|
|
4808
|
+
function resetReadingCursor(container) {
|
|
4809
|
+
readingCursors.set(container, 0);
|
|
4810
|
+
}
|
|
4811
|
+
/**
|
|
4812
|
+
* Highlight the currently spoken sentence in the prose. Returns the marked
|
|
4813
|
+
* Range (null when the sentence cannot be aligned — the read keeps going,
|
|
4814
|
+
* only the highlight skips).
|
|
4815
|
+
*/
|
|
4816
|
+
function markReadingSentence(container, sentence) {
|
|
4817
|
+
clearReadingMark(container);
|
|
4818
|
+
const trimmed = sentence.trim();
|
|
4819
|
+
if (trimmed === "") return null;
|
|
4820
|
+
const model = buildTextModel(container);
|
|
4821
|
+
const from = readingCursors.get(container) ?? 0;
|
|
4822
|
+
let m = matchSentenceAligned(model.text, trimmed, from);
|
|
4823
|
+
if (m === null && from > 0) m = matchSentenceAligned(model.text, trimmed, 0);
|
|
4824
|
+
if (m === null) return null;
|
|
4825
|
+
readingCursors.set(container, m[1]);
|
|
4826
|
+
const range = offsetsToRange(model, m[0], m[1]);
|
|
4827
|
+
if (range === null) return null;
|
|
4828
|
+
const cssLike = CSS;
|
|
4829
|
+
if (typeof Highlight !== "undefined" && cssLike.highlights !== void 0) try {
|
|
4830
|
+
cssLike.highlights.set(READING_HIGHLIGHT_NAME, new Highlight(range));
|
|
4831
|
+
return range;
|
|
4832
|
+
} catch {}
|
|
4833
|
+
const mark = document.createElement("span");
|
|
4834
|
+
mark.className = READING_MARK_CLASS;
|
|
4835
|
+
try {
|
|
4836
|
+
range.surroundContents(mark);
|
|
4837
|
+
return range;
|
|
4838
|
+
} catch {
|
|
4839
|
+
return range;
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Keep the marked sentence's FIRST-LINE rect inside the scroll viewport
|
|
4844
|
+
* (upstream v0.18.1): element-box centering pins on 3000px paragraphs; the
|
|
4845
|
+
* sentence's own line box is what the eye follows. Smooth-centers only when
|
|
4846
|
+
* the line leaves a 48px band.
|
|
4847
|
+
*/
|
|
4848
|
+
function centerReadingRangeInView(range, scroller) {
|
|
4849
|
+
const rects = range.getClientRects();
|
|
4850
|
+
const line = rects.length > 0 && rects[0].height > 0 ? rects[0] : null;
|
|
4851
|
+
if (line === null) return;
|
|
4852
|
+
if (scroller === null) return;
|
|
4853
|
+
const sTop = scroller.getBoundingClientRect().top;
|
|
4854
|
+
const rel = line.top - sTop;
|
|
4855
|
+
if (rel < 48 || rel > scroller.clientHeight - 48) scroller.scrollTo({
|
|
4856
|
+
top: scroller.scrollTop + rel - scroller.clientHeight / 2,
|
|
4857
|
+
behavior: "smooth"
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
//#endregion
|
|
4608
4861
|
//#region src/vendor/math-speech.ts
|
|
4609
4862
|
/**
|
|
4610
4863
|
* Vendored from LookatStudy shared/math-speech.ts (MIT License,
|
|
@@ -10121,10 +10374,11 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10121
10374
|
}, []);
|
|
10122
10375
|
const [enhanceTick, setEnhanceTick] = (0, react.useState)(0);
|
|
10123
10376
|
const [rateBusy, setRateBusy] = (0, react.useState)(false);
|
|
10377
|
+
const speakText = lesson !== null && langView === "translation" && lesson.translationSpeechText !== void 0 ? lesson.translationSpeechText : lesson?.speechText ?? "";
|
|
10124
10378
|
const startReading = () => {
|
|
10125
|
-
if (lesson === null ||
|
|
10379
|
+
if (lesson === null || speakText.trim() === "") return;
|
|
10126
10380
|
readCtl.current?.stop();
|
|
10127
|
-
const sentences = speechSentencesOf(
|
|
10381
|
+
const sentences = speechSentencesOf(speakText);
|
|
10128
10382
|
const voice = storedTtsVoice();
|
|
10129
10383
|
const prefetch = /* @__PURE__ */ new Map();
|
|
10130
10384
|
let currentAudio = null;
|
|
@@ -10206,6 +10460,38 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10206
10460
|
readCtl.current?.stop();
|
|
10207
10461
|
setRead(null);
|
|
10208
10462
|
};
|
|
10463
|
+
(0, react.useEffect)(() => {
|
|
10464
|
+
const prose = proseRef.current;
|
|
10465
|
+
if (prose === null) return;
|
|
10466
|
+
if (read === null || read.state !== "speaking" || tab !== "teach") {
|
|
10467
|
+
clearReadingMark(prose);
|
|
10468
|
+
return;
|
|
10469
|
+
}
|
|
10470
|
+
if (read.index === 0) resetReadingCursor(prose);
|
|
10471
|
+
const sentence = speechSentencesOf(speakText)[read.index];
|
|
10472
|
+
if (sentence === void 0) return;
|
|
10473
|
+
const timer = setTimeout(() => {
|
|
10474
|
+
const rg = markReadingSentence(prose, sentence);
|
|
10475
|
+
if (rg !== null) centerReadingRangeInView(rg, prose.closest(".lks14-note"));
|
|
10476
|
+
}, 30);
|
|
10477
|
+
return () => {
|
|
10478
|
+
clearTimeout(timer);
|
|
10479
|
+
};
|
|
10480
|
+
}, [
|
|
10481
|
+
read?.index,
|
|
10482
|
+
read?.state,
|
|
10483
|
+
tab,
|
|
10484
|
+
teachHtml
|
|
10485
|
+
]);
|
|
10486
|
+
(0, react.useEffect)(() => {
|
|
10487
|
+
if (read === null && proseRef.current !== null) clearReadingMark(proseRef.current);
|
|
10488
|
+
}, [lesson?.lessonId]);
|
|
10489
|
+
(0, react.useEffect)(() => () => {
|
|
10490
|
+
clearReadingHighlightRegistry();
|
|
10491
|
+
}, []);
|
|
10492
|
+
(0, react.useEffect)(() => {
|
|
10493
|
+
if (read !== null) stopReading();
|
|
10494
|
+
}, [langView]);
|
|
10209
10495
|
(0, react.useEffect)(() => {
|
|
10210
10496
|
if (tab !== "cmap" || diagRef.current === null || lesson === null) return;
|
|
10211
10497
|
const el = diagRef.current;
|
|
@@ -10239,7 +10525,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10239
10525
|
onClick: () => {
|
|
10240
10526
|
setTab("teach");
|
|
10241
10527
|
}
|
|
10242
|
-
}, tr("viewtab.teach")), (0, react.createElement)("button", {
|
|
10528
|
+
}, (0, react.createElement)(IconBookOutline16, { size: 13 }), tr("viewtab.teach")), (0, react.createElement)("button", {
|
|
10243
10529
|
className: `lks14-viewtab${tab === "cmap" ? " on" : ""}`,
|
|
10244
10530
|
"aria-pressed": String(tab === "cmap"),
|
|
10245
10531
|
"data-tooltip": tr("viewtab.cmap.title"),
|
|
@@ -10256,7 +10542,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10256
10542
|
setUnseenTick(Date.now());
|
|
10257
10543
|
}
|
|
10258
10544
|
}
|
|
10259
|
-
}, tr("bb.notes"), unseen.length > 0 ? (0, react.createElement)("span", { className: "lks-viewtab-badge" }, String(unseen.length)) : null), (0, react.createElement)("button", {
|
|
10545
|
+
}, (0, react.createElement)(IconPenOutline16, { size: 13 }), tr("bb.notes"), unseen.length > 0 ? (0, react.createElement)("span", { className: "lks-viewtab-badge" }, String(unseen.length)) : null), (0, react.createElement)("button", {
|
|
10260
10546
|
className: `lks14-viewtab${tab === "board" ? " on" : ""}`,
|
|
10261
10547
|
"aria-pressed": String(tab === "board"),
|
|
10262
10548
|
"data-tooltip": tr("bb.board.empty"),
|
|
@@ -10331,7 +10617,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
|
|
|
10331
10617
|
},
|
|
10332
10618
|
"data-tooltip": tr("read.stop"),
|
|
10333
10619
|
onClick: stopReading
|
|
10334
|
-
}, tr("read.stop")) : null, read !== null && read.state !== "idle" && lesson !== null ? (0, react.createElement)("span", { className: "lks-readbar-cur" }, (speechSentencesOf(
|
|
10620
|
+
}, tr("read.stop")) : null, read !== null && read.state !== "idle" && lesson !== null ? (0, react.createElement)("span", { className: "lks-readbar-cur" }, (speechSentencesOf(speakText)[read.index] ?? "").slice(0, 80)) : null, readError !== null ? (0, react.createElement)("span", { className: "lks-readbar-notice" }, readError) : null, read !== null && read.degraded ? (0, react.createElement)("span", { className: "lks-readbar-notice" }, tr("read.engine.system")) : null), (0, react.createElement)("div", { className: "lks14-prosewrap" }, (0, react.createElement)(ContentBoundary, {
|
|
10335
10621
|
content: lesson.markdown ?? "",
|
|
10336
10622
|
boundaryKey: `prose-${lesson.lessonId}`
|
|
10337
10623
|
}, (0, react.createElement)("div", {
|