@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
@@ -1,22 +1,20 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useContext, useId } from 'react';
3
+ import { useId } from 'react';
4
4
  import { cx } from '../class-names.js';
5
5
  import { assertListRow, listRowAccessory, } from '../list-options.js';
6
6
  import { tabStop } from '../tab-stop.js';
7
7
  import { plainPress } from './link-press.js';
8
8
  import { ListRowContent } from './list/ListRowContent.js';
9
- import { ListRowContainer } from './list/list-row-container.js';
10
- function ids(...parts) {
11
- const joined = parts.filter(Boolean).join(' ');
12
- return joined === '' ? undefined : joined;
13
- }
9
+ import { ListRowItem } from './list/ListRowItem.js';
10
+ import { rowNaming } from './list/row-naming.js';
14
11
  /**
15
12
  * One row of a `ListGroup`: it opens something (`onPress` or `href`, with
16
13
  * a chevron, the whole row pressing), picks one option in place
17
- * (`selected`), or only states something (neither).
14
+ * (`selected`), or only states something (neither). `unread` marks it new,
15
+ * `tag` adds a status before its end, and `action` a control beside it.
18
16
  */
19
- export function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', 'aria-label': ariaLabel, }) {
17
+ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled = false, tone = 'default', titleAppearance = 'strong', accessory = 'chevron', unread = false, unreadLabel, tag, action, 'aria-label': ariaLabel, }) {
20
18
  const pressable = onPress !== undefined || href !== undefined;
21
19
  assertListRow({
22
20
  title,
@@ -29,19 +27,21 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
29
27
  valueBadge,
30
28
  selected,
31
29
  pressable,
30
+ unread,
31
+ unreadLabel,
32
32
  });
33
- const container = useContext(ListRowContainer);
34
33
  const id = useId();
35
34
  const kind = listRowAccessory({ pressable, selected, accessory });
36
- const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, title: title, titleAppearance: titleAppearance, tone: tone, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
35
+ const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, tag: tag, title: title, titleAppearance: titleAppearance, tone: tone, unreadLabel: unread ? unreadLabel : undefined, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
37
36
  const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled');
38
- const naming = {
37
+ const naming = rowNaming(id, {
39
38
  'aria-label': ariaLabel,
40
- 'aria-labelledby': ariaLabel
41
- ? undefined
42
- : ids(`${id}-title`, value !== undefined && `${id}-value`),
43
- 'aria-describedby': ids(Boolean(detail) && `${id}-detail`, Boolean(valueDetail) && `${id}-value-detail`),
44
- };
39
+ value,
40
+ detail,
41
+ valueDetail,
42
+ unread,
43
+ tag,
44
+ });
45
45
  let row;
46
46
  if (href !== undefined)
47
47
  row = (_jsx("a", { ...naming, "aria-current": selected ? 'true' : undefined, "aria-disabled": disabled || undefined, className: className, href: disabled ? undefined : href, onClick: onPress && !disabled
@@ -56,5 +56,5 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
56
56
  row = (_jsx("button", { ...naming, "aria-pressed": selected, className: className, disabled: disabled, onClick: onPress, tabIndex: tabStop, type: "button", children: content }));
57
57
  else
58
58
  row = (_jsx("div", { "aria-label": ariaLabel, className: className, role: ariaLabel ? 'group' : undefined, children: content }));
59
- return container === 'li' ? (_jsx("li", { className: "sw-list-item", children: row })) : (_jsx("div", { className: "sw-list-item", children: row }));
59
+ return _jsx(ListRowItem, { action: action, children: row });
60
60
  }
@@ -0,0 +1,39 @@
1
+ import { type MarkdownHeading, type MarkdownOmission, type MarkdownOmissionCounts } from './markdown/markdown-model.js';
2
+ import { type MarkdownOptions } from './markdown/markdown-options.js';
3
+ export type { MarkdownHeading, MarkdownImage, MarkdownImageResolver, MarkdownOmission, MarkdownOmissionCounts, MarkdownOmissionKind, } from './markdown/markdown-model.js';
4
+ export { markdownOmissionKinds } from './markdown/markdown-model.js';
5
+ export type { MarkdownOptions } from './markdown/markdown-options.js';
6
+ export type MarkdownViewProps = MarkdownOptions & {
7
+ /** The Markdown, as written or pasted. */
8
+ source: string;
9
+ /**
10
+ * Hidden words after every link, which opens in a new tab, such as
11
+ * "(opens in a new tab)".
12
+ */
13
+ newTabLabel: string;
14
+ };
15
+ /** What `inspectMarkdown` finds. */
16
+ export type MarkdownInspection = {
17
+ /** The headings, with the ids the view gives them. */
18
+ headings: MarkdownHeading[];
19
+ /** The first 100 things left out, in order. */
20
+ omissions: MarkdownOmission[];
21
+ /** How many of each kind were left out, past the first 100 too. */
22
+ omissionCounts: MarkdownOmissionCounts;
23
+ };
24
+ /**
25
+ * What a source's view shows in its headings (for an "On this page" list,
26
+ * with the ids the view gives them) and what it leaves out (for an editor
27
+ * to say so before saving). Pure; pass the same options as the view, which
28
+ * it refuses the same way.
29
+ */
30
+ export declare function inspectMarkdown(source: string, options?: MarkdownOptions): MarkdownInspection;
31
+ /**
32
+ * Markdown drawn in Scalewing's type as React elements, never as HTML:
33
+ * paragraphs, headings (`#` drawn at the page's second level, an `h2`),
34
+ * lists, bold, italic, links (http, https and mailto only, in a new tab),
35
+ * block quotes as a highlighted box, line breaks and `photo:` images with
36
+ * their captions. Raw HTML, tables, other images and other link schemes
37
+ * are left out (a link's words stay); `inspectMarkdown` lists them.
38
+ */
39
+ export declare function MarkdownView(props: MarkdownViewProps): import("react").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { readMarkdown } from './markdown/read-markdown.js';
4
+ import { renderBlocks } from './markdown/render-markdown.js';
5
+ export { markdownOmissionKinds } from './markdown/markdown-model.js';
6
+ /**
7
+ * What a source's view shows in its headings (for an "On this page" list,
8
+ * with the ids the view gives them) and what it leaves out (for an editor
9
+ * to say so before saving). Pure; pass the same options as the view, which
10
+ * it refuses the same way.
11
+ */
12
+ export function inspectMarkdown(source, options = {}) {
13
+ const { headings, omissions, omissionCounts } = readMarkdown(source, options, 'inspectMarkdown');
14
+ return { headings, omissions, omissionCounts };
15
+ }
16
+ /**
17
+ * Markdown drawn in Scalewing's type as React elements, never as HTML:
18
+ * paragraphs, headings (`#` drawn at the page's second level, an `h2`),
19
+ * lists, bold, italic, links (http, https and mailto only, in a new tab),
20
+ * block quotes as a highlighted box, line breaks and `photo:` images with
21
+ * their captions. Raw HTML, tables, other images and other link schemes
22
+ * are left out (a link's words stay); `inspectMarkdown` lists them.
23
+ */
24
+ export function MarkdownView(props) {
25
+ const { source, resolveImage, idPrefix, newTabLabel } = props;
26
+ if (typeof newTabLabel !== 'string' || newTabLabel.trim() === '')
27
+ throw new RangeError('MarkdownView newTabLabel must be non-empty text');
28
+ const reading = useMemo(() => readMarkdown(source, { resolveImage, idPrefix }, 'MarkdownView'), [source, resolveImage, idPrefix]);
29
+ return (_jsx("div", { className: "sw-markdown", children: renderBlocks(reading.blocks, { newTabLabel }) }));
30
+ }
@@ -1,3 +1,6 @@
1
+ /** The pill's colours: `danger` for work waiting, `accent` for news. */
2
+ export declare const navCountTones: readonly ["danger", "accent"];
3
+ export type NavCountTone = (typeof navCountTones)[number];
1
4
  export type NavCountProps = {
2
5
  /**
3
6
  * How many things wait behind the destination, such as tasks to do: a
@@ -11,11 +14,21 @@ export type NavCountProps = {
11
14
  * pill then hidden from assistive technology.
12
15
  */
13
16
  countLabel?: string;
17
+ /**
18
+ * The pill's colour. `danger` (the default) is work waiting there, such
19
+ * as tasks to do; `accent` is news, such as what was added since the
20
+ * person last looked, where nothing is overdue. Give an accent count a
21
+ * `countLabel` ("3 new"), so its words, not its colour, say it is news.
22
+ * Place either tone on a neutral or ghost destination: a filled one of
23
+ * the same colour would hide it.
24
+ */
25
+ tone?: NavCountTone;
14
26
  };
15
27
  /**
16
28
  * A count after a Nav destination's words, placed inside its link or
17
29
  * Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
18
30
  * pill SectionNav's item count draws, and it is part of the destination's
19
- * accessible name, as there.
31
+ * accessible name, as there. `tone="accent"` draws it in the accent colour,
32
+ * for news rather than work.
20
33
  */
21
- export declare function NavCount({ count, countLabel }: NavCountProps): import("react").JSX.Element | null;
34
+ export declare function NavCount({ count, countLabel, tone, }: NavCountProps): import("react").JSX.Element | null;
@@ -1,23 +1,33 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- /** Fails closed on a count that is not a whole number or an empty label. */
3
- function assertNavCount({ count, countLabel }) {
2
+ import { cx } from '../class-names.js';
3
+ import { isOneOf } from '../is-one-of.js';
4
+ /** The pill's colours: `danger` for work waiting, `accent` for news. */
5
+ export const navCountTones = ['danger', 'accent'];
6
+ /**
7
+ * Fails closed on a count that is not a whole number, an empty label or an
8
+ * unknown tone.
9
+ */
10
+ function assertNavCount({ count, countLabel, tone }) {
4
11
  if (!Number.isInteger(count) || count < 0)
5
12
  throw new RangeError(`NavCount count must be a whole number from 0: ${String(count)}`);
6
13
  if (countLabel !== undefined &&
7
14
  (typeof countLabel !== 'string' || !countLabel.trim()))
8
15
  throw new RangeError('NavCount countLabel must be non-empty text');
16
+ if (tone !== undefined && !isOneOf(navCountTones, tone))
17
+ throw new RangeError(`Unknown NavCount tone: ${String(tone)}`);
9
18
  }
10
19
  /**
11
20
  * A count after a Nav destination's words, placed inside its link or
12
21
  * Button: `<a href="/tasks">Tasks<NavCount count={8} /></a>`. It is the
13
22
  * pill SectionNav's item count draws, and it is part of the destination's
14
- * accessible name, as there.
23
+ * accessible name, as there. `tone="accent"` draws it in the accent colour,
24
+ * for news rather than work.
15
25
  */
16
- export function NavCount({ count, countLabel }) {
17
- assertNavCount({ count, countLabel });
26
+ export function NavCount({ count, countLabel, tone = 'danger', }) {
27
+ assertNavCount({ count, countLabel, tone });
18
28
  if (count === 0)
19
29
  return null;
20
30
  // The space is its own text node, so every browser and the name's
21
31
  // computation keep it between the words and the count.
22
- return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: "sw-nav-count", children: count })] }));
32
+ return (_jsxs(_Fragment, { children: [' ', countLabel ? _jsx("span", { className: "sw-sr-only", children: countLabel }) : null, _jsx("span", { "aria-hidden": countLabel ? true : undefined, className: cx('sw-nav-count', tone === 'accent' && 'sw-nav-count-accent'), children: count })] }));
23
33
  }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
