@fasl-work/caos-app-shell 0.9.0 → 0.9.2

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/chart.d.ts CHANGED
@@ -68,6 +68,17 @@ declare function parityRange(...arrays: readonly (readonly (number | null)[])[])
68
68
  * by significant digits has no fixed resolution, and `data-ticks-repeat` reports what it does.
69
69
  */
70
70
  declare function tickSteps(fmt: FormatOptions | undefined): number[] | undefined;
71
+ /**
72
+ * uPlot ticks a log axis at 1 to 9 times every power of ten, from a precision table that ends near 1e-22: a log axis
73
+ * that reaches below it throws "Invalid array length" after stalling the page, and the chart is never drawn (known
74
+ * shell defect 31, p-values of 1e-124 in CAOS_Contraste, 2026-10-07). Below this floor the axis ticks at powers of ten
75
+ * only (`logDecades`), each one labelled.
76
+ */
77
+ declare const LOG_TABLE_FLOOR = 1e-22;
78
+ /** The powers of ten within [min, max], thinned to at most `most` (every k-th decade, the top one kept). */
79
+ declare function logDecades(min: number, max: number, most?: number): number[];
80
+ /** The smallest positive value among columns of values and the lower end of a fixed range; Infinity if none. */
81
+ declare function smallestPositive(columns: readonly (readonly (number | null)[])[], range?: readonly [number, number] | null): number;
71
82
  /** How many tick labels repeat the label before them (empty labels apart); the gate fails a chart with any (G6). */
72
83
  declare function repeatedLabels(labels: readonly (string | null | undefined)[]): number;
73
84
  /**
@@ -98,4 +109,4 @@ declare function drawMarks(u: uPlot, marks: {
98
109
  */
99
110
  declare function UPlotChart({ x, y, series, marks, height, onCursor, onPick, parity }: UPlotChartProps): react.JSX.Element;
100
111
 
