@scalewing/react 1.41.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +120 -1
  3. package/dist/components/Article.d.ts +60 -0
  4. package/dist/components/Article.js +42 -0
  5. package/dist/components/DocumentViewer.d.ts +41 -0
  6. package/dist/components/DocumentViewer.js +72 -0
  7. package/dist/components/FileField.d.ts +44 -0
  8. package/dist/components/FileField.js +66 -0
  9. package/dist/components/ListRow.d.ts +23 -2
  10. package/dist/components/ListRow.js +17 -17
  11. package/dist/components/MarkdownView.d.ts +39 -0
  12. package/dist/components/MarkdownView.js +30 -0
  13. package/dist/components/NavCount.d.ts +15 -2
  14. package/dist/components/NavCount.js +16 -6
  15. package/dist/components/chart/use-chart-width.js +2 -24
  16. package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
  17. package/dist/components/document-viewer/document-viewer-labels.js +23 -0
  18. package/dist/components/document-viewer/page-box.d.ts +8 -0
  19. package/dist/components/document-viewer/page-box.js +12 -0
  20. package/dist/components/document-viewer/pdf-document.d.ts +32 -0
  21. package/dist/components/document-viewer/pdf-document.js +66 -0
  22. package/dist/components/document-viewer/pdf-options.d.ts +36 -0
  23. package/dist/components/document-viewer/pdf-options.js +46 -0
  24. package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
  25. package/dist/components/document-viewer/render-pdf-page.js +38 -0
  26. package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
  27. package/dist/components/document-viewer/scroll-chain.js +23 -0
  28. package/dist/components/document-viewer/use-error-report.d.ts +5 -0
  29. package/dist/components/document-viewer/use-error-report.js +13 -0
  30. package/dist/components/document-viewer/use-page-render.d.ts +21 -0
  31. package/dist/components/document-viewer/use-page-render.js +70 -0
  32. package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
  33. package/dist/components/document-viewer/use-pdf-document.js +47 -0
  34. package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
  35. package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
  36. package/dist/components/document-viewer/use-zoom.d.ts +23 -0
  37. package/dist/components/document-viewer/use-zoom.js +70 -0
  38. package/dist/components/document-viewer/zoom.d.ts +29 -0
  39. package/dist/components/document-viewer/zoom.js +37 -0
  40. package/dist/components/file-field/file-field-checks.d.ts +13 -0
  41. package/dist/components/file-field/file-field-checks.js +21 -0
  42. package/dist/components/file-field/use-file-drop.d.ts +34 -0
  43. package/dist/components/file-field/use-file-drop.js +78 -0
  44. package/dist/components/file-field/use-swap-focus.d.ts +15 -0
  45. package/dist/components/file-field/use-swap-focus.js +40 -0
  46. package/dist/components/list/ListRowContent.d.ts +6 -2
  47. package/dist/components/list/ListRowContent.js +5 -3
  48. package/dist/components/list/ListRowItem.d.ts +10 -0
  49. package/dist/components/list/ListRowItem.js +17 -0
  50. package/dist/components/list/row-naming.d.ts +17 -0
  51. package/dist/components/list/row-naming.js +19 -0
  52. package/dist/components/list/row-slot.d.ts +8 -0
  53. package/dist/components/list/row-slot.js +12 -0
  54. package/dist/components/markdown/markdown-limits.d.ts +14 -0
  55. package/dist/components/markdown/markdown-limits.js +14 -0
  56. package/dist/components/markdown/markdown-model.d.ts +94 -0
  57. package/dist/components/markdown/markdown-model.js +8 -0
  58. package/dist/components/markdown/markdown-options.d.ts +18 -0
  59. package/dist/components/markdown/markdown-options.js +10 -0
  60. package/dist/components/markdown/markdown-rules.d.ts +28 -0
  61. package/dist/components/markdown/markdown-rules.js +81 -0
  62. package/dist/components/markdown/markdown-text.d.ts +17 -0
  63. package/dist/components/markdown/markdown-text.js +86 -0
  64. package/dist/components/markdown/parse-markdown.d.ts +4 -0
  65. package/dist/components/markdown/parse-markdown.js +21 -0
  66. package/dist/components/markdown/read-blocks.d.ts +8 -0
  67. package/dist/components/markdown/read-blocks.js +168 -0
  68. package/dist/components/markdown/read-inlines.d.ts +12 -0
  69. package/dist/components/markdown/read-inlines.js +126 -0
  70. package/dist/components/markdown/read-markdown.d.ts +9 -0
  71. package/dist/components/markdown/read-markdown.js +27 -0
  72. package/dist/components/markdown/reading-context.d.ts +18 -0
  73. package/dist/components/markdown/reading-context.js +35 -0
  74. package/dist/components/markdown/render-markdown.d.ts +10 -0
  75. package/dist/components/markdown/render-markdown.js +52 -0
  76. package/dist/components/use-element-width.d.ts +11 -0
  77. package/dist/components/use-element-width.js +38 -0
  78. package/dist/css/css-article.d.ts +2 -0
  79. package/dist/css/css-article.js +110 -0
  80. package/dist/css/css-components.js +12 -0
  81. package/dist/css/css-count-pill.js +14 -1
  82. package/dist/css/css-document-viewer.d.ts +2 -0
  83. package/dist/css/css-document-viewer.js +186 -0
  84. package/dist/css/css-file-field.d.ts +2 -0
  85. package/dist/css/css-file-field.js +180 -0
  86. package/dist/css/css-list-row-trailing.d.ts +2 -0
  87. package/dist/css/css-list-row-trailing.js +91 -0
  88. package/dist/css/css-list.js +5 -1
  89. package/dist/css/css-markdown.d.ts +2 -0
  90. package/dist/css/css-markdown.js +144 -0
  91. package/dist/css/css-variables.js +1 -0
  92. package/dist/css/stylesheet.js +8 -0
  93. package/dist/document-viewer-worker.d.ts +9 -0
  94. package/dist/document-viewer-worker.js +9 -0
  95. package/dist/index.d.ts +5 -1
  96. package/dist/index.js +5 -1
  97. package/dist/list-options.d.ts +5 -2
  98. package/dist/list-options.js +7 -2
  99. package/dist/styles.css +504 -0
  100. package/package.json +10 -2
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+ import { useEffect, useState } from 'react';
3
+ import { isCancelled } from './pdf-document.js';
4
+ /**
5
+ * Draws the shown page at `width` CSS pixels into the canvas and text
6
+ * layer, again whenever the page or the width changes, cancelling a draw
7
+ * that is no longer wanted. The page is visible while it draws, so it
8
+ * appears as PDF.js paints it. `aspect` is its height over its width, for
9
+ * the page's box, known before its first draw ends.
10
+ */
11
+ export function usePageRender({ document, page, width, canvas, text, onError, }) {
12
+ const [outcome, setOutcome] = useState(null);
13
+ const [shape, setShape] = useState(null);
14
+ useEffect(() => {
15
+ if (!document || !canvas.current || !text.current || width <= 0)
16
+ return;
17
+ const abort = new AbortController();
18
+ document
19
+ .renderPage({
20
+ page,
21
+ width,
22
+ canvas: canvas.current,
23
+ text: text.current,
24
+ signal: abort.signal,
25
+ })
26
+ .then((size) => {
27
+ if (abort.signal.aborted)
28
+ return;
29
+ setOutcome({
30
+ document,
31
+ page,
32
+ width,
33
+ state: 'drawn',
34
+ aspect: size.height / size.width,
35
+ });
36
+ }, (error) => {
37
+ if (abort.signal.aborted || isCancelled(error))
38
+ return;
39
+ setOutcome({ document, page, width, state: 'failed', aspect: null });
40
+ onError(error);
41
+ });
42
+ return () => abort.abort();
43
+ }, [document, page, width, canvas, text, onError]);
44
+ // The box takes the page's shape as soon as it is known.
45
+ useEffect(() => {
46
+ if (!document)
47
+ return;
48
+ let live = true;
49
+ document.pageAspect(page).then((aspect) => {
50
+ if (live)
51
+ setShape({ document, page, aspect });
52
+ }, () => undefined);
53
+ return () => {
54
+ live = false;
55
+ };
56
+ }, [document, page]);
57
+ const current = outcome &&
58
+ outcome.document === document &&
59
+ outcome.page === page &&
60
+ outcome.width === width
61
+ ? outcome
62
+ : null;
63
+ const known = shape && shape.document === document && shape.page === page
64
+ ? shape.aspect
65
+ : null;
66
+ return {
67
+ state: current?.state ?? 'drawing',
68
+ aspect: known ?? current?.aspect ?? null,
69
+ };
70
+ }
@@ -0,0 +1,22 @@
1
+ import { type PdfDocument } from './pdf-document.js';
2
+ export type PdfDocumentState = {
3
+ status: 'loading';
4
+ } | {
5
+ status: 'ready';
6
+ document: PdfDocument;
7
+ } | {
8
+ status: 'failed';
9
+ error: unknown;
10
+ };
11
+ /**
12
+ * Opens the PDF in `data` and closes it (and its worker) when the bytes
13
+ * change or the viewer goes away. New bytes start a new open and show as
14
+ * loading until it settles, so a consumer's Try again is new bytes (or a
15
+ * new `key`).
16
+ */
17
+ export declare function usePdfDocument(data: ArrayBuffer | Uint8Array, workerSrc: string, callbacks: {
18
+ onLoad?: (info: {
19
+ pageCount: number;
20
+ }) => void;
21
+ onError?: (error: unknown) => void;
22
+ }): PdfDocumentState;
@@ -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,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
+ }
@@ -14,9 +14,13 @@ export type ListRowContentProps = {
14
14
  tone: ListRowTone;
15
15
  titleAppearance: ListRowTitleAppearance;
16
16
  accessory: ListRowAccessory;
17
+ /** The unread words; the dot shows while they are given. */
18
+ unreadLabel?: string;
19
+ tag?: ReactNode;
17
20
  };