- import { useLayoutEffect, useState } from 'react';
3
2
  import { chartFallbackWidth } from '../../chart-metrics.js';
3
+ import { useElementWidth } from '../use-element-width.js';
4
4
  /**
5
5
  * Follows the width a chart is given, so its drawing is laid out in its
6
6
  * own pixels: on mount, before the first paint, and whenever its box
@@ -8,27 +8,5 @@ import { chartFallbackWidth } from '../../chart-metrics.js';
8
8
  * no ResizeObserver measures on window resizes instead.
9
9
  */
10
10
  export function useChartWidth(ref) {
11
- const [width, setWidth] = useState(chartFallbackWidth);
12
- // Before paint, so a phone never shows the wide layout first.
13
- useLayoutEffect(() => {
14
- const box = ref.current;
15
- if (!box)
16
- return;
17
- function measure() {
18
- if (!box)
19
- return;
20
- const next = Math.round(box.getBoundingClientRect().width);
21
- if (next > 0)
22
- setWidth(next);
23
- }
24
- measure();
25
- if (typeof ResizeObserver === 'undefined') {
26
- window.addEventListener('resize', measure);
27
- return () => window.removeEventListener('resize', measure);
28
- }
29
- const observer = new ResizeObserver(measure);
30
- observer.observe(box);
31
- return () => observer.disconnect();
32
- }, [ref]);
33
- return width;
11
+ return useElementWidth(ref, chartFallbackWidth);
34
12
  }
