paperlint 14.3.0 → 14.5.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.
@@ -3,8 +3,8 @@
3
3
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Roboto+Mono&display=swap">
4
4
  <style>
5
5
  /* One look, dark whatever the viewer's theme (Material Ocean, no purple): every color is a token below.
6
- One column, sections apart by a hairline and wide space, items flat; the only raised block of an item is
7
- the question being asked. Answer buttons are full-width rows with space between them, so a thumb cannot
6
+ One column, sections apart by a hairline and wide space; each item a bordered panel with wide space after
7
+ it, and the only raised block inside it the question being asked. Answer buttons are full-width rows with space between them, so a thumb cannot
8
8
  land between two of them, and the chosen one has a 2px accent border. */
9
9
  :root {
10
10
  color-scheme: dark;
@@ -28,8 +28,9 @@ p { margin: 0; max-width: var(--measure); }
28
28
  .muted { color: var(--muted); }
29
29
  .id { font-family: var(--mono); color: var(--muted); }
30
30
  section { display: flex; flex-direction: column; gap: var(--gap-item); }
31
- .item { display: flex; flex-direction: column; gap: 20px; padding-top: var(--gap-item);
32
- border-top: 1px solid var(--line); }
31
+ .item { display: flex; flex-direction: column; gap: 20px; padding: var(--gap-item);
32
+ border: 1px solid var(--line); border-radius: 14px; }
33
+ .item + .item { margin-top: 24px; }
33
34
  /* The evidence of a card: one labelled row per field, the label a short word above its content. The two
34
35
  readings sit in the same place on every card: A on a solid accent rule, B on a dashed one. */
35
36
  .row { display: flex; flex-direction: column; gap: 4px; }
@@ -37,7 +38,6 @@ section { display: flex; flex-direction: column; gap: var(--gap-item); }
37
38
  .reading { border-left: 3px solid var(--accent); padding-left: 12px; }
38
39
  .reading.b { border-left: 3px dashed var(--text); }
39
40
  mark { background: transparent; color: var(--mark); font-weight: 500; }
40
- .item:first-of-type { border-top: 0; padding-top: 0; }
41
41
  .ask { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 16px;
42
42
  display: flex; flex-direction: column; gap: 12px; }
43
43
  .q { color: var(--strong); font-weight: 500; }
@@ -143,6 +143,9 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
143
143
  .match.no { color: var(--bad); }
144
144
  .who { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--muted); }
145
145
  .who button { width: auto; min-height: 44px; border: 0; color: var(--accent); padding: 0 4px; }
146
+ /* The language switch: small buttons side by side under the title, the picked one pressed. */
147
+ .langs { display: flex; gap: 8px; }
148
+ .langs button { width: auto; min-width: 56px; min-height: 44px; text-align: center; }
146
149
  </style>
147
150
  <main id="app"><p class="muted">Loading…</p></main>
148
151
  <script type="application/json" id="data">__DATA__</script>
@@ -182,6 +185,34 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
182
185
  var WHY_PAUSE_MS = 800;
183
186
 
184
187
  try { if (localStorage.getItem("label-filter") === "next") state.filter = "next"; } catch (e) {}
