@scalewing/react 1.40.0 → 1.42.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/CHANGELOG.md +38 -0
- package/README.md +274 -7
- package/dist/calendar-labels.d.ts +2 -0
- package/dist/calendar-labels.js +7 -0
- package/dist/components/Article.d.ts +60 -0
- package/dist/components/Article.js +42 -0
- package/dist/components/BarChart.d.ts +20 -4
- package/dist/components/BarChart.js +13 -6
- package/dist/components/ButtonGroup.d.ts +8 -1
- package/dist/components/ButtonGroup.js +11 -2
- package/dist/components/DateField.d.ts +14 -2
- package/dist/components/DateField.js +14 -11
- package/dist/components/DenominationGrid.d.ts +3 -2
- package/dist/components/DenominationGrid.js +21 -3
- package/dist/components/Dialog.d.ts +29 -13
- package/dist/components/Dialog.js +22 -20
- package/dist/components/DocumentViewer.d.ts +41 -0
- package/dist/components/DocumentViewer.js +72 -0
- package/dist/components/Field.d.ts +13 -2
- package/dist/components/Field.js +14 -38
- package/dist/components/FileField.d.ts +44 -0
- package/dist/components/FileField.js +66 -0
- package/dist/components/FilterChips.d.ts +7 -37
- package/dist/components/FilterChips.js +32 -24
- package/dist/components/ListRow.d.ts +33 -3
- package/dist/components/ListRow.js +18 -17
- package/dist/components/MarkdownView.d.ts +39 -0
- package/dist/components/MarkdownView.js +30 -0
- package/dist/components/NavCount.d.ts +15 -2
- package/dist/components/NavCount.js +16 -6
- package/dist/components/SegmentedControl.d.ts +17 -1
- package/dist/components/SegmentedControl.js +11 -3
- package/dist/components/Switch.d.ts +16 -1
- package/dist/components/Switch.js +17 -6
- package/dist/components/action-bar/ActionBarSummary.d.ts +10 -2
- package/dist/components/action-bar/ActionBarSummary.js +11 -2
- package/dist/components/chart/use-chart-width.js +2 -24
- package/dist/components/date-field/CalendarEntryButton.d.ts +24 -0
- package/dist/components/date-field/CalendarEntryButton.js +16 -0
- package/dist/components/date-field/TypedEntry.d.ts +26 -0
- package/dist/components/date-field/TypedEntry.js +13 -0
- package/dist/components/denomination-grid/CountCell.d.ts +16 -0
- package/dist/components/denomination-grid/CountCell.js +14 -0
- package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -1
- package/dist/components/denomination-grid/DenominationStrip.js +2 -2
- package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
- package/dist/components/denomination-grid/EntryCell.js +2 -2
- package/dist/components/denomination-grid/StripRow.js +1 -10
- package/dist/components/denomination-grid/TransposedStrip.d.ts +17 -0
- package/dist/components/denomination-grid/TransposedStrip.js +43 -0
- package/dist/components/denomination-grid/types.d.ts +31 -0
- package/dist/components/denomination-grid/types.js +7 -1
- package/dist/components/denomination-grid/validate-options.d.ts +7 -0
- package/dist/components/denomination-grid/validate-options.js +27 -0
- package/dist/components/dialog/DialogFooter.d.ts +6 -5
- package/dist/components/dialog/DialogFooter.js +6 -6
- package/dist/components/dialog/validate.d.ts +24 -0
- package/dist/components/dialog/validate.js +23 -0
- package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
- package/dist/components/document-viewer/document-viewer-labels.js +23 -0
- package/dist/components/document-viewer/page-box.d.ts +8 -0
- package/dist/components/document-viewer/page-box.js +12 -0
- package/dist/components/document-viewer/pdf-document.d.ts +32 -0
- package/dist/components/document-viewer/pdf-document.js +66 -0
- package/dist/components/document-viewer/pdf-options.d.ts +36 -0
- package/dist/components/document-viewer/pdf-options.js +46 -0
- package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
- package/dist/components/document-viewer/render-pdf-page.js +38 -0
- package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
- package/dist/components/document-viewer/scroll-chain.js +23 -0
- package/dist/components/document-viewer/use-error-report.d.ts +5 -0
- package/dist/components/document-viewer/use-error-report.js +13 -0
- package/dist/components/document-viewer/use-page-render.d.ts +21 -0
- package/dist/components/document-viewer/use-page-render.js +70 -0
- package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
- package/dist/components/document-viewer/use-pdf-document.js +47 -0
- package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
- package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
- package/dist/components/document-viewer/use-zoom.d.ts +23 -0
- package/dist/components/document-viewer/use-zoom.js +70 -0
- package/dist/components/document-viewer/zoom.d.ts +29 -0
- package/dist/components/document-viewer/zoom.js +37 -0
- package/dist/components/field/field-checks.d.ts +34 -0
- package/dist/components/field/field-checks.js +42 -0
- package/dist/components/file-field/file-field-checks.d.ts +13 -0
- package/dist/components/file-field/file-field-checks.js +21 -0
- package/dist/components/file-field/use-file-drop.d.ts +34 -0
- package/dist/components/file-field/use-file-drop.js +78 -0
- package/dist/components/file-field/use-swap-focus.d.ts +15 -0
- package/dist/components/file-field/use-swap-focus.js +40 -0
- package/dist/components/filter-chips/FilterChip.d.ts +16 -0
- package/dist/components/filter-chips/FilterChip.js +20 -0
- package/dist/components/filter-chips/options.d.ts +75 -0
- package/dist/components/filter-chips/options.js +11 -0
- package/dist/components/filter-chips/validate.d.ts +9 -0
- package/dist/components/filter-chips/validate.js +48 -0
- package/dist/components/list/ListRowContent.d.ts +8 -2
- package/dist/components/list/ListRowContent.js +6 -3
- package/dist/components/list/ListRowItem.d.ts +10 -0
- package/dist/components/list/ListRowItem.js +17 -0
- package/dist/components/list/row-naming.d.ts +17 -0
- package/dist/components/list/row-naming.js +19 -0
- package/dist/components/list/row-slot.d.ts +8 -0
- package/dist/components/list/row-slot.js +12 -0
- package/dist/components/markdown/markdown-limits.d.ts +14 -0
- package/dist/components/markdown/markdown-limits.js +14 -0
- package/dist/components/markdown/markdown-model.d.ts +94 -0
- package/dist/components/markdown/markdown-model.js +8 -0
- package/dist/components/markdown/markdown-options.d.ts +18 -0
- package/dist/components/markdown/markdown-options.js +10 -0
- package/dist/components/markdown/markdown-rules.d.ts +28 -0
- package/dist/components/markdown/markdown-rules.js +81 -0
- package/dist/components/markdown/markdown-text.d.ts +17 -0
- package/dist/components/markdown/markdown-text.js +86 -0
- package/dist/components/markdown/parse-markdown.d.ts +4 -0
- package/dist/components/markdown/parse-markdown.js +21 -0
- package/dist/components/markdown/read-blocks.d.ts +8 -0
- package/dist/components/markdown/read-blocks.js +168 -0
- package/dist/components/markdown/read-inlines.d.ts +12 -0
- package/dist/components/markdown/read-inlines.js +126 -0
- package/dist/components/markdown/read-markdown.d.ts +9 -0
- package/dist/components/markdown/read-markdown.js +27 -0
- package/dist/components/markdown/reading-context.d.ts +18 -0
- package/dist/components/markdown/reading-context.js +35 -0
- package/dist/components/markdown/render-markdown.d.ts +10 -0
- package/dist/components/markdown/render-markdown.js +52 -0
- package/dist/components/use-element-width.d.ts +11 -0
- package/dist/components/use-element-width.js +38 -0
- package/dist/css/chart-tones.d.ts +2 -0
- package/dist/css/chart-tones.js +5 -1
- package/dist/css/css-article.d.ts +2 -0
- package/dist/css/css-article.js +110 -0
- package/dist/css/css-bar-chart.d.ts +17 -2
- package/dist/css/css-bar-chart.js +38 -7
- package/dist/css/css-button-group-attached.d.ts +3 -0
- package/dist/css/css-button-group-attached.js +18 -1
- package/dist/css/css-canvas-grouped.d.ts +17 -0
- package/dist/css/css-canvas-grouped.js +40 -0
- package/dist/css/css-components.js +18 -0
- package/dist/css/css-count-pill.js +14 -1
- package/dist/css/css-date-field-calendar-entry.d.ts +11 -0
- package/dist/css/css-date-field-calendar-entry.js +50 -0
- package/dist/css/css-date-field.js +4 -0
- package/dist/css/css-denomination-grid.js +4 -0
- package/dist/css/css-denomination-transposed.d.ts +4 -0
- package/dist/css/css-denomination-transposed.js +178 -0
- package/dist/css/css-dialog-canvas.d.ts +3 -0
- package/dist/css/css-dialog-canvas.js +20 -0
- package/dist/css/css-dialog-frame.js +19 -2
- package/dist/css/css-dialog-grabber.d.ts +4 -0
- package/dist/css/css-dialog-grabber.js +56 -0
- package/dist/css/css-dialog.js +3 -2
- package/dist/css/css-document-viewer.d.ts +2 -0
- package/dist/css/css-document-viewer.js +186 -0
- package/dist/css/css-document.js +3 -0
- package/dist/css/css-field-width.d.ts +12 -0
- package/dist/css/css-field-width.js +49 -0
- package/dist/css/css-field.js +4 -0
- package/dist/css/css-file-field.d.ts +2 -0
- package/dist/css/css-file-field.js +180 -0
- package/dist/css/css-filter-chips-layout.d.ts +2 -0
- package/dist/css/css-filter-chips-layout.js +42 -0
- package/dist/css/css-filter-chips.js +4 -0
- package/dist/css/css-list-row-trailing.d.ts +2 -0
- package/dist/css/css-list-row-trailing.js +91 -0
- package/dist/css/css-list-row.d.ts +6 -0
- package/dist/css/css-list-row.js +19 -3
- package/dist/css/css-list.js +5 -1
- package/dist/css/css-markdown.d.ts +2 -0
- package/dist/css/css-markdown.js +144 -0
- package/dist/css/css-segmented-sizing.d.ts +13 -0
- package/dist/css/css-segmented-sizing.js +66 -0
- package/dist/css/css-segmented.js +4 -0
- package/dist/css/css-switch.d.ts +6 -0
- package/dist/css/css-switch.js +39 -0
- package/dist/css/css-variables.js +1 -0
- package/dist/css/stacking.d.ts +1 -0
- package/dist/css/stacking.js +4 -1
- package/dist/css/stylesheet.js +12 -0
- package/dist/date-field-labels.d.ts +5 -0
- package/dist/date-field-labels.js +1 -0
- package/dist/document-viewer-worker.d.ts +9 -0
- package/dist/document-viewer-worker.js +9 -0
- package/dist/index.d.ts +15 -9
- package/dist/index.js +13 -7
- package/dist/list-options.d.ts +11 -7
- package/dist/list-options.js +20 -8
- package/dist/styles.css +889 -44
- package/dist/theme/ThemeProvider.d.ts +11 -1
- package/dist/theme/ThemeProvider.js +4 -2
- package/package.json +10 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { openPdf } from './pdf-document.js';
|
|
4
|
+
/** Closes a document no one shows any more; a failure to close is moot. */
|
|
5
|
+
function closeQuietly(document) {
|
|
6
|
+
document.destroy().catch(() => undefined);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Opens the PDF in `data` and closes it (and its worker) when the bytes
|
|
10
|
+
* change or the viewer goes away. New bytes start a new open and show as
|
|
11
|
+
* loading until it settles, so a consumer's Try again is new bytes (or a
|
|
12
|
+
* new `key`).
|
|
13
|
+
*/
|
|
14
|
+
export function usePdfDocument(data, workerSrc, callbacks) {
|
|
15
|
+
const [settled, setSettled] = useState(null);
|
|
16
|
+
// The callbacks are read when the open settles, never reasons to reopen.
|
|
17
|
+
const latest = useRef(callbacks);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
latest.current = callbacks;
|
|
20
|
+
});
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
let live = true;
|
|
23
|
+
let opened = null;
|
|
24
|
+
openPdf(data, workerSrc).then((document) => {
|
|
25
|
+
if (!live) {
|
|
26
|
+
closeQuietly(document);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
opened = document;
|
|
30
|
+
setSettled({ data, workerSrc, state: { status: 'ready', document } });
|
|
31
|
+
latest.current.onLoad?.({ pageCount: document.pageCount });
|
|
32
|
+
}, (error) => {
|
|
33
|
+
if (!live)
|
|
34
|
+
return;
|
|
35
|
+
setSettled({ data, workerSrc, state: { status: 'failed', error } });
|
|
36
|
+
latest.current.onError?.(error);
|
|
37
|
+
});
|
|
38
|
+
return () => {
|
|
39
|
+
live = false;
|
|
40
|
+
if (opened)
|
|
41
|
+
closeQuietly(opened);
|
|
42
|
+
};
|
|
43
|
+
}, [data, workerSrc]);
|
|
44
|
+
return settled && settled.data === data && settled.workerSrc === workerSrc
|
|
45
|
+
? settled.state
|
|
46
|
+
: { status: 'loading' };
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Two fingers on the page zoom it, between fit width and four times as
|
|
4
|
+
* wide, about the point between them. The page's box allows
|
|
5
|
+
* only panning (`touch-action: pan-x pan-y`), so the browser does not zoom
|
|
6
|
+
* the whole screen instead; a two-finger move is taken from the browser
|
|
7
|
+
* (a non-passive listener), one finger still scrolls.
|
|
8
|
+
*/
|
|
9
|
+
export declare function usePinchZoom(box: RefObject<HTMLElement | null>, zoom: number, zoomTo: (zoom: number, at: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}) => void): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { clampZoom } from './zoom.js';
|
|
4
|
+
function spread(touches) {
|
|
5
|
+
const [a, b] = [touches[0], touches[1]];
|
|
6
|
+
return {
|
|
7
|
+
distance: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY),
|
|
8
|
+
x: (a.clientX + b.clientX) / 2,
|
|
9
|
+
y: (a.clientY + b.clientY) / 2,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Two fingers on the page zoom it, between fit width and four times as
|
|
14
|
+
* wide, about the point between them. The page's box allows
|
|
15
|
+
* only panning (`touch-action: pan-x pan-y`), so the browser does not zoom
|
|
16
|
+
* the whole screen instead; a two-finger move is taken from the browser
|
|
17
|
+
* (a non-passive listener), one finger still scrolls.
|
|
18
|
+
*/
|
|
19
|
+
export function usePinchZoom(box, zoom, zoomTo) {
|
|
20
|
+
const current = useRef(zoom);
|
|
21
|
+
const pinch = useRef(null);
|
|
22
|
+
const setter = useRef(zoomTo);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
current.current = zoom;
|
|
25
|
+
setter.current = zoomTo;
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const element = box.current;
|
|
29
|
+
if (!element)
|
|
30
|
+
return;
|
|
31
|
+
function start(event) {
|
|
32
|
+
if (event.touches.length !== 2)
|
|
33
|
+
return;
|
|
34
|
+
pinch.current = {
|
|
35
|
+
distance: spread(event.touches).distance,
|
|
36
|
+
zoom: current.current,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function move(event) {
|
|
40
|
+
const begun = pinch.current;
|
|
41
|
+
if (!begun || event.touches.length !== 2 || !element)
|
|
42
|
+
return;
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
const now = spread(event.touches);
|
|
45
|
+
if (begun.distance <= 0)
|
|
46
|
+
return;
|
|
47
|
+
const next = clampZoom((begun.zoom * now.distance) / begun.distance);
|
|
48
|
+
if (next === current.current)
|
|
49
|
+
return;
|
|
50
|
+
setter.current(next, { x: now.x, y: now.y });
|
|
51
|
+
}
|
|
52
|
+
function end(event) {
|
|
53
|
+
if (event.touches.length < 2)
|
|
54
|
+
pinch.current = null;
|
|
55
|
+
}
|
|
56
|
+
element.addEventListener('touchstart', start, { passive: true });
|
|
57
|
+
element.addEventListener('touchmove', move, { passive: false });
|
|
58
|
+
element.addEventListener('touchend', end);
|
|
59
|
+
element.addEventListener('touchcancel', end);
|
|
60
|
+
return () => {
|
|
61
|
+
element.removeEventListener('touchstart', start);
|
|
62
|
+
element.removeEventListener('touchmove', move);
|
|
63
|
+
element.removeEventListener('touchend', end);
|
|
64
|
+
element.removeEventListener('touchcancel', end);
|
|
65
|
+
};
|
|
66
|
+
}, [box]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** How long a zoom rests before the page is drawn again at its size. */
|
|
2
|
+
export declare const zoomSettleMs = 150;
|
|
3
|
+
/**
|
|
4
|
+
* The page's zoom (1 is fit width) and the scrolling box it is shown in.
|
|
5
|
+
* `zoomTo` changes it about a point on the screen (`at`, in client
|
|
6
|
+
* pixels; the middle of the box's visible part when left out) so the spot
|
|
7
|
+
* of the page under it stays put: once the bigger or smaller page is laid
|
|
8
|
+
* out, the box scrolls across and the boxes around it (the window at
|
|
9
|
+
* last) scroll down by what keeps that spot in place. `drawnZoom`
|
|
10
|
+
* follows `zoom` once it rests, so a pinch stretches the drawn page and
|
|
11
|
+
* only its end draws it again. Another `resetKey` (another page) is fit
|
|
12
|
+
* width again.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useZoom(resetKey: string): {
|
|
15
|
+
box: import("react").RefObject<HTMLDivElement | null>;
|
|
16
|
+
page: import("react").RefObject<HTMLDivElement | null>;
|
|
17
|
+
zoom: number;
|
|
18
|
+
drawnZoom: number;
|
|
19
|
+
zoomTo: (next: number, at?: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
}) => void;
|
|
23
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { scrollChainBy } from './scroll-chain.js';
|
|
4
|
+
import { anchorShift, clampZoom, minZoom } from './zoom.js';
|
|
5
|
+
/** The middle of the part of `element` inside the window, in client pixels. */
|
|
6
|
+
function visibleMiddle(element) {
|
|
7
|
+
const rect = element.getBoundingClientRect();
|
|
8
|
+
const view = element.ownerDocument.documentElement;
|
|
9
|
+
const top = Math.max(rect.top, 0);
|
|
10
|
+
const bottom = Math.min(rect.bottom, view.clientHeight || rect.bottom);
|
|
11
|
+
return {
|
|
12
|
+
x: rect.left + rect.width / 2,
|
|
13
|
+
y: bottom > top ? (top + bottom) / 2 : rect.top + rect.height / 2,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** How long a zoom rests before the page is drawn again at its size. */
|
|
17
|
+
export const zoomSettleMs = 150;
|
|
18
|
+
/**
|
|
19
|
+
* The page's zoom (1 is fit width) and the scrolling box it is shown in.
|
|
20
|
+
* `zoomTo` changes it about a point on the screen (`at`, in client
|
|
21
|
+
* pixels; the middle of the box's visible part when left out) so the spot
|
|
22
|
+
* of the page under it stays put: once the bigger or smaller page is laid
|
|
23
|
+
* out, the box scrolls across and the boxes around it (the window at
|
|
24
|
+
* last) scroll down by what keeps that spot in place. `drawnZoom`
|
|
25
|
+
* follows `zoom` once it rests, so a pinch stretches the drawn page and
|
|
26
|
+
* only its end draws it again. Another `resetKey` (another page) is fit
|
|
27
|
+
* width again.
|
|
28
|
+
*/
|
|
29
|
+
export function useZoom(resetKey) {
|
|
30
|
+
const box = useRef(null);
|
|
31
|
+
const page = useRef(null);
|
|
32
|
+
const [keyed, setKeyed] = useState({ key: resetKey, zoom: minZoom });
|
|
33
|
+
const [drawn, setDrawn] = useState({ key: resetKey, zoom: minZoom });
|
|
34
|
+
const zoom = keyed.key === resetKey ? keyed.zoom : minZoom;
|
|
35
|
+
const drawnZoom = drawn.key === resetKey ? drawn.zoom : minZoom;
|
|
36
|
+
const anchor = useRef(null);
|
|
37
|
+
const shown = useRef(zoom);
|
|
38
|
+
useLayoutEffect(() => {
|
|
39
|
+
const from = shown.current;
|
|
40
|
+
shown.current = zoom;
|
|
41
|
+
const element = box.current;
|
|
42
|
+
const sheet = page.current;
|
|
43
|
+
const spot = anchor.current;
|
|
44
|
+
anchor.current = null;
|
|
45
|
+
if (!element || !sheet || !spot || from === zoom)
|
|
46
|
+
return;
|
|
47
|
+
const shift = anchorShift(spot.before, sheet.getBoundingClientRect(), spot.point);
|
|
48
|
+
scrollChainBy(element, 'x', shift.x);
|
|
49
|
+
scrollChainBy(element, 'y', shift.y);
|
|
50
|
+
}, [zoom]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const timer = setTimeout(() => setDrawn({ key: resetKey, zoom }), zoomSettleMs);
|
|
53
|
+
return () => clearTimeout(timer);
|
|
54
|
+
}, [resetKey, zoom]);
|
|
55
|
+
function zoomTo(next, at) {
|
|
56
|
+
const element = box.current;
|
|
57
|
+
const sheet = page.current;
|
|
58
|
+
const target = clampZoom(next);
|
|
59
|
+
if (target === shown.current)
|
|
60
|
+
return;
|
|
61
|
+
if (element && sheet) {
|
|
62
|
+
anchor.current = {
|
|
63
|
+
point: at ?? visibleMiddle(element),
|
|
64
|
+
before: sheet.getBoundingClientRect(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
setKeyed({ key: resetKey, zoom: target });
|
|
68
|
+
}
|
|
69
|
+
return { box, page, zoom, drawnZoom, zoomTo };
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Fit width is 1; a page can be drawn up to four times as wide. */
|
|
2
|
+
export declare const minZoom = 1;
|
|
3
|
+
export declare const maxZoom = 4;
|
|
4
|
+
/** One key press's step: a quarter of the fitted width. */
|
|
5
|
+
export declare const zoomStep = 0.25;
|
|
6
|
+
/** A zoom kept between fit width and the most, to two decimals. */
|
|
7
|
+
export declare function clampZoom(zoom: number): number;
|
|
8
|
+
/** The zoom a key asks for on the page: `+` or `=` in, `-` out, `0` fit. */
|
|
9
|
+
export declare function zoomForKey(key: string, zoom: number): number | null;
|
|
10
|
+
/** A box on the screen, as `getBoundingClientRect` gives it. */
|
|
11
|
+
export type ScreenBox = {
|
|
12
|
+
left: number;
|
|
13
|
+
top: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* How far to scroll, across and down, so the spot of the page that was
|
|
19
|
+
* under `point` before a zoom (the page at `before`) is under it again
|
|
20
|
+
* after (the page at `after`). Both boxes are the page's own, so the
|
|
21
|
+
* stage's padding and where the page sits on the screen count.
|
|
22
|
+
*/
|
|
23
|
+
export declare function anchorShift(before: ScreenBox, after: ScreenBox, point: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
}): {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Fit width is 1; a page can be drawn up to four times as wide. */
|
|
2
|
+
export const minZoom = 1;
|
|
3
|
+
export const maxZoom = 4;
|
|
4
|
+
/** One key press's step: a quarter of the fitted width. */
|
|
5
|
+
export const zoomStep = 0.25;
|
|
6
|
+
/** A zoom kept between fit width and the most, to two decimals. */
|
|
7
|
+
export function clampZoom(zoom) {
|
|
8
|
+
if (!Number.isFinite(zoom))
|
|
9
|
+
return minZoom;
|
|
10
|
+
return Math.round(Math.min(maxZoom, Math.max(minZoom, zoom)) * 100) / 100;
|
|
11
|
+
}
|
|
12
|
+
/** The zoom a key asks for on the page: `+` or `=` in, `-` out, `0` fit. */
|
|
13
|
+
export function zoomForKey(key, zoom) {
|
|
14
|
+
if (key === '+' || key === '=')
|
|
15
|
+
return clampZoom(zoom + zoomStep);
|
|
16
|
+
if (key === '-' || key === '_')
|
|
17
|
+
return clampZoom(zoom - zoomStep);
|
|
18
|
+
if (key === '0')
|
|
19
|
+
return minZoom;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* How far to scroll, across and down, so the spot of the page that was
|
|
24
|
+
* under `point` before a zoom (the page at `before`) is under it again
|
|
25
|
+
* after (the page at `after`). Both boxes are the page's own, so the
|
|
26
|
+
* stage's padding and where the page sits on the screen count.
|
|
27
|
+
*/
|
|
28
|
+
export function anchorShift(before, after, point) {
|
|
29
|
+
if (before.width <= 0 || before.height <= 0)
|
|
30
|
+
return { x: 0, y: 0 };
|
|
31
|
+
const across = (point.x - before.left) / before.width;
|
|
32
|
+
const down = (point.y - before.top) / before.height;
|
|
33
|
+
return {
|
|
34
|
+
x: after.left + across * after.width - point.x,
|
|
35
|
+
y: after.top + down * after.height - point.y,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type KeyboardEvent } from 'react';
|
|
2
|
+
import type { FieldProps } from '../Field.js';
|
|
3
|
+
export declare const fieldVariants: readonly ["outlined", "search"];
|
|
4
|
+
export type FieldVariant = (typeof fieldVariants)[number];
|
|
5
|
+
/** The props Field reads from, and sets on, its native control child. */
|
|
6
|
+
export type FieldControlProps = {
|
|
7
|
+
'aria-describedby'?: string;
|
|
8
|
+
'aria-label'?: string;
|
|
9
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
10
|
+
'aria-labelledby'?: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
type?: string;
|
|
16
|
+
value?: unknown;
|
|
17
|
+
enterKeyHint?: string;
|
|
18
|
+
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Fails closed on a Field it cannot draw: an unknown variant or width,
|
|
22
|
+
* search props on an outlined field, and a search field without its clear
|
|
23
|
+
* button, its handler or one controlled native input.
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertFieldProps(props: FieldProps, child: unknown): void;
|
|
26
|
+
/**
|
|
27
|
+
* `invalid` was passed, even as `false`, but not as `undefined` (a wrapper
|
|
28
|
+
* forwarding an optional prop passes nothing): such a field needs one native
|
|
29
|
+
* control from its first render, so a form learns it on the happy path
|
|
30
|
+
* instead of on its first failed submit. `error` keeps its older rule (it
|
|
31
|
+
* requires the control only while it holds a message), so a composed
|
|
32
|
+
* child beside `error={undefined}` still renders.
|
|
33
|
+
*/
|
|
34
|
+
export declare function passesInvalid(props: FieldProps): boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isValidElement } from 'react';
|
|
2
|
+
import { assertFieldWidth } from '../../css/css-field-width.js';
|
|
3
|
+
export const fieldVariants = ['outlined', 'search'];
|
|
4
|
+
/**
|
|
5
|
+
* Fails closed on a Field it cannot draw: an unknown variant or width,
|
|
6
|
+
* search props on an outlined field, and a search field without its clear
|
|
7
|
+
* button, its handler or one controlled native input.
|
|
8
|
+
*/
|
|
9
|
+
export function assertFieldProps(props, child) {
|
|
10
|
+
const { variant = 'outlined' } = props;
|
|
11
|
+
assertFieldWidth(props.width);
|
|
12
|
+
if (!fieldVariants.includes(variant))
|
|
13
|
+
throw new RangeError(`Unknown Field variant: ${String(variant)}`);
|
|
14
|
+
if (variant !== 'search') {
|
|
15
|
+
if (props.leading !== undefined ||
|
|
16
|
+
props.clearLabel !== undefined ||
|
|
17
|
+
props.onClear !== undefined)
|
|
18
|
+
throw new TypeError('Field leading, clearLabel and onClear are for variant="search"');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (!props.clearLabel?.trim())
|
|
22
|
+
throw new RangeError('A search Field needs a nonempty clearLabel');
|
|
23
|
+
if (typeof props.onClear !== 'function')
|
|
24
|
+
throw new TypeError('A search Field needs onClear');
|
|
25
|
+
if (props.prefix || props.suffix)
|
|
26
|
+
throw new TypeError('A search Field takes leading, not prefix or suffix');
|
|
27
|
+
if (!isValidElement(child) || child.type !== 'input')
|
|
28
|
+
throw new TypeError('A search Field needs one native input child');
|
|
29
|
+
if (child.props.value === undefined)
|
|
30
|
+
throw new TypeError('A search Field needs a controlled input: pass value (and onChange)');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `invalid` was passed, even as `false`, but not as `undefined` (a wrapper
|
|
34
|
+
* forwarding an optional prop passes nothing): such a field needs one native
|
|
35
|
+
* control from its first render, so a form learns it on the happy path
|
|
36
|
+
* instead of on its first failed submit. `error` keeps its older rule (it
|
|
37
|
+
* requires the control only while it holds a message), so a composed
|
|
38
|
+
* child beside `error={undefined}` still renders.
|
|
39
|
+
*/
|
|
40
|
+
export function passesInvalid(props) {
|
|
41
|
+
return props.invalid !== undefined || props.changed !== undefined;
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FileFieldValue } from '../FileField.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fails closed on a FileField it cannot name or show: an empty label,
|
|
4
|
+
* choose or change words, a blank drop hint, or a chosen file with no name
|
|
5
|
+
* or a blank detail.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertFileField(props: {
|
|
8
|
+
label: string;
|
|
9
|
+
chooseLabel: string;
|
|
10
|
+
changeLabel: string;
|
|
11
|
+
dropLabel: string | undefined;
|
|
12
|
+
value: FileFieldValue | null;
|
|
13
|
+
}): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function isBlank(text) {
|
|
2
|
+
return typeof text !== 'string' || text.trim() === '';
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Fails closed on a FileField it cannot name or show: an empty label,
|
|
6
|
+
* choose or change words, a blank drop hint, or a chosen file with no name
|
|
7
|
+
* or a blank detail.
|
|
8
|
+
*/
|
|
9
|
+
export function assertFileField(props) {
|
|
10
|
+
for (const key of ['label', 'chooseLabel', 'changeLabel'])
|
|
11
|
+
if (isBlank(props[key]))
|
|
12
|
+
throw new RangeError(`FileField ${key} must be non-empty text`);
|
|
13
|
+
if (props.dropLabel !== undefined && isBlank(props.dropLabel))
|
|
14
|
+
throw new RangeError('FileField dropLabel must be non-empty text');
|
|
15
|
+
if (props.value === null)
|
|
16
|
+
return;
|
|
17
|
+
if (isBlank(props.value.name))
|
|
18
|
+
throw new RangeError('FileField value name must be non-empty text');
|
|
19
|
+
if (props.value.detail !== undefined && isBlank(props.value.detail))
|
|
20
|
+
throw new RangeError('FileField value detail must be non-empty text');
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type DragEvent } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* A disabled field still keeps the browser from opening a file dropped on
|
|
4
|
+
* it, and shows no copy cursor: the drop does nothing.
|
|
5
|
+
*/
|
|
6
|
+
declare function refuse(event: DragEvent): void;
|
|
7
|
+
/**
|
|
8
|
+
* A drop target for one file: whether files are dragged over it, and the
|
|
9
|
+
* handlers that keep the browser from opening a dropped file and hand the
|
|
10
|
+
* first one to `onFile`. A counter follows the drag through the target's
|
|
11
|
+
* children, whose enter and leave events would otherwise flicker it. While
|
|
12
|
+
* `disabled`, a dropped file is refused and a drag in progress ends.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useFileDrop({ disabled, onFile, }: {
|
|
15
|
+
disabled: boolean;
|
|
16
|
+
onFile: (file: File) => void;
|
|
17
|
+
}): {
|
|
18
|
+
dragging: boolean;
|
|
19
|
+
handlers: {
|
|
20
|
+
onDragOver: typeof refuse;
|
|
21
|
+
onDragLeave: (event: DragEvent) => void;
|
|
22
|
+
onDrop: (event: DragEvent) => void;
|
|
23
|
+
onDragEnter?: undefined;
|
|
24
|
+
};
|
|
25
|
+
} | {
|
|
26
|
+
dragging: boolean;
|
|
27
|
+
handlers: {
|
|
28
|
+
onDragEnter: (event: DragEvent) => void;
|
|
29
|
+
onDragOver: (event: DragEvent) => void;
|
|
30
|
+
onDragLeave: (event: DragEvent) => void;
|
|
31
|
+
onDrop: (event: DragEvent) => void;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
/** Whether a drag carries files, not text or a link dragged from the page. */
|
|
3
|
+
function carriesFiles(event) {
|
|
4
|
+
return Array.from(event.dataTransfer?.types ?? []).includes('Files');
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A disabled field still keeps the browser from opening a file dropped on
|
|
8
|
+
* it, and shows no copy cursor: the drop does nothing.
|
|
9
|
+
*/
|
|
10
|
+
function refuse(event) {
|
|
11
|
+
if (!carriesFiles(event))
|
|
12
|
+
return;
|
|
13
|
+
event.preventDefault();
|
|
14
|
+
event.dataTransfer.dropEffect = 'none';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A drop target for one file: whether files are dragged over it, and the
|
|
18
|
+
* handlers that keep the browser from opening a dropped file and hand the
|
|
19
|
+
* first one to `onFile`. A counter follows the drag through the target's
|
|
20
|
+
* children, whose enter and leave events would otherwise flicker it. While
|
|
21
|
+
* `disabled`, a dropped file is refused and a drag in progress ends.
|
|
22
|
+
*/
|
|
23
|
+
export function useFileDrop({ disabled, onFile, }) {
|
|
24
|
+
const depth = useRef(0);
|
|
25
|
+
const [dragging, setDragging] = useState(false);
|
|
26
|
+
// A drag that began before the field was disabled ends here too, so its
|
|
27
|
+
// tint is gone when the field is enabled again.
|
|
28
|
+
const stop = (event) => {
|
|
29
|
+
refuse(event);
|
|
30
|
+
depth.current = 0;
|
|
31
|
+
setDragging(false);
|
|
32
|
+
};
|
|
33
|
+
if (disabled)
|
|
34
|
+
return {
|
|
35
|
+
dragging: false,
|
|
36
|
+
handlers: {
|
|
37
|
+
onDragOver: refuse,
|
|
38
|
+
// Leaving or dropping ends a drag that began while enabled.
|
|
39
|
+
onDragLeave: stop,
|
|
40
|
+
onDrop: stop,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
dragging,
|
|
45
|
+
handlers: {
|
|
46
|
+
onDragEnter: (event) => {
|
|
47
|
+
if (!carriesFiles(event))
|
|
48
|
+
return;
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
depth.current += 1;
|
|
51
|
+
setDragging(true);
|
|
52
|
+
},
|
|
53
|
+
onDragOver: (event) => {
|
|
54
|
+
if (!carriesFiles(event))
|
|
55
|
+
return;
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
58
|
+
},
|
|
59
|
+
onDragLeave: (event) => {
|
|
60
|
+
if (!carriesFiles(event))
|
|
61
|
+
return;
|
|
62
|
+
depth.current = Math.max(0, depth.current - 1);
|
|
63
|
+
if (depth.current === 0)
|
|
64
|
+
setDragging(false);
|
|
65
|
+
},
|
|
66
|
+
onDrop: (event) => {
|
|
67
|
+
if (!carriesFiles(event))
|
|
68
|
+
return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
depth.current = 0;
|
|
71
|
+
setDragging(false);
|
|
72
|
+
const file = event.dataTransfer.files[0];
|
|
73
|
+
if (file)
|
|
74
|
+
onFile(file);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FocusEvent, type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Keeps focus when the field swaps its zone for the chosen row (or back).
|
|
4
|
+
* The frame's focus handlers note whether focus is in the field. A blur
|
|
5
|
+
* that leaves the button in the page, with the window still focused, means
|
|
6
|
+
* the person moved on; one whose button was removed is the swap; and one
|
|
7
|
+
* while the window is away is the system picker. After a swap, if focus
|
|
8
|
+
* was in the field and is now nowhere, it moves to the button now there.
|
|
9
|
+
* Focus the person moved elsewhere (an answer that comes later) is never
|
|
10
|
+
* taken back.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useSwapFocus(button: RefObject<HTMLElement | null>, chosen: boolean): {
|
|
13
|
+
onFocus: () => void;
|
|
14
|
+
onBlur: (event: FocusEvent<HTMLElement>) => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLayoutEffect, useRef, } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Keeps focus when the field swaps its zone for the chosen row (or back).
|
|
5
|
+
* The frame's focus handlers note whether focus is in the field. A blur
|
|
6
|
+
* that leaves the button in the page, with the window still focused, means
|
|
7
|
+
* the person moved on; one whose button was removed is the swap; and one
|
|
8
|
+
* while the window is away is the system picker. After a swap, if focus
|
|
9
|
+
* was in the field and is now nowhere, it moves to the button now there.
|
|
10
|
+
* Focus the person moved elsewhere (an answer that comes later) is never
|
|
11
|
+
* taken back.
|
|
12
|
+
*/
|
|
13
|
+
export function useSwapFocus(button, chosen) {
|
|
14
|
+
const inside = useRef(false);
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
if (!inside.current)
|
|
17
|
+
return;
|
|
18
|
+
const active = document.activeElement;
|
|
19
|
+
if (active === null || active === document.body)
|
|
20
|
+
button.current?.focus();
|
|
21
|
+
}, [button, chosen]);
|
|
22
|
+
return {
|
|
23
|
+
onFocus: () => {
|
|
24
|
+
inside.current = true;
|
|
25
|
+
},
|
|
26
|
+
onBlur: (event) => {
|
|
27
|
+
const next = event.relatedTarget;
|
|
28
|
+
if (next instanceof Node && event.currentTarget.contains(next))
|
|
29
|
+
return;
|
|
30
|
+
const left = event.target;
|
|
31
|
+
// Removed by the swap, the button is gone by the next microtask; and
|
|
32
|
+
// while the system picker is open the window itself has lost focus,
|
|
33
|
+
// which is not the person leaving the field.
|
|
34
|
+
queueMicrotask(() => {
|
|
35
|
+
if (left.isConnected && document.hasFocus())
|
|
36
|
+
inside.current = false;
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type FilterChipCountAppearance, type FilterChipOption } from './options.js';
|
|
2
|
+
type FilterChipProps = {
|
|
3
|
+
option: FilterChipOption;
|
|
4
|
+
/** The group's shared input name. */
|
|
5
|
+
name: string;
|
|
6
|
+
type: 'radio' | 'checkbox';
|
|
7
|
+
checked: boolean;
|
|
8
|
+
required: boolean;
|
|
9
|
+
/** Whether this chip is a Tab stop: every checkbox, and one radio. */
|
|
10
|
+
tabStopHere: boolean;
|
|
11
|
+
countAppearance: FilterChipCountAppearance;
|
|
12
|
+
onCheckedChange: (checked: boolean) => void;
|
|
13
|
+
};
|
|
14
|
+
/** One chip: a native radio or checkbox under its face. */
|
|
15
|
+
export declare function FilterChip({ option, name, type, checked, required, tabStopHere, countAppearance, onCheckedChange, }: FilterChipProps): import("react").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../../class-names.js';
|
|
3
|
+
import { tabStop } from '../../tab-stop.js';
|
|
4
|
+
/**
|
|
5
|
+
* A chip's count: a muted figure after a space, or inline in the chip's
|
|
6
|
+
* own words after a middle dot hidden from assistive technology, so the
|
|
7
|
+
* name reads "Open 3" either way.
|
|
8
|
+
*/
|
|
9
|
+
function ChipCount({ count, appearance, }) {
|
|
10
|
+
if (appearance === 'inline')
|
|
11
|
+
return (_jsxs(_Fragment, { children: [' ', _jsx("span", { "aria-hidden": "true", children: "\u00B7" }), ' ', _jsx("span", { className: "sw-filter-chip-count-inline", children: count })] }));
|
|
12
|
+
return (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-filter-chip-count", children: count })] }));
|
|
13
|
+
}
|
|
14
|
+
/** One chip: a native radio or checkbox under its face. */
|
|
15
|
+
export function FilterChip({ option, name, type, checked, required, tabStopHere, countAppearance, onCheckedChange, }) {
|
|
16
|
+
return (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: checked, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
|
|
17
|
+
if (!option.disabled)
|
|
18
|
+
onCheckedChange(event.currentTarget.checked);
|
|
19
|
+
}, required: required, tabIndex: tabStopHere ? tabStop : undefined, type: type, value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsx(ChipCount, { appearance: countAppearance, count: option.count }))] })] }));
|
|
20
|
+
}
|