@vectara/vectara-ui 18.4.0 → 19.1.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/lib/components/card/SimpleCard.d.ts +1 -1
- package/lib/components/card/_index.scss +5 -30
- package/lib/components/chart/BarChart.d.ts +20 -0
- package/lib/components/chart/BarChart.js +28 -0
- package/lib/components/chart/ComposedChart.d.ts +21 -0
- package/lib/components/chart/ComposedChart.js +29 -0
- package/lib/components/chart/LineChart.d.ts +22 -0
- package/lib/components/chart/LineChart.js +42 -0
- package/lib/components/chart/PieChart.d.ts +15 -0
- package/lib/components/chart/PieChart.js +36 -0
- package/lib/components/chart/ScatterChart.d.ts +18 -0
- package/lib/components/chart/ScatterChart.js +34 -0
- package/lib/components/chart/Sparkline.d.ts +13 -0
- package/lib/components/chart/Sparkline.js +23 -0
- package/lib/components/chart/TreeMap.d.ts +13 -0
- package/lib/components/chart/TreeMap.js +51 -0
- package/lib/components/chart/_index.scss +19 -0
- package/lib/components/chart/chartTheme.d.ts +21 -0
- package/lib/components/chart/chartTheme.js +13 -0
- package/lib/components/chart/palette.d.ts +5 -0
- package/lib/components/chart/palette.js +29 -0
- package/lib/components/composer/Composer.d.ts +41 -0
- package/lib/components/composer/Composer.js +148 -0
- package/lib/components/composer/_index.scss +4 -0
- package/lib/components/composer/useComposerHistory.d.ts +34 -0
- package/lib/components/composer/useComposerHistory.js +127 -0
- package/lib/components/context/Theme.d.ts +32 -0
- package/lib/components/context/Theme.js +116 -3
- package/lib/components/fileDropTarget/FileDropTarget.d.ts +8 -0
- package/lib/components/fileDropTarget/FileDropTarget.js +69 -0
- package/lib/components/fileDropTarget/_index.scss +34 -0
- package/lib/components/index.d.ts +13 -2
- package/lib/components/index.js +12 -1
- package/lib/components/patch/VuiPatch.d.ts +10 -0
- package/lib/components/patch/VuiPatch.js +32 -0
- package/lib/components/patch/_index.scss +30 -0
- package/lib/styles/index.css +143 -27
- package/package.json +4 -3
- package/src/docs/Docs.tsx +10 -0
- package/src/docs/pages/barChart/Basic.tsx +21 -0
- package/src/docs/pages/barChart/Horizontal.tsx +20 -0
- package/src/docs/pages/barChart/Stacked.tsx +23 -0
- package/src/docs/pages/barChart/index.tsx +29 -0
- package/src/docs/pages/card/SimpleCard.tsx +72 -6
- package/src/docs/pages/colorPalette/CategoricalColors.tsx +89 -0
- package/src/docs/pages/colorPalette/ChartColors.tsx +68 -0
- package/src/docs/pages/colorPalette/NeutralColors.tsx +42 -0
- package/src/docs/pages/colorPalette/SemanticColors.tsx +64 -0
- package/src/docs/pages/colorPalette/Swatch.tsx +59 -0
- package/src/docs/pages/colorPalette/TextAndBorderColors.tsx +37 -0
- package/src/docs/pages/colorPalette/Usage.tsx +72 -0
- package/src/docs/pages/colorPalette/index.tsx +50 -0
- package/src/docs/pages/composedChart/Basic.tsx +25 -0
- package/src/docs/pages/composedChart/index.tsx +15 -0
- package/src/docs/pages/composer/Composer.tsx +130 -0
- package/src/docs/pages/composer/index.tsx +11 -0
- package/src/docs/pages/fileDropTarget/FileDropTarget.tsx +80 -0
- package/src/docs/pages/fileDropTarget/index.tsx +11 -0
- package/src/docs/pages/lineChart/Area.tsx +25 -0
- package/src/docs/pages/lineChart/Basic.tsx +23 -0
- package/src/docs/pages/lineChart/Curved.tsx +25 -0
- package/src/docs/pages/lineChart/StackedArea.tsx +27 -0
- package/src/docs/pages/lineChart/index.tsx +36 -0
- package/src/docs/pages/patch/Icons.tsx +16 -0
- package/src/docs/pages/patch/Sizes.tsx +20 -0
- package/src/docs/pages/patch/index.tsx +22 -0
- package/src/docs/pages/pieChart/Basic.tsx +13 -0
- package/src/docs/pages/pieChart/Donut.tsx +12 -0
- package/src/docs/pages/pieChart/ManyValues.tsx +51 -0
- package/src/docs/pages/pieChart/Unlabeled.tsx +15 -0
- package/src/docs/pages/pieChart/index.tsx +36 -0
- package/src/docs/pages/scatterChart/Basic.tsx +31 -0
- package/src/docs/pages/scatterChart/Dense.tsx +144 -0
- package/src/docs/pages/scatterChart/index.tsx +22 -0
- package/src/docs/pages/sparkline/Basic.tsx +7 -0
- package/src/docs/pages/sparkline/Inline.tsx +21 -0
- package/src/docs/pages/sparkline/index.tsx +22 -0
- package/src/docs/pages/treeMap/Basic.tsx +14 -0
- package/src/docs/pages/treeMap/Nested.tsx +88 -0
- package/src/docs/pages/treeMap/index.tsx +22 -0
- package/src/docs/pages.tsx +20 -3
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { BiPaperclip } from "react-icons/bi";
|
|
4
|
+
import classNames from "classnames";
|
|
5
|
+
import { VuiFlexContainer } from "../flex/FlexContainer";
|
|
6
|
+
import { VuiFlexItem } from "../flex/FlexItem";
|
|
7
|
+
import { VuiIcon } from "../icon/Icon";
|
|
8
|
+
import { VuiIconButton } from "../button/IconButton";
|
|
9
|
+
import { VuiButtonPrimary } from "../button/ButtonPrimary";
|
|
10
|
+
import { VuiBadge } from "../badge/Badge";
|
|
11
|
+
import { VuiSpacer } from "../spacer/Spacer";
|
|
12
|
+
import { VuiTextArea } from "../form";
|
|
13
|
+
import { VuiFileDropTarget } from "../fileDropTarget/FileDropTarget";
|
|
14
|
+
import { useComposerHistory } from "./useComposerHistory";
|
|
15
|
+
const MOD_ORDER = ["mod", "alt", "shift"];
|
|
16
|
+
// Single-character keys are matched case-insensitively; named keys keep their case.
|
|
17
|
+
const normalizeKeyName = (key) => (key.length === 1 ? key.toLowerCase() : key);
|
|
18
|
+
const canonical = (mods, key) => [...MOD_ORDER.filter((mod) => mods.has(mod)), normalizeKeyName(key)].join("+");
|
|
19
|
+
const eventSignature = (e) => {
|
|
20
|
+
const mods = new Set();
|
|
21
|
+
// Treat Cmd and Ctrl interchangeably so "Mod+k" works across platforms.
|
|
22
|
+
if (e.metaKey || e.ctrlKey)
|
|
23
|
+
mods.add("mod");
|
|
24
|
+
if (e.altKey)
|
|
25
|
+
mods.add("alt");
|
|
26
|
+
if (e.shiftKey)
|
|
27
|
+
mods.add("shift");
|
|
28
|
+
return canonical(mods, e.key);
|
|
29
|
+
};
|
|
30
|
+
const comboSignature = (combo) => {
|
|
31
|
+
var _a;
|
|
32
|
+
const segments = combo.split("+").map((segment) => segment.trim());
|
|
33
|
+
const key = (_a = segments.pop()) !== null && _a !== void 0 ? _a : "";
|
|
34
|
+
const mods = new Set();
|
|
35
|
+
for (const segment of segments) {
|
|
36
|
+
const mod = segment.toLowerCase();
|
|
37
|
+
mods.add(mod === "ctrl" || mod === "meta" ? "mod" : mod);
|
|
38
|
+
}
|
|
39
|
+
return canonical(mods, key);
|
|
40
|
+
};
|
|
41
|
+
export const VuiComposer = ({ onSubmit, isRunning, onCancel, isSendDisabled, sendLabel = "Send", cancelLabel = "Cancel", placeholder, autoFocus, onChange, value, enableHistory, historyKey, canUploadFiles, accept, validateFile, onFilesRejected, fileDropScope = "document", fileDropMessage, onShortcutKeys, leadingActions, footer, className, "data-testid": dataTestId }) => {
|
|
42
|
+
const isControlled = value !== undefined;
|
|
43
|
+
const [internalValue, setInternalValue] = useState("");
|
|
44
|
+
const [files, setFiles] = useState([]);
|
|
45
|
+
const fileInputRef = useRef(null);
|
|
46
|
+
const composerRef = useRef(null);
|
|
47
|
+
const currentValue = isControlled ? value : internalValue;
|
|
48
|
+
const setValue = (next) => {
|
|
49
|
+
if (!isControlled)
|
|
50
|
+
setInternalValue(next);
|
|
51
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(next);
|
|
52
|
+
};
|
|
53
|
+
const clear = () => setValue("");
|
|
54
|
+
const history = useComposerHistory({ storageKey: historyKey, value: currentValue, setValue });
|
|
55
|
+
const isEmptyMessage = currentValue.trim() === "" && files.length === 0;
|
|
56
|
+
const addFiles = (incoming) => {
|
|
57
|
+
const existingNames = new Set(files.map((file) => file.name));
|
|
58
|
+
const deduped = incoming.filter((file) => !existingNames.has(file.name));
|
|
59
|
+
if (!validateFile) {
|
|
60
|
+
if (deduped.length > 0)
|
|
61
|
+
setFiles((prev) => [...prev, ...deduped]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const valid = [];
|
|
65
|
+
const rejected = [];
|
|
66
|
+
for (const file of deduped) {
|
|
67
|
+
const message = validateFile(file);
|
|
68
|
+
if (message)
|
|
69
|
+
rejected.push({ file, message });
|
|
70
|
+
else
|
|
71
|
+
valid.push(file);
|
|
72
|
+
}
|
|
73
|
+
if (valid.length > 0)
|
|
74
|
+
setFiles((prev) => [...prev, ...valid]);
|
|
75
|
+
if (rejected.length > 0)
|
|
76
|
+
onFilesRejected === null || onFilesRejected === void 0 ? void 0 : onFilesRejected(rejected);
|
|
77
|
+
};
|
|
78
|
+
const removeFile = (index) => {
|
|
79
|
+
setFiles((prev) => prev.filter((_, i) => i !== index));
|
|
80
|
+
};
|
|
81
|
+
const submit = () => {
|
|
82
|
+
if (isEmptyMessage || isSendDisabled)
|
|
83
|
+
return;
|
|
84
|
+
onSubmit({ text: currentValue, files });
|
|
85
|
+
if (enableHistory)
|
|
86
|
+
history.record(currentValue);
|
|
87
|
+
setValue("");
|
|
88
|
+
setFiles([]);
|
|
89
|
+
};
|
|
90
|
+
const handleChange = (e) => {
|
|
91
|
+
setValue(e.target.value);
|
|
92
|
+
// Any edit cancels in-progress history navigation.
|
|
93
|
+
if (enableHistory)
|
|
94
|
+
history.reset();
|
|
95
|
+
};
|
|
96
|
+
const handleKeyDown = (e) => {
|
|
97
|
+
var _a;
|
|
98
|
+
// Consumer shortcuts run first; preventDefault suppresses built-in handling.
|
|
99
|
+
if (onShortcutKeys) {
|
|
100
|
+
const signature = eventSignature(e);
|
|
101
|
+
for (const combo in onShortcutKeys) {
|
|
102
|
+
if (comboSignature(combo) === signature) {
|
|
103
|
+
onShortcutKeys[combo](e, { value: currentValue, setValue, clear });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (e.defaultPrevented)
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Enter submits; Shift+Enter falls through so the textarea inserts a newline.
|
|
111
|
+
if (!e.shiftKey && e.code === "Enter") {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
submit();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Only let history cycling kick in when the caret is at the textarea's
|
|
117
|
+
// vertical boundary in the direction of the arrow key — otherwise the native
|
|
118
|
+
// textarea must move the caret between internal lines. Without this gate, the
|
|
119
|
+
// hook's primed→cycling state machine (designed for a single-line input)
|
|
120
|
+
// would replace the textarea contents regardless of caret position.
|
|
121
|
+
if (enableHistory && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
122
|
+
const el = e.currentTarget;
|
|
123
|
+
const pos = (_a = el.selectionStart) !== null && _a !== void 0 ? _a : 0;
|
|
124
|
+
// A caret at position 0 is always on the first line. We special-case it
|
|
125
|
+
// because lastIndexOf("\n", -1) clamps its negative fromIndex to 0 and then
|
|
126
|
+
// inspects index 0 — so a value that begins with a newline would be wrongly
|
|
127
|
+
// reported as "not first line", trapping history navigation. Otherwise,
|
|
128
|
+
// lastIndexOf("\n", pos - 1) scans backward from the character just before
|
|
129
|
+
// the caret; -1 means no newline precedes it, so it is on the first line.
|
|
130
|
+
const cursorOnFirstLine = pos === 0 || el.value.lastIndexOf("\n", pos - 1) === -1;
|
|
131
|
+
// indexOf("\n", pos) scans forward from the caret; -1 means no newline at or
|
|
132
|
+
// after it, so it is on the last line.
|
|
133
|
+
const cursorOnLastLine = el.value.indexOf("\n", pos) === -1;
|
|
134
|
+
if (e.key === "ArrowUp" && !cursorOnFirstLine)
|
|
135
|
+
return;
|
|
136
|
+
if (e.key === "ArrowDown" && !cursorOnLastLine)
|
|
137
|
+
return;
|
|
138
|
+
history.handleKeyDown(e);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const handleFileInputChange = (e) => {
|
|
142
|
+
if (e.target.files)
|
|
143
|
+
addFiles(Array.from(e.target.files));
|
|
144
|
+
// Reset so selecting the same file again still fires a change event.
|
|
145
|
+
e.target.value = "";
|
|
146
|
+
};
|
|
147
|
+
return (_jsxs("div", Object.assign({ className: classNames("vuiComposer", className), "data-testid": dataTestId, ref: composerRef }, { children: [_jsxs(VuiFlexContainer, Object.assign({ alignItems: "end", fullWidth: true, spacing: "xs" }, { children: [canUploadFiles && (_jsx(VuiFlexItem, { children: _jsx(VuiIconButton, { color: "neutral", "aria-label": "Attach files", icon: _jsx(VuiIcon, { children: _jsx(BiPaperclip, {}) }), onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } }) })), leadingActions, _jsx(VuiFlexItem, Object.assign({ grow: 1 }, { children: _jsx(VuiTextArea, { autoFocus: autoFocus, autoGrow: true, fullWidth: true, rows: 1, placeholder: placeholder, value: currentValue, onChange: handleChange, onKeyDown: handleKeyDown }) })), _jsx(VuiFlexItem, { children: isRunning ? (_jsx(VuiButtonPrimary, Object.assign({ color: "subdued", size: "l", onClick: onCancel }, { children: cancelLabel }))) : (_jsx(VuiButtonPrimary, Object.assign({ color: "primary", size: "l", onClick: submit, isDisabled: isEmptyMessage || isSendDisabled }, { children: sendLabel }))) })] })), canUploadFiles && (_jsx("input", { ref: fileInputRef, type: "file", multiple: true, accept: accept, style: { display: "none" }, onChange: handleFileInputChange })), files.length > 0 && (_jsxs(_Fragment, { children: [_jsx(VuiSpacer, { size: "s" }), _jsx(VuiFlexContainer, Object.assign({ fullWidth: true, wrap: true, spacing: "xxs" }, { children: files.map((file, index) => (_jsx(VuiBadge, Object.assign({ color: "primary", onClose: () => removeFile(index) }, { children: file.name }), file.name))) }))] })), footer, canUploadFiles && (_jsx(VuiFileDropTarget, { onFilesDropped: addFiles, message: fileDropMessage, scopeRef: fileDropScope === "composer" ? composerRef : undefined }))] })));
|
|
148
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { KeyboardEvent } from "react";
|
|
2
|
+
type Args = {
|
|
3
|
+
storageKey?: string;
|
|
4
|
+
value: string;
|
|
5
|
+
setValue: (value: string) => void;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Cycles through previous submissions with UP/DOWN arrow keys.
|
|
9
|
+
*
|
|
10
|
+
* State machine for navigation (3 phases):
|
|
11
|
+
*
|
|
12
|
+
* idle — no navigation active.
|
|
13
|
+
* primed — first key press (UP/DOWN) consumed by native cursor movement.
|
|
14
|
+
* cycling — UP/DOWN keys walk through the history stack.
|
|
15
|
+
*
|
|
16
|
+
* Transitions on arrow key press:
|
|
17
|
+
*
|
|
18
|
+
* idle + empty input + UP → cycling (skip primed, start navigating immediately)
|
|
19
|
+
* idle + empty input + DOWN → idle (nothing to cycle to)
|
|
20
|
+
* idle + has text + UP/DN → primed (let native cursor move first)
|
|
21
|
+
* primed + same direction → cycling (second press enters history)
|
|
22
|
+
* primed + diff direction → primed (re-prime for the new direction)
|
|
23
|
+
* cycling + same direction → cycling (keep walking history)
|
|
24
|
+
* cycling + diff direction → primed (let native cursor adjust, re-prime)
|
|
25
|
+
*
|
|
26
|
+
* The composer calls reset() on any user edit and record() on submit, both of
|
|
27
|
+
* which return the machine to idle.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useComposerHistory({ storageKey, value, setValue }: Args): {
|
|
30
|
+
handleKeyDown: (e: KeyboardEvent) => void;
|
|
31
|
+
record: (input: string) => void;
|
|
32
|
+
reset: () => void;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
function loadHistory(storageKey) {
|
|
3
|
+
try {
|
|
4
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
5
|
+
return stored ? JSON.parse(stored) : [];
|
|
6
|
+
}
|
|
7
|
+
catch (_a) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function saveHistory(storageKey, history) {
|
|
12
|
+
try {
|
|
13
|
+
sessionStorage.setItem(storageKey, JSON.stringify(history));
|
|
14
|
+
}
|
|
15
|
+
catch (_a) {
|
|
16
|
+
// Quota exceeded or unavailable — silently drop.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Cycles through previous submissions with UP/DOWN arrow keys.
|
|
21
|
+
*
|
|
22
|
+
* State machine for navigation (3 phases):
|
|
23
|
+
*
|
|
24
|
+
* idle — no navigation active.
|
|
25
|
+
* primed — first key press (UP/DOWN) consumed by native cursor movement.
|
|
26
|
+
* cycling — UP/DOWN keys walk through the history stack.
|
|
27
|
+
*
|
|
28
|
+
* Transitions on arrow key press:
|
|
29
|
+
*
|
|
30
|
+
* idle + empty input + UP → cycling (skip primed, start navigating immediately)
|
|
31
|
+
* idle + empty input + DOWN → idle (nothing to cycle to)
|
|
32
|
+
* idle + has text + UP/DN → primed (let native cursor move first)
|
|
33
|
+
* primed + same direction → cycling (second press enters history)
|
|
34
|
+
* primed + diff direction → primed (re-prime for the new direction)
|
|
35
|
+
* cycling + same direction → cycling (keep walking history)
|
|
36
|
+
* cycling + diff direction → primed (let native cursor adjust, re-prime)
|
|
37
|
+
*
|
|
38
|
+
* The composer calls reset() on any user edit and record() on submit, both of
|
|
39
|
+
* which return the machine to idle.
|
|
40
|
+
*/
|
|
41
|
+
export function useComposerHistory({ storageKey, value, setValue }) {
|
|
42
|
+
const historyRef = useRef(storageKey ? loadHistory(storageKey) : []);
|
|
43
|
+
const [historyIndex, setHistoryIndex] = useState(null);
|
|
44
|
+
const draftRef = useRef("");
|
|
45
|
+
const phaseRef = useRef("idle");
|
|
46
|
+
const directionRef = useRef(null);
|
|
47
|
+
const reset = () => {
|
|
48
|
+
setHistoryIndex(null);
|
|
49
|
+
phaseRef.current = "idle";
|
|
50
|
+
directionRef.current = null;
|
|
51
|
+
};
|
|
52
|
+
const record = (input) => {
|
|
53
|
+
if (input.trim()) {
|
|
54
|
+
historyRef.current = [...historyRef.current, input];
|
|
55
|
+
if (storageKey)
|
|
56
|
+
saveHistory(storageKey, historyRef.current);
|
|
57
|
+
}
|
|
58
|
+
reset();
|
|
59
|
+
};
|
|
60
|
+
const handleKeyDown = (e) => {
|
|
61
|
+
if (e.key !== "ArrowUp" && e.key !== "ArrowDown")
|
|
62
|
+
return;
|
|
63
|
+
const history = historyRef.current;
|
|
64
|
+
if (history.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const direction = e.key === "ArrowUp" ? "up" : "down";
|
|
67
|
+
const phase = phaseRef.current;
|
|
68
|
+
const lastDirection = directionRef.current;
|
|
69
|
+
// Idle with text in input — let native cursor handle the first press.
|
|
70
|
+
if (phase === "idle" && value.length > 0) {
|
|
71
|
+
phaseRef.current = "primed";
|
|
72
|
+
directionRef.current = direction;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Idle with empty input — skip primed and start cycling immediately on UP; ignore DOWN.
|
|
76
|
+
if (phase === "idle") {
|
|
77
|
+
if (direction === "down")
|
|
78
|
+
return;
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
draftRef.current = value;
|
|
81
|
+
const newIndex = history.length - 1;
|
|
82
|
+
setHistoryIndex(newIndex);
|
|
83
|
+
setValue(history[newIndex]);
|
|
84
|
+
phaseRef.current = "cycling";
|
|
85
|
+
directionRef.current = direction;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Direction changed — let native cursor handle it, re-prime.
|
|
89
|
+
if (direction !== lastDirection) {
|
|
90
|
+
phaseRef.current = "primed";
|
|
91
|
+
directionRef.current = direction;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// Same direction: primed → cycling, or cycling → cycling.
|
|
95
|
+
// Primed DOWN with no active cycling — nothing to cycle to.
|
|
96
|
+
if (direction === "down" && historyIndex === null)
|
|
97
|
+
return;
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
if (direction === "up") {
|
|
100
|
+
if (historyIndex === null) {
|
|
101
|
+
draftRef.current = value;
|
|
102
|
+
const newIndex = history.length - 1;
|
|
103
|
+
setHistoryIndex(newIndex);
|
|
104
|
+
setValue(history[newIndex]);
|
|
105
|
+
}
|
|
106
|
+
else if (historyIndex > 0) {
|
|
107
|
+
const newIndex = historyIndex - 1;
|
|
108
|
+
setHistoryIndex(newIndex);
|
|
109
|
+
setValue(history[newIndex]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
if (historyIndex !== null && historyIndex < history.length - 1) {
|
|
114
|
+
const newIndex = historyIndex + 1;
|
|
115
|
+
setHistoryIndex(newIndex);
|
|
116
|
+
setValue(history[newIndex]);
|
|
117
|
+
}
|
|
118
|
+
else if (historyIndex !== null) {
|
|
119
|
+
setHistoryIndex(null);
|
|
120
|
+
setValue(draftRef.current);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
phaseRef.current = "cycling";
|
|
124
|
+
directionRef.current = direction;
|
|
125
|
+
};
|
|
126
|
+
return { handleKeyDown, record, reset };
|
|
127
|
+
}
|
|
@@ -55,6 +55,38 @@ export type Theme = {
|
|
|
55
55
|
colorBorderMediumRgb?: string;
|
|
56
56
|
colorBorderLight?: string;
|
|
57
57
|
colorBorderLightRgb?: string;
|
|
58
|
+
colorIndigoBackground?: string;
|
|
59
|
+
colorIndigoText?: string;
|
|
60
|
+
colorEmeraldBackground?: string;
|
|
61
|
+
colorEmeraldText?: string;
|
|
62
|
+
colorAmberBackground?: string;
|
|
63
|
+
colorAmberText?: string;
|
|
64
|
+
colorPinkBackground?: string;
|
|
65
|
+
colorPinkText?: string;
|
|
66
|
+
colorSkyBackground?: string;
|
|
67
|
+
colorSkyText?: string;
|
|
68
|
+
colorOrangeBackground?: string;
|
|
69
|
+
colorOrangeText?: string;
|
|
70
|
+
colorSlateBackground?: string;
|
|
71
|
+
colorSlateText?: string;
|
|
72
|
+
colorTealBackground?: string;
|
|
73
|
+
colorTealText?: string;
|
|
74
|
+
colorLimeBackground?: string;
|
|
75
|
+
colorLimeText?: string;
|
|
76
|
+
colorPurpleBackground?: string;
|
|
77
|
+
colorPurpleText?: string;
|
|
78
|
+
colorFuchsiaBackground?: string;
|
|
79
|
+
colorFuchsiaText?: string;
|
|
80
|
+
colorRedBackground?: string;
|
|
81
|
+
colorRedText?: string;
|
|
82
|
+
colorChart1?: string;
|
|
83
|
+
colorChart2?: string;
|
|
84
|
+
colorChart3?: string;
|
|
85
|
+
colorChart4?: string;
|
|
86
|
+
colorChart5?: string;
|
|
87
|
+
colorChart6?: string;
|
|
88
|
+
colorChart7?: string;
|
|
89
|
+
colorChart8?: string;
|
|
58
90
|
};
|
|
59
91
|
export declare const toRgba: (hex: string, alpha: number) => string;
|
|
60
92
|
export declare const toRgb: (hex: string) => string;
|
|
@@ -42,6 +42,45 @@ const colorDarkerShade = "#1c1d22";
|
|
|
42
42
|
const colorFullShade = "#0b0c0e";
|
|
43
43
|
// Border colors
|
|
44
44
|
const colorBorderLightShade = "#e3e4f3";
|
|
45
|
+
// Categorical colors
|
|
46
|
+
// Each hue pairs a tinted background with a saturated foreground for text and icons.
|
|
47
|
+
const colorIndigoBackground = "#eef2ff";
|
|
48
|
+
const colorIndigoText = "#4f46e5";
|
|
49
|
+
const colorEmeraldBackground = "#ecfdf5";
|
|
50
|
+
const colorEmeraldText = "#059669";
|
|
51
|
+
const colorAmberBackground = "#fef3c7";
|
|
52
|
+
const colorAmberText = "#b45309";
|
|
53
|
+
const colorPinkBackground = "#fce7f3";
|
|
54
|
+
const colorPinkText = "#be185d";
|
|
55
|
+
const colorSkyBackground = "#e0f2fe";
|
|
56
|
+
const colorSkyText = "#0369a1";
|
|
57
|
+
const colorOrangeBackground = "#ffedd5";
|
|
58
|
+
const colorOrangeText = "#c2410c";
|
|
59
|
+
const colorSlateBackground = "#f1f5f9";
|
|
60
|
+
const colorSlateText = "#475569";
|
|
61
|
+
const colorTealBackground = "#ccfbf1";
|
|
62
|
+
const colorTealText = "#0f766e";
|
|
63
|
+
const colorLimeBackground = "#ecfccb";
|
|
64
|
+
const colorLimeText = "#4d7c0f";
|
|
65
|
+
const colorPurpleBackground = "#f3e8ff";
|
|
66
|
+
const colorPurpleText = "#7e22ce";
|
|
67
|
+
const colorFuchsiaBackground = "#fae8ff";
|
|
68
|
+
const colorFuchsiaText = "#a21caf";
|
|
69
|
+
const colorRedBackground = "#fee2e2";
|
|
70
|
+
const colorRedText = "#b91c1c";
|
|
71
|
+
// Chart colors
|
|
72
|
+
// Grounded in the Okabe-Ito colorblind-safe palette, ordered for maximum
|
|
73
|
+
// contrast between adjacent series and anchored to the brand's blue and purple.
|
|
74
|
+
// The light gold and pure black of the original palette are darkened and
|
|
75
|
+
// softened so they read on a light background.
|
|
76
|
+
const colorChart1 = "#0072b2"; // Blue, neighbors the brand primary.
|
|
77
|
+
const colorChart2 = "#e69f00"; // Orange.
|
|
78
|
+
const colorChart3 = "#009e73"; // Teal-green.
|
|
79
|
+
const colorChart4 = "#cc79a7"; // Mauve, echoes the brand accent.
|
|
80
|
+
const colorChart5 = "#56b4e9"; // Sky.
|
|
81
|
+
const colorChart6 = "#d55e00"; // Vermillion.
|
|
82
|
+
const colorChart7 = "#9a8200"; // Deep gold.
|
|
83
|
+
const colorChart8 = "#3f4551"; // Dark slate.
|
|
45
84
|
export const LIGHT_THEME = {
|
|
46
85
|
// Font
|
|
47
86
|
fontFamily,
|
|
@@ -104,7 +143,41 @@ export const LIGHT_THEME = {
|
|
|
104
143
|
colorBorderMedium: colorMediumShade,
|
|
105
144
|
colorBorderMediumRgb: toRgb(colorMediumShade),
|
|
106
145
|
colorBorderLight: colorBorderLightShade,
|
|
107
|
-
colorBorderLightRgb: toRgb(colorBorderLightShade)
|
|
146
|
+
colorBorderLightRgb: toRgb(colorBorderLightShade),
|
|
147
|
+
// Categorical colors
|
|
148
|
+
colorIndigoBackground,
|
|
149
|
+
colorIndigoText,
|
|
150
|
+
colorEmeraldBackground,
|
|
151
|
+
colorEmeraldText,
|
|
152
|
+
colorAmberBackground,
|
|
153
|
+
colorAmberText,
|
|
154
|
+
colorPinkBackground,
|
|
155
|
+
colorPinkText,
|
|
156
|
+
colorSkyBackground,
|
|
157
|
+
colorSkyText,
|
|
158
|
+
colorOrangeBackground,
|
|
159
|
+
colorOrangeText,
|
|
160
|
+
colorSlateBackground,
|
|
161
|
+
colorSlateText,
|
|
162
|
+
colorTealBackground,
|
|
163
|
+
colorTealText,
|
|
164
|
+
colorLimeBackground,
|
|
165
|
+
colorLimeText,
|
|
166
|
+
colorPurpleBackground,
|
|
167
|
+
colorPurpleText,
|
|
168
|
+
colorFuchsiaBackground,
|
|
169
|
+
colorFuchsiaText,
|
|
170
|
+
colorRedBackground,
|
|
171
|
+
colorRedText,
|
|
172
|
+
// Chart colors
|
|
173
|
+
colorChart1,
|
|
174
|
+
colorChart2,
|
|
175
|
+
colorChart3,
|
|
176
|
+
colorChart4,
|
|
177
|
+
colorChart5,
|
|
178
|
+
colorChart6,
|
|
179
|
+
colorChart7,
|
|
180
|
+
colorChart8
|
|
108
181
|
};
|
|
109
182
|
export const DARK_THEME = {
|
|
110
183
|
// Special colors
|
|
@@ -129,7 +202,13 @@ export const DARK_THEME = {
|
|
|
129
202
|
colorText: colorLightShade,
|
|
130
203
|
colorLabel: colorLightShade,
|
|
131
204
|
// Border color
|
|
132
|
-
colorBorderLight: "#323338"
|
|
205
|
+
colorBorderLight: "#323338",
|
|
206
|
+
// Chart colors
|
|
207
|
+
// Lighten the dark anchors so series stay legible on a dark background; the
|
|
208
|
+
// mid-toned hues fall through to their light-theme values.
|
|
209
|
+
colorChart1: "#4aa3e0",
|
|
210
|
+
colorChart7: "#d4b400",
|
|
211
|
+
colorChart8: "#aab3c2" // Light slate.
|
|
133
212
|
};
|
|
134
213
|
export const toStyle = (theme) => {
|
|
135
214
|
const vars = {
|
|
@@ -194,7 +273,41 @@ export const toStyle = (theme) => {
|
|
|
194
273
|
"--vui-color-border-medium": theme.colorBorderMedium,
|
|
195
274
|
"--vui-color-border-medium-rgb": theme.colorBorderMediumRgb,
|
|
196
275
|
"--vui-color-border-light": theme.colorBorderLight,
|
|
197
|
-
"--vui-color-border-light-rgb": theme.colorBorderLightRgb
|
|
276
|
+
"--vui-color-border-light-rgb": theme.colorBorderLightRgb,
|
|
277
|
+
// Categorical colors
|
|
278
|
+
"--vui-color-indigo-background": theme.colorIndigoBackground,
|
|
279
|
+
"--vui-color-indigo-text": theme.colorIndigoText,
|
|
280
|
+
"--vui-color-emerald-background": theme.colorEmeraldBackground,
|
|
281
|
+
"--vui-color-emerald-text": theme.colorEmeraldText,
|
|
282
|
+
"--vui-color-amber-background": theme.colorAmberBackground,
|
|
283
|
+
"--vui-color-amber-text": theme.colorAmberText,
|
|
284
|
+
"--vui-color-pink-background": theme.colorPinkBackground,
|
|
285
|
+
"--vui-color-pink-text": theme.colorPinkText,
|
|
286
|
+
"--vui-color-sky-background": theme.colorSkyBackground,
|
|
287
|
+
"--vui-color-sky-text": theme.colorSkyText,
|
|
288
|
+
"--vui-color-orange-background": theme.colorOrangeBackground,
|
|
289
|
+
"--vui-color-orange-text": theme.colorOrangeText,
|
|
290
|
+
"--vui-color-slate-background": theme.colorSlateBackground,
|
|
291
|
+
"--vui-color-slate-text": theme.colorSlateText,
|
|
292
|
+
"--vui-color-teal-background": theme.colorTealBackground,
|
|
293
|
+
"--vui-color-teal-text": theme.colorTealText,
|
|
294
|
+
"--vui-color-lime-background": theme.colorLimeBackground,
|
|
295
|
+
"--vui-color-lime-text": theme.colorLimeText,
|
|
296
|
+
"--vui-color-purple-background": theme.colorPurpleBackground,
|
|
297
|
+
"--vui-color-purple-text": theme.colorPurpleText,
|
|
298
|
+
"--vui-color-fuchsia-background": theme.colorFuchsiaBackground,
|
|
299
|
+
"--vui-color-fuchsia-text": theme.colorFuchsiaText,
|
|
300
|
+
"--vui-color-red-background": theme.colorRedBackground,
|
|
301
|
+
"--vui-color-red-text": theme.colorRedText,
|
|
302
|
+
// Chart colors
|
|
303
|
+
"--vui-chart-1": theme.colorChart1,
|
|
304
|
+
"--vui-chart-2": theme.colorChart2,
|
|
305
|
+
"--vui-chart-3": theme.colorChart3,
|
|
306
|
+
"--vui-chart-4": theme.colorChart4,
|
|
307
|
+
"--vui-chart-5": theme.colorChart5,
|
|
308
|
+
"--vui-chart-6": theme.colorChart6,
|
|
309
|
+
"--vui-chart-7": theme.colorChart7,
|
|
310
|
+
"--vui-chart-8": theme.colorChart8
|
|
198
311
|
};
|
|
199
312
|
// Remove undefined values.
|
|
200
313
|
return Object.fromEntries(Object.entries(vars).filter(([_, v]) => v !== undefined));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ReactNode, RefObject } from "react";
|
|
2
|
+
type Props = {
|
|
3
|
+
onFilesDropped: (files: File[]) => void;
|
|
4
|
+
scopeRef?: RefObject<HTMLElement>;
|
|
5
|
+
message?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
export declare const VuiFileDropTarget: ({ onFilesDropped, scopeRef, message }: Props) => import("react/jsx-runtime").JSX.Element | null;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { BiUpload } from "react-icons/bi";
|
|
5
|
+
import { VuiScreenBlock } from "../screenBlock/ScreenBlock";
|
|
6
|
+
import { VuiFlexContainer } from "../flex/FlexContainer";
|
|
7
|
+
import { VuiFlexItem } from "../flex/FlexItem";
|
|
8
|
+
import { VuiIcon } from "../icon/Icon";
|
|
9
|
+
import { VuiText } from "../typography/Text";
|
|
10
|
+
const defaultMessage = (_jsxs(VuiFlexContainer, Object.assign({ direction: "column", alignItems: "center", justifyContent: "center" }, { children: [_jsx(VuiFlexItem, { children: _jsx(VuiIcon, Object.assign({ size: "xxxl", color: "empty" }, { children: _jsx(BiUpload, {}) })) }), _jsx(VuiFlexItem, { children: _jsx(VuiText, Object.assign({ align: "center", size: "l" }, { children: _jsx("p", { children: "Drop files to add to upload" }) })) })] })));
|
|
11
|
+
export const VuiFileDropTarget = ({ onFilesDropped, scopeRef, message = defaultMessage }) => {
|
|
12
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
13
|
+
const dragCounterRef = useRef(0);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
var _a;
|
|
16
|
+
const target = (_a = scopeRef === null || scopeRef === void 0 ? void 0 : scopeRef.current) !== null && _a !== void 0 ? _a : document;
|
|
17
|
+
// Reset whenever the target changes (e.g. when scopeRef attaches or the parent
|
|
18
|
+
// swaps between scoped and global modes) so a stale counter doesn't suppress events.
|
|
19
|
+
dragCounterRef.current = 0;
|
|
20
|
+
setIsDragging(false);
|
|
21
|
+
const handleDragEnter = (e) => {
|
|
22
|
+
var _a;
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
dragCounterRef.current += 1;
|
|
25
|
+
// Only show the overlay for drags that carry files, not e.g. selected text.
|
|
26
|
+
if (dragCounterRef.current === 1 && ((_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.items) && e.dataTransfer.items.length > 0) {
|
|
27
|
+
setIsDragging(true);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const handleDragLeave = (e) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
dragCounterRef.current -= 1;
|
|
33
|
+
if (dragCounterRef.current === 0) {
|
|
34
|
+
setIsDragging(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const handleDragOver = (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
};
|
|
40
|
+
const handleDrop = (e) => {
|
|
41
|
+
var _a;
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
setIsDragging(false);
|
|
44
|
+
dragCounterRef.current = 0;
|
|
45
|
+
const files = (_a = e.dataTransfer) === null || _a === void 0 ? void 0 : _a.files;
|
|
46
|
+
if (files && files.length > 0) {
|
|
47
|
+
onFilesDropped(Array.from(files));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
target.addEventListener("dragenter", handleDragEnter);
|
|
51
|
+
target.addEventListener("dragleave", handleDragLeave);
|
|
52
|
+
target.addEventListener("dragover", handleDragOver);
|
|
53
|
+
target.addEventListener("drop", handleDrop);
|
|
54
|
+
return () => {
|
|
55
|
+
target.removeEventListener("dragenter", handleDragEnter);
|
|
56
|
+
target.removeEventListener("dragleave", handleDragLeave);
|
|
57
|
+
target.removeEventListener("dragover", handleDragOver);
|
|
58
|
+
target.removeEventListener("drop", handleDrop);
|
|
59
|
+
};
|
|
60
|
+
}, [scopeRef, onFilesDropped]);
|
|
61
|
+
if (!isDragging)
|
|
62
|
+
return null;
|
|
63
|
+
// Scoped mode: overlay covers only the parent element.
|
|
64
|
+
if (scopeRef === null || scopeRef === void 0 ? void 0 : scopeRef.current) {
|
|
65
|
+
return createPortal(_jsx("div", Object.assign({ className: "vuiFileDropTarget__scopedOverlay" }, { children: _jsx("div", Object.assign({ className: "vuiFileDropTarget__message" }, { children: message })) })), scopeRef.current);
|
|
66
|
+
}
|
|
67
|
+
// Global mode: full-screen overlay.
|
|
68
|
+
return (_jsx(VuiScreenBlock, Object.assign({ color: "primary" }, { children: _jsx("div", Object.assign({ className: "vuiFileDropTarget__messageContainer" }, { children: _jsx("div", Object.assign({ className: "vuiFileDropTarget__message" }, { children: message })) })) })));
|
|
69
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.vuiFileDropTarget__messageContainer {
|
|
2
|
+
position: fixed;
|
|
3
|
+
left: 0;
|
|
4
|
+
top: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
display: flex;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
align-items: center;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.vuiFileDropTarget__message {
|
|
13
|
+
padding: $sizeL;
|
|
14
|
+
max-width: 400px;
|
|
15
|
+
width: 100%;
|
|
16
|
+
pointer-events: none;
|
|
17
|
+
|
|
18
|
+
p,
|
|
19
|
+
span {
|
|
20
|
+
color: var(--vui-color-empty-shade) !important;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Scoped mode: portal-mounted overlay covering only the parent element (which
|
|
25
|
+
// must be non-statically positioned).
|
|
26
|
+
.vuiFileDropTarget__scopedOverlay {
|
|
27
|
+
position: absolute;
|
|
28
|
+
inset: 0;
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
background-color: rgba(var(--vui-color-primary-shade-rgb), 0.6);
|
|
33
|
+
z-index: 10;
|
|
34
|
+
}
|