@adecore/ui 0.14.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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/AccentSwatches.d.ts +15 -0
- package/dist/AccentSwatches.js +21 -0
- package/dist/Banner.d.ts +12 -0
- package/dist/Banner.js +18 -0
- package/dist/Button.d.ts +9 -0
- package/dist/Button.js +28 -0
- package/dist/ButtonGroup.d.ts +3 -0
- package/dist/ButtonGroup.js +11 -0
- package/dist/Checkbox.d.ts +11 -0
- package/dist/Checkbox.js +9 -0
- package/dist/ChoiceCards.d.ts +23 -0
- package/dist/ChoiceCards.js +21 -0
- package/dist/CloseButton.d.ts +15 -0
- package/dist/CloseButton.js +11 -0
- package/dist/ColorSwatch.d.ts +14 -0
- package/dist/ColorSwatch.js +28 -0
- package/dist/ColumnResizeHandle.d.ts +9 -0
- package/dist/ColumnResizeHandle.js +12 -0
- package/dist/DisabledReason.d.ts +6 -0
- package/dist/DisabledReason.js +8 -0
- package/dist/DockShell.d.ts +9 -0
- package/dist/DockShell.js +49 -0
- package/dist/EmptyState.d.ts +16 -0
- package/dist/EmptyState.js +11 -0
- package/dist/ErrorBoundary.d.ts +24 -0
- package/dist/ErrorBoundary.js +47 -0
- package/dist/Field.d.ts +17 -0
- package/dist/Field.js +48 -0
- package/dist/FileIcon.d.ts +8 -0
- package/dist/FileIcon.js +15 -0
- package/dist/Icon.d.ts +9 -0
- package/dist/Icon.js +12 -0
- package/dist/IconButton.d.ts +23 -0
- package/dist/IconButton.js +25 -0
- package/dist/IconPicker.d.ts +17 -0
- package/dist/IconPicker.js +93 -0
- package/dist/Input.d.ts +14 -0
- package/dist/Input.js +25 -0
- package/dist/Kbd.d.ts +23 -0
- package/dist/Kbd.js +20 -0
- package/dist/ListRow.d.ts +10 -0
- package/dist/ListRow.js +20 -0
- package/dist/Meter.d.ts +12 -0
- package/dist/Meter.js +7 -0
- package/dist/PanelEmpty.d.ts +16 -0
- package/dist/PanelEmpty.js +7 -0
- package/dist/PanelHeader.d.ts +6 -0
- package/dist/PanelHeader.js +20 -0
- package/dist/Pill.d.ts +27 -0
- package/dist/Pill.js +25 -0
- package/dist/ProjectSwitcher.d.ts +30 -0
- package/dist/ProjectSwitcher.js +18 -0
- package/dist/PromptDialog.d.ts +40 -0
- package/dist/PromptDialog.js +47 -0
- package/dist/SectionLabel.d.ts +3 -0
- package/dist/SectionLabel.js +16 -0
- package/dist/SegmentBar.d.ts +18 -0
- package/dist/SegmentBar.js +44 -0
- package/dist/Segmented.d.ts +17 -0
- package/dist/Segmented.js +7 -0
- package/dist/Select.d.ts +27 -0
- package/dist/Select.js +34 -0
- package/dist/Separator.d.ts +7 -0
- package/dist/Separator.js +8 -0
- package/dist/ShortcutHints.d.ts +1 -0
- package/dist/ShortcutHints.js +94 -0
- package/dist/Skeleton.d.ts +6 -0
- package/dist/Skeleton.js +6 -0
- package/dist/SlidingColumn.d.ts +19 -0
- package/dist/SlidingColumn.js +41 -0
- package/dist/Spinner.d.ts +6 -0
- package/dist/Spinner.js +7 -0
- package/dist/Stepper.d.ts +14 -0
- package/dist/Stepper.js +18 -0
- package/dist/Surface.d.ts +3 -0
- package/dist/Surface.js +16 -0
- package/dist/Switch.d.ts +10 -0
- package/dist/Switch.js +7 -0
- package/dist/TextMenu.d.ts +5 -0
- package/dist/TextMenu.js +31 -0
- package/dist/Tile.d.ts +11 -0
- package/dist/Tile.js +10 -0
- package/dist/Toasts.d.ts +9 -0
- package/dist/Toasts.js +58 -0
- package/dist/Tooltip.d.ts +16 -0
- package/dist/Tooltip.js +21 -0
- package/dist/UIProvider.d.ts +9 -0
- package/dist/UIProvider.js +26 -0
- package/dist/Waveform.d.ts +16 -0
- package/dist/Waveform.js +123 -0
- package/dist/Wipe.d.ts +12 -0
- package/dist/Wipe.js +47 -0
- package/dist/ZoomControls.d.ts +30 -0
- package/dist/ZoomControls.js +16 -0
- package/dist/class-name.d.ts +2 -0
- package/dist/class-name.js +3 -0
- package/dist/clipboard.d.ts +2 -0
- package/dist/clipboard.js +16 -0
- package/dist/context-menu/index.parts.d.ts +1 -0
- package/dist/context-menu/index.parts.js +2 -0
- package/dist/dialog/index.parts.d.ts +1 -0
- package/dist/dialog/index.parts.js +1 -0
- package/dist/dialog/parts.d.ts +36 -0
- package/dist/dialog/parts.js +55 -0
- package/dist/dialog-layer.d.ts +1 -0
- package/dist/dialog-layer.js +27 -0
- package/dist/error-boundary.d.ts +4 -0
- package/dist/error-boundary.js +34 -0
- package/dist/error-message.d.ts +1 -0
- package/dist/error-message.js +4 -0
- package/dist/field-context.d.ts +7 -0
- package/dist/field-context.js +4 -0
- package/dist/file-icon.d.ts +4 -0
- package/dist/file-icon.js +46 -0
- package/dist/floating.d.ts +5 -0
- package/dist/floating.js +6 -0
- package/dist/format/datetime.d.ts +11 -0
- package/dist/format/datetime.js +50 -0
- package/dist/format/duration.d.ts +5 -0
- package/dist/format/duration.js +76 -0
- package/dist/format/index.d.ts +6 -0
- package/dist/format/index.js +7 -0
- package/dist/format/locale.d.ts +17 -0
- package/dist/format/locale.js +105 -0
- package/dist/format/number.d.ts +8 -0
- package/dist/format/number.js +72 -0
- package/dist/format/regions.d.ts +6 -0
- package/dist/format/regions.js +15 -0
- package/dist/format/time-zone.d.ts +1 -0
- package/dist/format/time-zone.js +2 -0
- package/dist/icon-button-size.d.ts +3 -0
- package/dist/icon-button-size.js +3 -0
- package/dist/icon-picker.d.ts +23 -0
- package/dist/icon-picker.js +80 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +74 -0
- package/dist/lazy.d.ts +12 -0
- package/dist/lazy.js +104 -0
- package/dist/locales/en.json +59 -0
- package/dist/locales/nl.json +59 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +17 -0
- package/dist/menu/index.parts.d.ts +1 -0
- package/dist/menu/index.parts.js +1 -0
- package/dist/menu/parts.d.ts +67 -0
- package/dist/menu/parts.js +113 -0
- package/dist/merge-refs.d.ts +2 -0
- package/dist/merge-refs.js +11 -0
- package/dist/modality.d.ts +1 -0
- package/dist/modality.js +29 -0
- package/dist/platform.d.ts +2 -0
- package/dist/platform.js +9 -0
- package/dist/popover/index.parts.d.ts +1 -0
- package/dist/popover/index.parts.js +1 -0
- package/dist/popover/parts.d.ts +26 -0
- package/dist/popover/parts.js +28 -0
- package/dist/prefetch.d.ts +21 -0
- package/dist/prefetch.js +99 -0
- package/dist/preview-card/index.parts.d.ts +1 -0
- package/dist/preview-card/index.parts.js +1 -0
- package/dist/project-switcher-row.d.ts +6 -0
- package/dist/project-switcher-row.js +16 -0
- package/dist/selection.d.ts +2 -0
- package/dist/selection.js +22 -0
- package/dist/seti-icons.d.ts +5 -0
- package/dist/seti-icons.js +445 -0
- package/dist/settings/ConfirmDialog.d.ts +9 -0
- package/dist/settings/ConfirmDialog.js +9 -0
- package/dist/settings/DetailHeader.d.ts +10 -0
- package/dist/settings/DetailHeader.js +6 -0
- package/dist/settings/MasterDetail.d.ts +24 -0
- package/dist/settings/MasterDetail.js +16 -0
- package/dist/settings/SettingsDialog.d.ts +46 -0
- package/dist/settings/SettingsDialog.js +96 -0
- package/dist/settings/SettingsRow.d.ts +22 -0
- package/dist/settings/SettingsRow.js +30 -0
- package/dist/settings/SettingsSection.d.ts +14 -0
- package/dist/settings/SettingsSection.js +8 -0
- package/dist/settings/index.d.ts +7 -0
- package/dist/settings/index.js +8 -0
- package/dist/settings/target.d.ts +7 -0
- package/dist/settings/target.js +3 -0
- package/dist/shortcut-hints.d.ts +38 -0
- package/dist/shortcut-hints.js +81 -0
- package/dist/shortcut.d.ts +32 -0
- package/dist/shortcut.js +137 -0
- package/dist/testing/dedupe.d.ts +1 -0
- package/dist/testing/dedupe.js +73 -0
- package/dist/testing/fake-source.d.ts +8 -0
- package/dist/testing/fake-source.js +15 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/theme.css +562 -0
- package/dist/toast-store.d.ts +38 -0
- package/dist/toast-store.js +61 -0
- package/dist/useAsyncAction.d.ts +8 -0
- package/dist/useAsyncAction.js +23 -0
- package/dist/useColumnResize.d.ts +14 -0
- package/dist/useColumnResize.js +49 -0
- package/dist/useContentSize.d.ts +5 -0
- package/dist/useContentSize.js +21 -0
- package/dist/useMeasuredWidth.d.ts +1 -0
- package/dist/useMeasuredWidth.js +6 -0
- package/dist/useNow.d.ts +3 -0
- package/dist/useNow.js +41 -0
- package/dist/wipe-split.d.ts +7 -0
- package/dist/wipe-split.js +20 -0
- package/dist/zoom.d.ts +3 -0
- package/dist/zoom.js +6 -0
- package/package.json +92 -0
- package/src/AccentSwatches.tsx +73 -0
- package/src/Banner.tsx +43 -0
- package/src/Button.tsx +55 -0
- package/src/ButtonGroup.tsx +14 -0
- package/src/Checkbox.tsx +44 -0
- package/src/ChoiceCards.tsx +108 -0
- package/src/CloseButton.tsx +21 -0
- package/src/ColorSwatch.tsx +41 -0
- package/src/ColumnResizeHandle.tsx +23 -0
- package/src/DisabledReason.tsx +23 -0
- package/src/DockShell.tsx +76 -0
- package/src/EmptyState.tsx +48 -0
- package/src/ErrorBoundary.tsx +116 -0
- package/src/Field.tsx +116 -0
- package/src/FileIcon.tsx +27 -0
- package/src/Icon.tsx +22 -0
- package/src/IconButton.tsx +84 -0
- package/src/IconPicker.tsx +257 -0
- package/src/Input.tsx +78 -0
- package/src/Kbd.tsx +67 -0
- package/src/ListRow.tsx +26 -0
- package/src/Meter.tsx +41 -0
- package/src/PanelEmpty.tsx +37 -0
- package/src/PanelHeader.tsx +31 -0
- package/src/Pill.tsx +61 -0
- package/src/ProjectSwitcher.tsx +127 -0
- package/src/PromptDialog.tsx +168 -0
- package/src/SectionLabel.tsx +20 -0
- package/src/SegmentBar.tsx +128 -0
- package/src/Segmented.tsx +51 -0
- package/src/Select.tsx +135 -0
- package/src/Separator.tsx +15 -0
- package/src/ShortcutHints.tsx +118 -0
- package/src/Skeleton.tsx +13 -0
- package/src/SlidingColumn.tsx +91 -0
- package/src/Spinner.tsx +32 -0
- package/src/Stepper.tsx +67 -0
- package/src/Surface.tsx +20 -0
- package/src/Switch.tsx +32 -0
- package/src/TextMenu.tsx +59 -0
- package/src/Tile.tsx +49 -0
- package/src/Toasts.tsx +151 -0
- package/src/Tooltip.tsx +70 -0
- package/src/UIProvider.tsx +41 -0
- package/src/Waveform.tsx +199 -0
- package/src/Wipe.tsx +99 -0
- package/src/ZoomControls.tsx +99 -0
- package/src/class-name.ts +8 -0
- package/src/clipboard.ts +17 -0
- package/src/context-menu/index.parts.ts +21 -0
- package/src/dialog/index.parts.ts +10 -0
- package/src/dialog/parts.tsx +112 -0
- package/src/dialog-layer.ts +31 -0
- package/src/error-boundary.ts +40 -0
- package/src/error-message.ts +5 -0
- package/src/field-context.ts +12 -0
- package/src/file-icon.ts +52 -0
- package/src/floating.ts +9 -0
- package/src/format/datetime.ts +63 -0
- package/src/format/duration.ts +83 -0
- package/src/format/index.ts +20 -0
- package/src/format/locale.ts +153 -0
- package/src/format/number.ts +87 -0
- package/src/format/regions.ts +20 -0
- package/src/format/time-zone.ts +2 -0
- package/src/icon-button-size.ts +6 -0
- package/src/icon-picker.ts +112 -0
- package/src/index.ts +103 -0
- package/src/lazy.tsx +124 -0
- package/src/locales/en.json +59 -0
- package/src/locales/nl.json +59 -0
- package/src/locales.ts +21 -0
- package/src/menu/index.parts.ts +20 -0
- package/src/menu/parts.tsx +296 -0
- package/src/merge-refs.ts +14 -0
- package/src/modality.ts +37 -0
- package/src/platform.ts +10 -0
- package/src/popover/index.parts.ts +8 -0
- package/src/popover/parts.tsx +119 -0
- package/src/prefetch.ts +113 -0
- package/src/preview-card/index.parts.ts +1 -0
- package/src/project-switcher-row.tsx +51 -0
- package/src/selection.ts +24 -0
- package/src/seti-icons.ts +451 -0
- package/src/settings/ConfirmDialog.tsx +30 -0
- package/src/settings/DetailHeader.tsx +25 -0
- package/src/settings/MasterDetail.tsx +67 -0
- package/src/settings/SettingsDialog.tsx +342 -0
- package/src/settings/SettingsRow.tsx +89 -0
- package/src/settings/SettingsSection.tsx +46 -0
- package/src/settings/index.ts +16 -0
- package/src/settings/target.ts +12 -0
- package/src/shortcut-hints.ts +117 -0
- package/src/shortcut.ts +170 -0
- package/src/testing/dedupe.ts +93 -0
- package/src/testing/fake-source.ts +21 -0
- package/src/testing/index.ts +3 -0
- package/src/theme.css +562 -0
- package/src/toast-store.ts +120 -0
- package/src/useAsyncAction.ts +32 -0
- package/src/useColumnResize.ts +75 -0
- package/src/useContentSize.ts +27 -0
- package/src/useMeasuredWidth.ts +7 -0
- package/src/useNow.ts +49 -0
- package/src/wipe-split.ts +24 -0
- package/src/zoom.ts +9 -0
package/dist/Toasts.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { CircleAlert, CircleCheck, Trash, X } from 'lucide-react';
|
|
6
|
+
import { Icon } from './Icon.js';
|
|
7
|
+
import { IconButton } from './IconButton.js';
|
|
8
|
+
import { Kbd } from './Kbd.js';
|
|
9
|
+
import { Spinner } from './Spinner.js';
|
|
10
|
+
import { Surface } from './Surface.js';
|
|
11
|
+
import { elapsedOf } from './toast-store.js';
|
|
12
|
+
const ICON = {
|
|
13
|
+
success: CircleCheck,
|
|
14
|
+
error: CircleAlert,
|
|
15
|
+
deleted: Trash
|
|
16
|
+
};
|
|
17
|
+
const TONE = {
|
|
18
|
+
progress: 'text-text-muted',
|
|
19
|
+
success: 'text-status-idle',
|
|
20
|
+
error: 'text-status-error',
|
|
21
|
+
deleted: 'text-text-muted'
|
|
22
|
+
};
|
|
23
|
+
/* The box is as tall as the title's own line, so what sits beside the title centers on that line. */
|
|
24
|
+
const TITLE_LINE = 'flex h-(--text-sm--line-height) shrink-0 items-center';
|
|
25
|
+
/*
|
|
26
|
+
* How long the offer in a toast still stands, drawn from the timer that takes the toast away: the
|
|
27
|
+
* ring starts as far along as that timer already is, so both end together. With motion off it stays full.
|
|
28
|
+
*/
|
|
29
|
+
function TimerRing({ deadline, onDismiss }) {
|
|
30
|
+
const { t } = useTranslation('ui');
|
|
31
|
+
const lifetime = deadline.end - deadline.start;
|
|
32
|
+
const [elapsed] = useState(() => elapsedOf(deadline, Date.now()));
|
|
33
|
+
return (_jsxs(IconButton, { size: "xs", label: t('action.dismiss'), className: "group/ring relative -my-px", onClick: onDismiss, children: [_jsxs("svg", { width: 16, height: 16, viewBox: "0 0 16 16", className: "-rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: 8, cy: 8, r: 6, fill: "none", strokeWidth: 2, className: "stroke-border" }), _jsx("circle", { cx: 8, cy: 8, r: 6, fill: "none", strokeWidth: 2, pathLength: 1, strokeDasharray: 1, className: "toast-timer-arc stroke-current", style: { animationDuration: `${lifetime}ms`, animationDelay: `${-elapsed}ms` } })] }), _jsx(Icon, { icon: X, size: 8, className: "absolute opacity-0 group-hover/ring:opacity-100 group-focus-visible/ring:opacity-100" })] }));
|
|
34
|
+
}
|
|
35
|
+
function ActionButton({ action }) {
|
|
36
|
+
return (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx("button", { className: "rounded-sm text-sm font-semibold text-text hover:text-text-muted", onClick: action.run, children: action.label }), action.shortcut && _jsx(Kbd, { shortcut: action.shortcut, className: "font-sans text-xs text-text-faint" })] }));
|
|
37
|
+
}
|
|
38
|
+
function ToastCard({ toast, footer, onDismiss }) {
|
|
39
|
+
const { t } = useTranslation('ui');
|
|
40
|
+
// A toast that goes by itself keeps its close button out of sight until the pointer or the keyboard is on it.
|
|
41
|
+
const leaves = toast.kind === 'deleted' || (toast.kind === 'success' && toast.persist !== true);
|
|
42
|
+
const actions = [...(toast.action === undefined ? [] : [toast.action]), ...(toast.actions ?? [])];
|
|
43
|
+
// An offer that runs out shows how long it has left instead.
|
|
44
|
+
const countdown = actions.length > 0 ? toast.deadline : undefined;
|
|
45
|
+
return (_jsxs(Surface, { className: "group flex items-start gap-3 rounded-[10px] py-2.5 pr-2 pl-3", children: [_jsx("span", { className: clsx(TITLE_LINE, 'w-4 justify-center'), children: toast.kind === 'progress' ? (_jsx(Spinner, { size: 16, className: TONE.progress })) : (_jsx(Icon, { icon: ICON[toast.kind], size: 16, className: TONE[toast.kind] })) }), _jsxs("div", { className: "flex min-w-0 grow flex-col gap-1", children: [_jsx("span", { className: "text-sm text-text", children: toast.title }), toast.description !== undefined && toast.description !== '' && (_jsx("span", { className: "text-xs break-words text-pretty text-text-muted", children: toast.description })), actions.length > 1 && (_jsx("span", { className: "mt-1 flex flex-wrap items-center gap-x-4 gap-y-1", children: actions.map((action) => (_jsx(ActionButton, { action: action }, action.label))) })), footer?.(toast)] }), actions.length === 1 && (_jsx("span", { className: TITLE_LINE, children: _jsx(ActionButton, { action: actions[0] }) })), countdown !== undefined ? (_jsx(TimerRing, { deadline: countdown, onDismiss: onDismiss }, countdown.start)) : (_jsx(IconButton, { icon: X, size: "xs", label: t('action.dismiss'), className: clsx('-my-px', leaves && 'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100'), onClick: onDismiss }))] }));
|
|
46
|
+
}
|
|
47
|
+
/*
|
|
48
|
+
* Bottom right, over everything: what an action is doing while it runs and how it went when it is
|
|
49
|
+
* over, and a deletion that can still be taken back. A success takes itself away after four seconds,
|
|
50
|
+
* a deletion after eight, and a failure waits to be read.
|
|
51
|
+
*/
|
|
52
|
+
export function Toasts({ store, footer, className, ref }) {
|
|
53
|
+
const toasts = store((state) => state.toasts);
|
|
54
|
+
if (toasts.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return (_jsx("div", { ref: ref, className: clsx('fixed right-4 bottom-4 z-(--z-popup) flex w-90 max-w-[calc(100vw-32px)] flex-col gap-2', className), role: "status", "aria-live": "polite", children: toasts.map((toast) => (_jsx(ToastCard, { toast: toast, footer: footer, onDismiss: () => store.getState().dismiss(toast.id) }, toast.id))) }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactElement, type ReactNode } from 'react';
|
|
2
|
+
import type { Shortcut } from './shortcut.ts';
|
|
3
|
+
export type TooltipSide = 'top' | 'bottom' | 'left' | 'right';
|
|
4
|
+
export declare function TooltipProvider({ children }: {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export interface TooltipProps {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
kbd?: Shortcut | string;
|
|
10
|
+
side?: TooltipSide;
|
|
11
|
+
sideOffset?: number;
|
|
12
|
+
name?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
children: ReactElement<Record<string, unknown>>;
|
|
15
|
+
}
|
|
16
|
+
export declare function Tooltip({ label, kbd, side, sideOffset, name, disabled, children }: TooltipProps): import("react").JSX.Element;
|
package/dist/Tooltip.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
|
|
4
|
+
import { Kbd } from './Kbd.js';
|
|
5
|
+
import { registerShortcutHint } from './shortcut-hints.js';
|
|
6
|
+
/* One provider per app. Tooltips share a delay, so moving along a row of buttons feels instant. `UIProvider` mounts it. */
|
|
7
|
+
export function TooltipProvider({ children }) {
|
|
8
|
+
return (_jsx(BaseTooltip.Provider, { delay: 150, closeDelay: 0, children: children }));
|
|
9
|
+
}
|
|
10
|
+
/* A hint on hover and on keyboard focus. Every hint in an app is one of these, never a `title`. */
|
|
11
|
+
export function Tooltip({ label, kbd, side = 'top', sideOffset = 6, name = false, disabled = false, children }) {
|
|
12
|
+
// Held Cmd (Ctrl off macOS) prints this shortcut under the trigger (`ShortcutHints`).
|
|
13
|
+
const hintRef = useCallback((element) => {
|
|
14
|
+
if (element === null || kbd === undefined || typeof kbd === 'string') {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return registerShortcutHint(element, kbd);
|
|
18
|
+
}, [kbd]);
|
|
19
|
+
return (_jsxs(BaseTooltip.Root, { disabled: disabled, children: [_jsx(BaseTooltip.Trigger, { ref: hintRef, render: children, "aria-label": name && typeof label === 'string' ? label : undefined }), _jsx(BaseTooltip.Portal, { children: _jsx(BaseTooltip.Positioner, { side: side, sideOffset: sideOffset, className: "tooltip-positioner", children: _jsx(BaseTooltip.Popup, { className: "tooltip-popup", children: _jsxs(BaseTooltip.Viewport, { className: "max-w-72 px-[9px] py-[5px] text-pretty", children: [_jsx("span", { className: "wrap-anywhere", children: label }), kbd !== undefined &&
|
|
20
|
+
(typeof kbd === 'string' ? (_jsx(Kbd, { variant: "inline", className: "ml-2", children: kbd })) : (_jsx(Kbd, { shortcut: kbd, variant: "inline", className: "ml-2" })))] }) }) }) })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { i18n as I18n } from 'i18next';
|
|
3
|
+
import { type FormatSource } from './format/locale.ts';
|
|
4
|
+
export interface UIProviderProps {
|
|
5
|
+
i18n: I18n;
|
|
6
|
+
formatSource?: FormatSource;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export declare function UIProvider({ i18n, formatSource, children }: UIProviderProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { I18nextProvider } from 'react-i18next';
|
|
4
|
+
import { setFormatSource } from './format/locale.js';
|
|
5
|
+
import { addUiResources } from './locales.js';
|
|
6
|
+
import { startInputModality } from './modality.js';
|
|
7
|
+
import { TooltipProvider } from './Tooltip.js';
|
|
8
|
+
/*
|
|
9
|
+
* Everything the library needs from an app, once, around the whole tree: its words in the app's
|
|
10
|
+
* i18next, the source of the formatters, the shared tooltip delay and the note of which input device
|
|
11
|
+
* is in use. Mount it above the first component of the library.
|
|
12
|
+
*/
|
|
13
|
+
export function UIProvider({ i18n, formatSource, children }) {
|
|
14
|
+
const [handed, setHanded] = useState({ i18n: null, source: undefined });
|
|
15
|
+
// In render and not in an effect, so the first child already reads the words and the region.
|
|
16
|
+
// Both calls only store what they are handed, so a render React throws away leaves nothing wrong behind.
|
|
17
|
+
if (handed.i18n !== i18n || handed.source !== formatSource) {
|
|
18
|
+
addUiResources(i18n);
|
|
19
|
+
if (formatSource !== undefined) {
|
|
20
|
+
setFormatSource(formatSource);
|
|
21
|
+
}
|
|
22
|
+
setHanded({ i18n, source: formatSource });
|
|
23
|
+
}
|
|
24
|
+
useLayoutEffect(startInputModality, []);
|
|
25
|
+
return (_jsx(I18nextProvider, { i18n: i18n, children: _jsx(TooltipProvider, { children: children }) }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type KeyboardEvent, type Ref } from 'react';
|
|
2
|
+
export interface WaveformProps {
|
|
3
|
+
levels: readonly number[];
|
|
4
|
+
duration: number;
|
|
5
|
+
range?: readonly [from: number, to: number];
|
|
6
|
+
value?: number;
|
|
7
|
+
onValueChange?(value: number): void;
|
|
8
|
+
onValueCommitted?(value: number): void;
|
|
9
|
+
step?: number;
|
|
10
|
+
onKeyDown?(e: KeyboardEvent<HTMLDivElement>): void;
|
|
11
|
+
marks?: readonly number[];
|
|
12
|
+
label: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
ref?: Ref<HTMLDivElement>;
|
|
15
|
+
}
|
|
16
|
+
export declare function Waveform({ levels, duration, range, value, onValueChange, onValueCommitted, step, onKeyDown, marks, label, className, ref }: WaveformProps): import("react").JSX.Element;
|
package/dist/Waveform.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useRef } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatClockDuration } from './format/duration.js';
|
|
6
|
+
import { mergeRefs } from './merge-refs.js';
|
|
7
|
+
import { useMeasuredWidth } from './useMeasuredWidth.js';
|
|
8
|
+
/* About one bar per 3px, each 2px wide; a bar sits at its own time, so it lines up with the marks and the playhead. */
|
|
9
|
+
const BAR_PITCH = 3;
|
|
10
|
+
const BAR_WIDTH = 2;
|
|
11
|
+
/* The height of a silent bar, so silence still reads as a line. */
|
|
12
|
+
const SILENCE = 0.04;
|
|
13
|
+
/* Page Up and Page Down move this many steps, as a Base UI slider does. */
|
|
14
|
+
const PAGE_STEPS = 10;
|
|
15
|
+
/* The loudest sample under each bar, or null for a bar past either end of the audio. */
|
|
16
|
+
const barsOf = (levels, duration, from, to, count) => {
|
|
17
|
+
if (count <= 0 || levels.length === 0 || duration <= 0 || to <= from) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const perSecond = levels.length / duration;
|
|
21
|
+
return Array.from({ length: count }, (_, i) => {
|
|
22
|
+
const start = from + ((to - from) * i) / count;
|
|
23
|
+
const end = from + ((to - from) * (i + 1)) / count;
|
|
24
|
+
if (end <= 0 || start >= duration) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const first = Math.max(0, Math.floor(start * perSecond));
|
|
28
|
+
const last = Math.min(levels.length - 1, Math.max(first, Math.ceil(end * perSecond) - 1));
|
|
29
|
+
let level = 0;
|
|
30
|
+
for (let index = first; index <= last; index++) {
|
|
31
|
+
level = Math.max(level, levels[index] ?? 0);
|
|
32
|
+
}
|
|
33
|
+
return Math.min(1, Math.max(SILENCE, level));
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
/* The loudness of a piece of audio over time, played up to the playhead, where a press or a drag seeks. */
|
|
37
|
+
export function Waveform({ levels, duration, range, value, onValueChange, onValueCommitted, step = 1, onKeyDown, marks = [], label, className, ref }) {
|
|
38
|
+
const { t } = useTranslation('ui');
|
|
39
|
+
const [measure, width] = useMeasuredWidth();
|
|
40
|
+
const rootRef = useMemo(() => mergeRefs(ref, measure), [ref, measure]);
|
|
41
|
+
const dragged = useRef(0);
|
|
42
|
+
const [from, to] = range ?? [0, duration];
|
|
43
|
+
const span = to - from;
|
|
44
|
+
const count = Math.floor(width / BAR_PITCH);
|
|
45
|
+
const bars = useMemo(() => barsOf(levels, duration, from, to, count), [levels, duration, from, to, count]);
|
|
46
|
+
const positionOf = (time) => (span > 0 ? (time - from) / span : 0);
|
|
47
|
+
const inView = (time) => positionOf(time) >= 0 && positionOf(time) <= 1;
|
|
48
|
+
const clamp = (time) => Math.min(duration, Math.max(0, time));
|
|
49
|
+
const seekAt = (e) => {
|
|
50
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
51
|
+
if (rect.width > 0) {
|
|
52
|
+
dragged.current = clamp(from + ((e.clientX - rect.left) / rect.width) * span);
|
|
53
|
+
onValueChange?.(dragged.current);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const letGo = (e) => {
|
|
57
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
58
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
59
|
+
onValueCommitted?.(dragged.current);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const moveByKey = (e) => {
|
|
63
|
+
onKeyDown?.(e);
|
|
64
|
+
if (e.defaultPrevented) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const at = value ?? 0;
|
|
68
|
+
const moves = {
|
|
69
|
+
ArrowLeft: at - step,
|
|
70
|
+
ArrowDown: at - step,
|
|
71
|
+
ArrowRight: at + step,
|
|
72
|
+
ArrowUp: at + step,
|
|
73
|
+
PageDown: at - step * PAGE_STEPS,
|
|
74
|
+
PageUp: at + step * PAGE_STEPS,
|
|
75
|
+
Home: 0,
|
|
76
|
+
End: duration
|
|
77
|
+
};
|
|
78
|
+
const next = moves[e.key];
|
|
79
|
+
if (next === undefined) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// The slider owns these keys; a transport listening higher up would move the playhead a second time.
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
onValueChange?.(clamp(next));
|
|
86
|
+
onValueCommitted?.(clamp(next));
|
|
87
|
+
};
|
|
88
|
+
const slider = onValueChange === undefined
|
|
89
|
+
? { role: 'img' }
|
|
90
|
+
: {
|
|
91
|
+
role: 'slider',
|
|
92
|
+
tabIndex: 0,
|
|
93
|
+
'aria-valuemin': 0,
|
|
94
|
+
'aria-valuemax': duration,
|
|
95
|
+
'aria-valuenow': value ?? 0,
|
|
96
|
+
'aria-valuetext': t('waveform.position', {
|
|
97
|
+
time: formatClockDuration((value ?? 0) * 1000),
|
|
98
|
+
duration: formatClockDuration(duration * 1000)
|
|
99
|
+
}),
|
|
100
|
+
onPointerDown: (e) => {
|
|
101
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
102
|
+
seekAt(e);
|
|
103
|
+
},
|
|
104
|
+
onPointerMove: (e) => {
|
|
105
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
106
|
+
seekAt(e);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
onPointerUp: letGo,
|
|
110
|
+
onPointerCancel: letGo,
|
|
111
|
+
onKeyDown: moveByKey
|
|
112
|
+
};
|
|
113
|
+
return (_jsxs("div", { ref: rootRef, "aria-label": label, ...slider, className: clsx('relative select-none', onValueChange !== undefined && 'focus-ring cursor-ew-resize touch-none', className), children: [_jsxs("svg", { className: "absolute inset-0 size-full", viewBox: `0 0 ${Math.max(1, width)} 100`, preserveAspectRatio: "none", "aria-hidden": true, children: [bars.map((level, i) => {
|
|
114
|
+
if (level === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const center = from + (span * (i + 0.5)) / count;
|
|
118
|
+
return (_jsx("rect", { x: Math.round((i * width) / count), y: (100 - level * 100) / 2, width: BAR_WIDTH, height: level * 100, className: value !== undefined && center <= value ? 'fill-accent' : 'fill-text-faint' }, i));
|
|
119
|
+
}), marks.filter(inView).map((mark) => {
|
|
120
|
+
const x = Math.round(positionOf(mark) * width) + 0.5;
|
|
121
|
+
return (_jsx("line", { x1: x, x2: x, y1: 0, y2: 100, strokeWidth: 1, vectorEffect: "non-scaling-stroke", className: "stroke-accent opacity-60" }, mark));
|
|
122
|
+
})] }), value !== undefined && inView(value) && (_jsx("span", { className: "pointer-events-none absolute inset-y-0 w-px bg-accent", style: { left: `${positionOf(value) * 100}%` } }))] }));
|
|
123
|
+
}
|
package/dist/Wipe.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
export interface WipeProps {
|
|
3
|
+
value: number;
|
|
4
|
+
onValueChange(value: number): void;
|
|
5
|
+
before: ReactNode;
|
|
6
|
+
after: ReactNode;
|
|
7
|
+
label: string;
|
|
8
|
+
handle?: 'knob' | 'line';
|
|
9
|
+
className?: string;
|
|
10
|
+
ref?: Ref<HTMLDivElement>;
|
|
11
|
+
}
|
|
12
|
+
export declare function Wipe({ value: split, onValueChange: onSplit, before, after, label, handle, className, ref }: WipeProps): import("react").JSX.Element;
|
package/dist/Wipe.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { ChevronsLeftRight } from 'lucide-react';
|
|
5
|
+
import { Icon } from './Icon.js';
|
|
6
|
+
import { formatPercent } from './format/number.js';
|
|
7
|
+
import { mergeRefs } from './merge-refs.js';
|
|
8
|
+
import { splitAt, splitForKey } from './wipe-split.js';
|
|
9
|
+
/*
|
|
10
|
+
* Two versions of one picture in one frame: `before` left of the split and `after` right of it, with a
|
|
11
|
+
* handle between them a person drags or moves with the arrow keys. It fills the positioned element it
|
|
12
|
+
* sits in. The handle is a line with a round knob on it, or the plain line, which is dragged anywhere
|
|
13
|
+
* along its height.
|
|
14
|
+
*/
|
|
15
|
+
export function Wipe({ value: split, onValueChange: onSplit, before, after, label, handle = 'knob', className, ref }) {
|
|
16
|
+
const frame = useRef(null);
|
|
17
|
+
const percent = `${split * 100}%`;
|
|
18
|
+
function moveTo(clientX) {
|
|
19
|
+
const rect = frame.current?.getBoundingClientRect();
|
|
20
|
+
const next = rect ? splitAt(clientX, rect) : null;
|
|
21
|
+
if (next !== null) {
|
|
22
|
+
onSplit(next);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function onPointerDown(e) {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
e.stopPropagation();
|
|
28
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
29
|
+
moveTo(e.clientX);
|
|
30
|
+
}
|
|
31
|
+
function onPointerMove(e) {
|
|
32
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
33
|
+
moveTo(e.clientX);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function onKeyDown(e) {
|
|
37
|
+
const next = splitForKey(split, e.key);
|
|
38
|
+
if (next === null) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Only while the handle has the focus, so a canvas or a timeline around it never hears these keys.
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
onSplit(next);
|
|
45
|
+
}
|
|
46
|
+
return (_jsxs("div", { ref: mergeRefs(frame, ref), className: clsx('absolute inset-0', className), children: [_jsx("div", { className: "absolute inset-0", children: before }), _jsx("div", { className: "absolute inset-0", style: { clipPath: `inset(0 0 0 ${percent})` }, children: after }), _jsxs("div", { role: "slider", tabIndex: 0, "aria-label": label, "aria-orientation": "horizontal", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(split * 100), "aria-valuetext": formatPercent(Math.round(split * 100)), className: clsx('group absolute inset-y-0 z-10 -ml-3.5 w-7 cursor-ew-resize touch-none', handle === 'line' && 'outline-none'), style: { left: percent }, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onKeyDown: onKeyDown, children: [_jsx("span", { className: clsx('absolute inset-y-0 left-3.25 w-0.5 bg-media-handle shadow-media-handle-edge', handle === 'line' && 'group-focus-visible:bg-accent') }), handle === 'knob' && (_jsx("span", { className: "absolute top-1/2 left-3.5 grid h-7 w-7 -translate-1/2 place-items-center rounded-full bg-media-handle text-media-handle-text shadow-media-handle", children: _jsx(Icon, { icon: ChevronsLeftRight, size: 14 }) }))] })] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
2
|
+
import type { Shortcut } from './shortcut.ts';
|
|
3
|
+
export interface ZoomSelection {
|
|
4
|
+
label: string;
|
|
5
|
+
shortcut?: Shortcut;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
onZoom(): void;
|
|
8
|
+
}
|
|
9
|
+
export interface ZoomLabels {
|
|
10
|
+
out?: string;
|
|
11
|
+
in?: string;
|
|
12
|
+
presets?: string;
|
|
13
|
+
fit?: string;
|
|
14
|
+
fitEverything?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ZoomControlsProps {
|
|
17
|
+
zoom: number;
|
|
18
|
+
onZoomChange(zoom: number): void;
|
|
19
|
+
onFitAll(): void;
|
|
20
|
+
presets?: readonly number[];
|
|
21
|
+
labels?: ZoomLabels;
|
|
22
|
+
shortcuts?: {
|
|
23
|
+
zoomReset?: Shortcut;
|
|
24
|
+
fitAll?: Shortcut;
|
|
25
|
+
};
|
|
26
|
+
selection?: ZoomSelection;
|
|
27
|
+
className?: string;
|
|
28
|
+
ref?: Ref<HTMLDivElement>;
|
|
29
|
+
}
|
|
30
|
+
export declare function ZoomControls({ zoom, onZoomChange, onFitAll, presets, labels, shortcuts, selection, className, ref }: ZoomControlsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Maximize, Minus, Plus, Scan } from 'lucide-react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { ButtonGroup } from './ButtonGroup.js';
|
|
5
|
+
import { Icon } from './Icon.js';
|
|
6
|
+
import { IconButton } from './IconButton.js';
|
|
7
|
+
import { Kbd } from './Kbd.js';
|
|
8
|
+
import { MenuItem, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuRoot, MenuSeparator, MenuTrigger } from './menu/parts.js';
|
|
9
|
+
import { Tooltip } from './Tooltip.js';
|
|
10
|
+
import { activeZoomPreset, steppedZoom, ZOOM_PRESETS } from './zoom.js';
|
|
11
|
+
/* The zoom group of a dock: out, the readout with its presets, in, and fit everything. */
|
|
12
|
+
export function ZoomControls({ zoom, onZoomChange, onFitAll, presets = ZOOM_PRESETS, labels = {}, shortcuts = {}, selection, className, ref }) {
|
|
13
|
+
const { t } = useTranslation('ui');
|
|
14
|
+
const preset = activeZoomPreset(zoom, presets);
|
|
15
|
+
return (_jsxs(ButtonGroup, { ref: ref, className: className, children: [_jsx(IconButton, { icon: Minus, label: labels.out ?? t('zoom.out'), onClick: () => onZoomChange(steppedZoom(zoom, -10)) }), _jsxs(MenuRoot, { children: [_jsx(Tooltip, { label: labels.presets ?? t('zoom.presets'), children: _jsxs(MenuTrigger, { className: "h-8 min-w-14 rounded-lg px-1 text-xs text-text-muted tabular-nums hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text", children: [Math.round(zoom * 100), "%"] }) }), _jsxs(MenuPopup, { side: "top", sideOffset: 10, align: "center", className: "min-w-44", children: [_jsx(MenuRadioGroup, { value: preset, onValueChange: (value) => onZoomChange(value / 100), children: presets.map((percent) => (_jsxs(MenuRadioItem, { value: percent, children: [_jsxs("span", { className: "tabular-nums", children: [percent, "%"] }), percent === 100 && shortcuts.zoomReset && _jsx(Kbd, { shortcut: shortcuts.zoomReset })] }, percent))) }), _jsx(MenuSeparator, {}), _jsxs(MenuItem, { onClick: onFitAll, children: [_jsx("span", { className: "grid h-4 w-4 place-items-center", children: _jsx(Icon, { icon: Maximize, size: 14 }) }), labels.fit ?? t('zoom.fit'), " ", shortcuts.fitAll && _jsx(Kbd, { shortcut: shortcuts.fitAll })] }), selection && (_jsxs(MenuItem, { disabled: !selection.enabled, onClick: selection.onZoom, children: [_jsx("span", { className: "grid h-4 w-4 place-items-center", children: _jsx(Icon, { icon: Scan, size: 14 }) }), selection.label, " ", selection.shortcut && _jsx(Kbd, { shortcut: selection.shortcut })] }))] })] }), _jsx(IconButton, { icon: Plus, label: labels.in ?? t('zoom.in'), onClick: () => onZoomChange(steppedZoom(zoom, 10)) }), _jsx(IconButton, { icon: Maximize, label: labels.fitEverything ?? t('zoom.fitEverything'), kbd: shortcuts.fitAll, onClick: onFitAll })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The clipboard, as the app uses it. A browser hands text over only to a document that is focused
|
|
3
|
+
* and allowed, and there is nothing a person can do about a refusal, so both sides stay quiet.
|
|
4
|
+
*/
|
|
5
|
+
export const copyText = (text) => {
|
|
6
|
+
void navigator.clipboard?.writeText(text).catch(() => undefined);
|
|
7
|
+
};
|
|
8
|
+
/* What is on the clipboard, or nothing at all when the browser will not say. */
|
|
9
|
+
export const readClipboardText = async () => {
|
|
10
|
+
try {
|
|
11
|
+
return (await navigator.clipboard?.readText()) ?? '';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MenuCheck as Check, MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuHint as Hint, MenuItem as Item, MenuLabel as Label, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, ContextMenuRoot as Root, MenuRow as Row, MenuRowAction as RowAction, MenuRowSubmenuTrigger as RowSubmenuTrigger, MenuSeparator as Separator, MenuSubmenuRoot as SubmenuRoot, MenuSubmenuTrigger as SubmenuTrigger, ContextMenuTrigger as Trigger } from '../menu/parts.tsx';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/* A context menu is a menu that opens at the pointer; every part inside it is the menu's own. */
|
|
2
|
+
export { MenuCheck as Check, MenuCheckboxItem as CheckboxItem, MenuGroup as Group, MenuGroupLabel as GroupLabel, MenuHint as Hint, MenuItem as Item, MenuLabel as Label, MenuPopup as Popup, MenuRadioGroup as RadioGroup, MenuRadioItem as RadioItem, ContextMenuRoot as Root, MenuRow as Row, MenuRowAction as RowAction, MenuRowSubmenuTrigger as RowSubmenuTrigger, MenuSeparator as Separator, MenuSubmenuRoot as SubmenuRoot, MenuSubmenuTrigger as SubmenuTrigger, ContextMenuTrigger as Trigger } from '../menu/parts.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DialogClose as Close, DialogDescription as Description, DialogFooter as Footer, DialogPopup as Popup, DialogRoot as Root, DialogText as Text, DialogTitle as Title, DialogTrigger as Trigger } from './parts.tsx';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DialogClose as Close, DialogDescription as Description, DialogFooter as Footer, DialogPopup as Popup, DialogRoot as Root, DialogText as Text, DialogTitle as Title, DialogTrigger as Trigger } from './parts.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react';
|
|
2
|
+
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
|
3
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
4
|
+
declare const SIZE: {
|
|
5
|
+
readonly sm: 'w-[420px] p-5';
|
|
6
|
+
};
|
|
7
|
+
export declare function DialogRoot({ open, defaultOpen, onOpenChange, ...props }: ComponentProps<typeof BaseDialog.Root>): import("react").JSX.Element;
|
|
8
|
+
export declare const DialogTrigger: BaseDialog.Trigger;
|
|
9
|
+
export declare const DialogClose: import("react").ForwardRefExoticComponent<import("@base-ui-components/react/dialog").DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
10
|
+
export type DialogPopupProps = ComponentProps<typeof BaseDialog.Popup> & {
|
|
11
|
+
size?: keyof typeof SIZE;
|
|
12
|
+
nested?: boolean;
|
|
13
|
+
backdrop?: boolean;
|
|
14
|
+
backdropClassName?: string;
|
|
15
|
+
keepMounted?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function DialogPopup({ size, nested, backdrop, backdropClassName, keepMounted, className, ...props }: DialogPopupProps): import("react").JSX.Element;
|
|
18
|
+
export type DialogTitleProps = ComponentProps<typeof BaseDialog.Title> & {
|
|
19
|
+
size?: 'base' | 'lg';
|
|
20
|
+
};
|
|
21
|
+
export declare function DialogTitle({ size, className, ...props }: DialogTitleProps): import("react").JSX.Element;
|
|
22
|
+
declare const DESCRIPTION: {
|
|
23
|
+
readonly sm: 'text-sm text-text-muted';
|
|
24
|
+
readonly xs: 'text-xs text-text-muted';
|
|
25
|
+
};
|
|
26
|
+
export type DialogDescriptionProps = ComponentProps<typeof BaseDialog.Description> & {
|
|
27
|
+
size?: keyof typeof DESCRIPTION;
|
|
28
|
+
};
|
|
29
|
+
export declare function DialogDescription({ size, className, ...props }: DialogDescriptionProps): import("react").JSX.Element;
|
|
30
|
+
export type DialogTextProps = useRender.ComponentProps<'p'> & {
|
|
31
|
+
size?: keyof typeof DESCRIPTION;
|
|
32
|
+
};
|
|
33
|
+
export declare function DialogText({ size, render, className, ref, ...props }: DialogTextProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
34
|
+
export type DialogFooterProps = useRender.ComponentProps<'div'>;
|
|
35
|
+
export declare function DialogFooter({ render, className, ref, ...props }: DialogFooterProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
|
|
5
|
+
import { useRender } from '@base-ui-components/react/use-render';
|
|
6
|
+
import { withClass } from '../class-name.js';
|
|
7
|
+
import { useDialogLayer } from '../dialog-layer.js';
|
|
8
|
+
/* Whether the dialog is open, for its popup to decide whether it stacks over another one. */
|
|
9
|
+
const DialogOpenContext = createContext(false);
|
|
10
|
+
/* A question or a form of a few fields. Every one is as wide as the others, whatever it asks. */
|
|
11
|
+
const SIZE = {
|
|
12
|
+
sm: 'w-[420px] p-5'
|
|
13
|
+
};
|
|
14
|
+
export function DialogRoot({ open, defaultOpen, onOpenChange, ...props }) {
|
|
15
|
+
// Followed rather than taken over, so a dialog that keeps its own state still does.
|
|
16
|
+
const [seen, setSeen] = useState(defaultOpen ?? false);
|
|
17
|
+
return (_jsx(DialogOpenContext, { value: open ?? seen, children: _jsx(BaseDialog.Root, { open: open, defaultOpen: defaultOpen, onOpenChange: (next, details) => {
|
|
18
|
+
onOpenChange?.(next, details);
|
|
19
|
+
if (!details.isCanceled) {
|
|
20
|
+
setSeen(next);
|
|
21
|
+
}
|
|
22
|
+
}, ...props }) }));
|
|
23
|
+
}
|
|
24
|
+
export const DialogTrigger = BaseDialog.Trigger;
|
|
25
|
+
export const DialogClose = BaseDialog.Close;
|
|
26
|
+
/*
|
|
27
|
+
* The portal, the backdrop and the popup of a dialog in one part. A dialog that opens while another
|
|
28
|
+
* one is up, such as one from the menu bar over a settings dialog, takes the nested steps and a
|
|
29
|
+
* backdrop of its own, or the one underneath stays on top and keeps taking clicks.
|
|
30
|
+
*/
|
|
31
|
+
export function DialogPopup({ size, nested = false, backdrop = true, backdropClassName, keepMounted, className, ...props }) {
|
|
32
|
+
const stacked = useDialogLayer(useContext(DialogOpenContext)) || nested;
|
|
33
|
+
return (_jsxs(BaseDialog.Portal, { keepMounted: keepMounted, children: [backdrop && (_jsx(BaseDialog.Backdrop, { className: clsx('dialog-backdrop', stacked && 'dialog-backdrop-nested', backdropClassName), forceRender: stacked })), _jsx(BaseDialog.Popup, { className: withClass(clsx('dialog-popup', size !== undefined && SIZE[size], stacked && 'dialog-popup-nested'), className), ...props })] }));
|
|
34
|
+
}
|
|
35
|
+
export function DialogTitle({ size = 'base', className, ...props }) {
|
|
36
|
+
return _jsx(BaseDialog.Title, { className: withClass(clsx(size === 'lg' ? 'text-lg' : 'text-base', 'font-semibold text-text'), className), ...props });
|
|
37
|
+
}
|
|
38
|
+
const DESCRIPTION = { sm: 'text-sm text-text-muted', xs: 'text-xs text-text-muted' };
|
|
39
|
+
/* The sentence under the title, which is also what a screen reader reads as the dialog's description. It carries no margin. */
|
|
40
|
+
export function DialogDescription({ size = 'sm', className, ...props }) {
|
|
41
|
+
return _jsx(BaseDialog.Description, { className: withClass(DESCRIPTION[size], className), ...props });
|
|
42
|
+
}
|
|
43
|
+
/* A further line in the style of the description that is not the dialog's description: a warning, a note under a choice. */
|
|
44
|
+
export function DialogText({ size = 'sm', render, className, ref, ...props }) {
|
|
45
|
+
return useRender({ render, ref, defaultTagName: 'p', props: { ...props, className: clsx(DESCRIPTION[size], className) } });
|
|
46
|
+
}
|
|
47
|
+
/* The buttons at the foot of a dialog, the main action last and so on the right. */
|
|
48
|
+
export function DialogFooter({ render, className, ref, ...props }) {
|
|
49
|
+
return useRender({
|
|
50
|
+
render,
|
|
51
|
+
ref,
|
|
52
|
+
defaultTagName: 'div',
|
|
53
|
+
props: { ...props, className: clsx('mt-4 flex flex-wrap items-center justify-end gap-2', className) }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useDialogLayer(isOpen: boolean): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
// The app-level dialogs open right now. Each one is its own root, so Base UI does not see them as nested.
|
|
3
|
+
const open = new Set();
|
|
4
|
+
/*
|
|
5
|
+
* Whether a dialog opened while another app-level dialog was already up, such as one opened from the
|
|
6
|
+
* menu bar over a settings dialog. Such a dialog takes the nested steps (`dialog-backdrop-nested`,
|
|
7
|
+
* `dialog-popup-nested`, a forced backdrop), or the one underneath stays on top and keeps taking clicks.
|
|
8
|
+
*/
|
|
9
|
+
export function useDialogLayer(isOpen) {
|
|
10
|
+
const id = useRef(Symbol('dialog'));
|
|
11
|
+
// Decided in render at the moment the dialog opens, so its first frame is already on the right step.
|
|
12
|
+
const [seen, setSeen] = useState({ open: false, stacked: false });
|
|
13
|
+
if (seen.open !== isOpen) {
|
|
14
|
+
setSeen({ open: isOpen, stacked: isOpen && open.size > 0 });
|
|
15
|
+
}
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
if (!isOpen) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const self = id.current;
|
|
21
|
+
open.add(self);
|
|
22
|
+
return () => {
|
|
23
|
+
open.delete(self);
|
|
24
|
+
};
|
|
25
|
+
}, [isOpen]);
|
|
26
|
+
return isOpen && seen.stacked;
|
|
27
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type ResetKeys = readonly unknown[];
|
|
2
|
+
export declare const shouldReset: (failed: boolean, previous: ResetKeys, next: ResetKeys) => boolean;
|
|
3
|
+
export declare const errorMessageOf: (error: unknown) => string;
|
|
4
|
+
export declare const errorReport: (label: string, error: unknown, componentStack: string | null) => string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The pure half of `ErrorBoundary`: when a failed subtree gets another try, and what a person copies
|
|
3
|
+
* out of the message. Kept apart so it can be tested without a DOM.
|
|
4
|
+
*/
|
|
5
|
+
/*
|
|
6
|
+
* Whether a boundary that holds an error should draw its children again. Only a boundary that
|
|
7
|
+
* failed resets. Comparing keys while the children are fine would remount a terminal or an embedded
|
|
8
|
+
* page every time the data under it changes.
|
|
9
|
+
*/
|
|
10
|
+
export const shouldReset = (failed, previous, next) => {
|
|
11
|
+
if (!failed) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (previous.length !== next.length) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return previous.some((key, i) => !Object.is(key, next[i]));
|
|
18
|
+
};
|
|
19
|
+
/* A thrown value is not always an Error, and the message has to say something either way. */
|
|
20
|
+
export const errorMessageOf = (error) => {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
return error.message || error.name;
|
|
23
|
+
}
|
|
24
|
+
return typeof error === 'string' ? error : String(error);
|
|
25
|
+
};
|
|
26
|
+
/* The text behind the copy button: what a bug report needs, the component stack included. */
|
|
27
|
+
export const errorReport = (label, error, componentStack) => {
|
|
28
|
+
const stack = error instanceof Error && error.stack ? error.stack : errorMessageOf(error);
|
|
29
|
+
const parts = [label, stack];
|
|
30
|
+
if (componentStack) {
|
|
31
|
+
parts.push(`Component stack:${componentStack}`);
|
|
32
|
+
}
|
|
33
|
+
return parts.join('\n\n');
|
|
34
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const messageOf: (e: unknown, fallback?: string) => string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/* The sentence of an error, for a person to read. One that crossed an Electron bridge (`ipcRenderer.invoke`)
|
|
2
|
+
carries Electron's own prefix, which says nothing to a person. A throw that is not an Error has no
|
|
3
|
+
sentence in it, so a caller that knows the step names it. */
|
|
4
|
+
export const messageOf = (e, fallback) => (e instanceof Error ? e.message : (fallback ?? String(e))).replace(/^Error invoking remote method '[^']+': (?:[A-Za-z]*Error: )?/, '');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const FieldContext = createContext(null);
|
|
3
|
+
/* What an `Input` or a `TextArea` inside a `Field` takes over: the id its label points at, the hint and the error it is described by. */
|
|
4
|
+
export const useFieldControl = () => useContext(FieldContext);
|