188
+ var BASE = { title: DATA.title, intro: DATA.intro, labels: DATA.labels, questions: DATA.questions };
189
+ // ── the page's own words, in the language picked (DATA.translations, #394) ─────────────────
190
+ // Only the page's words change: the quote, the code and every note on a card stay as built.
191
+ var LANGS = DATA.translations ? [DATA.lang].concat(Object.keys(DATA.translations)) : [];
192
+ state.lang = DATA.lang || "en";
193
+ try {
194
+ var said = localStorage.getItem("label-lang");
195
+ if (LANGS.indexOf(said) !== -1) state.lang = said;
196
+ } catch (e) {}
197
+ function words() { return state.lang === DATA.lang || !DATA.translations ? BASE : DATA.translations[state.lang]; }
198
+ function Qs() { return words().questions; }
199
+ function pickLang(lang) {
200
+ state.lang = lang;
201
+ try { localStorage.setItem("label-lang", lang); } catch (e) {}
202
+ render();
203
+ }
204
+ // One button per language, its tag on it ("EN", "RU"); the one picked is pressed.
205
+ function langSwitch() {
206
+ var row = mark(el("div", "langs"), "lang-switch", { lang: state.lang, langs: LANGS });
207
+ LANGS.forEach(function (lang) {
208
+ var b = mark(el("button", "", lang.toUpperCase()), "lang-option", { lang: lang });
209
+ b.type = "button";
210
+ b.setAttribute("aria-pressed", String(state.lang === lang));
211
+ b.addEventListener("click", function () { pickLang(lang); });
212
+ row.appendChild(b);
213
+ });
214
+ return row;
215
+ }
185
216
  // ── who is answering (DATA.answerers): each answerer's documents are "<answerer>~<id>" ─────
186
217
  // "~" is outside the build's name pattern, so no answerer, item, group or practice id holds it.
187
218
  var SEP = "~";
@@ -240,7 +271,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
240
271
  function unsaved(id) { return !state.answers[id] && state.firstSeconds[id] === undefined; }
241
272
  function locked(id) { return MIN_SECONDS > 0 && unsaved(id) && dwellMs(id) < MIN_SECONDS * 1000; }
242
273
  function waitSeconds(id) { return Math.ceil((MIN_SECONDS * 1000 - dwellMs(id)) / 1000); }
243
- function waitText(id) { return fill(DATA.labels.wait, { s: waitSeconds(id) }); }
274
+ function waitText(id) { return fill(words().labels.wait, { s: waitSeconds(id) }); }
244
275
  // The seconds stored with an item: those of its first save, kept on every later write.
245
276
  function secondsOf(id) {
246
277
  var d = state.answers[id];
@@ -341,11 +372,11 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
341
372
  return d[qid];
342
373
  }
343
374
  function needsWhy(q, v) { return (DATA.allowed.needs_why[q.id] || []).indexOf(v) !== -1; }
344
- function questionOf(qid) { return DATA.questions.filter(function (q) { return q.id === qid; })[0]; }
375
+ function questionOf(qid) { return Qs().filter(function (q) { return q.id === qid; })[0]; }
345
376
  // What an item still needs: an answer, or the reason its answer asks for (named "<short> (why)").