@@ -0,0 +1,23 @@
1
+ /** The viewer's words, all in the consumer's language. */
2
+ export type DocumentViewerLabels = {
3
+ /** Names the step to the page before, such as "Previous page". */
4
+ previous: string;
5
+ /** Names the step to the page after, such as "Next page". */
6
+ next: string;
7
+ /** Where the reader is, such as `(page, count) => \`Page ${page} of ${count}\``. */
8
+ page: (page: number, pageCount: number) => string;
9
+ /** While the PDF opens, such as "Opening the PDF…". */
10
+ loading: string;
11
+ /** When it cannot be shown, such as "This PDF couldn't be shown". */
12
+ failed: string;
13
+ /** A line under `failed`, such as what the reader can do instead. */
14
+ failedDetail?: string;
15
+ };
16
+ /** Fails closed on missing words: the viewer has no words of its own. */
17
+ export declare function assertDocumentViewer({ label, labels, workerSrc, }: {
18
+ label: string;
19
+ labels: DocumentViewerLabels;
20
+ workerSrc: string;
21
+ }): void;
22
+ /** The page shown: the one asked for, kept between 1 and the last page. */
23
+ export declare function shownPage(page: number, pageCount: number): number;
@@ -0,0 +1,23 @@
1
+ function isBlank(text) {
2
+ return typeof text !== 'string' || text.trim() === '';
3
+ }
4
+ /** Fails closed on missing words: the viewer has no words of its own. */
5
+ export function assertDocumentViewer({ label, labels, workerSrc, }) {
6
+ if (isBlank(label))
7
+ throw new RangeError('DocumentViewer label must be non-empty text');
8
+ if (isBlank(workerSrc))
9
+ throw new RangeError('DocumentViewer workerSrc must be a URL');
10
+ for (const key of ['previous', 'next', 'loading', 'failed'])
11
+ if (isBlank(labels?.[key]))
12
+ throw new RangeError(`DocumentViewer labels.${key} must be non-empty text`);
13
+ if (typeof labels.page !== 'function')
14
+ throw new RangeError('DocumentViewer labels.page must be a function');
15
+ if (labels.failedDetail !== undefined && isBlank(labels.failedDetail))
16
+ throw new RangeError('DocumentViewer labels.failedDetail must be non-empty text');
17
+ }
18
+ /** The page shown: the one asked for, kept between 1 and the last page. */
19
+ export function shownPage(page, pageCount) {
20
+ if (!Number.isInteger(page))
21
+ return 1;
22
+ return Math.min(Math.max(page, 1), Math.max(pageCount, 1));
23
+ }
@@ -0,0 +1,8 @@
1
+ import { type CSSProperties } from 'react';
2
+ /** A US Letter page's height over its width, the box's shape until known. */
3
+ export declare const letterAspect: number;
4
+ /**
5
+ * The page box's size, read by the generated `sw-document-viewer-page`
6
+ * rule: its width in CSS pixels and its height over its width.
7
+ */
8
+ export declare function pageBoxStyle(width: number, aspect: number | null): CSSProperties;
@@ -0,0 +1,12 @@
1
+ /** A US Letter page's height over its width, the box's shape until known. */
2
+ export const letterAspect = 11 / 8.5;
3
+ /**
4
+ * The page box's size, read by the generated `sw-document-viewer-page`
5
+ * rule: its width in CSS pixels and its height over its width.
6
+ */
7
+ export function pageBoxStyle(width, aspect) {
8
+ return {
9
+ '--sw-document-page-width': `${Math.max(0, Math.round(width))}px`,
10
+ '--sw-document-page-aspect': String(aspect ?? letterAspect),
11
+ };
12
+ }
@@ -0,0 +1,32 @@
1
+ import { type PdfPageSize } from './render-pdf-page.js';
2
+ export type { PdfPageSize };
3
+ /** An open PDF, as DocumentViewer uses it. */
4
+ export type PdfDocument = {
5
+ pageCount: number;
6
+ /**
7
+ * Draws page `page` (from 1) `width` CSS pixels wide into `canvas`, and
8
+ * its text into `text` (transparent, for reading and selecting), and
9
+ * resolves with its size. `signal` cancels it.
10
+ */
11
+ renderPage: (options: {
12
+ page: number;
13
+ width: number;
14
+ canvas: HTMLCanvasElement;
15
+ text: HTMLElement;
16
+ signal: AbortSignal;
17
+ }) => Promise<PdfPageSize>;
18
+ /** The aspect (height over width) of page `page`, without drawing it. */
19
+ pageAspect: (page: number) => Promise<number>;
20
+ destroy: () => Promise<void>;
21
+ };
22
+ /** Whether an error is PDF.js reporting a cancelled render or text layer. */
23
+ export declare function isCancelled(error: unknown): boolean;
24
+ /**
25
+ * Opens a PDF from its bytes with PDF.js's legacy build (the modern one
26
+ * needs browsers newer than the ones stores run, such as for
27
+ * `Map.prototype.getOrInsertComputed`), loaded on first use (so a page
28
+ * without a viewer never downloads it), in a module worker started from
29
+ * `workerSrc` and ended with the document. Rejects when the bytes are not
30
+ * a PDF PDF.js can open.
31
+ */
32
+ export declare function openPdf(data: ArrayBuffer | Uint8Array, workerSrc: string): Promise<PdfDocument>;
@@ -0,0 +1,66 @@
1
+ import { copyPdfBytes, hardenedPdfOptions } from './pdf-options.js';
2
+ import { renderPdfPage } from './render-pdf-page.js';
3
+ /** Whether an error is PDF.js reporting a cancelled render or text layer. */
4
+ export function isCancelled(error) {
5
+ return (error instanceof Error &&
6
+ (error.name === 'RenderingCancelledException' ||
7
+ error.name === 'AbortException'));
8
+ }
9
+ /**
10
+ * Opens a PDF from its bytes with PDF.js's legacy build (the modern one
11
+ * needs browsers newer than the ones stores run, such as for
12
+ * `Map.prototype.getOrInsertComputed`), loaded on first use (so a page
13
+ * without a viewer never downloads it), in a module worker started from
14
+ * `workerSrc` and ended with the document. Rejects when the bytes are not
15
+ * a PDF PDF.js can open.
16
+ */
17
+ export async function openPdf(data, workerSrc) {
18
+ const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
19
+ const port = new Worker(workerSrc, { type: 'module' });
20
+ const worker = pdfjs.PDFWorker.create({
21
+ port,
22
+ verbosity: hardenedPdfOptions.verbosity,
23
+ });
24
+ const task = pdfjs.getDocument({
25
+ ...hardenedPdfOptions,
26
+ data: copyPdfBytes(data),
27
+ worker,
28
+ });
29
+ // The worker always ends, even when PDF.js fails to close the document.
30
+ async function destroy() {
31
+ try {
32
+ await task.destroy();
33
+ }
34
+ finally {
35
+ worker.destroy();
36
+ port.terminate();
37
+ }
38
+ }
39
+ let document;
40
+ try {
41
+ document = await task.promise;
42
+ }
43
+ catch (error) {
44
+ // The open error is the one reported, never a failure to close.
45
+ await destroy().catch(() => undefined);
46
+ throw error;
47
+ }
48
+ const loaded = document;
49
+ return {
50
+ pageCount: loaded.numPages,
51
+ destroy,
52
+ async pageAspect(page) {
53
+ const { width, height } = (await loaded.getPage(page)).getViewport({
54
+ scale: 1,
55
+ });
56
+ return height / width;
57
+ },
58
+ async renderPage({ page, ...target }) {
59
+ return renderPdfPage({
60
+ page: await loaded.getPage(page),
61
+ ...target,
62
+ createTextLayer: (options) => new pdfjs.TextLayer(options),
63
+ });
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The options every document opens with: nothing is fetched beyond the
3
+ * bytes given, no font, character map, colour profile or WebAssembly file
4
+ * (PDF.js falls back to system fonts and skips what it cannot decode), no
5
+ * XFA forms, no streaming or range requests, and no code evaluation.
6
+ *
7
+ * `isEvalSupported` is absent from PDF.js 6.4.299, whose bundles hold no
8
+ * `eval` or `new Function`; it stays here, set to `false`, so the
9
+ * hardening reads the same as the Teisoro API's and survives a downgrade.
10
+ */
11
+ export declare const hardenedPdfOptions: {
12
+ readonly isEvalSupported: false;
13
+ readonly useWorkerFetch: false;
14
+ readonly useWasm: false;
15
+ readonly useSystemFonts: true;
16
+ readonly disableFontFace: false;
17
+ readonly enableXfa: false;
18
+ readonly disableRange: true;
19
+ readonly disableStream: true;
20
+ readonly disableAutoFetch: true;
21
+ readonly stopAtErrors: false;
22
+ readonly verbosity: 0;
23
+ };
24
+ /** The largest canvas drawn, in device pixels: within every browser's limit. */
25
+ export declare const maxCanvasPixels = 16000000;
26
+ /**
27
+ * The device-pixel ratio a page of `width` by `height` CSS pixels is drawn
28
+ * at: the screen's, lowered so the canvas stays under `maxCanvasPixels`.
29
+ */
30
+ export declare function canvasRatio(width: number, height: number, deviceRatio: number): number;
31
+ /**
32
+ * A private copy of the bytes: PDF.js hands its data to the worker, which
33
+ * detaches the buffer, so the consumer's own bytes (kept for a download)
34
+ * are never touched.
35
+ */
36
+ export declare function copyPdfBytes(data: ArrayBuffer | Uint8Array): Uint8Array;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The options every document opens with: nothing is fetched beyond the
3
+ * bytes given, no font, character map, colour profile or WebAssembly file
4
+ * (PDF.js falls back to system fonts and skips what it cannot decode), no
5
+ * XFA forms, no streaming or range requests, and no code evaluation.
6
+ *
7
+ * `isEvalSupported` is absent from PDF.js 6.4.299, whose bundles hold no
8
+ * `eval` or `new Function`; it stays here, set to `false`, so the
9
+ * hardening reads the same as the Teisoro API's and survives a downgrade.
10
+ */
11
+ export const hardenedPdfOptions = {
12
+ isEvalSupported: false,
13
+ useWorkerFetch: false,
14
+ useWasm: false,
15
+ useSystemFonts: true,
16
+ disableFontFace: false,
17
+ enableXfa: false,
18
+ disableRange: true,
19
+ disableStream: true,
20
+ disableAutoFetch: true,
21
+ stopAtErrors: false,
22
+ verbosity: 0,
23
+ };
24
+ /** The largest canvas drawn, in device pixels: within every browser's limit. */
25
+ export const maxCanvasPixels = 16_000_000;
26
+ /**
27
+ * The device-pixel ratio a page of `width` by `height` CSS pixels is drawn
28
+ * at: the screen's, lowered so the canvas stays under `maxCanvasPixels`.
29
+ */
30
+ export function canvasRatio(width, height, deviceRatio) {
31
+ const ratio = deviceRatio > 0 ? deviceRatio : 1;
32
+ const area = width * height * ratio * ratio;
33
+ if (area <= maxCanvasPixels)
34
+ return ratio;
35
+ return Math.sqrt(maxCanvasPixels / (width * height));
36
+ }
37
+ /**
38
+ * A private copy of the bytes: PDF.js hands its data to the worker, which
39
+ * detaches the buffer, so the consumer's own bytes (kept for a download)
40
+ * are never touched.
41
+ */
42
+ export function copyPdfBytes(data) {
43
+ return data instanceof Uint8Array
44
+ ? data.slice()
45
+ : new Uint8Array(data.slice(0));
46
+ }
@@ -0,0 +1,20 @@
1
+ import { type PDFPageProxy, type TextLayer } from 'pdfjs-dist';
2
+ /** A page drawn: its CSS size at the width asked for. */
3
+ export type PdfPageSize = {
4
+ width: number;
5
+ height: number;
6
+ };
7
+ /**
8
+ * Draws one page `width` CSS pixels wide into `canvas`, at the screen's
9
+ * pixel ratio (capped), and its text into `text` (PDF.js's transparent
10
+ * text layer, at the scale the generated rules read). `signal` cancels
11
+ * both.
12
+ */
13
+ export declare function renderPdfPage({ page, width, canvas, text, signal, createTextLayer, }: {
14
+ page: PDFPageProxy;
15
+ width: number;
16
+ canvas: HTMLCanvasElement;
17
+ text: HTMLElement;
18
+ signal: AbortSignal;
19
+ createTextLayer: (options: ConstructorParameters<typeof TextLayer>[0]) => TextLayer;
20
+ }): Promise<PdfPageSize>;
@@ -0,0 +1,38 @@
1
+ import { canvasRatio } from './pdf-options.js';
2
+ /**
3
+ * Draws one page `width` CSS pixels wide into `canvas`, at the screen's
4
+ * pixel ratio (capped), and its text into `text` (PDF.js's transparent
5
+ * text layer, at the scale the generated rules read). `signal` cancels
6
+ * both.
7
+ */
8
+ export async function renderPdfPage({ page, width, canvas, text, signal, createTextLayer, }) {
9
+ const base = page.getViewport({ scale: 1 });
10
+ const viewport = page.getViewport({ scale: width / base.width });
11
+ const ratio = canvasRatio(viewport.width, viewport.height, window.devicePixelRatio);
12
+ canvas.width = Math.floor(viewport.width * ratio);
13
+ canvas.height = Math.floor(viewport.height * ratio);
14
+ const render = page.render({
15
+ canvas,
16
+ viewport,
17
+ transform: ratio === 1 ? undefined : [ratio, 0, 0, ratio, 0, 0],
18
+ });
19
+ text.replaceChildren();
20
+ text.style.setProperty('--total-scale-factor', String(viewport.scale));
21
+ const layer = createTextLayer({
22
+ textContentSource: page.streamTextContent(),
23
+ container: text,
24
+ viewport,
25
+ });
26
+ const cancel = () => {
27
+ render.cancel();
28
+ layer.cancel();
29
+ };
30
+ signal.addEventListener('abort', cancel, { once: true });
31
+ try {
32
+ await Promise.all([render.promise, layer.render()]);
33
+ }
34
+ finally {
35
+ signal.removeEventListener('abort', cancel);
36
+ }
37
+ return { width: viewport.width, height: viewport.height };
38
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Scrolls by `amount` pixels along one axis as a finger would: the
3
+ * element first, then each box around it that scrolls, up to the
4
+ * document, each taking what it can until the amount is spent.
5
+ */
6
+ export declare function scrollChainBy(start: Element, axis: 'x' | 'y', amount: number): void;
@@ -0,0 +1,23 @@
1
+ const scrolling = /^(auto|scroll|overlay)$/;
2
+ function scrolls(element, axis) {
3
+ if (element === element.ownerDocument.scrollingElement)
4
+ return true;
5
+ const style = getComputedStyle(element);
6
+ return scrolling.test(axis === 'x' ? style.overflowX : style.overflowY);
7
+ }
8
+ /**
9
+ * Scrolls by `amount` pixels along one axis as a finger would: the
10
+ * element first, then each box around it that scrolls, up to the
11
+ * document, each taking what it can until the amount is spent.
12
+ */
13
+ export function scrollChainBy(start, axis, amount) {
14
+ let left = amount;
15
+ for (let element = start; element && Math.abs(left) >= 0.5; element = element.parentElement) {
16
+ if (!scrolls(element, axis))
17
+ continue;
18
+ const key = axis === 'x' ? 'scrollLeft' : 'scrollTop';
19
+ const was = element[key];
20
+ element[key] = was + left;
21
+ left -= element[key] - was;
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * A stable function that reports an error to the consumer's latest
3
+ * `onError`, so a new callback each render never restarts a page's draw.
4
+ */
5
+ export declare function useErrorReport(onError: ((error: unknown) => void) | undefined): (error: unknown) => void;
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ /**
4
+ * A stable function that reports an error to the consumer's latest
5
+ * `onError`, so a new callback each render never restarts a page's draw.
6
+ */
7
+ export function useErrorReport(onError) {
8
+ const latest = useRef(onError);
9
+ useEffect(() => {
10
+ latest.current = onError;
11
+ });
12
+ return useCallback((error) => latest.current?.(error), []);
13
+ }
@@ -0,0 +1,21 @@
1
+ import { type RefObject } from 'react';
2
+ import { type PdfDocument } from './pdf-document.js';
3
+ export type PageRenderState = 'drawing' | 'drawn' | 'failed';
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 declare function usePageRender({ document, page, width, canvas, text, onError, }: {
12
+ document: PdfDocument | null;
13
+ page: number;
14
+ width: number;
15
+ canvas: RefObject<HTMLCanvasElement | null>;
16
+ text: RefObject<HTMLDivElement | null>;
17
+ onError: (error: unknown) => void;
18
+ }): {
19
+ state: PageRenderState;
20
+ aspect: number | null;
21
+ };