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.
- package/dist/skills/build-label-page/scripts/label-page-core.d.ts +168 -1
- package/package.json +1 -1
- package/skills/build-label-page/SKILL.md +12 -2
- package/skills/build-label-page/SKILL.md.spec.ts +11 -1
- package/skills/build-label-page/references/checklist.md +3 -3
- package/skills/build-label-page/references/closed-choice-generator.md +73 -13
- package/skills/build-label-page/scripts/fixtures/v7-cards-ru.json +23 -0
- package/skills/build-label-page/scripts/label-page-blocks.test.ts +8 -9
- package/skills/build-label-page/scripts/label-page-browser.test.ts +278 -0
- package/skills/build-label-page/scripts/label-page-core.js +126 -14
- package/skills/build-label-page/scripts/label-page-core.test.ts +68 -0
- package/skills/build-label-page/scripts/label-page-core.ts +193 -13
- package/skills/build-label-page/scripts/label-page-html.test.ts +29 -0
- package/skills/build-label-page/scripts/label-page-translations.test.ts +371 -0
- package/skills/build-label-page/scripts/page-template.html +83 -40
|
@@ -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
|
|
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
|
|
32
|
-
border
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
575
|
-
s.textContent = "
|
|
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(
|
|
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],
|
|
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
|
-
|
|
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 ?
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
960
|
-
|
|
961
|
-
var
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
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", "",
|
|
1036
|
+
body.appendChild(el("summary", "", heading));
|
|
996
1037
|
}
|
|
997
|
-
|
|
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",
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
1137
|
-
|
|
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 " +
|