@tensor_1/react 2.36.0 → 2.38.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 +16 -0
- package/dist/components/SchedulingAgenda.js +3 -1
- package/dist/components/SchedulingTimeGrid.js +6 -4
- package/dist/components/SchedulingTimeline.js +5 -2
- package/dist/components/Stat.d.ts +7 -0
- package/dist/components/Stat.js +6 -3
- package/dist/hooks/usePrinting.d.ts +15 -0
- package/dist/hooks/usePrinting.js +29 -0
- package/dist/index.cjs +270 -233
- package/dist/index.d.cts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -198,12 +198,15 @@ Radial mode still shortens long labels to fit its ring, but every node carries i
|
|
|
198
198
|
- **`Tabs overflow="scroll"`** keeps a long tab row on one line that scrolls sideways. The selected tab is always scrolled into view, and edge buttons appear only on the side that has hidden tabs. The buttons are pointer-only: keyboard users move with Arrow, Home and End. Combine it with `TabPanel keepMounted` on a record page with many sections.
|
|
199
199
|
- **`Stat tone`** (`warning`, `danger`) draws a toned outline and value; the label stays muted, so the tone never replaces words. `size="compact"` fits dense fact bands.
|
|
200
200
|
- **`Stat onActivate`** makes the whole tile a button that opens an editor (`aria-haspopup="dialog"`). Pass `activateLabel` (e.g. "change") so the name reads "Priority: P1, change", and `expanded` while the editor is open. The ref points at the button, for anchoring a popover.
|
|
201
|
+
- **`Stat pending`** is for a tile whose action is still running (a save, a state change). The button gets `aria-busy="true"` and `aria-disabled="true"`, is dimmed with a progress cursor, and calls neither `onActivate` nor your `onClick` / `onKeyDown` until `pending` is false again. It is never natively `disabled`, so it keeps focus and a screen reader can still read it. Only a tile with `onActivate` has the state; a read-only tile and a link tile ignore the prop.
|
|
202
|
+
- **Tiles in one row line up.** A grid or flex row stretches its tiles to the tallest one. Read-only, button and link tiles all start their label and value at the same offset from the top edge, whatever the height (before 2.38.0 a button tile centred its rows).
|
|
201
203
|
- **`CopyButton`** copies `value`. Name it for what it copies (`label="Copy email"`), pass `copiedLabel`, and pass `failedLabel` so a refused copy is explained rather than silent.
|
|
202
204
|
|
|
203
205
|
```tsx
|
|
204
206
|
<Tabs value={tab} onChange={setTab} overflow="scroll">{tabs}</Tabs>
|
|
205
207
|
<Stat label={t('sla')} value={remaining} tone={breached ? 'danger' : 'neutral'} size="compact" />
|
|
206
208
|
<Stat ref={anchor} label={t('priority')} value="P1" onActivate={openEditor} activateLabel={t('change')} expanded={open} size="compact" />
|
|
209
|
+
<Stat label={t('state')} value={state} onActivate={resolve} activateLabel={t('resolve')} haspopup={false} pending={saving} size="compact" />
|
|
207
210
|
<CopyButton value={email} label={t('copyEmail')} copiedLabel={t('copied')} failedLabel={t('copyFailed')} />
|
|
208
211
|
```
|
|
209
212
|
|
|
@@ -460,6 +463,18 @@ const spanLayout = layoutMonthSpans(spansInSalienceOrder, grid, { timeZone, week
|
|
|
460
463
|
- **`<List roving>`**: the items are one tab stop (ArrowUp/ArrowDown, Home, End; Enter or Space activates the
|
|
461
464
|
focused item's `onClick`). A control inside an item keeps its own tab stop and keys. Items may be wrapped or
|
|
462
465
|
rendered later by a child component.
|
|
466
|
+
- **Print (HAR-1541).** The stylesheet has `@media print` rules; add no print CSS of your own. On paper the same
|
|
467
|
+
markup has no scroller, fits the page width, and keeps a week row or an agenda row on one page. The colours are
|
|
468
|
+
the `--uix-print-*` tokens (light, whatever theme the screen has). Nothing depends on a background: the `high`
|
|
469
|
+
band is a heavy edge on a paper surface (on paper `--calendar-band-high-fill` is the paper's and the edge reads
|
|
470
|
+
the new `--calendar-band-high-edge`; no screen rule reads it), each window pattern has an edge style of its own (`solid`, `diagonal` dashed, `cross`
|
|
471
|
+
double, `dotted` dotted), and the hour lines are drawn as lines. Month chips wrap to show the whole title, and
|
|
472
|
+
a time-grid item is at least as tall as its time and grows to hold its words (one too short for a line on a
|
|
473
|
+
screen prints them too). The header's buttons, the "now" line and a pending move are not printed; the period
|
|
474
|
+
title, the zone label and the legend are. A long agenda mounts every row while the page prints (`beforeprint`,
|
|
475
|
+
or print media) and is a window of rows again afterwards. One limit: `window.print()` called from inside a
|
|
476
|
+
React effect, on mount or later, runs before those rows are there. Such a page calls it from an event handler
|
|
477
|
+
or a timer, or passes `virtualizeAbove={Infinity}`.
|
|
463
478
|
- **Colour.** Every calendar colour is named once at the top of `scheduling-calendar.css` (`--calendar-*`).
|
|
464
479
|
- **Deprecated, still working in 2.x:** `entry.state` / `SchedulingEntryState`, `overlay.kind` /
|
|
465
480
|
`SchedulingOverlayKind` and the `previous` / `next` labels. An entry with `state` is no longer tinted: the two
|
|
@@ -502,6 +517,7 @@ time zone, the same proposal model for moves.
|
|
|
502
517
|
- **Notice and counts:** `notice` is shown above the axis; `columnNotes` puts a note on the column of a day (`YYYY-MM-DD` in `timeZone`).
|
|
503
518
|
- **Row heights:** with `groups`, `laneIds` or a virtual window every row has a fixed height (a lane is its sub-rows, a group head one line) and a lane label that does not fit is cut with an ellipsis. A timeline that uses none of them keeps rows that grow with their label.
|
|
504
519
|
- **Colour:** every colour is named once at the top of `scheduling-timeline.css` (`--timeline-*`). Forced colours are handled in `forced-colors.css`, as for the calendar.
|
|
520
|
+
- **Print (HAR-1541).** The stylesheet has `@media print` rules; add no print CSS of your own. On paper the timeline has no scroller and nothing sticks: the axis is fitted to the page width and the bars follow (every position is a share of the axis), and a lane stays on one page. The colours are the `--uix-print-*` tokens (light, whatever theme the screen has). Nothing depends on a background: the `high` band is a heavy edge on a paper surface (on paper `--timeline-band-high-fill` is the paper's and the edge reads the new `--timeline-band-high-edge`; no screen rule reads it), each window pattern has an edge style of its own, and the grid lines are drawn as lines. A note on the column of a day is not cut at its column (notes on neighbouring days can meet on a narrow page). The "now" line and a pending move are not printed. A timeline over `virtualizeAbove` rows mounts every row and bar while the page prints and windows again afterwards; a page that calls `window.print()` from inside a React effect calls it from an event handler or a timer, or passes `virtualizeAbove={Infinity}`. Limits: a bar keeps its share of the axis, so on a narrow page a short bar shows its time and not its title (the title is still the bar's name); and the bars are packed into sub-rows for the width the timeline had on the screen, so print from a window about as wide as the page when bars are short and close together.
|
|
505
521
|
- **Deprecated, still working in 2.x:** `item.state` / `SchedulingEntryState`, `overlay.kind` / `SchedulingTimelineOverlayKind` and the `states` / `overlays` labels. A bar with `state` is no longer tinted: the two problem states show a marker with the word for that state, and the kinds are told apart by pattern.
|
|
506
522
|
- Pure helpers are exported for tests and server code: `timelineTicks`, `timelineSubTicks`, `timelineRepeatedHourOffset`, `placeSpan`, `layoutLane`, `shiftSpan`, `snapToStep`, `pixelsToMs`, `defaultTimelineStep`, `timelineStepDelta`.
|
|
507
523
|
|
|
@@ -4,6 +4,7 @@ import { createElement, useEffect, useLayoutEffect, useRef, useState } from "rea
|
|
|
4
4
|
import { addCalendarDays, zonedDateKey, zonedTimeOfDay } from "../calendar-model.js";
|
|
5
5
|
import { itemDaySpan } from "../scheduling-calendar-model.js";
|
|
6
6
|
import { useVirtualRows } from "../hooks/useVirtualRows.js";
|
|
7
|
+
import { usePrinting } from "../hooks/usePrinting.js";
|
|
7
8
|
import { fillLabel } from "../fill-label.js";
|
|
8
9
|
import { syncRovingStop } from "../roving.js";
|
|
9
10
|
const AGENDA_VIRTUAL_ROW = 44;
|
|
@@ -58,7 +59,8 @@ function AgendaBody({
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
const [narrow, setNarrow] = useState(false);
|
|
61
|
-
const
|
|
62
|
+
const printing = usePrinting(flat);
|
|
63
|
+
const virtual = useVirtualRows(flatRows, { rowHeight: narrow ? AGENDA_NARROW_ROW : AGENDA_VIRTUAL_ROW, threshold: 0, estimatedViewportHeight: AGENDA_VIRTUAL_VIEWPORT, enabled: !printing });
|
|
62
64
|
const scroller = virtual.containerRef;
|
|
63
65
|
useLayoutEffect(() => {
|
|
64
66
|
const box = scroller.current;
|
|
@@ -25,16 +25,18 @@ const GridItem = memo(function GridItem2({ entry, segment, column, alone, name,
|
|
|
25
25
|
"--uix-scheduling-calendar-lane": segment.lane,
|
|
26
26
|
"--uix-scheduling-calendar-lanes": segment.laneCount
|
|
27
27
|
});
|
|
28
|
-
const
|
|
28
|
+
const custom = renderEntry?.(entry, { availableLines: lines });
|
|
29
|
+
const text = custom ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
30
|
!continuation && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
30
31
|
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__time", children: zonedTimeOfDay(entry.start, timeZone) }),
|
|
31
32
|
" "
|
|
32
33
|
] }),
|
|
33
34
|
/* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__title", children: entry.title })
|
|
34
|
-
] })
|
|
35
|
+
] });
|
|
36
|
+
const noRoom = lines === 0 && (custom === void 0 || custom === null) ? "none" : void 0;
|
|
35
37
|
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
36
|
-
continuation &&
|
|
37
|
-
(!continuation || alone) && text != null && text !== false && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__entry-text", children: text }),
|
|
38
|
+
continuation && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__tg-from", "data-room": lines === 0 ? "none" : void 0, children: continuesFrom }),
|
|
39
|
+
(!continuation || alone) && text != null && text !== false && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__entry-text", "data-room": noRoom, children: text }),
|
|
38
40
|
markers.length > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__markers", children: markers.map((marker) => /* @__PURE__ */ jsx("span", { children: renderMarker(marker) }, marker.id)) })
|
|
39
41
|
] });
|
|
40
42
|
const shared = {
|
|
@@ -6,6 +6,7 @@ import { proposeMove } from "../scheduling-calendar-model.js";
|
|
|
6
6
|
import { cx } from "../cx.js";
|
|
7
7
|
import { fillLabel } from "../fill-label.js";
|
|
8
8
|
import { useUixLabels } from "../labels-context.js";
|
|
9
|
+
import { usePrinting } from "../hooks/usePrinting.js";
|
|
9
10
|
import { defaultTimelineStep, layoutLane, pixelsToMs, placeSpan, shiftSpan, timelineRepeatedHourOffset, timelineStepDelta, timelineSubTicks, timelineTicks } from "../scheduling-timeline-model.js";
|
|
10
11
|
import { timelineRowExtents, timelineWindow } from "../scheduling-timeline-rows.js";
|
|
11
12
|
const DEFAULT_SCHEDULING_TIMELINE_LABELS = {
|
|
@@ -253,8 +254,10 @@ function SchedulingTimeline({
|
|
|
253
254
|
return set;
|
|
254
255
|
}, [rows, overlays, range.start, range.end]);
|
|
255
256
|
const extents = useMemo(() => timelineRowExtents(rows.map((row, index) => row.kind === "head" ? { kind: "head", subRows: strips.has(index) ? 1 : 0 } : { kind: "lane", subRows: row.subRows })), [rows, strips]);
|
|
256
|
-
const
|
|
257
|
-
const
|
|
257
|
+
const windowed = lanes.length + (groups?.length ?? 0) > virtualizeAbove;
|
|
258
|
+
const printing = usePrinting(windowed);
|
|
259
|
+
const virtual = windowed && !printing;
|
|
260
|
+
const fixed = windowed || groups !== void 0 || overlays.some((overlay) => overlay.laneIds !== void 0) || markers.some((marker) => marker.laneIds !== void 0);
|
|
258
261
|
useEffect(() => {
|
|
259
262
|
if (!virtual || !scroller) return void 0;
|
|
260
263
|
let frame = 0;
|
|
@@ -33,6 +33,13 @@ interface StatProps extends HTMLAttributes<HTMLElement> {
|
|
|
33
33
|
};
|
|
34
34
|
/** Whether the editor this tile opens is currently open (`aria-expanded`). */
|
|
35
35
|
expanded?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* What the tile started is still running (HAR-1753). The button is announced as busy and
|
|
38
|
+
* unavailable (`aria-busy`, `aria-disabled`) and stays focusable; `onActivate`, `onClick` and
|
|
39
|
+
* `onKeyDown` are not called until it ends. Only with `onActivate`: a read-only tile and a
|
|
40
|
+
* link tile ignore it.
|
|
41
|
+
*/
|
|
42
|
+
pending?: boolean;
|
|
36
43
|
/**
|
|
37
44
|
* What `onActivate` opens, for `aria-haspopup` (HAR-1350; TENSOR B6). Default `'dialog'`;
|
|
38
45
|
* `'menu'` for a status menu; `false` for an in-place action (no chevron either).
|
package/dist/components/Stat.js
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef, useId } from "react";
|
|
|
3
3
|
import { cx } from "../cx.js";
|
|
4
4
|
import { renderUixLink } from "../link.js";
|
|
5
5
|
const Chevron = () => /* @__PURE__ */ jsx("span", { className: "uix-stat__chevron", "aria-hidden": "true", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" }) }) });
|
|
6
|
-
const Stat = forwardRef(function Stat2({ label, value, icon, meta, trend, tone = "neutral", size = "hero", onActivate, activateLabel, expanded, trendLabels, haspopup = "dialog", href, current, renderLink, className, onClick, ...props }, ref) {
|
|
6
|
+
const Stat = forwardRef(function Stat2({ label, value, icon, meta, trend, tone = "neutral", size = "hero", onActivate, activateLabel, expanded, trendLabels, haspopup = "dialog", href, current, renderLink, className, onClick, onKeyDown, pending = false, ...props }, ref) {
|
|
7
7
|
const id = useId();
|
|
8
8
|
const interactive = onActivate != null;
|
|
9
9
|
const linked = !interactive && href != null;
|
|
@@ -33,11 +33,11 @@ const Stat = forwardRef(function Stat2({ label, value, icon, meta, trend, tone =
|
|
|
33
33
|
] })
|
|
34
34
|
] });
|
|
35
35
|
if (linked) {
|
|
36
|
-
const linkProps = { ...props, href, className: classes, onClick, "aria-current": current ? "page" : void 0, children: body };
|
|
36
|
+
const linkProps = { ...props, href, className: classes, onClick, onKeyDown, "aria-current": current ? "page" : void 0, children: body };
|
|
37
37
|
return /* @__PURE__ */ jsx(Fragment, { children: renderLink ? renderUixLink(renderLink, linkProps) : /* @__PURE__ */ jsx("a", { ref, ...linkProps }) });
|
|
38
38
|
}
|
|
39
39
|
if (!interactive) {
|
|
40
|
-
return /* @__PURE__ */ jsx("div", { ref, className: classes, onClick, ...props, children: body });
|
|
40
|
+
return /* @__PURE__ */ jsx("div", { ref, className: classes, onClick, onKeyDown, ...props, children: body });
|
|
41
41
|
}
|
|
42
42
|
return /* @__PURE__ */ jsxs(
|
|
43
43
|
"button",
|
|
@@ -50,10 +50,13 @@ const Stat = forwardRef(function Stat2({ label, value, icon, meta, trend, tone =
|
|
|
50
50
|
"aria-expanded": expanded,
|
|
51
51
|
"aria-labelledby": `${id}-label ${id}-sep ${id}-value${activateLabel ? ` ${id}-action` : ""}`,
|
|
52
52
|
"aria-describedby": meta != null || trend != null ? `${id}-meta` : void 0,
|
|
53
|
+
...pending ? { "aria-busy": true, "aria-disabled": true } : null,
|
|
53
54
|
onClick: (e) => {
|
|
55
|
+
if (pending) return;
|
|
54
56
|
onClick?.(e);
|
|
55
57
|
if (!e.defaultPrevented) onActivate();
|
|
56
58
|
},
|
|
59
|
+
onKeyDown: pending ? void 0 : onKeyDown,
|
|
57
60
|
children: [
|
|
58
61
|
/* @__PURE__ */ jsx("span", { id: `${id}-sep`, className: "uix-visually-hidden", children: ":" }),
|
|
59
62
|
activateLabel && /* @__PURE__ */ jsxs("span", { id: `${id}-action`, className: "uix-visually-hidden", children: [
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `true` while the page is printed, or shown with print media.
|
|
3
|
+
*
|
|
4
|
+
* A view that mounts only the rows near its viewport uses this to mount all of them for the
|
|
5
|
+
* paper. The browser lays the page out for print as soon as the `beforeprint` handlers return,
|
|
6
|
+
* so the state is committed inside the event (`flushSync`), not in a later task.
|
|
7
|
+
*
|
|
8
|
+
* It cannot answer in time when `window.print()` is called from inside a React effect or
|
|
9
|
+
* lifecycle method, on mount or later: React commits the change after the effect, which is
|
|
10
|
+
* after the page was laid out. A page that prints itself that way turns windowing off through
|
|
11
|
+
* the component's own prop instead, or calls `print()` from an event handler or a timer.
|
|
12
|
+
*/
|
|
13
|
+
declare function usePrinting(enabled?: boolean): boolean;
|
|
14
|
+
|
|
15
|
+
export { usePrinting };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { flushSync } from "react-dom";
|
|
4
|
+
function usePrinting(enabled = true) {
|
|
5
|
+
const [printing, setPrinting] = useState(false);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (!enabled || typeof window === "undefined") return void 0;
|
|
8
|
+
const set = (next) => {
|
|
9
|
+
flushSync(() => setPrinting(next));
|
|
10
|
+
};
|
|
11
|
+
const before = () => set(true);
|
|
12
|
+
const after = () => set(false);
|
|
13
|
+
const query = typeof window.matchMedia === "function" ? window.matchMedia("print") : null;
|
|
14
|
+
const changed = (event) => set(event.matches);
|
|
15
|
+
window.addEventListener("beforeprint", before);
|
|
16
|
+
window.addEventListener("afterprint", after);
|
|
17
|
+
query?.addEventListener?.("change", changed);
|
|
18
|
+
setPrinting(Boolean(query?.matches));
|
|
19
|
+
return () => {
|
|
20
|
+
window.removeEventListener("beforeprint", before);
|
|
21
|
+
window.removeEventListener("afterprint", after);
|
|
22
|
+
query?.removeEventListener?.("change", changed);
|
|
23
|
+
};
|
|
24
|
+
}, [enabled]);
|
|
25
|
+
return enabled && printing;
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
usePrinting
|
|
29
|
+
};
|