@columenlabs/lumen-ui 0.1.108 → 0.1.109

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.
@@ -246,7 +246,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
246
246
  useLayoutEffect(() => {
247
247
  const el = scroller.current;
248
248
  if (el && layout === 'grid')
249
- el.scrollLeft = el.scrollWidth;
249
+ snapToEnd(el);
250
250
  edge.update();
251
251
  }, [strip]);
252
252
  const sums = mobile ? summary.filter((s) => s === 'complete') : summary;
@@ -476,7 +476,7 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
476
476
  link: rowSpec.link,
477
477
  };
478
478
  let group;
479
- body = (_jsxs("div", { className: cn('grid min-w-min items-center gap-[3px]', fade), style: grid, ...routes, children: [!mobile && (_jsx("span", { className: "sticky left-0 z-1 bg-card text-xs text-muted-foreground", children: rowSpec.title })), cols.map((k) => (_jsx("span", { className: "num h-4 overflow-visible text-[11px] whitespace-nowrap text-muted-foreground", children: headLabel(k, block, cols.length) }, k))), sums.map((s) => (_jsx("span", { className: "pl-3 text-right text-xs text-muted-foreground", children: s === 'rows' || s === 'counts' ? rowsHead : SUMMARY_HEAD[s] }, s))), rowList.flatMap((r) => {
479
+ body = (_jsxs("div", { className: cn('grid min-w-min items-center gap-[3px]', fade), style: grid, ...routes, children: [!mobile && (_jsx("span", { className: "sticky left-0 z-1 bg-card text-xs text-muted-foreground", children: rowSpec.title })), cols.map((k) => (_jsx("span", { "data-col": "", className: "num h-4 overflow-visible text-[11px] whitespace-nowrap text-muted-foreground", children: headLabel(k, block, cols.length) }, k))), sums.map((s) => (_jsx("span", { className: "pl-3 text-right text-xs text-muted-foreground", children: s === 'rows' || s === 'counts' ? rowsHead : SUMMARY_HEAD[s] }, s))), rowList.flatMap((r) => {
480
480
  const out = [];
481
481
  const shut = groupBy && folds.has(foldKey + (r.group ?? ''));
482
482
  if (groupBy && r.group !== group) {
@@ -567,3 +567,23 @@ export function LumenCoverageMap({ source, title, icon = Grid3x3Icon, rows: rowS
567
567
  ? 'Click a block to see its days'
568
568
  : undefined) }) })] }) }) }) }));
569
569
  }
570
+ /** Scrolls a sideways strip to its end (today) so its left edge falls on a whole block, not mid
571
+ * block with its label cut: the strip's content takes just enough room at its end to land there. */
572
+ function snapToEnd(el) {
573
+ const content = el.firstElementChild;
574
+ if (!content)
575
+ return;
576
+ content.style.paddingRight = '';
577
+ el.scrollLeft = el.scrollWidth;
578
+ const box = el.getBoundingClientRect().left;
579
+ const lefts = [...el.querySelectorAll('[data-col]')].map((c) => c.getBoundingClientRect().left - box);
580
+ // The first block cut by the left edge; the strip moves on to the block after it.
581
+ const cut = lefts.find((x, i) => x < -0.5 && (lefts[i + 1] ?? 1) > 0.5);
582
+ if (cut === undefined)
583
+ return;
584
+ const next = lefts[lefts.indexOf(cut) + 1];
585
+ if (next === undefined || next <= 0.5)
586
+ return;
587
+ content.style.paddingRight = `${Math.ceil(next)}px`;
588
+ el.scrollLeft = el.scrollWidth;
589
+ }
package/dist/index.d.ts CHANGED
@@ -28,6 +28,7 @@ export { RankedBars, StackedBars, StackedColumns, TimeLines } from './lumen-widg
28
28
  export { type ShareSegment, ShareMeter } from './share-meter.js';
29
29
  export { type Pool, poolShare } from './pool-share.js';
30
30
  export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
31
+ export { contrast, readableInk } from './ink.js';
31
32
  export { UnitValue } from './unit-value.js';
32
33
  export { type BurnRate, burnRate, durationText, PACE_WINDOWS, type Pace, type PaceWindow, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
33
34
  export { PaceWindowPicker, paceText, TimeToCap } from './time-to-cap.js';
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ export { RankedBars, StackedBars, StackedColumns, TimeLines } from './lumen-widg
35
35
  export { ShareMeter } from './share-meter.js';
36
36
  export { poolShare } from './pool-share.js';
37
37
  export { METER_LEVEL_COLOR, meterLevel, meterValueTone } from './meter-level.js';
38
+ export { contrast, readableInk } from './ink.js';
38
39
  export { UnitValue } from './unit-value.js';
39
40
  export { burnRate, durationText, PACE_WINDOWS, paceHours, setDefaultPaceWindow, timeToCap, usePaceWindow, } from './pace.js';
40
41
  export { PaceWindowPicker, paceText, TimeToCap } from './time-to-cap.js';
package/dist/ink.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /** WCAG contrast of two hex colours, 1 to 21; null when either is not a hex colour. */
2
+ export declare function contrast(a: string, b: string): number | null;
3
+ /**
4
+ * The text colour for a chip of background `bg`: its own `ink` when that reads at `min`:1 (WCAG AA,
5
+ * 4.5), otherwise near-black or white, whichever reads better. A colour that is not hex keeps `ink`.
6
+ */
7
+ export declare function readableInk(bg: string, ink?: string, min?: number): string | undefined;
package/dist/ink.js ADDED
@@ -0,0 +1,39 @@
1
+ // Text that stays readable on a colour the app does not choose (a mailbox's own label colours).
2
+ const hex = (c) => {
3
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c.trim());
4
+ const d = m?.[1];
5
+ if (!d)
6
+ return null;
7
+ const h = d.length === 3 ? [...d].map((x) => x + x).join('') : d;
8
+ return [0, 2, 4].map((i) => Number.parseInt(h.slice(i, i + 2), 16));
9
+ };
10
+ const luminance = ([r, g, b]) => {
11
+ const lin = (v) => {
12
+ const s = v / 255;
13
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
14
+ };
15
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
16
+ };
17
+ /** WCAG contrast of two hex colours, 1 to 21; null when either is not a hex colour. */
18
+ export function contrast(a, b) {
19
+ const x = hex(a);
20
+ const y = hex(b);
21
+ if (!x || !y)
22
+ return null;
23
+ const p = luminance(x);
24
+ const q = luminance(y);
25
+ return (Math.max(p, q) + 0.05) / (Math.min(p, q) + 0.05);
26
+ }
27
+ /**
28
+ * The text colour for a chip of background `bg`: its own `ink` when that reads at `min`:1 (WCAG AA,
29
+ * 4.5), otherwise near-black or white, whichever reads better. A colour that is not hex keeps `ink`.
30
+ */
31
+ export function readableInk(bg, ink, min = 4.5) {
32
+ if (ink && (contrast(bg, ink) ?? min) >= min)
33
+ return ink;
34
+ const dark = contrast(bg, '#111111');
35
+ const light = contrast(bg, '#ffffff');
36
+ if (dark === null || light === null)
37
+ return ink;
38
+ return dark >= light ? '#111111' : '#ffffff';
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.108",
52
+ "@columenlabs/lumen-ui-types": "0.1.109",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",