@scalewing/react 1.41.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +180 -1
- package/dist/components/ActionMenu.d.ts +4 -2
- package/dist/components/ActionMenu.js +4 -8
- package/dist/components/Article.d.ts +60 -0
- package/dist/components/Article.js +42 -0
- package/dist/components/DescriptionList.d.ts +12 -2
- package/dist/components/DescriptionList.js +7 -3
- package/dist/components/DocumentViewer.d.ts +41 -0
- package/dist/components/DocumentViewer.js +72 -0
- package/dist/components/FileField.d.ts +55 -0
- package/dist/components/FileField.js +75 -0
- package/dist/components/FilterChips.d.ts +1 -1
- package/dist/components/FilterChips.js +1 -1
- package/dist/components/FilterChipsRow.d.ts +24 -0
- package/dist/components/FilterChipsRow.js +14 -0
- package/dist/components/ListRow.d.ts +31 -2
- package/dist/components/ListRow.js +19 -18
- package/dist/components/MarkdownView.d.ts +39 -0
- package/dist/components/MarkdownView.js +30 -0
- package/dist/components/NavCount.d.ts +15 -2
- package/dist/components/NavCount.js +16 -6
- package/dist/components/action-menu/assert-action-menu.d.ts +13 -0
- package/dist/components/action-menu/assert-action-menu.js +21 -0
- package/dist/components/chart/use-chart-width.js +2 -24
- package/dist/components/description-list-layout.d.ts +10 -0
- package/dist/components/description-list-layout.js +13 -0
- package/dist/components/document-viewer/document-viewer-labels.d.ts +23 -0
- package/dist/components/document-viewer/document-viewer-labels.js +23 -0
- package/dist/components/document-viewer/page-box.d.ts +8 -0
- package/dist/components/document-viewer/page-box.js +12 -0
- package/dist/components/document-viewer/pdf-document.d.ts +32 -0
- package/dist/components/document-viewer/pdf-document.js +66 -0
- package/dist/components/document-viewer/pdf-options.d.ts +36 -0
- package/dist/components/document-viewer/pdf-options.js +46 -0
- package/dist/components/document-viewer/render-pdf-page.d.ts +20 -0
- package/dist/components/document-viewer/render-pdf-page.js +38 -0
- package/dist/components/document-viewer/scroll-chain.d.ts +6 -0
- package/dist/components/document-viewer/scroll-chain.js +23 -0
- package/dist/components/document-viewer/use-error-report.d.ts +5 -0
- package/dist/components/document-viewer/use-error-report.js +13 -0
- package/dist/components/document-viewer/use-page-render.d.ts +21 -0
- package/dist/components/document-viewer/use-page-render.js +70 -0
- package/dist/components/document-viewer/use-pdf-document.d.ts +22 -0
- package/dist/components/document-viewer/use-pdf-document.js +47 -0
- package/dist/components/document-viewer/use-pinch-zoom.d.ts +12 -0
- package/dist/components/document-viewer/use-pinch-zoom.js +67 -0
- package/dist/components/document-viewer/use-zoom.d.ts +23 -0
- package/dist/components/document-viewer/use-zoom.js +70 -0
- package/dist/components/document-viewer/zoom.d.ts +29 -0
- package/dist/components/document-viewer/zoom.js +37 -0
- package/dist/components/file-field/file-field-checks.d.ts +13 -0
- package/dist/components/file-field/file-field-checks.js +23 -0
- package/dist/components/file-field/use-file-drop.d.ts +34 -0
- package/dist/components/file-field/use-file-drop.js +78 -0
- package/dist/components/file-field/use-swap-focus.d.ts +15 -0
- package/dist/components/file-field/use-swap-focus.js +40 -0
- package/dist/components/filter-chips/FilterChip.js +1 -1
- package/dist/components/filter-chips/options.d.ts +10 -0
- package/dist/components/filter-chips/options.js +8 -0
- package/dist/components/filter-chips/validate.d.ts +3 -3
- package/dist/components/filter-chips/validate.js +8 -4
- package/dist/components/list/ListRowContent.d.ts +9 -2
- package/dist/components/list/ListRowContent.js +6 -3
- package/dist/components/list/ListRowItem.d.ts +10 -0
- package/dist/components/list/ListRowItem.js +17 -0
- package/dist/components/list/row-naming.d.ts +17 -0
- package/dist/components/list/row-naming.js +19 -0
- package/dist/components/markdown/markdown-limits.d.ts +14 -0
- package/dist/components/markdown/markdown-limits.js +14 -0
- package/dist/components/markdown/markdown-model.d.ts +94 -0
- package/dist/components/markdown/markdown-model.js +8 -0
- package/dist/components/markdown/markdown-options.d.ts +18 -0
- package/dist/components/markdown/markdown-options.js +10 -0
- package/dist/components/markdown/markdown-rules.d.ts +28 -0
- package/dist/components/markdown/markdown-rules.js +81 -0
- package/dist/components/markdown/markdown-text.d.ts +17 -0
- package/dist/components/markdown/markdown-text.js +86 -0
- package/dist/components/markdown/parse-markdown.d.ts +4 -0
- package/dist/components/markdown/parse-markdown.js +21 -0
- package/dist/components/markdown/read-blocks.d.ts +8 -0
- package/dist/components/markdown/read-blocks.js +168 -0
- package/dist/components/markdown/read-inlines.d.ts +12 -0
- package/dist/components/markdown/read-inlines.js +126 -0
- package/dist/components/markdown/read-markdown.d.ts +9 -0
- package/dist/components/markdown/read-markdown.js +27 -0
- package/dist/components/markdown/reading-context.d.ts +18 -0
- package/dist/components/markdown/reading-context.js +35 -0
- package/dist/components/markdown/render-markdown.d.ts +10 -0
- package/dist/components/markdown/render-markdown.js +52 -0
- package/dist/components/slot.d.ts +9 -0
- package/dist/components/slot.js +13 -0
- package/dist/components/use-element-width.d.ts +11 -0
- package/dist/components/use-element-width.js +38 -0
- package/dist/css/css-action-menu-trigger.d.ts +6 -2
- package/dist/css/css-action-menu-trigger.js +50 -2
- package/dist/css/css-action-menu.js +4 -2
- package/dist/css/css-article.d.ts +2 -0
- package/dist/css/css-article.js +110 -0
- package/dist/css/css-components.js +15 -0
- package/dist/css/css-count-pill.js +14 -1
- package/dist/css/css-description-list.d.ts +1 -0
- package/dist/css/css-description-list.js +24 -0
- package/dist/css/css-document-viewer.d.ts +2 -0
- package/dist/css/css-document-viewer.js +186 -0
- package/dist/css/css-file-field.d.ts +2 -0
- package/dist/css/css-file-field.js +194 -0
- package/dist/css/css-filter-chip-tones.d.ts +3 -0
- package/dist/css/css-filter-chip-tones.js +28 -0
- package/dist/css/css-filter-chips-layout.d.ts +1 -0
- package/dist/css/css-filter-chips-layout.js +1 -1
- package/dist/css/css-filter-chips-row.d.ts +2 -0
- package/dist/css/css-filter-chips-row.js +79 -0
- package/dist/css/css-filter-chips.js +8 -0
- package/dist/css/css-font-mono.d.ts +3 -0
- package/dist/css/css-font-mono.js +23 -0
- package/dist/css/css-list-row-trailing.d.ts +2 -0
- package/dist/css/css-list-row-trailing.js +91 -0
- package/dist/css/css-list-row-truncate.d.ts +2 -0
- package/dist/css/css-list-row-truncate.js +29 -0
- package/dist/css/css-list.js +9 -1
- package/dist/css/css-markdown.d.ts +2 -0
- package/dist/css/css-markdown.js +144 -0
- package/dist/css/css-utilities.js +3 -0
- package/dist/css/css-variables.js +1 -0
- package/dist/css/spacing-classes.d.ts +1 -1
- package/dist/css/spacing-classes.js +1 -0
- package/dist/css/stylesheet.js +10 -0
- package/dist/document-viewer-worker.d.ts +9 -0
- package/dist/document-viewer-worker.js +9 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +8 -3
- package/dist/list-options.d.ts +5 -2
- package/dist/list-options.js +7 -2
- package/dist/styles.css +649 -0
- package/package.json +10 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef, useState, } from 'react';
|
|
4
|
+
import { Button } from './Button.js';
|
|
5
|
+
import { Spinner } from './Spinner.js';
|
|
6
|
+
import { Text } from './Text.js';
|
|
7
|
+
import { assertDocumentViewer, shownPage, } from './document-viewer/document-viewer-labels.js';
|
|
8
|
+
import { pageBoxStyle } from './document-viewer/page-box.js';
|
|
9
|
+
import { useErrorReport } from './document-viewer/use-error-report.js';
|
|
10
|
+
import { usePageRender } from './document-viewer/use-page-render.js';
|
|
11
|
+
import { usePdfDocument } from './document-viewer/use-pdf-document.js';
|
|
12
|
+
import { usePinchZoom } from './document-viewer/use-pinch-zoom.js';
|
|
13
|
+
import { useZoom } from './document-viewer/use-zoom.js';
|
|
14
|
+
import { zoomForKey } from './document-viewer/zoom.js';
|
|
15
|
+
import { useElementWidth } from './use-element-width.js';
|
|
16
|
+
/**
|
|
17
|
+
* A PDF drawn one page at a time from its bytes with PDF.js, fitted to the
|
|
18
|
+
* width it is given, with previous and next steps and "Page n of m" in a
|
|
19
|
+
* polite live region. Two fingers, or `+`, `-` and `0` on the focused
|
|
20
|
+
* page, zoom it. The page's text sits over the drawing, transparent, so it
|
|
21
|
+
* can be read by assistive technology and selected. It opens nothing from
|
|
22
|
+
* the network beyond the worker, and has no download or print of its own.
|
|
23
|
+
*/
|
|
24
|
+
export function DocumentViewer({ data, workerSrc, label, labels, page: pageProp, onPageChange, failedAction, onLoad, onError, }) {
|
|
25
|
+
assertDocumentViewer({ label, labels, workerSrc });
|
|
26
|
+
const statusId = useId();
|
|
27
|
+
// The viewer's own page belongs to its bytes: new bytes start at page 1.
|
|
28
|
+
const [own, setOwn] = useState({ data, page: 1 });
|
|
29
|
+
const ownPage = own.data === data ? own.page : 1;
|
|
30
|
+
const canvas = useRef(null);
|
|
31
|
+
const text = useRef(null);
|
|
32
|
+
const opened = usePdfDocument(data, workerSrc, { onLoad, onError });
|
|
33
|
+
const reportError = useErrorReport(onError);
|
|
34
|
+
const document = opened.status === 'ready' ? opened.document : null;
|
|
35
|
+
const pageCount = document?.pageCount ?? 0;
|
|
36
|
+
const page = shownPage(pageProp ?? ownPage, pageCount);
|
|
37
|
+
const { box: stage, page: sheet, zoom, drawnZoom, zoomTo, } = useZoom(`${page}`);
|
|
38
|
+
const fitWidth = useElementWidth(stage, 0, { content: true });
|
|
39
|
+
usePinchZoom(stage, zoom, zoomTo);
|
|
40
|
+
const drawn = usePageRender({
|
|
41
|
+
document,
|
|
42
|
+
page,
|
|
43
|
+
width: Math.round(fitWidth * drawnZoom),
|
|
44
|
+
canvas,
|
|
45
|
+
text,
|
|
46
|
+
onError: reportError,
|
|
47
|
+
});
|
|
48
|
+
// A page that cannot be drawn keeps the steps, so another can be read.
|
|
49
|
+
const failed = opened.status === 'failed' || drawn.state === 'failed';
|
|
50
|
+
const open = document !== null;
|
|
51
|
+
function go(target) {
|
|
52
|
+
if (target < 1 || target > pageCount || target === page)
|
|
53
|
+
return;
|
|
54
|
+
setOwn({ data, page: target });
|
|
55
|
+
onPageChange?.(target);
|
|
56
|
+
}
|
|
57
|
+
function onKeyDown(event) {
|
|
58
|
+
if (event.altKey || event.ctrlKey || event.metaKey)
|
|
59
|
+
return;
|
|
60
|
+
const next = zoomForKey(event.key, zoom);
|
|
61
|
+
if (next === null)
|
|
62
|
+
return;
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
zoomTo(next);
|
|
65
|
+
}
|
|
66
|
+
const step = (direction) => {
|
|
67
|
+
const target = direction === 'previous' ? page - 1 : page + 1;
|
|
68
|
+
const off = target < 1 || target > pageCount;
|
|
69
|
+
return (_jsx(Button, { "aria-disabled": off || undefined, "aria-label": labels[direction], className: "sw-document-viewer-step", disabledAppearance: "neutral", onPress: () => go(target), variant: "neutral", children: _jsx("span", { "aria-hidden": "true", className: `sw-pagination-glyph sw-pagination-glyph-${direction}` }) }));
|
|
70
|
+
};
|
|
71
|
+
return (_jsxs("section", { "aria-label": label, className: "sw-document-viewer", children: [_jsxs("div", { "aria-labelledby": open && !failed ? statusId : undefined, className: "sw-document-viewer-stage", onKeyDown: open && !failed ? onKeyDown : undefined, ref: stage, role: open && !failed ? 'group' : undefined, tabIndex: open && !failed ? 0 : undefined, children: [failed ? (_jsxs("div", { className: "sw-document-viewer-message", role: "alert", children: [_jsx(Text, { as: "p", variant: "title", children: labels.failed }), labels.failedDetail ? (_jsx(Text, { as: "p", children: labels.failedDetail })) : null, failedAction ? (_jsx("div", { className: "sw-document-viewer-actions", children: failedAction })) : null] })) : null, opened.status === 'loading' ? (_jsxs("div", { className: "sw-document-viewer-message", children: [_jsx(Spinner, { label: labels.loading }), _jsx(Text, { "aria-hidden": true, as: "p", children: labels.loading })] })) : null, open ? (_jsxs("div", { className: "sw-document-viewer-page", "data-zoomed": zoom > 1 || undefined, hidden: failed, ref: sheet, style: pageBoxStyle(fitWidth * zoom, drawn.aspect), children: [_jsx("canvas", { "aria-hidden": "true", ref: canvas }), _jsx("div", { className: "sw-document-viewer-text", ref: text })] })) : null] }), open ? (_jsxs("div", { className: "sw-document-viewer-bar", children: [pageCount > 1 ? step('previous') : null, _jsx("span", { "aria-atomic": "true", "aria-live": "polite", className: "sw-document-viewer-status", id: statusId, children: labels.page(page, pageCount) }), pageCount > 1 ? step('next') : null] })) : null] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** The chosen file as the field shows it: its name and a line about it. */
|
|
3
|
+
export type FileFieldValue = {
|
|
4
|
+
name: string;
|
|
5
|
+
/** A muted line under the name, such as "4 pages · 1.6 MB". */
|
|
6
|
+
detail?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The address of a small image of a chosen photo, such as an object URL
|
|
9
|
+
* the consumer made from the `File` (and revokes): drawn as a thumbnail
|
|
10
|
+
* before the name in place of `icon`, hidden from assistive technology,
|
|
11
|
+
* so the name says which file it is.
|
|
12
|
+
*/
|
|
13
|
+
preview?: string;
|
|
14
|
+
};
|
|
15
|
+
export type FileFieldProps = {
|
|
16
|
+
/** The field's label, drawn above it as Field's, and part of its button's name. */
|
|
17
|
+
label: string;
|
|
18
|
+
labelVisuallyHidden?: boolean;
|
|
19
|
+
/** A muted hint under the field, such as "Up to 25 MB". */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Replaces the hint with an error, in a polite live region. */
|
|
22
|
+
error?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The file types the picker offers, as the native `accept` attribute
|
|
25
|
+
* (".pdf,application/pdf"). A dropped file is not filtered by it: check
|
|
26
|
+
* what `onChange` receives and say why in `error`.
|
|
27
|
+
*/
|
|
28
|
+
accept?: string;
|
|
29
|
+
/** The chosen file to show, or nothing chosen (`null` or left out). */
|
|
30
|
+
value?: FileFieldValue | null;
|
|
31
|
+
/** Called with the file the person chose or dropped. Nothing is uploaded. */
|
|
32
|
+
onChange: (file: File) => void;
|
|
33
|
+
/** The empty field's words, such as "Choose a PDF". */
|
|
34
|
+
chooseLabel: string;
|
|
35
|
+
/** The button beside a chosen file, such as "Choose another". */
|
|
36
|
+
changeLabel: string;
|
|
37
|
+
/** Muted words after `chooseLabel` for a fine pointer, such as "or drop it here". */
|
|
38
|
+
dropLabel?: string;
|
|
39
|
+
/**
|
|
40
|
+
* A consumer mark before the chosen file's name (Lucide), hidden from
|
|
41
|
+
* assistive technology; not drawn when `value.preview` is set.
|
|
42
|
+
*/
|
|
43
|
+
icon?: ReactNode;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Prefixes the ids the field gives its parts (its button is `${id}-button`). */
|
|
46
|
+
id?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* One file chosen by a press or dropped on the field: empty, a wide zone
|
|
50
|
+
* that opens the system picker; chosen, the file's name and detail (after
|
|
51
|
+
* a thumbnail for a photo with a `preview`) with a button to choose
|
|
52
|
+
* another. It hands the `File` to the consumer and shows what the
|
|
53
|
+
* consumer passes back as `value`; it never reads or uploads it.
|
|
54
|
+
*/
|
|
55
|
+
export declare function FileField({ label, labelVisuallyHidden, description, error, accept, value, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled, id, }: FileFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef } from 'react';
|
|
4
|
+
import { cx } from '../class-names.js';
|
|
5
|
+
import { tabStop } from '../tab-stop.js';
|
|
6
|
+
import { Button } from './Button.js';
|
|
7
|
+
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
8
|
+
import { FieldLabelText } from './FieldLabelText.js';
|
|
9
|
+
import { assertFileField } from './file-field/file-field-checks.js';
|
|
10
|
+
import { useFileDrop } from './file-field/use-file-drop.js';
|
|
11
|
+
import { useSwapFocus } from './file-field/use-swap-focus.js';
|
|
12
|
+
import { Stack } from './Stack.js';
|
|
13
|
+
/** Whether the consumer passed a mark to draw (not empty JSX). */
|
|
14
|
+
function hasMark(icon) {
|
|
15
|
+
return (icon !== undefined &&
|
|
16
|
+
icon !== null &&
|
|
17
|
+
icon !== '' &&
|
|
18
|
+
typeof icon !== 'boolean');
|
|
19
|
+
}
|
|
20
|
+
/** Before a chosen file's name: its photo's thumbnail, else the consumer's mark. */
|
|
21
|
+
function ChosenMark({ icon, preview }) {
|
|
22
|
+
if (preview !== undefined)
|
|
23
|
+
return _jsx("img", { alt: "", className: "sw-file-field-preview", src: preview });
|
|
24
|
+
if (!hasMark(icon))
|
|
25
|
+
return null;
|
|
26
|
+
return (_jsx("span", { "aria-hidden": "true", className: "sw-file-field-icon", children: icon }));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One file chosen by a press or dropped on the field: empty, a wide zone
|
|
30
|
+
* that opens the system picker; chosen, the file's name and detail (after
|
|
31
|
+
* a thumbnail for a photo with a `preview`) with a button to choose
|
|
32
|
+
* another. It hands the `File` to the consumer and shows what the
|
|
33
|
+
* consumer passes back as `value`; it never reads or uploads it.
|
|
34
|
+
*/
|
|
35
|
+
export function FileField({ label, labelVisuallyHidden = false, description, error, accept, value = null, onChange, chooseLabel, changeLabel, dropLabel, icon, disabled = false, id, }) {
|
|
36
|
+
assertFileField({ label, chooseLabel, changeLabel, dropLabel, value });
|
|
37
|
+
const ownId = useId();
|
|
38
|
+
const baseId = id ?? ownId;
|
|
39
|
+
const inputRef = useRef(null);
|
|
40
|
+
const buttonRef = useRef(null);
|
|
41
|
+
const chosen = value !== null;
|
|
42
|
+
const drop = useFileDrop({ disabled, onFile: onChange });
|
|
43
|
+
const focusHandlers = useSwapFocus(buttonRef, chosen);
|
|
44
|
+
function onInput(event) {
|
|
45
|
+
const file = event.currentTarget.files?.[0];
|
|
46
|
+
// Cleared, so choosing the same file again is a change too.
|
|
47
|
+
event.currentTarget.value = '';
|
|
48
|
+
if (file)
|
|
49
|
+
onChange(file);
|
|
50
|
+
}
|
|
51
|
+
const ids = {
|
|
52
|
+
label: `${baseId}-label`,
|
|
53
|
+
button: `${baseId}-button`,
|
|
54
|
+
name: `${baseId}-name`,
|
|
55
|
+
detail: `${baseId}-detail`,
|
|
56
|
+
hint: `${baseId}-hint`,
|
|
57
|
+
error: `${baseId}-error`,
|
|
58
|
+
};
|
|
59
|
+
// The error is in the button's description and the polite region; the
|
|
60
|
+
// button role takes no aria-invalid.
|
|
61
|
+
const naming = {
|
|
62
|
+
'aria-labelledby': `${ids.label} ${ids.button}`,
|
|
63
|
+
'aria-describedby': [
|
|
64
|
+
chosen && ids.name,
|
|
65
|
+
chosen && value.detail && ids.detail,
|
|
66
|
+
description && !error && ids.hint,
|
|
67
|
+
error && ids.error,
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join(' ') || undefined,
|
|
71
|
+
id: ids.button,
|
|
72
|
+
};
|
|
73
|
+
const open = () => inputRef.current?.click();
|
|
74
|
+
return (_jsxs(Stack, { className: cx('sw-field', 'sw-file-field', error && 'sw-field-invalid'), gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("span", { id: ids.label, children: _jsx(FieldLabelText, { label: label, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: cx('sw-file-field-frame', chosen ? 'sw-file-field-chosen' : 'sw-file-field-empty', drop.dragging && 'sw-file-field-dragging', disabled && 'sw-file-field-disabled'), ...drop.handlers, ...focusHandlers, children: [_jsx("input", { accept: accept, "aria-label": label, disabled: disabled, hidden: true, onChange: onInput, ref: inputRef, tabIndex: -1, type: "file" }), chosen ? (_jsxs(_Fragment, { children: [_jsx(ChosenMark, { icon: icon, preview: value.preview }), _jsxs("span", { className: "sw-file-field-file", children: [_jsx("span", { className: "sw-file-field-name", id: ids.name, children: value.name }), value.detail ? (_jsx("span", { className: "sw-file-field-detail", id: ids.detail, children: value.detail })) : null] }), _jsx(Button, { ...naming, disabled: disabled, disabledAppearance: "neutral", onPress: open, ref: buttonRef, variant: "neutral", children: changeLabel })] })) : (_jsxs("button", { ...naming, className: "sw-file-field-zone", disabled: disabled, onClick: open, ref: buttonRef, tabIndex: tabStop, type: "button", children: [_jsx("span", { className: "sw-file-field-choose", children: chooseLabel }), dropLabel ? ' ' : null, dropLabel ? (_jsx("span", { className: "sw-file-field-drop", children: dropLabel })) : null] }))] }), description && !error ? (_jsx("span", { className: "sw-field-description", id: ids.hint, children: description })) : null, _jsx(FieldErrorRegion, { className: "sw-field-error", id: ids.error, message: error })] }));
|
|
75
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FilterChipsProps } from './filter-chips/options.js';
|
|
2
|
-
export { filterChipCountAppearances, filterChipsOverflows, type FilterChipCountAppearance, type FilterChipOption, type FilterChipsOverflow, type FilterChipsProps, } from './filter-chips/options.js';
|
|
2
|
+
export { filterChipCountAppearances, filterChipsOverflows, filterChipTones, type FilterChipCountAppearance, type FilterChipOption, type FilterChipsOverflow, type FilterChipsProps, type FilterChipTone, } from './filter-chips/options.js';
|
|
3
3
|
/**
|
|
4
4
|
* A labelled row of chips: one choice (radios), or several at once
|
|
5
5
|
* (`multiple`, checkboxes), each chip optionally counted.
|
|
@@ -7,7 +7,7 @@ import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
|
7
7
|
import { showsRequiredMark } from './FieldLabelText.js';
|
|
8
8
|
import { FilterChip } from './filter-chips/FilterChip.js';
|
|
9
9
|
import { assertFilterChips } from './filter-chips/validate.js';
|
|
10
|
-
export { filterChipCountAppearances, filterChipsOverflows, } from './filter-chips/options.js';
|
|
10
|
+
export { filterChipCountAppearances, filterChipsOverflows, filterChipTones, } from './filter-chips/options.js';
|
|
11
11
|
/** The values a change leaves on, in the options' order. */
|
|
12
12
|
function toggled(options, chosen, value, checked) {
|
|
13
13
|
return options
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type FilterChipsOverflow } from './filter-chips/options.js';
|
|
3
|
+
export type FilterChipsRowProps = {
|
|
4
|
+
/** The row's chip groups, each a `FilterChips` with its own name. */
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* A group set apart at the row's end, such as one toggle of another kind
|
|
8
|
+
* after the filters ("Missing a translation"); on its own line's end when
|
|
9
|
+
* the row wraps.
|
|
10
|
+
*/
|
|
11
|
+
end?: ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* `wrap` (the default): the groups wrap onto more lines. `scroll`: every
|
|
14
|
+
* group's chips stay on one row that scrolls sideways together inside
|
|
15
|
+
* the row's own box, never the page; a group's own `overflow` gives way.
|
|
16
|
+
*/
|
|
17
|
+
overflow?: FilterChipsOverflow;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Several chip groups on one row: the groups keep their own names and
|
|
21
|
+
* choices (a radio group and a checkbox, say), and the row lays them out
|
|
22
|
+
* together, wrapping or scrolling as one, with an optional group at its end.
|
|
23
|
+
*/
|
|
24
|
+
export declare function FilterChipsRow({ children, end, overflow, }: FilterChipsRowProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../class-names.js';
|
|
3
|
+
import { hasSlot } from './slot.js';
|
|
4
|
+
import { filterChipsOverflows, } from './filter-chips/options.js';
|
|
5
|
+
/**
|
|
6
|
+
* Several chip groups on one row: the groups keep their own names and
|
|
7
|
+
* choices (a radio group and a checkbox, say), and the row lays them out
|
|
8
|
+
* together, wrapping or scrolling as one, with an optional group at its end.
|
|
9
|
+
*/
|
|
10
|
+
export function FilterChipsRow({ children, end, overflow = 'wrap', }) {
|
|
11
|
+
if (!filterChipsOverflows.includes(overflow))
|
|
12
|
+
throw new RangeError(`Unknown FilterChipsRow overflow: ${String(overflow)}`);
|
|
13
|
+
return (_jsxs("div", { className: cx('sw-filter-chips-row', overflow === 'scroll' && 'sw-filter-chips-row-scroll'), children: [children, hasSlot(end) ? (_jsx("div", { className: "sw-filter-chips-row-end", children: end })) : null] }));
|
|
14
|
+
}
|
|
@@ -44,6 +44,33 @@ export type ListRowProps = {
|
|
|
44
44
|
tone?: ListRowTone;
|
|
45
45
|
/** `plain` sets the title at the body weight, for a list of plain statements. */
|
|
46
46
|
titleAppearance?: ListRowTitleAppearance;
|
|
47
|
+
/**
|
|
48
|
+
* Marks the row as new for the person, such as a document they have not
|
|
49
|
+
* opened: an accent dot after the title, read as `unreadLabel`.
|
|
50
|
+
*/
|
|
51
|
+
unread?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* What the dot means in the consumer's words ("New"), read after the
|
|
54
|
+
* title (in a pressable row's name). Required while `unread`.
|
|
55
|
+
*/
|
|
56
|
+
unreadLabel?: string;
|
|
57
|
+
/**
|
|
58
|
+
* A short status before the accessory, such as a `Badge` ("Only in
|
|
59
|
+
* English"); a pressable row is described by it.
|
|
60
|
+
*/
|
|
61
|
+
tag?: ReactNode;
|
|
62
|
+
/**
|
|
63
|
+
* A control after the row and outside its press area, such as an
|
|
64
|
+
* `ActionMenu` of the row's own actions. It needs its own name.
|
|
65
|
+
*/
|
|
66
|
+
action?: ReactNode;
|
|
67
|
+
/**
|
|
68
|
+
* Keeps the title and the detail on one line each, cut with "…" where
|
|
69
|
+
* they do not fit, for a long list read at a glance; the full words stay
|
|
70
|
+
* in the row's name and description, and in a `title` tooltip. The
|
|
71
|
+
* value, its detail and the tag keep their words whole.
|
|
72
|
+
*/
|
|
73
|
+
truncate?: boolean;
|
|
47
74
|
/** `none` hides the chevron of a pressable row that acts in place. */
|
|
48
75
|
accessory?: 'chevron' | 'none';
|
|
49
76
|
/**
|
|
@@ -55,6 +82,8 @@ export type ListRowProps = {
|
|
|
55
82
|
/**
|
|
56
83
|
* One row of a `ListGroup`: it opens something (`onPress` or `href`, with
|
|
57
84
|
* a chevron, the whole row pressing), picks one option in place
|
|
58
|
-
* (`selected`), or only states something (neither).
|
|
85
|
+
* (`selected`), or only states something (neither). `unread` marks it new,
|
|
86
|
+
* `tag` adds a status before its end, `action` a control beside it, and
|
|
87
|
+
* `truncate` keeps its title and detail to one line each.
|
|
59
88
|
*/
|
|
60
|
-
export declare function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
|
|
89
|
+
export declare function ListRow({ title, detail, value, valueDetail, valueTone, valueBadge, leading, onPress, href, selected, disabled, tone, titleAppearance, accessory, unread, unreadLabel, tag, action, truncate, 'aria-label': ariaLabel, }: ListRowProps): import("react").JSX.Element;
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
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 {
|
|
10
|
-
|
|
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, `action` a control beside it, and
|
|
16
|
+
* `truncate` keeps its title and detail to one line each.
|
|
18
17
|
*/
|
|
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, }) {
|
|
18
|
+
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, truncate = false, 'aria-label': ariaLabel, }) {
|
|
20
19
|
const pressable = onPress !== undefined || href !== undefined;
|
|
21
20
|
assertListRow({
|
|
22
21
|
title,
|
|
@@ -29,19 +28,21 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
|
|
|
29
28
|
valueBadge,
|
|
30
29
|
selected,
|
|
31
30
|
pressable,
|
|
31
|
+
unread,
|
|
32
|
+
unreadLabel,
|
|
32
33
|
});
|
|
33
|
-
const container = useContext(ListRowContainer);
|
|
34
34
|
const id = useId();
|
|
35
35
|
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 }));
|
|
37
|
-
const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled');
|
|
38
|
-
const naming = {
|
|
36
|
+
const content = (_jsx(ListRowContent, { accessory: kind, detail: detail, id: id, leading: leading, tag: tag, title: title, titleAppearance: titleAppearance, tone: tone, truncate: truncate, unreadLabel: unread ? unreadLabel : undefined, value: value, valueBadge: valueBadge, valueDetail: valueDetail, valueTone: valueTone }));
|
|
37
|
+
const className = cx('sw-list-row', pressable && 'sw-list-row-pressable', disabled && 'sw-list-row-disabled', truncate && 'sw-list-row-truncate');
|
|
38
|
+
const naming = rowNaming(id, {
|
|
39
39
|
'aria-label': ariaLabel,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
value,
|
|
41
|
+
detail,
|
|
42
|
+
valueDetail,
|
|
43
|
+
unread,
|
|
44
|
+
tag,
|
|
45
|
+
});
|
|
45
46
|
let row;
|
|
46
47
|
if (href !== undefined)
|
|
47
48
|
row = (_jsx("a", { ...naming, "aria-current": selected ? 'true' : undefined, "aria-disabled": disabled || undefined, className: className, href: disabled ? undefined : href, onClick: onPress && !disabled
|
|
@@ -56,5 +57,5 @@ export function ListRow({ title, detail, value, valueDetail, valueTone, valueBad
|
|
|
56
57
|
row = (_jsx("button", { ...naming, "aria-pressed": selected, className: className, disabled: disabled, onClick: onPress, tabIndex: tabStop, type: "button", children: content }));
|
|
57
58
|
else
|
|
58
59
|
row = (_jsx("div", { "aria-label": ariaLabel, className: className, role: ariaLabel ? 'group' : undefined, children: content }));
|
|
59
|
-
return
|
|
60
|
+
return _jsx(ListRowItem, { action: action, children: row });
|
|
60
61
|
}
|
|
@@ -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
|
-
|
|
3
|
-
|
|
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:
|
|
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
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ActionMenuTriggerAppearance } from '../../css/css-action-menu-trigger.js';
|
|
3
|
+
import { type ActionMenuItem } from '../ActionMenu.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fails closed on a menu it cannot draw: an item's blank detail or
|
|
6
|
+
* detailLang (or a detailLang with no detail), an unknown trigger
|
|
7
|
+
* appearance, or an icon trigger with no glyph.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertActionMenu({ items, trigger, triggerAppearance, }: {
|
|
10
|
+
items: readonly ActionMenuItem[];
|
|
11
|
+
trigger: ReactNode;
|
|
12
|
+
triggerAppearance: ActionMenuTriggerAppearance;
|
|
13
|
+
}): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { actionMenuTriggerAppearances, } from '../../css/css-action-menu-trigger.js';
|
|
2
|
+
import { hasSlot } from '../slot.js';
|
|
3
|
+
/** Whether an icon trigger has a glyph to draw (blank words are none). */
|
|
4
|
+
function hasGlyph(trigger) {
|
|
5
|
+
return (hasSlot(trigger) && !(typeof trigger === 'string' && trigger.trim() === ''));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fails closed on a menu it cannot draw: an item's blank detail or
|
|
9
|
+
* detailLang (or a detailLang with no detail), an unknown trigger
|
|
10
|
+
* appearance, or an icon trigger with no glyph.
|
|
11
|
+
*/
|
|
12
|
+
export function assertActionMenu({ items, trigger, triggerAppearance, }) {
|
|
13
|
+
if (items.some((item) => (item.detail !== undefined && !item.detail.trim()) ||
|
|
14
|
+
(item.detailLang !== undefined &&
|
|
15
|
+
(!item.detailLang.trim() || item.detail === undefined))))
|
|
16
|
+
throw new RangeError('ActionMenu item detail and detailLang must not be blank; detailLang needs a detail');
|
|
17
|
+
if (!actionMenuTriggerAppearances.includes(triggerAppearance))
|
|
18
|
+
throw new RangeError(`Unknown ActionMenu triggerAppearance: ${String(triggerAppearance)}`);
|
|
19
|
+
if (triggerAppearance === 'icon' && !hasGlyph(trigger))
|
|
20
|
+
throw new RangeError('An icon ActionMenu trigger needs its glyph');
|
|
21
|
+
}
|
|
@@ -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
|
-
|
|
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,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a DescriptionList lays out its rows: `columns` (the default), the
|
|
3
|
+
* terms in a start column and the details beside them, each term over its
|
|
4
|
+
* detail below `md`; `spread`, each row's term at its start and its detail
|
|
5
|
+
* at its end, side by side at every width.
|
|
6
|
+
*/
|
|
7
|
+
export declare const descriptionListLayouts: readonly ["columns", "spread"];
|
|
8
|
+
export type DescriptionListLayout = (typeof descriptionListLayouts)[number];
|
|
9
|
+
/** Fails closed on a layout the list cannot draw. */
|
|
10
|
+
export declare function assertDescriptionListLayout(layout: unknown): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isOneOf } from '../is-one-of.js';
|
|
2
|
+
/**
|
|
3
|
+
* How a DescriptionList lays out its rows: `columns` (the default), the
|
|
4
|
+
* terms in a start column and the details beside them, each term over its
|
|
5
|
+
* detail below `md`; `spread`, each row's term at its start and its detail
|
|
6
|
+
* at its end, side by side at every width.
|
|
7
|
+
*/
|
|
8
|
+
export const descriptionListLayouts = ['columns', 'spread'];
|
|
9
|
+
/** Fails closed on a layout the list cannot draw. */
|
|
10
|
+
export function assertDescriptionListLayout(layout) {
|
|
11
|
+
if (!isOneOf(descriptionListLayouts, layout))
|
|
12
|
+
throw new RangeError(`Unknown DescriptionList layout: ${String(layout)}`);
|
|
13
|
+
}
|