346
377
  function missingFor(id) {
347
378
  var a = answersOf(id), r = reasonsOf(id);
348
- return DATA.questions.map(function (q) {
379
+ return Qs().map(function (q) {
349
380
  if (!valid(DATA.allowed.questions[q.id], a[q.id])) return q;
350
381
  if (needsWhy(q, a[q.id]) && !(r[q.id] || "").trim()) {
351
382
  return { id: q.id, text: q.text + " (why)", short: (q.short || q.text) + " (why)" };
@@ -373,12 +404,12 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
373
404
  }
374
405
  function practiceMissing(pid) {
375
406
  var a = practiceAnswers(pid);
376
- return DATA.questions.filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
407
+ return Qs().filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
377
408
  }
378
409
  function practiceAnswered(p) { return practiceMissing(p.id).length === 0; }
379
410
  function practiceMatched(p) {
380
411
  var a = practiceAnswers(p.id);
381
- return DATA.questions.every(function (q) { return a[q.id] === p.expected[q.id]; });
412
+ return Qs().every(function (q) { return a[q.id] === p.expected[q.id]; });
382
413
  }
383
414
  function practiceOpen() { return !DATA.practice.items.every(practiceAnswered); }
384
415
  function groupDone(g) { return familiarity(g) !== undefined && groupItems(g).every(function (i) { return itemDone(i.id); }); }
@@ -540,7 +571,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
540
571
  if (o.icon) b.appendChild(el("span", "icon", o.icon));
541
572
  b.appendChild(el("span", "label", o.label));
542
573
  if (notes && notes[o.value]) b.appendChild(mark(rich("span", "sub", notes[o.value]), "answer-sub", { question: qid, value: o.value }));
543
- if (withExamples && o.example) b.appendChild(rich("span", "ex", DATA.labels.practice_example + o.example));
574
+ if (withExamples && o.example) b.appendChild(rich("span", "ex", words().labels.practice_example + o.example));
544
575
  b.type = "button";
545
576
  b.setAttribute("aria-pressed", String(current === o.value));
546
577
  b.disabled = disabled;
@@ -569,11 +600,14 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
569
600
  } else if (state.inflight[key]) {
570
601
  as("saving");
571
602
  s.textContent = "Saving…";
603
+ } else if (missing.length && !savedAt) {
604
+ // The open question and its save button already say what is missing: nothing to show.
605
+ as("missing");
606
+ s.hidden = true;
572
607
  } else if (missing.length) {
573
608
  as("missing");
574
- s.className = "status missing";
575
- s.textContent = "Still needs: " + missing.map(function (q) { return q.short || q.text; }).join("; ") +
576
- (savedAt ? ". Saved " + timeOf(savedAt) : "");
609
+ s.className = "status saved";
610
+ s.textContent = "Saved " + timeOf(savedAt);
577
611
  } else if (savedAt) {
578
612
  as("saved");
579
613
  s.className = "status saved";
@@ -586,12 +620,12 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
586
620
  var shown = itemsShown();
587
621
  if (MIN_SECONDS && unsaved(item.id)) card.setAttribute("data-id", item.id);
588
622
  mark(card, "item", { id: item.id, index: shown.indexOf(item) + 1, total: shown.length });
589
- card.appendChild(el("div", "id", fill(DATA.labels.place, { id: item.id, n: shown.indexOf(item) + 1, total: shown.length })));
623
+ card.appendChild(el("div", "id", fill(words().labels.place, { id: item.id, n: shown.indexOf(item) + 1, total: shown.length })));
590
624
  var ctx = termsOf(item);
591
625
  evidence(card, item, ctx);
592
626
  var a = answersOf(item.id);
593
627
  var gate = locked(item.id);
594
- var ask = askOne(card, a, state.editing[item.id], ctx, function (q, v) {
628
+ var ask = askOne(card, a, state.editing[item.id], function (q, v) {
595
629
  delete state.editing[item.id];
596
630
  answer(item.id, q.id, v);
597
631
  }, function (q) { state.editing[item.id] = q.id; render(); }, off || gate, false, item);
@@ -599,7 +633,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
599
633
  var note = ask.appendChild(mark(el("p", "gate", waitText(item.id)), "wait", { id: item.id, seconds: waitSeconds(item.id) }));
600
634
  state.gates[item.id] = { note: note, buttons: Array.prototype.slice.call(ask.querySelectorAll(".opts button")) };
601
635
  }
602
- DATA.questions.forEach(function (q) {
636
+ Qs().forEach(function (q) {
603
637
  var o = q.options.filter(function (x) { return x.value === a[q.id] && x.ask_why; })[0];
604
638
  if (!o) return;
605
639
  card.appendChild(el("p", "q", o.ask_why));
@@ -713,7 +747,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
713
747
  if (prev && l.n > prev.n + 1) {
714
748
  var from = prev.n + 1, to = l.n - 1;
715
749
  pre.appendChild(mark(codeRow(" gap", "\u22ee", "", withGutter,
716
- fill(from === to ? DATA.labels.hidden_one : DATA.labels.hidden, { from: from, to: to })).row, "gap", { from: from, to: to }));
750
+ fill(from === to ? words().labels.hidden_one : words().labels.hidden, { from: from, to: to })).row, "gap", { from: from, to: to }));
717
751
  }
718
752
  var r = codeRow((l.mark ? " hit" : "") + (l.context ? " ctx" : ""), (l.mark ? "\u25b6" : "") + l.n,
719
753
  l.gutter, withGutter, l.text);
@@ -927,15 +961,17 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
927
961
  if (item.table) card.appendChild(table(item.table));
928
962
  }
929
963
  // What a card shows above its question, one labelled row per field (an item and a practice item alike).
964
+ // A card's quote comes first, under its title: the words asked about, then the code they are about.
930
965
  function evidence(card, item, ctx) {
931
966
  if (item.title) card.appendChild(rich("h3", "", item.title));
967
+ if (item.quote) card.appendChild(quoteBlock(item, ctx));
932
968
  if (item.body !== undefined) {
933
969
  var body = rich("p", "body", item.body);
934
- card.appendChild(DATA.labels.body ? row(DATA.labels.body, body) : body);
970
+ card.appendChild(words().labels.body ? row(words().labels.body, body) : body);
935
971
  }
936
- if (item.repeat && item.repeat.of > 1) card.appendChild(el("p", "muted", fill(DATA.labels.repeat, item.repeat)));
972
+ if (item.repeat && item.repeat.of > 1) card.appendChild(el("p", "muted", fill(words().labels.repeat, item.repeat)));
937
973
  if (item.code) card.appendChild(codeBlock(item.code));
938
- if (item.unit) card.appendChild(row(item.unit.of > 1 ? fill(DATA.labels.line_part, { line: DATA.labels.line, part: item.unit.part, of: item.unit.of }) : DATA.labels.line,
974
+ if (item.unit) card.appendChild(row(item.unit.of > 1 ? fill(words().labels.line_part, { line: words().labels.line, part: item.unit.part, of: item.unit.of }) : words().labels.line,
939
975
  markedLine(item.unit)));
940
976
  // A rule the item names, with what it does in the linter's own words (looked up at build time).
941
977
  (item.rules || []).forEach(function (r) {
@@ -954,16 +990,21 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
954
990
  var o = q.options.filter(function (x) { return x.value === v; })[0];
955
991
  return o ? o.label : v;
956
992
  }
993
+ // A card's own wording of the first question, or the translation's, with `{n}` its marked line's number.
994
+ function situationQuestion(item) {
995
+ var own = words().situation_question;
996
+ if (own === undefined) return situated("p", "q", item.situation.question, item.situation.line);
997
+ var hit = item.excerpt && item.excerpt.lines.filter(function (l) { return l.mark; })[0];
998
+ var line = hit ? { n: hit.n, text: hit.text.trim() } : null;
999
+ return situated("p", "q", line ? fill(own, { n: line.n }) : own, line);
1000
+ }
957
1001
  // One question per screen: the answered ones stay as a line (with "Change"), and only one question
958
1002
  // is asked: the one being changed, else the first unanswered.
959
- // A card's quote sits at the top of its question box, or, with nothing left to ask, in the card itself.
960
- function askOne(card, a, editing, ctx, onPick, onChange, off, withExamples, item) {
961
- var open = DATA.questions.filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
962
- var asked = DATA.questions.filter(function (q) { return q.id === editing; })[0] || open[0];
1003
+ function askOne(card, a, editing, onPick, onChange, off, withExamples, item) {
1004
+ var open = Qs().filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
1005
+ var asked = Qs().filter(function (q) { return q.id === editing; })[0] || open[0];
963
1006
  var situation = item.situation, notes = item.answer_notes || {};
964
- var quote = item.quote ? quoteBlock(item, ctx) : null;
965
- if (quote && !asked) card.appendChild(quote);
966
- DATA.questions.forEach(function (q) {
1007
+ Qs().forEach(function (q) {
967
1008
  if (q === asked || !valid(DATA.allowed.questions[q.id], a[q.id])) return;
968
1009
  var line = el("p", "answered", (q.short || q.text) + ": " + labelOf(q, a[q.id]) + " ");
969
1010
  if (onChange) {
@@ -977,9 +1018,8 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
977
1018
  });
978
1019
  if (!asked) return null;
979
1020
  var ask = el("div", "ask");
980
- if (quote) ask.appendChild(quote);
981
1021
  // A card's situation asks the page's first question in its own words; the others keep theirs.
982
- ask.appendChild(mark(situation && asked === DATA.questions[0] ? situated("p", "q", situation.question, situation.line) : rich("p", "q", asked.text),
1022
+ ask.appendChild(mark(situation && asked === Qs()[0] ? situationQuestion(item) : rich("p", "q", asked.text),
983
1023
  "question", { question: asked.id }));
984
1024
  ask.appendChild(options(asked.options, a[asked.id], function (v) { onPick(asked, v); }, off, withExamples, notes[asked.id], asked.id));
985
1025
  card.appendChild(ask);
@@ -989,18 +1029,19 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
989
1029
  function howTo() {
990
1030
  var sec = el("section");
991
1031
  var body = sec;
992
- if (practiceOpen()) sec.appendChild(el("h2", "", "📖 How to answer"));
1032
+ var heading = words().labels.how_to;
1033
+ if (practiceOpen()) sec.appendChild(el("h2", "", heading));
993
1034
  else {
994
1035
  body = sec.appendChild(el("details"));
995
- body.appendChild(el("summary", "", "📖 How to answer"));
1036
+ body.appendChild(el("summary", "", heading));
996
1037
  }
997
- DATA.questions.forEach(function (q) {
998
- body.appendChild(el("p", "q", q.text));
1038
+ Qs().forEach(function (q) {
1039
+ body.appendChild(mark(el("p", "q", q.text), "how-to-question", { question: q.id }));
999
1040
  var list = el("div", "examples");
1000
1041
  q.options.forEach(function (o) {
1001
- var row = el("div", "example");
1042
+ var row = mark(el("div", "example"), "how-to-answer", { question: q.id, value: o.value });
1002
1043
  row.appendChild(el("b", "", (o.icon ? o.icon + " " : "") + o.label));
1003
- row.appendChild(rich("span", "muted", "For example: " + o.example));
1044
+ row.appendChild(rich("span", "muted", words().labels.for_example + o.example));
1004
1045
  list.appendChild(row);
1005
1046
  });
1006
1047
  body.appendChild(list);
@@ -1013,9 +1054,9 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
1013
1054
  var ctx = termsOf(p);
1014
1055
  evidence(card, p, ctx);
1015
1056
  var a = practiceAnswers(p.id);
1016
- askOne(card, a, undefined, ctx, function (q, v) { answerPractice(p.id, q.id, v); }, null, off, true, p);
1057
+ askOne(card, a, undefined, function (q, v) { answerPractice(p.id, q.id, v); }, null, off, true, p);
1017
1058
  if (practiceAnswered(p)) {
1018
- var expected = DATA.questions.map(function (q) { return labelOf(q, p.expected[q.id]); }).join(" · ");
1059
+ var expected = Qs().map(function (q) { return labelOf(q, p.expected[q.id]); }).join(" · ");
1019
1060
  card.appendChild(rich("p", practiceMatched(p) ? "status saved" : "quote",
1020
1061
  (practiceMatched(p) ? "Matched. " : "Not matched. Expected: " + expected + ". ") + p.why));
1021
1062
  }
@@ -1133,8 +1174,10 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
1133
1174
  app.textContent = "";
1134
1175
  var top = el("header", "top");
1135
1176
  app.appendChild(top);
1136
- top.appendChild(el("h1", "", DATA.title));
1137
- if (DATA.intro) top.appendChild(el("p", "muted", DATA.intro));
1177
+ try { document.documentElement.lang = state.lang; } catch (e) {}
1178
+ top.appendChild(el("h1", "", words().title));
1179
+ if (LANGS.length) top.appendChild(langSwitch());
1180
+ if (words().intro) top.appendChild(el("p", "muted", words().intro));
1138
1181
  if (state.store === "none") {
1139
1182
  top.appendChild(el("div", "banner",
1140
1183
  "Answers cannot be saved in this view, so it is read-only. Open the page from the claude.ai " +