18
21
  /**
19
22
  * A list row's parts in reading order: the hidden leading mark, the title
20
- * over its detail, the value over its detail, then the chevron or check.
23
+ * (with the unread dot and its hidden words) over its detail, the value
24
+ * over its detail, the tag, then the chevron or check.
21
25
  */
22
- export declare function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, }: ListRowContentProps): import("react").JSX.Element;
26
+ export declare function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, }: ListRowContentProps): import("react").JSX.Element;
@@ -2,10 +2,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { cx } from '../../class-names.js';
3
3
  import { Badge } from '../Badge.js';
4
4
  import { ListRowAccessoryMark } from './ListRowAccessoryMark.js';
5
+ import { hasSlot } from './row-slot.js';
5
6
  /**
6
7
  * A list row's parts in reading order: the hidden leading mark, the title
7
- * over its detail, the value over its detail, then the chevron or check.
8
+ * (with the unread dot and its hidden words) over its detail, the value
9
+ * over its detail, the tag, then the chevron or check.
8
10
  */
9
- export function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, }) {
10
- return (_jsxs(_Fragment, { children: [leading === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsx("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, children: title }), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
11
+ export function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, }) {
12
+ return (_jsxs(_Fragment, { children: [!hasSlot(leading) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsxs("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, children: [title, unreadLabel === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-unread" }))] }), unreadLabel === undefined ? null : (_jsx("span", { className: "sw-sr-only", id: `${id}-unread`, children: unreadLabel })), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), !hasSlot(tag) ? null : (_jsx("span", { className: "sw-list-row-tag", id: `${id}-tag`, children: tag })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
11
13
  }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * A row's list item: an `li` in a ListGroup's list, a `div` elsewhere. A
4
+ * row with an `action` holds that control after the row, a sibling of the
5
+ * row's button or link, so it is never inside the row's press area.
6
+ */
7
+ export declare function ListRowItem({ action, children, }: {
8
+ action: ReactNode;
9
+ children: ReactNode;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useContext } from 'react';
3
+ import { cx } from '../../class-names.js';
4
+ import { ListRowContainer } from './list-row-container.js';
5
+ import { hasSlot } from './row-slot.js';
6
+ /**
7
+ * A row's list item: an `li` in a ListGroup's list, a `div` elsewhere. A
8
+ * row with an `action` holds that control after the row, a sibling of the
9
+ * row's button or link, so it is never inside the row's press area.
10
+ */
11
+ export function ListRowItem({ action, children, }) {
12
+ const container = useContext(ListRowContainer);
13
+ const hasAction = hasSlot(action);
14
+ const content = (_jsxs(_Fragment, { children: [children, hasAction ? _jsx("div", { className: "sw-list-row-action", children: action }) : null] }));
15
+ const className = cx('sw-list-item', hasAction && 'sw-list-item-action');
16
+ return container === 'li' ? (_jsx("li", { className: className, children: content })) : (_jsx("div", { className: className, children: content }));
17
+ }