@efolusi/meridian 1.5.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/README.md +12 -0
- package/ai/AgentRun.d.ts +20 -0
- package/ai/AgentRun.js +100 -0
- package/ai/ChatMessage.d.ts +19 -0
- package/ai/ChatMessage.js +81 -0
- package/ai/Citation.d.ts +23 -0
- package/ai/Citation.js +146 -0
- package/ai/Confirmation.d.ts +26 -0
- package/ai/Confirmation.js +84 -0
- package/ai/Conversation.d.ts +15 -0
- package/ai/Conversation.js +81 -0
- package/ai/DocumentCard.d.ts +17 -0
- package/ai/DocumentCard.js +101 -0
- package/ai/FeedbackBar.d.ts +14 -0
- package/ai/FeedbackBar.js +84 -0
- package/ai/GeneratedImage.d.ts +22 -0
- package/ai/GeneratedImage.js +100 -0
- package/ai/ModelSelector.d.ts +21 -0
- package/ai/ModelSelector.js +112 -0
- package/ai/Player.d.ts +18 -0
- package/ai/Player.js +201 -0
- package/ai/PromptComposer.d.ts +28 -0
- package/ai/PromptComposer.js +111 -0
- package/ai/PromptSteps.d.ts +18 -0
- package/ai/PromptSteps.js +167 -0
- package/ai/Reasoning.d.ts +17 -0
- package/ai/Reasoning.js +57 -0
- package/ai/RichComposer.d.ts +25 -0
- package/ai/RichComposer.js +247 -0
- package/ai/Sandbox.d.ts +20 -0
- package/ai/Sandbox.js +96 -0
- package/ai/SelectionQuote.d.ts +15 -0
- package/ai/SelectionQuote.js +123 -0
- package/ai/SourceCard.d.ts +17 -0
- package/ai/SourceCard.js +57 -0
- package/ai/Suggestions.d.ts +10 -0
- package/ai/Suggestions.js +38 -0
- package/ai/Task.d.ts +15 -0
- package/ai/Task.js +49 -0
- package/ai/Todo.d.ts +13 -0
- package/ai/Todo.js +87 -0
- package/ai/ToolCall.d.ts +30 -0
- package/ai/ToolCall.js +147 -0
- package/ai/Transcript.d.ts +27 -0
- package/ai/Transcript.js +96 -0
- package/ai/UsageMeter.d.ts +21 -0
- package/ai/UsageMeter.js +81 -0
- package/ai/WebPreview.d.ts +19 -0
- package/ai/WebPreview.js +179 -0
- package/assets/fonts/BricolageGrotesque-Variable.ttf +0 -0
- package/assets/fonts/Figtree-Italic-Variable.ttf +0 -0
- package/assets/fonts/Figtree-Variable.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Italic-Variable.ttf +0 -0
- package/assets/fonts/JetBrainsMono-Variable.ttf +0 -0
- package/assets/fonts/OFL-BricolageGrotesque.txt +93 -0
- package/assets/fonts/OFL-Figtree.txt +93 -0
- package/assets/fonts/OFL-JetBrainsMono.txt +93 -0
- package/code/CodeBlock.d.ts +17 -0
- package/code/CodeBlock.js +99 -0
- package/code/Console.d.ts +19 -0
- package/code/Console.js +124 -0
- package/code/CopyField.d.ts +10 -0
- package/code/CopyField.js +54 -0
- package/code/Diff.d.ts +26 -0
- package/code/Diff.js +358 -0
- package/code/EnvList.d.ts +14 -0
- package/code/EnvList.js +89 -0
- package/code/Exception.d.ts +25 -0
- package/code/Exception.js +90 -0
- package/code/Terminal.d.ts +20 -0
- package/code/Terminal.js +58 -0
- package/data/BarChart.d.ts +14 -0
- package/data/BarChart.js +44 -0
- package/data/DonutChart.d.ts +17 -0
- package/data/DonutChart.js +114 -0
- package/data/EmptyState.d.ts +13 -0
- package/data/EmptyState.js +39 -0
- package/data/KeyValueList.d.ts +14 -0
- package/data/KeyValueList.js +34 -0
- package/data/LineChart.d.ts +15 -0
- package/data/LineChart.js +85 -0
- package/data/Skeleton.d.ts +11 -0
- package/data/Skeleton.js +49 -0
- package/data/Sparkline.d.ts +14 -0
- package/data/Sparkline.js +42 -0
- package/data/Stat.d.ts +14 -0
- package/data/Stat.js +40 -0
- package/data/StatusDot.d.ts +10 -0
- package/data/StatusDot.js +34 -0
- package/data/Table.d.ts +53 -0
- package/data/Table.js +174 -0
- package/dates/Calendar.d.ts +8 -0
- package/dates/Calendar.js +143 -0
- package/dates/DatePicker.d.ts +12 -0
- package/dates/DatePicker.js +118 -0
- package/display/Accordion.d.ts +15 -0
- package/display/Accordion.js +52 -0
- package/display/AspectRatio.d.ts +8 -0
- package/display/AspectRatio.js +24 -0
- package/display/Avatar.d.ts +17 -0
- package/display/Avatar.js +52 -0
- package/display/Badge.d.ts +10 -0
- package/display/Badge.js +34 -0
- package/display/Card.d.ts +17 -0
- package/display/Card.js +51 -0
- package/display/Carousel.d.ts +16 -0
- package/display/Carousel.js +97 -0
- package/display/Collapsible.d.ts +12 -0
- package/display/Collapsible.js +46 -0
- package/display/Divider.d.ts +9 -0
- package/display/Divider.js +33 -0
- package/display/Kbd.d.ts +7 -0
- package/display/Kbd.js +18 -0
- package/display/Link.d.ts +12 -0
- package/display/Link.js +41 -0
- package/display/ListItem.d.ts +19 -0
- package/display/ListItem.js +61 -0
- package/display/Resizable.d.ts +16 -0
- package/display/Resizable.js +90 -0
- package/display/ScrollArea.d.ts +8 -0
- package/display/ScrollArea.js +28 -0
- package/display/Tag.d.ts +9 -0
- package/display/Tag.js +41 -0
- package/display/Toolbar.d.ts +22 -0
- package/display/Toolbar.js +55 -0
- package/display/TreeList.d.ts +20 -0
- package/display/TreeList.js +101 -0
- package/feedback/Alert.d.ts +14 -0
- package/feedback/Alert.js +56 -0
- package/feedback/Banner.d.ts +15 -0
- package/feedback/Banner.js +49 -0
- package/feedback/ConfirmDialog.d.ts +18 -0
- package/feedback/ConfirmDialog.js +49 -0
- package/feedback/Dialog.d.ts +13 -0
- package/feedback/Dialog.js +107 -0
- package/feedback/Loader.d.ts +12 -0
- package/feedback/Loader.js +41 -0
- package/feedback/Progress.d.ts +15 -0
- package/feedback/Progress.js +48 -0
- package/feedback/Spinner.d.ts +9 -0
- package/feedback/Spinner.js +38 -0
- package/feedback/Toast.d.ts +70 -0
- package/feedback/Toast.js +216 -0
- package/feedback/Tooltip.d.ts +12 -0
- package/feedback/Tooltip.js +86 -0
- package/files/FileDrop.d.ts +15 -0
- package/files/FileDrop.js +75 -0
- package/files/FileTile.d.ts +19 -0
- package/files/FileTile.js +96 -0
- package/files/FileTypeIcon.d.ts +11 -0
- package/files/FileTypeIcon.js +121 -0
- package/finance/PaymentCard.d.ts +17 -0
- package/finance/PaymentCard.js +109 -0
- package/forms/Button.d.ts +15 -0
- package/forms/Button.js +82 -0
- package/forms/ButtonGroup.d.ts +5 -0
- package/forms/ButtonGroup.js +22 -0
- package/forms/ButtonTile.d.ts +21 -0
- package/forms/ButtonTile.js +69 -0
- package/forms/Checkbox.d.ts +6 -0
- package/forms/Checkbox.js +52 -0
- package/forms/Combobox.d.ts +17 -0
- package/forms/Combobox.js +162 -0
- package/forms/DigitEntry.d.ts +15 -0
- package/forms/DigitEntry.js +88 -0
- package/forms/FormField.d.ts +39 -0
- package/forms/FormField.js +127 -0
- package/forms/IconButton.d.ts +11 -0
- package/forms/IconButton.js +41 -0
- package/forms/Input.d.ts +14 -0
- package/forms/Input.js +75 -0
- package/forms/InputGroup.d.ts +15 -0
- package/forms/InputGroup.js +59 -0
- package/forms/Label.d.ts +11 -0
- package/forms/Label.js +29 -0
- package/forms/Radio.d.ts +6 -0
- package/forms/Radio.js +46 -0
- package/forms/Select.d.ts +10 -0
- package/forms/Select.js +68 -0
- package/forms/Slider.d.ts +18 -0
- package/forms/Slider.js +75 -0
- package/forms/Switch.d.ts +6 -0
- package/forms/Switch.js +46 -0
- package/forms/Textarea.d.ts +8 -0
- package/forms/Textarea.js +58 -0
- package/forms/Toggle.d.ts +26 -0
- package/forms/Toggle.js +85 -0
- package/icons/Icon.d.ts +13 -0
- package/icons/Icon.js +54 -0
- package/index.d.ts +106 -0
- package/index.js +106 -0
- package/navigation/Breadcrumbs.d.ts +12 -0
- package/navigation/Breadcrumbs.js +41 -0
- package/navigation/Menubar.d.ts +10 -0
- package/navigation/Menubar.js +173 -0
- package/navigation/PageControl.d.ts +11 -0
- package/navigation/PageControl.js +36 -0
- package/navigation/Pagination.d.ts +9 -0
- package/navigation/Pagination.js +68 -0
- package/navigation/SegmentedControl.d.ts +17 -0
- package/navigation/SegmentedControl.js +42 -0
- package/navigation/SideNav.d.ts +18 -0
- package/navigation/SideNav.js +87 -0
- package/navigation/Steps.d.ts +15 -0
- package/navigation/Steps.js +61 -0
- package/navigation/Tabs.d.ts +17 -0
- package/navigation/Tabs.js +76 -0
- package/navigation/TopNav.d.ts +10 -0
- package/navigation/TopNav.js +26 -0
- package/overlay/CommandPalette.d.ts +21 -0
- package/overlay/CommandPalette.js +164 -0
- package/overlay/ContextMenu.d.ts +11 -0
- package/overlay/ContextMenu.js +140 -0
- package/overlay/Drawer.d.ts +14 -0
- package/overlay/Drawer.js +99 -0
- package/overlay/HoverCard.d.ts +17 -0
- package/overlay/HoverCard.js +74 -0
- package/overlay/Menu.d.ts +24 -0
- package/overlay/Menu.js +153 -0
- package/overlay/Popover.d.ts +16 -0
- package/overlay/Popover.js +96 -0
- package/overlay/Portal.d.ts +10 -0
- package/overlay/Portal.js +127 -0
- package/package.json +41 -0
- package/styles.css +6 -0
- package/tokens/base.css +10 -0
- package/tokens/colors.css +38 -0
- package/tokens/effects.css +22 -0
- package/tokens/fonts.css +5 -0
- package/tokens/spacing.css +12 -0
- package/tokens/typography.css +9 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface DialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
/** Called on ✕, ESC, or overlay click; omit to hide ✕ */
|
|
4
|
+
onClose?: () => void;
|
|
5
|
+
title: React.ReactNode;
|
|
6
|
+
description?: React.ReactNode;
|
|
7
|
+
/** Right-aligned action row on sunken strip */
|
|
8
|
+
footer?: React.ReactNode;
|
|
9
|
+
/** Max width px @default 440 */
|
|
10
|
+
width?: number;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare const Dialog: React.ForwardRefExoticComponent<DialogProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Icon } from "../icons/Icon.js";
|
|
4
|
+
import { IconButton } from "../forms/IconButton.js";
|
|
5
|
+
import { injectEfCss } from "../forms/Button.js";
|
|
6
|
+
const CSS = `
|
|
7
|
+
.ef-dialog__overlay{position:fixed;inset:0;background:var(--overlay-scrim);display:flex;align-items:center;justify-content:center;padding:24px;z-index:var(--z-overlay);animation:ef-fade var(--dur-med) var(--ease-out)}
|
|
8
|
+
.ef-dialog{width:100%;max-width:440px;background:var(--surface-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-pop);animation:ef-pop var(--dur-slow) var(--ease-spring);overflow:hidden}
|
|
9
|
+
.ef-dialog:focus{outline:none}
|
|
10
|
+
.ef-dialog__head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:20px 24px 0}
|
|
11
|
+
.ef-dialog__title{font-family:var(--font-display);font-size:var(--text-xl);font-weight:var(--weight-bold);letter-spacing:var(--tracking-tight);color:var(--text-primary)}
|
|
12
|
+
.ef-dialog__desc{font-size:var(--text-md);color:var(--text-secondary);margin-top:6px}
|
|
13
|
+
.ef-dialog__body{padding:16px 24px 24px}
|
|
14
|
+
.ef-dialog__foot{display:flex;justify-content:flex-end;gap:8px;padding:14px 24px;background:var(--surface-subtle);border-top:1px solid var(--border-default)}
|
|
15
|
+
@keyframes ef-fade{from{opacity:0}}
|
|
16
|
+
@keyframes ef-pop{from{opacity:0;transform:scale(.94) translateY(8px)}}
|
|
17
|
+
`;
|
|
18
|
+
const FOCUSABLE = 'button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])';
|
|
19
|
+
export const Dialog = /*#__PURE__*/React.forwardRef(function Dialog({
|
|
20
|
+
open,
|
|
21
|
+
onClose,
|
|
22
|
+
title,
|
|
23
|
+
description,
|
|
24
|
+
footer,
|
|
25
|
+
width = 440,
|
|
26
|
+
children,
|
|
27
|
+
...rest
|
|
28
|
+
}, ref) {
|
|
29
|
+
injectEfCss('ef-css-dialog', CSS);
|
|
30
|
+
const panelRef = React.useRef(null);
|
|
31
|
+
const prevFocus = React.useRef(null);
|
|
32
|
+
const titleId = React.useId();
|
|
33
|
+
const descId = React.useId();
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (!open) return;
|
|
36
|
+
prevFocus.current = document.activeElement;
|
|
37
|
+
const panel = panelRef.current;
|
|
38
|
+
const first = panel && panel.querySelector(FOCUSABLE);
|
|
39
|
+
(first || panel).focus();
|
|
40
|
+
return () => {
|
|
41
|
+
if (prevFocus.current && prevFocus.current.focus) prevFocus.current.focus();
|
|
42
|
+
};
|
|
43
|
+
}, [open]);
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (!open) return;
|
|
46
|
+
const onKey = e => {
|
|
47
|
+
if (e.key === 'Escape') {
|
|
48
|
+
if (onClose) onClose();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (e.key !== 'Tab') return;
|
|
52
|
+
const panel = panelRef.current;
|
|
53
|
+
const f = panel ? Array.from(panel.querySelectorAll(FOCUSABLE)).filter(el => !el.disabled) : [];
|
|
54
|
+
if (!f.length) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const i = f.indexOf(document.activeElement);
|
|
59
|
+
if (e.shiftKey && i <= 0) {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
f[f.length - 1].focus();
|
|
62
|
+
} else if (!e.shiftKey && (i === -1 || i === f.length - 1)) {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
f[0].focus();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
document.addEventListener('keydown', onKey);
|
|
68
|
+
return () => document.removeEventListener('keydown', onKey);
|
|
69
|
+
}, [open, onClose]);
|
|
70
|
+
if (!open) return null;
|
|
71
|
+
return /*#__PURE__*/React.createElement("div", _extends({
|
|
72
|
+
ref: ref
|
|
73
|
+
}, rest, {
|
|
74
|
+
className: "ef-dialog__overlay",
|
|
75
|
+
onMouseDown: e => {
|
|
76
|
+
if (e.target === e.currentTarget && onClose) onClose();
|
|
77
|
+
}
|
|
78
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
79
|
+
className: "ef-dialog",
|
|
80
|
+
role: "dialog",
|
|
81
|
+
"aria-modal": "true",
|
|
82
|
+
ref: panelRef,
|
|
83
|
+
tabIndex: -1,
|
|
84
|
+
"aria-labelledby": title ? titleId : undefined,
|
|
85
|
+
"aria-describedby": description ? descId : undefined,
|
|
86
|
+
style: {
|
|
87
|
+
maxWidth: width
|
|
88
|
+
}
|
|
89
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
90
|
+
className: "ef-dialog__head"
|
|
91
|
+
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
|
|
92
|
+
className: "ef-dialog__title",
|
|
93
|
+
id: titleId
|
|
94
|
+
}, title), description ? /*#__PURE__*/React.createElement("div", {
|
|
95
|
+
className: "ef-dialog__desc",
|
|
96
|
+
id: descId
|
|
97
|
+
}, description) : null), onClose ? /*#__PURE__*/React.createElement(IconButton, {
|
|
98
|
+
icon: "x",
|
|
99
|
+
label: "Close",
|
|
100
|
+
size: "sm",
|
|
101
|
+
onClick: onClose
|
|
102
|
+
}) : null), /*#__PURE__*/React.createElement("div", {
|
|
103
|
+
className: "ef-dialog__body"
|
|
104
|
+
}, children), footer ? /*#__PURE__*/React.createElement("div", {
|
|
105
|
+
className: "ef-dialog__foot"
|
|
106
|
+
}, footer) : null));
|
|
107
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LoaderProps {
|
|
2
|
+
/** 'pulse' | 'shimmer' */
|
|
3
|
+
variant?: 'pulse' | 'shimmer';
|
|
4
|
+
/** Trailing animated ellipsis */
|
|
5
|
+
dots?: boolean;
|
|
6
|
+
/** Cycle duration in seconds */
|
|
7
|
+
duration?: number;
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function Loader(props: LoaderProps): React.JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { injectEfCss } from "../forms/Button.js";
|
|
4
|
+
const CSS = `
|
|
5
|
+
.ef-loader{display:inline-flex;align-items:baseline;font-family:var(--font-sans);font-size:13.5px;color:var(--text-muted)}
|
|
6
|
+
.ef-loader--pulse{animation:ef-loader-pulse var(--ef-loader-dur,1.4s) ease-in-out infinite both}
|
|
7
|
+
@keyframes ef-loader-pulse{0%,100%{opacity:.4}50%{opacity:1}}
|
|
8
|
+
.ef-loader--shimmer{background:linear-gradient(90deg,color-mix(in oklab,currentColor 40%,transparent) 0%,color-mix(in oklab,currentColor 40%,transparent) 35%,currentColor 50%,color-mix(in oklab,currentColor 40%,transparent) 65%,color-mix(in oklab,currentColor 40%,transparent) 100%);background-size:200% 100%;-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;animation:ef-loader-shimmer var(--ef-loader-dur,2.2s) linear infinite}
|
|
9
|
+
@keyframes ef-loader-shimmer{0%{background-position:100% 0}100%{background-position:-100% 0}}
|
|
10
|
+
.ef-loader__dots{margin-left:2px;display:inline-flex;-webkit-text-fill-color:currentColor}
|
|
11
|
+
.ef-loader__dots span{animation:ef-loader-dot var(--ef-loader-dur,1.4s) ease-in-out infinite both}
|
|
12
|
+
.ef-loader__dots span:nth-child(2){animation-delay:calc(var(--ef-loader-dur,1.4s)*.14)}
|
|
13
|
+
.ef-loader__dots span:nth-child(3){animation-delay:calc(var(--ef-loader-dur,1.4s)*.28)}
|
|
14
|
+
@keyframes ef-loader-dot{0%,80%,100%{opacity:0}40%{opacity:1}}
|
|
15
|
+
`;
|
|
16
|
+
export function Loader({
|
|
17
|
+
variant = 'pulse',
|
|
18
|
+
dots,
|
|
19
|
+
duration,
|
|
20
|
+
children = 'Thinking',
|
|
21
|
+
style,
|
|
22
|
+
className,
|
|
23
|
+
...rest
|
|
24
|
+
}) {
|
|
25
|
+
injectEfCss('ef-css-loader', CSS);
|
|
26
|
+
const vars = duration ? {
|
|
27
|
+
'--ef-loader-dur': duration + 's'
|
|
28
|
+
} : null;
|
|
29
|
+
return /*#__PURE__*/React.createElement("span", _extends({}, rest, {
|
|
30
|
+
role: "status",
|
|
31
|
+
"aria-label": typeof children === 'string' ? children : 'Loading',
|
|
32
|
+
className: `ef-loader ef-loader--${variant}${className ? ' ' + className : ''}`,
|
|
33
|
+
style: {
|
|
34
|
+
...vars,
|
|
35
|
+
...style
|
|
36
|
+
}
|
|
37
|
+
}), children, dots ? /*#__PURE__*/React.createElement("span", {
|
|
38
|
+
className: "ef-loader__dots",
|
|
39
|
+
"aria-hidden": "true"
|
|
40
|
+
}, /*#__PURE__*/React.createElement("span", null, "."), /*#__PURE__*/React.createElement("span", null, "."), /*#__PURE__*/React.createElement("span", null, ".")) : null);
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ProgressProps {
|
|
2
|
+
/** @default 0 */
|
|
3
|
+
value?: number;
|
|
4
|
+
/** @default 100 */
|
|
5
|
+
max?: number;
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Show value right of the label (default = percent) */
|
|
8
|
+
showValue?: boolean;
|
|
9
|
+
format?: (value: number, max: number) => string;
|
|
10
|
+
/** @default 'default' (ink) */
|
|
11
|
+
tone?: 'default' | 'warning' | 'danger';
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function Progress(props: ProgressProps): React.JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { injectEfCss } from "../forms/Button.js";
|
|
4
|
+
const CSS = `
|
|
5
|
+
.ef-progress{display:flex;flex-direction:column;gap:6px}
|
|
6
|
+
.ef-progress__head{display:flex;justify-content:space-between;align-items:baseline}
|
|
7
|
+
.ef-progress__label{font-size:var(--text-sm);font-weight:var(--weight-medium);color:var(--text-primary)}
|
|
8
|
+
.ef-progress__val{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-muted)}
|
|
9
|
+
.ef-progress__track{height:4px;border-radius:var(--radius-full);background:var(--sand-200);overflow:hidden}
|
|
10
|
+
.ef-progress__fill{height:100%;border-radius:var(--radius-full);background:var(--accent);transition:width var(--dur-slow) var(--ease-out)}
|
|
11
|
+
.ef-progress--warning .ef-progress__fill{background:var(--warning-600)}
|
|
12
|
+
.ef-progress--danger .ef-progress__fill{background:var(--danger-600)}
|
|
13
|
+
`;
|
|
14
|
+
export function Progress({
|
|
15
|
+
value = 0,
|
|
16
|
+
max = 100,
|
|
17
|
+
label,
|
|
18
|
+
showValue,
|
|
19
|
+
format,
|
|
20
|
+
tone = 'default',
|
|
21
|
+
style,
|
|
22
|
+
className,
|
|
23
|
+
...rest
|
|
24
|
+
}) {
|
|
25
|
+
injectEfCss('ef-css-progress', CSS);
|
|
26
|
+
const pct = Math.min(100, Math.max(0, value / max * 100));
|
|
27
|
+
return /*#__PURE__*/React.createElement("div", _extends({}, rest, {
|
|
28
|
+
className: `ef-progress ef-progress--${tone}${className ? ' ' + className : ''}`,
|
|
29
|
+
style: style,
|
|
30
|
+
role: "progressbar",
|
|
31
|
+
"aria-valuenow": value,
|
|
32
|
+
"aria-valuemin": 0,
|
|
33
|
+
"aria-valuemax": max
|
|
34
|
+
}), (label || showValue) && /*#__PURE__*/React.createElement("div", {
|
|
35
|
+
className: "ef-progress__head"
|
|
36
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
37
|
+
className: "ef-progress__label"
|
|
38
|
+
}, label), showValue ? /*#__PURE__*/React.createElement("span", {
|
|
39
|
+
className: "ef-progress__val"
|
|
40
|
+
}, format ? format(value, max) : Math.round(pct) + '%') : null), /*#__PURE__*/React.createElement("div", {
|
|
41
|
+
className: "ef-progress__track"
|
|
42
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
43
|
+
className: "ef-progress__fill",
|
|
44
|
+
style: {
|
|
45
|
+
width: pct + '%'
|
|
46
|
+
}
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SpinnerProps {
|
|
2
|
+
/** Px @default 16 */
|
|
3
|
+
size?: number;
|
|
4
|
+
/** Optional text right of the spinner (also the aria-label) */
|
|
5
|
+
label?: string;
|
|
6
|
+
style?: React.CSSProperties;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function Spinner(props: SpinnerProps): React.JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Icon } from "../icons/Icon.js";
|
|
4
|
+
import { injectEfCss } from "../forms/Button.js";
|
|
5
|
+
const CSS = `
|
|
6
|
+
@keyframes ef-spinner{to{transform:rotate(360deg)}}
|
|
7
|
+
.ef-spinner{display:inline-flex;color:var(--text-muted);animation:ef-spinner .7s linear infinite}
|
|
8
|
+
`;
|
|
9
|
+
export function Spinner({
|
|
10
|
+
size = 16,
|
|
11
|
+
label,
|
|
12
|
+
style,
|
|
13
|
+
className,
|
|
14
|
+
...rest
|
|
15
|
+
}) {
|
|
16
|
+
injectEfCss('ef-css-spinner', CSS);
|
|
17
|
+
return /*#__PURE__*/React.createElement("span", _extends({}, rest, {
|
|
18
|
+
role: "status",
|
|
19
|
+
"aria-label": label || 'Loading',
|
|
20
|
+
style: {
|
|
21
|
+
display: 'inline-flex',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
gap: 8,
|
|
24
|
+
...style
|
|
25
|
+
},
|
|
26
|
+
className: className
|
|
27
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
28
|
+
className: "ef-spinner"
|
|
29
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
30
|
+
name: "loader-circle",
|
|
31
|
+
size: size
|
|
32
|
+
})), label ? /*#__PURE__*/React.createElement("span", {
|
|
33
|
+
style: {
|
|
34
|
+
fontSize: 'var(--text-sm)',
|
|
35
|
+
color: 'var(--text-muted)'
|
|
36
|
+
}
|
|
37
|
+
}, label) : null);
|
|
38
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface ToastProps {
|
|
2
|
+
/** @default 'info' */
|
|
3
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
4
|
+
title: React.ReactNode;
|
|
5
|
+
description?: React.ReactNode;
|
|
6
|
+
actionLabel?: string;
|
|
7
|
+
onAction?: () => void;
|
|
8
|
+
/** Shows a dismiss ✕ */
|
|
9
|
+
onClose?: () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Live-region role. Inside a `Toaster` the stack owns the live region and each
|
|
12
|
+
* toast renders with `null`, so a queue announces once rather than as N
|
|
13
|
+
* competing regions. @default 'status'
|
|
14
|
+
*/
|
|
15
|
+
role?: 'status' | 'alert' | null;
|
|
16
|
+
style?: React.CSSProperties;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function Toast(props: ToastProps): React.JSX.Element;
|
|
20
|
+
|
|
21
|
+
export interface ToastStackProps {
|
|
22
|
+
/** Toasts, stacked fixed bottom-right */
|
|
23
|
+
children?: React.ReactNode;
|
|
24
|
+
style?: React.CSSProperties;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function ToastStack(props: ToastStackProps): React.JSX.Element;
|
|
28
|
+
|
|
29
|
+
export interface ToastOptions {
|
|
30
|
+
title: React.ReactNode;
|
|
31
|
+
description?: React.ReactNode;
|
|
32
|
+
/** @default 'info' */
|
|
33
|
+
tone?: 'info' | 'success' | 'warning' | 'danger';
|
|
34
|
+
actionLabel?: string;
|
|
35
|
+
onAction?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* ms before auto-dismiss; 0 never dismisses.
|
|
38
|
+
* @default the Toaster's `duration`, or 0 when `actionLabel` is set — a
|
|
39
|
+
* control must not vanish on a timer the user cannot adjust (WCAG 2.2.1).
|
|
40
|
+
*/
|
|
41
|
+
duration?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ToastApi {
|
|
44
|
+
/** Queue a toast; returns its id. */
|
|
45
|
+
notify: (options: ToastOptions) => string;
|
|
46
|
+
dismiss: (id: string) => void;
|
|
47
|
+
dismissAll: () => void;
|
|
48
|
+
}
|
|
49
|
+
export interface ToasterProps {
|
|
50
|
+
/** ms before a toast auto-dismisses; 0 never. @default 5000 */
|
|
51
|
+
duration?: number;
|
|
52
|
+
/** aria-label of the live region. @default 'Notifications' */
|
|
53
|
+
label?: string;
|
|
54
|
+
/** Your app. Toaster renders a Fragment, never a wrapper element. */
|
|
55
|
+
children?: React.ReactNode;
|
|
56
|
+
style?: React.CSSProperties;
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Owns the toast queue: ids, timers, the live region and the portal. Timers
|
|
61
|
+
* pause while the stack is hovered or focused.
|
|
62
|
+
*
|
|
63
|
+
* The hook is published as a static — `Toaster.useToast()` — because only
|
|
64
|
+
* capitalised exports reach the global namespace.
|
|
65
|
+
*/
|
|
66
|
+
export declare function Toaster(props: ToasterProps): React.JSX.Element;
|
|
67
|
+
export declare namespace Toaster {
|
|
68
|
+
/** Must be called inside a `<Toaster>`; throws otherwise. */
|
|
69
|
+
function useToast(): ToastApi;
|
|
70
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Icon } from "../icons/Icon.js";
|
|
4
|
+
import { IconButton } from "../forms/IconButton.js";
|
|
5
|
+
import { injectEfCss } from "../forms/Button.js";
|
|
6
|
+
import { Portal } from "../overlay/Portal.js";
|
|
7
|
+
const CSS = `
|
|
8
|
+
.ef-toast{display:flex;align-items:flex-start;gap:10px;width:360px;max-width:100%;padding:12px 14px;background:var(--surface-inverse);color:var(--text-inverse);border-radius:var(--radius-md);box-shadow:var(--shadow-lg);animation:ef-toast-in var(--dur-slow) var(--ease-spring)}
|
|
9
|
+
.ef-toast__icon{display:inline-flex;margin-top:1px}
|
|
10
|
+
.ef-toast--success .ef-toast__icon{color:#7FD08D}
|
|
11
|
+
.ef-toast--danger .ef-toast__icon{color:#F49B93}
|
|
12
|
+
.ef-toast--warning .ef-toast__icon{color:#F2C078}
|
|
13
|
+
.ef-toast--info .ef-toast__icon{color:#EFCFAC}
|
|
14
|
+
.ef-toast__title{font-size:var(--text-md);font-weight:var(--weight-semibold);line-height:1.35}
|
|
15
|
+
.ef-toast__desc{font-size:var(--text-sm);color:var(--text-inverse);opacity:.75;margin-top:2px;line-height:1.4}
|
|
16
|
+
.ef-toast__action{margin-top:8px;background:none;border:none;padding:0;color:var(--peach-200);font-family:var(--font-sans);font-size:var(--text-sm);font-weight:var(--weight-semibold);cursor:pointer}
|
|
17
|
+
.ef-toast__action:hover{color:var(--cream-50);text-decoration:underline}
|
|
18
|
+
.ef-toast .ef-iconbtn{color:rgba(250,249,246,.6)}
|
|
19
|
+
.ef-toast .ef-iconbtn:hover:not(:disabled){background:color-mix(in srgb,var(--text-inverse) 12%,transparent);color:var(--text-inverse)}
|
|
20
|
+
.ef-toast-stack{position:fixed;bottom:24px;right:24px;display:flex;flex-direction:column;gap:10px;z-index:var(--z-toast)}
|
|
21
|
+
[data-theme="dark"] .ef-toast--success .ef-toast__icon{color:var(--success-600)}
|
|
22
|
+
[data-theme="dark"] .ef-toast--danger .ef-toast__icon{color:var(--danger-600)}
|
|
23
|
+
[data-theme="dark"] .ef-toast--warning .ef-toast__icon{color:var(--warning-600)}
|
|
24
|
+
[data-theme="dark"] .ef-toast--info .ef-toast__icon{color:var(--brand-600)}
|
|
25
|
+
[data-theme="dark"] .ef-toast__action{color:var(--brand-700)}
|
|
26
|
+
[data-theme="dark"] .ef-toast .ef-iconbtn{color:rgba(30,26,20,.55)}
|
|
27
|
+
@keyframes ef-toast-in{from{opacity:0;transform:translateY(12px) scale(.96)}}
|
|
28
|
+
`;
|
|
29
|
+
const ICONS = {
|
|
30
|
+
success: 'circle-check',
|
|
31
|
+
danger: 'circle-alert',
|
|
32
|
+
warning: 'triangle-alert',
|
|
33
|
+
info: 'info'
|
|
34
|
+
};
|
|
35
|
+
export function Toast({
|
|
36
|
+
tone = 'info',
|
|
37
|
+
title,
|
|
38
|
+
description,
|
|
39
|
+
actionLabel,
|
|
40
|
+
onAction,
|
|
41
|
+
onClose,
|
|
42
|
+
role = 'status',
|
|
43
|
+
style,
|
|
44
|
+
className,
|
|
45
|
+
...rest
|
|
46
|
+
}) {
|
|
47
|
+
injectEfCss('ef-css-toast', CSS);
|
|
48
|
+
return /*#__PURE__*/React.createElement("div", _extends({}, rest, {
|
|
49
|
+
className: `ef-toast ef-toast--${tone}${className ? ' ' + className : ''}`,
|
|
50
|
+
role: role || undefined,
|
|
51
|
+
style: style
|
|
52
|
+
}), /*#__PURE__*/React.createElement("span", {
|
|
53
|
+
className: "ef-toast__icon"
|
|
54
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
55
|
+
name: ICONS[tone] || 'info',
|
|
56
|
+
size: 18
|
|
57
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
58
|
+
style: {
|
|
59
|
+
flex: 1
|
|
60
|
+
}
|
|
61
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
62
|
+
className: "ef-toast__title"
|
|
63
|
+
}, title), description ? /*#__PURE__*/React.createElement("div", {
|
|
64
|
+
className: "ef-toast__desc"
|
|
65
|
+
}, description) : null, actionLabel ? /*#__PURE__*/React.createElement("button", {
|
|
66
|
+
className: "ef-toast__action",
|
|
67
|
+
onClick: onAction
|
|
68
|
+
}, actionLabel) : null), onClose ? /*#__PURE__*/React.createElement(IconButton, {
|
|
69
|
+
icon: "x",
|
|
70
|
+
label: "Dismiss",
|
|
71
|
+
size: "sm",
|
|
72
|
+
onClick: onClose
|
|
73
|
+
}) : null);
|
|
74
|
+
}
|
|
75
|
+
export function ToastStack({
|
|
76
|
+
children,
|
|
77
|
+
style,
|
|
78
|
+
className,
|
|
79
|
+
...rest
|
|
80
|
+
}) {
|
|
81
|
+
injectEfCss('ef-css-toast', CSS);
|
|
82
|
+
return /*#__PURE__*/React.createElement("div", _extends({}, rest, {
|
|
83
|
+
className: `ef-toast-stack${className ? ' ' + className : ''}`,
|
|
84
|
+
style: style
|
|
85
|
+
}), children);
|
|
86
|
+
}
|
|
87
|
+
const ToastCtx = /*#__PURE__*/React.createContext(null);
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The toast queue API: notify, dismiss, dismissAll.
|
|
91
|
+
*
|
|
92
|
+
* Lowercase, so the compiler files it under unexposedExports and it never
|
|
93
|
+
* reaches the global namespace directly. Consumers get it as `Toaster.useToast`
|
|
94
|
+
* (assigned at the bottom of this file), which is the one sanctioned way to
|
|
95
|
+
* publish a hook when only capitalised exports are exposed.
|
|
96
|
+
*
|
|
97
|
+
* Throws outside a <Toaster> rather than degrading to a no-op: a notification
|
|
98
|
+
* that silently never appears is the harder bug to find.
|
|
99
|
+
*/
|
|
100
|
+
export function useToast() {
|
|
101
|
+
const ctx = React.useContext(ToastCtx);
|
|
102
|
+
if (!ctx) throw new Error('useToast must be called inside a <Toaster>');
|
|
103
|
+
return ctx;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Owns the toast queue: ids, timers, the live region, and the portal.
|
|
108
|
+
*
|
|
109
|
+
* Renders a Fragment around your app plus one portaled stack, so it can sit at
|
|
110
|
+
* the root without introducing a wrapper element.
|
|
111
|
+
*/
|
|
112
|
+
export function Toaster({
|
|
113
|
+
duration = 5000,
|
|
114
|
+
label = 'Notifications',
|
|
115
|
+
children,
|
|
116
|
+
...rest
|
|
117
|
+
}) {
|
|
118
|
+
injectEfCss('ef-css-toast', CSS);
|
|
119
|
+
const uid = React.useId();
|
|
120
|
+
const seq = React.useRef(0);
|
|
121
|
+
const [toasts, setToasts] = React.useState([]);
|
|
122
|
+
const timers = React.useRef(new Map());
|
|
123
|
+
const dismiss = React.useCallback(id => {
|
|
124
|
+
const rec = timers.current.get(id);
|
|
125
|
+
if (rec && rec.handle) clearTimeout(rec.handle);
|
|
126
|
+
timers.current.delete(id);
|
|
127
|
+
setToasts(list => list.filter(x => x.id !== id));
|
|
128
|
+
}, []);
|
|
129
|
+
const arm = React.useCallback((id, ms) => {
|
|
130
|
+
const rec = {
|
|
131
|
+
remaining: ms,
|
|
132
|
+
startedAt: Date.now(),
|
|
133
|
+
handle: null
|
|
134
|
+
};
|
|
135
|
+
rec.handle = setTimeout(() => dismiss(id), ms);
|
|
136
|
+
timers.current.set(id, rec);
|
|
137
|
+
}, [dismiss]);
|
|
138
|
+
const notify = React.useCallback(options => {
|
|
139
|
+
const id = uid + seq.current++;
|
|
140
|
+
const {
|
|
141
|
+
duration: own,
|
|
142
|
+
...props
|
|
143
|
+
} = options || {};
|
|
144
|
+
// A toast carrying an action must not time out: WCAG 2.2.1 does not allow a
|
|
145
|
+
// control to disappear on a timer the user cannot adjust.
|
|
146
|
+
const ms = own != null ? own : props.actionLabel ? 0 : duration;
|
|
147
|
+
setToasts(list => [...list, {
|
|
148
|
+
id,
|
|
149
|
+
props
|
|
150
|
+
}]);
|
|
151
|
+
if (ms > 0) arm(id, ms);
|
|
152
|
+
return id;
|
|
153
|
+
}, [uid, duration, arm]);
|
|
154
|
+
const dismissAll = React.useCallback(() => {
|
|
155
|
+
timers.current.forEach(rec => {
|
|
156
|
+
if (rec.handle) clearTimeout(rec.handle);
|
|
157
|
+
});
|
|
158
|
+
timers.current.clear();
|
|
159
|
+
setToasts([]);
|
|
160
|
+
}, []);
|
|
161
|
+
React.useEffect(() => () => {
|
|
162
|
+
timers.current.forEach(rec => {
|
|
163
|
+
if (rec.handle) clearTimeout(rec.handle);
|
|
164
|
+
});
|
|
165
|
+
timers.current.clear();
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
// Hovering or focusing the stack holds every pending timer, so a slow reader
|
|
169
|
+
// or a keyboard user can actually reach the action inside a toast.
|
|
170
|
+
const pause = () => {
|
|
171
|
+
timers.current.forEach(rec => {
|
|
172
|
+
if (!rec.handle) return;
|
|
173
|
+
clearTimeout(rec.handle);
|
|
174
|
+
rec.remaining = Math.max(0, rec.remaining - (Date.now() - rec.startedAt));
|
|
175
|
+
rec.handle = null;
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
const resume = () => {
|
|
179
|
+
// A toast whose time ran out while paused must go on unpause, not become
|
|
180
|
+
// permanent. Collect them first: dismiss() mutates the map being iterated.
|
|
181
|
+
const expired = [];
|
|
182
|
+
timers.current.forEach((rec, id) => {
|
|
183
|
+
if (rec.handle) return;
|
|
184
|
+
if (rec.remaining <= 0) {
|
|
185
|
+
expired.push(id);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
rec.startedAt = Date.now();
|
|
189
|
+
rec.handle = setTimeout(() => dismiss(id), rec.remaining);
|
|
190
|
+
});
|
|
191
|
+
expired.forEach(dismiss);
|
|
192
|
+
};
|
|
193
|
+
const api = React.useMemo(() => ({
|
|
194
|
+
notify,
|
|
195
|
+
dismiss,
|
|
196
|
+
dismissAll
|
|
197
|
+
}), [notify, dismiss, dismissAll]);
|
|
198
|
+
return /*#__PURE__*/React.createElement(ToastCtx.Provider, {
|
|
199
|
+
value: api
|
|
200
|
+
}, children, /*#__PURE__*/React.createElement(Portal, null, /*#__PURE__*/React.createElement(ToastStack, _extends({}, rest, {
|
|
201
|
+
"aria-label": label,
|
|
202
|
+
role: "log",
|
|
203
|
+
"aria-live": "polite",
|
|
204
|
+
"aria-relevant": "additions",
|
|
205
|
+
onMouseEnter: pause,
|
|
206
|
+
onMouseLeave: resume,
|
|
207
|
+
onFocusCapture: pause,
|
|
208
|
+
onBlurCapture: resume
|
|
209
|
+
}), toasts.map(t => /*#__PURE__*/React.createElement(Toast, _extends({
|
|
210
|
+
key: t.id
|
|
211
|
+
}, t.props, {
|
|
212
|
+
role: null,
|
|
213
|
+
onClose: () => dismiss(t.id)
|
|
214
|
+
}))))));
|
|
215
|
+
}
|
|
216
|
+
Toaster.useToast = useToast;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface TooltipProps {
|
|
2
|
+
label: React.ReactNode;
|
|
3
|
+
/** Preferred side; flips automatically when there is no room. @default 'top' */
|
|
4
|
+
position?: 'top' | 'bottom';
|
|
5
|
+
/** Hover delay in ms before showing. Focus always shows immediately. @default 200 */
|
|
6
|
+
delay?: number;
|
|
7
|
+
/** The trigger element */
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
style?: React.CSSProperties;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function Tooltip(props: TooltipProps): React.JSX.Element;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { injectEfCss } from "../forms/Button.js";
|
|
4
|
+
import { Portal, useAnchoredStyle } from "../overlay/Portal.js";
|
|
5
|
+
const CSS = `
|
|
6
|
+
.ef-tooltip{position:relative;display:inline-flex}
|
|
7
|
+
.ef-tooltip__bubble{position:fixed;background:var(--surface-inverse);color:var(--text-inverse);font-size:var(--text-xs);font-weight:var(--weight-medium);line-height:1.35;padding:5px 9px;border-radius:6px;white-space:nowrap;pointer-events:none;z-index:var(--z-tooltip);animation:ef-tooltip-in var(--dur-fast) var(--ease-out)}
|
|
8
|
+
.ef-tooltip__bubble::after{content:'';position:absolute;top:100%;left:var(--ef-tt-arrow,50%);transform:translateX(-50%);border:4px solid transparent;border-top-color:var(--surface-inverse)}
|
|
9
|
+
.ef-tooltip__bubble--bottom::after{top:auto;bottom:100%;border-top-color:transparent;border-bottom-color:var(--surface-inverse)}
|
|
10
|
+
@keyframes ef-tooltip-in{from{opacity:0}to{opacity:1}}
|
|
11
|
+
`;
|
|
12
|
+
export function Tooltip({
|
|
13
|
+
label,
|
|
14
|
+
position = 'top',
|
|
15
|
+
delay = 200,
|
|
16
|
+
children,
|
|
17
|
+
style,
|
|
18
|
+
className,
|
|
19
|
+
...rest
|
|
20
|
+
}) {
|
|
21
|
+
injectEfCss('ef-css-tooltip', CSS);
|
|
22
|
+
const id = React.useId();
|
|
23
|
+
const [open, setOpen] = React.useState(false);
|
|
24
|
+
const ref = React.useRef(null);
|
|
25
|
+
const bubbleRef = React.useRef(null);
|
|
26
|
+
const timer = React.useRef(null);
|
|
27
|
+
const {
|
|
28
|
+
style: anchored,
|
|
29
|
+
side
|
|
30
|
+
} = useAnchoredStyle(ref, bubbleRef, {
|
|
31
|
+
open,
|
|
32
|
+
placement: position === 'bottom' ? 'bottom' : 'top',
|
|
33
|
+
align: 'center',
|
|
34
|
+
offset: 8
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Keep the arrow over the trigger's centre even when the bubble has been
|
|
38
|
+
// shifted to stay on screen, so it never points at empty space.
|
|
39
|
+
const [arrowX, setArrowX] = React.useState(null);
|
|
40
|
+
React.useLayoutEffect(() => {
|
|
41
|
+
if (!open || !ref.current || !bubbleRef.current) {
|
|
42
|
+
setArrowX(null);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const a = ref.current.getBoundingClientRect();
|
|
46
|
+
const b = bubbleRef.current.getBoundingClientRect();
|
|
47
|
+
if (!b.width) return;
|
|
48
|
+
const x = a.left + a.width / 2 - b.left;
|
|
49
|
+
setArrowX(Math.max(8, Math.min(x, b.width - 8)));
|
|
50
|
+
}, [open, anchored.left, anchored.top]);
|
|
51
|
+
const show = immediate => {
|
|
52
|
+
clearTimeout(timer.current);
|
|
53
|
+
if (immediate) setOpen(true);else timer.current = setTimeout(() => setOpen(true), delay);
|
|
54
|
+
};
|
|
55
|
+
const hide = () => {
|
|
56
|
+
clearTimeout(timer.current);
|
|
57
|
+
setOpen(false);
|
|
58
|
+
};
|
|
59
|
+
React.useEffect(() => () => clearTimeout(timer.current), []);
|
|
60
|
+
const child = React.Children.count(children) === 1 && /*#__PURE__*/React.isValidElement(children) ? /*#__PURE__*/React.cloneElement(children, {
|
|
61
|
+
'aria-describedby': open ? id : undefined
|
|
62
|
+
}) : children;
|
|
63
|
+
return /*#__PURE__*/React.createElement("span", _extends({}, rest, {
|
|
64
|
+
ref: ref,
|
|
65
|
+
className: `ef-tooltip${className ? ' ' + className : ''}`,
|
|
66
|
+
style: style,
|
|
67
|
+
onMouseEnter: () => show(false),
|
|
68
|
+
onMouseLeave: hide,
|
|
69
|
+
onFocus: () => show(true),
|
|
70
|
+
onBlur: hide,
|
|
71
|
+
onKeyDown: e => {
|
|
72
|
+
if (e.key === 'Escape') hide();
|
|
73
|
+
}
|
|
74
|
+
}), child, open ? /*#__PURE__*/React.createElement(Portal, null, /*#__PURE__*/React.createElement("span", {
|
|
75
|
+
ref: bubbleRef,
|
|
76
|
+
role: "tooltip",
|
|
77
|
+
id: id,
|
|
78
|
+
className: `ef-tooltip__bubble${side === 'bottom' ? ' ef-tooltip__bubble--bottom' : ''}`,
|
|
79
|
+
style: {
|
|
80
|
+
...anchored,
|
|
81
|
+
...(arrowX == null ? null : {
|
|
82
|
+
'--ef-tt-arrow': arrowX + 'px'
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
}, label)) : null);
|
|
86
|
+
}
|