@fasl-work/caos-app-shell 0.14.3 → 0.15.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/README.md CHANGED
@@ -110,7 +110,7 @@ const config: ShellConfig = {
110
110
  (G1) or mode (G2), console errors and 4xx/5xx (G3), broken deep links or HTML for an artifact (G4), anything a
111
111
  reader cannot scroll to, see whole or click (G5), blank or underfilled drawings and an instrument under half the
112
112
  viewport (G6), a view that never settles (G7), loops at rest (G8), controls that change nothing (G9), labels cut or
113
- overlapping in a drawing, also in a wider fallback font (G10), a decimal point on a Spanish page (G11), a vertical
113
+ overlapping in a drawing, also in a wider fallback font (G10), a decimal point on a Spanish page (G11), a number or percentage broken across lines (G19), a vertical
114
114
  sub-tab list that scrolls away (G12), text under WCAG AA contrast (G13, every language on a documentation page), and a documentation page without the
115
115
  required areas of its kind (`ADR-0017.2`: executive and technical Introduction, Methodology, Implementation,
116
116
  Experiments, Benchmark; declared with `<DocSection area="...">`, waived only with a reason of 12 letters or digits in two
package/gate/inpage.mjs CHANGED
@@ -1281,6 +1281,75 @@ export function installLib() {
1281
1281
  return [...new Set(out)].slice(0, 12);
1282
1282
  };
1283
1283
 
1284
+ /**
1285
+ * G19, values whole (BL-065): a short value written in the page's text sits on one line. Shell 0.14.2 fixed a share
1286
+ * "35%" that broke into "3", "5", "%" in a squeezed flex row; no other check sees it, because the pieces are neither
1287
+ * clipped nor outside their box. A short value is a run of characters without breaking whitespace (U+0020, tab, CR,
1288
+ * LF; U+00A0 and U+202F do not end a run) inside one text node, 2 to 16 characters long once the punctuation of the
1289
+ * sentence is stripped from its ends, with a digit, only digits, signs (the hyphens and dashes included), separators,
1290
+ * currency signs, % per mille x and degree signs and at most 4 letters (35%, 0,96, US$1.2bn, 2026-10-15, -3,2pp, 1e-6,
1291
+ * ADR-0071). A value is cut into segments right after a hyphen (U+002D, U+2010, U+2011), an en or em dash and a slash;
1292
+ * a break at those places is a normal line break, so the pieces between them are what must stay whole. A leading sign
1293
+ * stays in the first segment (a lone "-" apart from "3,2" is a broken value). The lines of a segment of 2 or more
1294
+ * characters are the groups of its Range's client rects by top (2px); a segment on more than one line fails the value,
1295
+ * also when it is longer than its container: a segment is never cut into pieces. Two values apart by a regular space
1296
+ * ("12 %") are two runs and may wrap between them. Limits: text nodes only (a value split across elements, <b>35</b>%,
1297
+ * is not read); SVG text is G10's
1298
+ * subject and canvas text cannot be read; code, formulas and anything marked translate="no" are skipped.
1299
+ */
1300
+ G.brokenValues = () => {
1301
+ const out = [];
1302
+ const skip = 'code, pre, kbd, samp, .katex, svg, canvas, textarea, [translate="no"], [aria-hidden="true"], script, style';
1303
+ const runRe = /[^ \t\r\n]+/g;
1304
+ const BREAK_AFTER = '-\u2010\u2011\u2013\u2014/';
1305
+ const strip = /^[()[\]"',;:.]+|[()[\]"',;:.]+$/g;
1306
+ const allowed = /^[\p{Nd}+\-\u2010\u2011\u2013\u2014\u2212\u00b1.,:/'\u00a0\u202f$\u20ac\u00a3\u00a5%\u2030\u00d7\u00b0\p{L}]+$/u;
1307
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
1308
+ const range = document.createRange();
1309
+ const seen = new Set();
1310
+ for (let n = walker.nextNode(); n && out.length < 12; n = walker.nextNode()) {
1311
+ const text = n.nodeValue || '';
1312
+ if (!/\d/.test(text)) continue;
1313
+ const el = n.parentElement;
1314
+ if (!el || el.closest(skip) || !shown(el)) continue;
1315
+ for (const m of text.matchAll(runRe)) {
1316
+ const run = m[0];
1317
+ if (!/\d/.test(run)) continue;
1318
+ const lead = run.length - run.replace(/^[()[\]"',;:.]+/, '').length;
1319
+ const value = run.replace(strip, '');
1320
+ if (value.length < 2 || value.length > 16 || !/\d/.test(value) || !allowed.test(value)) continue;
1321
+ if ((value.match(/\p{L}/gu) || []).length > 4) continue;
1322
+ const start = m.index + lead;
1323
+ // a break right after a hyphen, a dash or a slash is a normal line break; the segments between them must stay whole.
1324
+ // A leading sign belongs to the first segment (a "-" apart from "3,2" is a broken value).
1325
+ const cuts = [0];
1326
+ for (let i = 1; i < value.length - 1; i += 1) if (BREAK_AFTER.includes(value[i])) cuts.push(i + 1);
1327
+ cuts.push(value.length);
1328
+ let worst = 1;
1329
+ for (let k = 0; k + 1 < cuts.length; k += 1) {
1330
+ if (cuts[k + 1] - cuts[k] < 2) continue;
1331
+ range.setStart(n, start + cuts[k]);
1332
+ range.setEnd(n, start + cuts[k + 1]);
1333
+ const tops = [];
1334
+ for (const r of range.getClientRects()) {
1335
+ if (r.width <= 0.5) continue;
1336
+ if (!tops.some((t) => Math.abs(t - r.top) <= 2)) tops.push(r.top);
1337
+ }
1338
+ worst = Math.max(worst, tops.length);
1339
+ }
1340
+ if (worst > 1) {
1341
+ const msg = `"${value}" breaks across ${worst} lines (${describe(el)})`;
1342
+ if (!seen.has(msg)) {
1343
+ seen.add(msg);
1344
+ out.push(msg);
1345
+ if (out.length >= 12) break;
1346
+ }
1347
+ }
1348
+ }
1349
+ }
1350
+ return out;
1351
+ };
1352
+
1284
1353
  /** The scroll container of an element: the nearest ancestor that scrolls vertically, or the document. */
1285
1354
  const scroller = (el) => {
1286
1355
  for (let a = el.parentElement; a && a !== document.body; a = a.parentElement) {
package/gate/run.mjs CHANGED
@@ -29,6 +29,7 @@
29
29
  // G18 Focus mode the App's Focus control opens the case's open group in the full viewport and returns to the same case and
30
30
  // group (by the exit control and by Escape); no page scroll, the rail without scroll, one tab row; no stage
31
31
  // control under the Focus chrome (label, HUD, actions); no Focus control inside the Focus frame
32
+ // G19 values whole a short value (number, percentage, amount, unit) sits on one line in every state
32
33
  // plus the ADR-0071 and ADR-0017 measures (viewport, one-row tabs, rail without scroll, centred prose, captions,
33
34
  // references).
34
35
 
@@ -465,6 +466,7 @@ export async function runGate(o) {
465
466
  if (big) for (const c of f.fillStages ?? []) if (c.h < opt.stageFloor) fail('G6', where, `the drawing of ${c.label} keeps ${Math.round(c.h)} px under its note (floor ${opt.stageFloor} px)`);
466
467
  for (const m of await page.evaluate(() => window.__caosGate.drawingText())) fail('G10', where, m);
467
468
  if (s.lang === 'es') for (const m of await page.evaluate(() => window.__caosGate.decimals())) fail('G11', where, m);
469
+ for (const m of await page.evaluate(() => window.__caosGate.brokenValues())) fail('G19', where, m);
468
470
  if (s.w > 760) for (const m of await page.evaluate(() => window.__caosGate.sticky())) fail('G12', where, m);
469
471
  if (s.contrast) for (const m of await page.evaluate(() => window.__caosGate.contrast())) fail('G13', where, m);
470
472
  if (f.sectionsWithoutRefs) fail('ADR-0017.4', where, `${f.sectionsWithoutRefs} section(s) end without references or a stated reason`);
@@ -893,6 +895,7 @@ export async function runGate(o) {
893
895
  for (const c of f.covered) fail('G18', at, `${label}: ${c.chrome} covers the control "${c.control}" (the stage must clear the Focus chrome)`);
894
896
  if (f.enterInside > 0) fail('G18', at, `${label}: the Focus control shows inside Focus mode (it opens the mode the reader is already in)`);
895
897
  for (const m of await page.evaluate(() => window.__caosGate.drawingText())) fail('G10', at, m);
898
+ for (const m of await page.evaluate(() => window.__caosGate.brokenValues())) fail('G19', at, m);
896
899
  if (s.contrast) for (const m of await page.evaluate(() => window.__caosGate.contrast())) fail('G13', at, m);
897
900
  if (index === 0) await saveShot(page, `focus_${slug(s.route)}_${s.w}x${s.h}_${s.theme}_${s.lang}.png`);
898
901
  // leave: the exit control for one case, Escape for the next, so both ways are walked
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.14.3",
3
+ "version": "0.15.0",
4
4
  "description": "Shared web-app shell, workbench primitives, content primitives, design system and measured gate for the CAOS / Faena public apps (ADR-0016, ADR-0017, ADR-0071).",
5
5
  "license": "MIT",
6
6
  "author": "Felipe Santibáñez-Leal",