@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.
Files changed (191) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +274 -7
  3. package/dist/calendar-labels.d.ts +2 -0
  4. package/dist/calendar-labels.js +7 -0
  5. package/dist/components/Article.d.ts +60 -0
  6. package/dist/components/Article.js +42 -0
  7. package/dist/components/BarChart.d.ts +20 -4
  8. package/dist/components/BarChart.js +13 -6
  9. package/dist/components/ButtonGroup.d.ts +8 -1
  10. package/dist/components/ButtonGroup.js +11 -2
  11. package/dist/components/DateField.d.ts +14 -2
  12. package/dist/components/DateField.js +14 -11
  13. package/dist/components/DenominationGrid.d.ts +3 -2
  14. package/dist/components/DenominationGrid.js +21 -3
  15. package/dist/components/Dialog.d.ts +29 -13
  16. package/dist/components/Dialog.js +22 -20
  17. package/dist/components/DocumentViewer.d.ts +41 -0
  18. package/dist/components/DocumentViewer.js +72 -0
  19. package/dist/components/Field.d.ts +13 -2
  20. package/dist/components/Field.js +14 -38
  21. package/dist/components/FileField.d.ts +44 -0
  22. package/dist/components/FileField.js +66 -0
  23. package/dist/components/FilterChips.d.ts +7 -37
  24. package/dist/components/FilterChips.js +32 -24
  25. package/dist/components/ListRow.d.ts +33 -3
  26. package/dist/components/ListRow.js +18 -17
  27. package/dist/components/MarkdownView.d.ts +39 -0
  28. package/dist/components/MarkdownView.js +30 -0
  29. package/dist/components/NavCount.d.ts +15 -2
  30. package/dist/components/NavCount.js +16 -6
  31. package/dist/components/SegmentedControl.d.ts +17 -1
  32. package/dist/components/SegmentedControl.js +11 -3
  33. package/dist/components/Switch.d.ts +16 -1
  34. package/dist/components/Switch.js +17 -6
  35. package/dist/components/action-bar/ActionBarSummary.d.ts +10 -2
  36. package/dist/components/action-bar/ActionBarSummary.js +11 -2
  37. package/dist/components/chart/use-chart-width.js +2 -24
  38. package/dist/components/date-field/CalendarEntryButton.d.ts +24 -0
  39. package/dist/components/date-field/CalendarEntryButton.js +16 -0
  40. package/dist/components/date-field/TypedEntry.d.ts +26 -0
  41. package/dist/components/date-field/TypedEntry.js +13 -0
  42. package/dist/components/denomination-grid/CountCell.d.ts +16 -0
  43. package/dist/components/denomination-grid/CountCell.js +14 -0
  44. package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -1
  45. package/dist/components/denomination-grid/DenominationStrip.js +2 -2
  46. package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
  47. package/dist/components/denomination-grid/EntryCell.js +2 -2
  48. package/dist/components/denomination-grid/StripRow.js +1 -10
  49. package/dist/components/denomination-grid/TransposedStrip.d.ts +17 -0
  50. package/dist/components/denomination-grid/TransposedStrip.js +43 -0
  51. package/dist/components/denomination-grid/types.d.ts +31 -0
  52. package/dist/components/denomination-grid/types.js +7 -1
  53. package/dist/components/denomination-grid/validate-options.d.ts +7 -0
  54. package/dist/components/denomination-grid/validate-options.js +27 -0
  55. package/dist/components/dialog/DialogFooter.d.ts +6 -5
  56. package/dist/components/dialog/DialogFooter.js +6 -6
  57. package/dist/components/dialog/validate.d.ts +24 -0
  58. package/dist/components/dialog/validate.js +23 -0
  59. package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
  60. package/dist/components/document-viewer/document-viewer-labels.js +23 -0
  61. package/dist/components/document-viewer/page-box.d.ts +8 -0
  62. package/dist/components/document-viewer/page-box.js +12 -0
  63. package/dist/components/document-viewer/pdf-document.d.ts +32 -0
  64. package/dist/components/document-viewer/pdf-document.js +66 -0
  65. package/dist/components/document-viewer/pdf-options.d.ts +36 -0
  66. package/dist/components/document-viewer/pdf-options.js +46 -0
  67. package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
  68. package/dist/components/document-viewer/render-pdf-page.js +38 -0
  69. package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
  70. package/dist/components/document-viewer/scroll-chain.js +23 -0
  71. package/dist/components/document-viewer/use-error-report.d.ts +5 -0
  72. package/dist/components/document-viewer/use-error-report.js +13 -0
  73. package/dist/components/document-viewer/use-page-render.d.ts +21 -0
  74. package/dist/components/document-viewer/use-page-render.js +70 -0
  75. package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
  76. package/dist/components/document-viewer/use-pdf-document.js +47 -0
  77. package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
  78. package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
  79. package/dist/components/document-viewer/use-zoom.d.ts +23 -0
  80. package/dist/components/document-viewer/use-zoom.js +70 -0
  81. package/dist/components/document-viewer/zoom.d.ts +29 -0
  82. package/dist/components/document-viewer/zoom.js +37 -0
  83. package/dist/components/field/field-checks.d.ts +34 -0
  84. package/dist/components/field/field-checks.js +42 -0
  85. package/dist/components/file-field/file-field-checks.d.ts +13 -0
  86. package/dist/components/file-field/file-field-checks.js +21 -0
  87. package/dist/components/file-field/use-file-drop.d.ts +34 -0
  88. package/dist/components/file-field/use-file-drop.js +78 -0
  89. package/dist/components/file-field/use-swap-focus.d.ts +15 -0
  90. package/dist/components/file-field/use-swap-focus.js +40 -0
  91. package/dist/components/filter-chips/FilterChip.d.ts +16 -0
  92. package/dist/components/filter-chips/FilterChip.js +20 -0
  93. package/dist/components/filter-chips/options.d.ts +75 -0
  94. package/dist/components/filter-chips/options.js +11 -0
  95. package/dist/components/filter-chips/validate.d.ts +9 -0
  96. package/dist/components/filter-chips/validate.js +48 -0
  97. package/dist/components/list/ListRowContent.d.ts +8 -2
  98. package/dist/components/list/ListRowContent.js +6 -3
  99. package/dist/components/list/ListRowItem.d.ts +10 -0
  100. package/dist/components/list/ListRowItem.js +17 -0
  101. package/dist/components/list/row-naming.d.ts +17 -0
  102. package/dist/components/list/row-naming.js +19 -0
  103. package/dist/components/list/row-slot.d.ts +8 -0
  104. package/dist/components/list/row-slot.js +12 -0
  105. package/dist/components/markdown/markdown-limits.d.ts +14 -0
  106. package/dist/components/markdown/markdown-limits.js +14 -0
  107. package/dist/components/markdown/markdown-model.d.ts +94 -0
  108. package/dist/components/markdown/markdown-model.js +8 -0
  109. package/dist/components/markdown/markdown-options.d.ts +18 -0
  110. package/dist/components/markdown/markdown-options.js +10 -0
  111. package/dist/components/markdown/markdown-rules.d.ts +28 -0
  112. package/dist/components/markdown/markdown-rules.js +81 -0
  113. package/dist/components/markdown/markdown-text.d.ts +17 -0
  114. package/dist/components/markdown/markdown-text.js +86 -0
  115. package/dist/components/markdown/parse-markdown.d.ts +4 -0
  116. package/dist/components/markdown/parse-markdown.js +21 -0
  117. package/dist/components/markdown/read-blocks.d.ts +8 -0
  118. package/dist/components/markdown/read-blocks.js +168 -0
  119. package/dist/components/markdown/read-inlines.d.ts +12 -0
  120. package/dist/components/markdown/read-inlines.js +126 -0
  121. package/dist/components/markdown/read-markdown.d.ts +9 -0
  122. package/dist/components/markdown/read-markdown.js +27 -0
  123. package/dist/components/markdown/reading-context.d.ts +18 -0
  124. package/dist/components/markdown/reading-context.js +35 -0
  125. package/dist/components/markdown/render-markdown.d.ts +10 -0
  126. package/dist/components/markdown/render-markdown.js +52 -0
  127. package/dist/components/use-element-width.d.ts +11 -0
  128. package/dist/components/use-element-width.js +38 -0
  129. package/dist/css/chart-tones.d.ts +2 -0
  130. package/dist/css/chart-tones.js +5 -1
  131. package/dist/css/css-article.d.ts +2 -0
  132. package/dist/css/css-article.js +110 -0
  133. package/dist/css/css-bar-chart.d.ts +17 -2
  134. package/dist/css/css-bar-chart.js +38 -7
  135. package/dist/css/css-button-group-attached.d.ts +3 -0
  136. package/dist/css/css-button-group-attached.js +18 -1
  137. package/dist/css/css-canvas-grouped.d.ts +17 -0
  138. package/dist/css/css-canvas-grouped.js +40 -0
  139. package/dist/css/css-components.js +18 -0
  140. package/dist/css/css-count-pill.js +14 -1
  141. package/dist/css/css-date-field-calendar-entry.d.ts +11 -0
  142. package/dist/css/css-date-field-calendar-entry.js +50 -0
  143. package/dist/css/css-date-field.js +4 -0
  144. package/dist/css/css-denomination-grid.js +4 -0
  145. package/dist/css/css-denomination-transposed.d.ts +4 -0
  146. package/dist/css/css-denomination-transposed.js +178 -0
  147. package/dist/css/css-dialog-canvas.d.ts +3 -0
  148. package/dist/css/css-dialog-canvas.js +20 -0
  149. package/dist/css/css-dialog-frame.js +19 -2
  150. package/dist/css/css-dialog-grabber.d.ts +4 -0
  151. package/dist/css/css-dialog-grabber.js +56 -0
  152. package/dist/css/css-dialog.js +3 -2
  153. package/dist/css/css-document-viewer.d.ts +2 -0
  154. package/dist/css/css-document-viewer.js +186 -0
  155. package/dist/css/css-document.js +3 -0
  156. package/dist/css/css-field-width.d.ts +12 -0
  157. package/dist/css/css-field-width.js +49 -0
  158. package/dist/css/css-field.js +4 -0
  159. package/dist/css/css-file-field.d.ts +2 -0
  160. package/dist/css/css-file-field.js +180 -0
  161. package/dist/css/css-filter-chips-layout.d.ts +2 -0
  162. package/dist/css/css-filter-chips-layout.js +42 -0
  163. package/dist/css/css-filter-chips.js +4 -0
  164. package/dist/css/css-list-row-trailing.d.ts +2 -0
  165. package/dist/css/css-list-row-trailing.js +91 -0
  166. package/dist/css/css-list-row.d.ts +6 -0
  167. package/dist/css/css-list-row.js +19 -3
  168. package/dist/css/css-list.js +5 -1
  169. package/dist/css/css-markdown.d.ts +2 -0
  170. package/dist/css/css-markdown.js +144 -0
  171. package/dist/css/css-segmented-sizing.d.ts +13 -0
  172. package/dist/css/css-segmented-sizing.js +66 -0
  173. package/dist/css/css-segmented.js +4 -0
  174. package/dist/css/css-switch.d.ts +6 -0
  175. package/dist/css/css-switch.js +39 -0
  176. package/dist/css/css-variables.js +1 -0
  177. package/dist/css/stacking.d.ts +1 -0
  178. package/dist/css/stacking.js +4 -1
  179. package/dist/css/stylesheet.js +12 -0
  180. package/dist/date-field-labels.d.ts +5 -0
  181. package/dist/date-field-labels.js +1 -0
  182. package/dist/document-viewer-worker.d.ts +9 -0
  183. package/dist/document-viewer-worker.js +9 -0
  184. package/dist/index.d.ts +15 -9
  185. package/dist/index.js +13 -7
  186. package/dist/list-options.d.ts +11 -7
  187. package/dist/list-options.js +20 -8
  188. package/dist/styles.css +889 -44
  189. package/dist/theme/ThemeProvider.d.ts +11 -1
  190. package/dist/theme/ThemeProvider.js +4 -2
  191. 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
+ }