101
- export { type ChartAxis, type ChartSeries, UPlotChart, type UPlotChartProps, drawMarks, parityRange, repeatedLabels, seriesStyle, sortOrder, tickSteps };
112
+ export { type ChartAxis, type ChartSeries, LOG_TABLE_FLOOR, UPlotChart, type UPlotChartProps, drawMarks, logDecades, parityRange, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
package/dist/chart.js CHANGED
@@ -37,6 +37,26 @@ function tickSteps(fmt) {
37
37
  const res = 10 ** -fmt.decimals / (fmt.percent ? 100 : 1);
38
38
  return STEPS.filter((s) => s >= res * (1 - 1e-9) && Math.abs(s / res - Math.round(s / res)) < 1e-6);
39
39
  }
40
+ var LOG_TABLE_FLOOR = 1e-22;
41
+ function logDecades(min, max, most = 8) {
42
+ if (!(min > 0) || !(max > 0) || !Number.isFinite(min) || !Number.isFinite(max) || max < min) return [];
43
+ const a = Math.ceil(Math.log10(min) - 1e-9);
44
+ const b = Math.floor(Math.log10(max) + 1e-9);
45
+ if (b < a) return [];
46
+ const step = Math.max(1, Math.ceil((b - a + 1) / most));
47
+ const out = [];
48
+ for (let k = b; k >= a; k -= step) out.unshift(Number(`1e${k}`));
49
+ return out;
50
+ }
51
+ function smallestPositive(columns, range) {
52
+ let lo = range && range[0] > 0 ? range[0] : Infinity;
53
+ for (const c of columns) for (const v of c) if (v !== null && Number.isFinite(v) && v > 0 && v < lo) lo = v;
54
+ return lo;
55
+ }
56
+ var DECADE_AXIS = {
57
+ splits: (_u, _i, min, max) => logDecades(min, max),
58
+ filter: (_u, splits) => splits
59
+ };
40
60
  function repeatedLabels(labels) {
41
61
  let n = 0;
42
62
  for (let i = 1; i < labels.length; i++) if (labels[i] && labels[i] === labels[i - 1]) n++;
@@ -110,16 +130,23 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
110
130
  const xs = useMemo(() => order ? order.map((i) => x.values[i]) : x.values, [order, x.values]);
111
131
  const ys = useMemo(() => series.map((s) => order ? order.map((i) => s.values[i] ?? null) : s.values), [order, series]);
112
132
  const range = useMemo(() => parity ? parityRange(xs, ...ys) : null, [parity, xs, ys]);
133
+ const tiny = useMemo(
134
+ () => ({
135
+ x: Boolean(x.log) && smallestPositive([xs], range) < LOG_TABLE_FLOOR,
136
+ y: Boolean(y.log) && smallestPositive(ys, range ?? y.range) < LOG_TABLE_FLOOR
137
+ }),
138
+ [x.log, y.log, xs, ys, range, y.range]
139
+ );
113
140
  const xLabel = pick(x.label, lang);
114
141
  const yLabel = pick(y.label, lang);
115
142
  const xUnit = x.unit ? pick(x.unit, lang) : "";
116
143
  const yUnit = y.unit ? pick(y.unit, lang) : "";
117
144
  const structure = JSON.stringify({
118
145
  s: series.map((s, i) => [pick(s.label, lang), seriesStyle(s, i), s.width ?? 2, s.mode ?? "line"]),
119
- x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null],
146
+ x: [xLabel, xUnit, Boolean(x.time), Boolean(x.log), x.format ?? null, tiny.x],
120
147
  p: range,
121
148
  pick: Boolean(onPick),
122
- y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null],
149
+ y: [yLabel, yUnit, Boolean(y.log), y.range ?? null, y.format ?? null, tiny.y],
123
150
  m: (marks ?? []).map((m) => [m.x, pick(m.label, lang)]),
124
151
  theme,
125
152
  lang,
@@ -168,7 +195,8 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
168
195
  label: xUnit ? `${xLabel} (${xUnit})` : xLabel,
169
196
  labelFont: font,
170
197
  values: x.time ? void 0 : tick(x.format, "x"),
171
- ...xSteps ? { incrs: xSteps } : {}
198
+ ...xSteps ? { incrs: xSteps } : {},
199
+ ...tiny.x ? DECADE_AXIS : {}
172
200
  },
173
201
  {
174
202
  stroke: fg,
@@ -179,6 +207,7 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
179
207
  labelFont: font,
180
208
  values: tick(y.format, "y"),
181
209
  ...ySteps ? { incrs: ySteps } : {},
210
+ ...tiny.y ? DECADE_AXIS : {},
182
211
  // Sized to the longest tick label. uPlot draws in device pixels with a font scaled by its pixel ratio, so the
183
212
  // label is measured in that font and divided back (measuring the unscaled font and dividing by the ratio
184
213
  // made the axis too narrow on every high-density screen; the gate runs at ratio 1 and never saw it).
@@ -282,4 +311,4 @@ function UPlotChart({ x, y, series, marks, height = 280, onCursor, onPick, parit
282
311
  );
283
312
  }
284
313
 
285
- export { UPlotChart, drawMarks, parityRange, repeatedLabels, seriesStyle, sortOrder, tickSteps };
314
+ export { LOG_TABLE_FLOOR, UPlotChart, drawMarks, logDecades, parityRange, repeatedLabels, seriesStyle, smallestPositive, sortOrder, tickSteps };
package/gate/inpage.mjs CHANGED
@@ -531,20 +531,27 @@ export function installLib() {
531
531
  // ---- 0.8.0 measures ----------------------------------------------------------------------------------------
532
532
 
533
533
  /** The part of the page the ancestors of an element let it show when they CUT their content (overflow hidden or
534
- * clip). A scroll container does not cut (a reader scrolls it), and neither does the viewport. */
534
+ * clip). A scroll container does not cut (a reader scrolls it), and neither does the viewport; and what lies beyond
535
+ * the first scroll container on an axis is a matter of scroll position, so the walk stops there on that axis (0.9.1:
536
+ * the contained `.app-shell`, viewport-high with its overflow hidden, read every documentation figure below the fold
537
+ * as "outside the drawing"). */
535
538
  const cutArea = (el) => {
536
539
  const area = { left: -Infinity, top: -Infinity, right: Infinity, bottom: Infinity };
537
- for (let a = el.parentElement; a && a !== document.body && a !== document.documentElement; a = a.parentElement) {
540
+ let x = true;
541
+ let y = true;
542
+ for (let a = el.parentElement; a && a !== document.body && a !== document.documentElement && (x || y); a = a.parentElement) {
538
543
  const s = getComputedStyle(a);
539
544
  const ar = a.getBoundingClientRect();
540
- if (s.overflowX === 'hidden' || s.overflowX === 'clip') {
545
+ if (x && (s.overflowX === 'hidden' || s.overflowX === 'clip')) {
541
546
  area.left = Math.max(area.left, ar.left);
542
547
  area.right = Math.min(area.right, ar.right);
543
548
  }
544
- if (s.overflowY === 'hidden' || s.overflowY === 'clip') {
549
+ if (y && (s.overflowY === 'hidden' || s.overflowY === 'clip')) {
545
550
  area.top = Math.max(area.top, ar.top);
546
551
  area.bottom = Math.min(area.bottom, ar.bottom);
547
552
  }
553
+ if (s.overflowX === 'auto' || s.overflowX === 'scroll') x = false;
554
+ if (s.overflowY === 'auto' || s.overflowY === 'scroll') y = false;
548
555
  }
549
556
  return area;
550
557
  };
@@ -719,6 +726,29 @@ export function installLib() {
719
726
  for (let i = layers.length - 1; i >= 0; i -= 1) base = over(layers[i], base);
720
727
  return base;
721
728
  };
729
+ /** The colour an SVG text element is painted on: the last filled shape painted before it whose box holds its centre. */
730
+ const svgBackdrop = (text, page) => {
731
+ const svg = text.closest('svg');
732
+ if (!svg) return page;
733
+ const r = text.getBoundingClientRect();
734
+ const cx = r.left + r.width / 2;
735
+ const cy = r.top + r.height / 2;
736
+ let found = null;
737
+ for (const shape of svg.querySelectorAll('rect, circle, ellipse, polygon, path')) {
738
+ if (!(shape.compareDocumentPosition(text) & Node.DOCUMENT_POSITION_FOLLOWING)) continue;
739
+ const ss = getComputedStyle(shape);
740
+ if (ss.display === 'none' || ss.visibility === 'hidden') continue;
741
+ const fill = parseColor(ss.fill);
742
+ if (!fill || fill.a === 0) continue;
743
+ const b = shape.getBoundingClientRect();
744
+ if (cx < b.left || cx > b.right || cy < b.top || cy > b.bottom) continue;
745
+ const a = fill.a * Number(ss.fillOpacity || 1) * opacityOf(shape);
746
+ if (a <= 0.05) continue;
747
+ found = { ...fill, a };
748
+ }
749
+ return found ? over(found, page) : page;
750
+ };
751
+
722
752
  const opacityOf = (el) => {
723
753
  let o = 1;
724
754
  for (let a = el; a; a = a.parentElement) o *= Number(getComputedStyle(a).opacity || 1);
@@ -753,8 +783,12 @@ export function installLib() {
753
783
  const fg = parseColor(isSvg ? s.fill : s.color);
754
784
  if (!fg) continue;
755
785
  const host = isSvg ? el.closest('svg')?.parentElement ?? el : el;
756
- const bg = backdrop(host);
757
- if (!bg) continue;
786
+ const page = backdrop(host);
787
+ if (!page) continue;
788
+ // SVG text sits on what the drawing painted under it (a label on a bar): the last filled shape before it whose
789
+ // box holds the text's centre, composited over the page (0.9.1: a marker in the page colour on a red bar read
790
+ // 1.06:1 against the page)
791
+ const bg = isSvg ? svgBackdrop(el, page) : page;
758
792
  const alpha = fg.a * opacityOf(el);
759
793
  const shownColor = over({ ...fg, a: alpha }, bg);
760
794
  const l1 = lum(shownColor);
@@ -763,7 +797,7 @@ export function installLib() {
763
797
  const px = parseFloat(s.fontSize) || 16;
764
798
  const bold = Number(s.fontWeight) >= 700;
765
799
  const need = px >= 24 || (bold && px >= 18.66) ? 3 : 4.5;
766
- if (ratio + 1e-6 < need) out.push(`${describe(el)}: contrast ${ratio.toFixed(2)}:1, below ${need}:1 (${s.color || s.fill} on rgb(${Math.round(bg.r)}, ${Math.round(bg.g)}, ${Math.round(bg.b)}))`);
800
+ if (ratio + 1e-6 < need) out.push(`${describe(el)}: contrast ${ratio.toFixed(2)}:1, below ${need}:1 (${isSvg ? s.fill : s.color} on rgb(${Math.round(bg.r)}, ${Math.round(bg.g)}, ${Math.round(bg.b)}))`);
767
801
  }
768
802
  return out.slice(0, 10);
769
803
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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",
package/styles.css CHANGED
@@ -56,8 +56,8 @@
56
56
  --color-accent-fg: #ffffff;
57
57
  --color-accent-soft: #ddf4ff;
58
58
  --color-accent-2: #0a7886;
59
- --color-magenta: #bf3989;
60
- --color-good: #1a7f37;
59
+ --color-magenta: #b93785;
60
+ --color-good: #197c36;
61
61
  --color-warn: #946300;
62
62
  --color-bad: #cf222e;
63
63
  --color-shadow: 0 1px 2px rgba(31, 35, 40, 0.06), 0 4px 10px rgba(31, 35, 40, 0.04);