@tensor_1/react 2.36.0 → 2.37.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
@@ -460,6 +460,18 @@ const spanLayout = layoutMonthSpans(spansInSalienceOrder, grid, { timeZone, week
460
460
  - **`<List roving>`**: the items are one tab stop (ArrowUp/ArrowDown, Home, End; Enter or Space activates the
461
461
  focused item's `onClick`). A control inside an item keeps its own tab stop and keys. Items may be wrapped or
462
462
  rendered later by a child component.
463
+ - **Print (HAR-1541).** The stylesheet has `@media print` rules; add no print CSS of your own. On paper the same
464
+ markup has no scroller, fits the page width, and keeps a week row or an agenda row on one page. The colours are
465
+ the `--uix-print-*` tokens (light, whatever theme the screen has). Nothing depends on a background: the `high`
466
+ band is a heavy edge on a paper surface (on paper `--calendar-band-high-fill` is the paper's and the edge reads
467
+ 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`
468
+ double, `dotted` dotted), and the hour lines are drawn as lines. Month chips wrap to show the whole title, and
469
+ 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
470
+ screen prints them too). The header's buttons, the "now" line and a pending move are not printed; the period
471
+ title, the zone label and the legend are. A long agenda mounts every row while the page prints (`beforeprint`,
472
+ or print media) and is a window of rows again afterwards. One limit: `window.print()` called from inside a
473
+ React effect, on mount or later, runs before those rows are there. Such a page calls it from an event handler
474
+ or a timer, or passes `virtualizeAbove={Infinity}`.
463
475
  - **Colour.** Every calendar colour is named once at the top of `scheduling-calendar.css` (`--calendar-*`).
464
476
  - **Deprecated, still working in 2.x:** `entry.state` / `SchedulingEntryState`, `overlay.kind` /
465
477
  `SchedulingOverlayKind` and the `previous` / `next` labels. An entry with `state` is no longer tinted: the two
@@ -502,6 +514,7 @@ time zone, the same proposal model for moves.
502
514
  - **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
515
  - **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
516
  - **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.
517
+ - **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
518
  - **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
519
  - Pure helpers are exported for tests and server code: `timelineTicks`, `timelineSubTicks`, `timelineRepeatedHourOffset`, `placeSpan`, `layoutLane`, `shiftSpan`, `snapToStep`, `pixelsToMs`, `defaultTimelineStep`, `timelineStepDelta`.
507
520
 
@@ -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 virtual = useVirtualRows(flatRows, { rowHeight: narrow ? AGENDA_NARROW_ROW : AGENDA_VIRTUAL_ROW, threshold: 0, estimatedViewportHeight: AGENDA_VIRTUAL_VIEWPORT });
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 text = renderEntry?.(entry, { availableLines: lines }) ?? (lines > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
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
- ] }) : null);
35
+ ] });
36
+ const noRoom = lines === 0 && (custom === void 0 || custom === null) ? "none" : void 0;
35
37
  const body = /* @__PURE__ */ jsxs(Fragment, { children: [
36
- continuation && lines > 0 && /* @__PURE__ */ jsx("span", { className: "uix-scheduling-calendar__tg-from", children: continuesFrom }),
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 virtual = lanes.length + (groups?.length ?? 0) > virtualizeAbove;
257
- const fixed = virtual || groups !== void 0 || overlays.some((overlay) => overlay.laneIds !== void 0) || markers.some((marker) => marker.laneIds !== void 0);
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;
@@ -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
+ };