@inkra/react 0.0.0-stage → 0.2.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 +316 -2
- package/THIRD_PARTY_NOTICES.md +39 -0
- package/Yozai-OFL.txt +94 -0
- package/dist/Button.d.ts +36 -0
- package/dist/Button.js +89 -0
- package/dist/PencilIcon.d.ts +22 -0
- package/dist/PencilIcon.js +210 -0
- package/dist/PencilLine.d.ts +10 -0
- package/dist/PencilLine.js +91 -0
- package/dist/PencilSurface.d.ts +19 -0
- package/dist/PencilSurface.js +19 -0
- package/dist/PencilText.d.ts +16 -0
- package/dist/PencilText.js +338 -0
- package/dist/accordion/Accordion.d.ts +27 -0
- package/dist/accordion/Accordion.js +40 -0
- package/dist/accordion/index.d.ts +2 -0
- package/dist/accordion/index.js +1 -0
- package/dist/alert-dialog/index.d.ts +64 -0
- package/dist/alert-dialog/index.js +109 -0
- package/dist/assets/ui-hand-yozai.woff2 +0 -0
- package/dist/audio-player/index.d.ts +60 -0
- package/dist/audio-player/index.js +101 -0
- package/dist/breadcrumb/index.d.ts +16 -0
- package/dist/breadcrumb/index.js +25 -0
- package/dist/calendar/index.d.ts +21 -0
- package/dist/calendar/index.js +41 -0
- package/dist/carousel/index.d.ts +51 -0
- package/dist/carousel/index.js +290 -0
- package/dist/choice-grid/index.d.ts +38 -0
- package/dist/choice-grid/index.js +85 -0
- package/dist/color-picker/color.d.ts +15 -0
- package/dist/color-picker/color.js +55 -0
- package/dist/color-picker/index.d.ts +31 -0
- package/dist/color-picker/index.js +69 -0
- package/dist/command/index.d.ts +23 -0
- package/dist/command/index.js +39 -0
- package/dist/content/index.d.ts +106 -0
- package/dist/content/index.js +80 -0
- package/dist/context-menu/index.d.ts +76 -0
- package/dist/context-menu/index.js +240 -0
- package/dist/data-table/index.d.ts +47 -0
- package/dist/data-table/index.js +91 -0
- package/dist/date-picker/index.d.ts +50 -0
- package/dist/date-picker/index.js +180 -0
- package/dist/day-agenda/index.d.ts +43 -0
- package/dist/day-agenda/index.js +47 -0
- package/dist/day-agenda/layout.d.ts +20 -0
- package/dist/day-agenda/layout.js +60 -0
- package/dist/display/ProgressRing.d.ts +6 -0
- package/dist/display/ProgressRing.js +146 -0
- package/dist/display/index.d.ts +59 -0
- package/dist/display/index.js +75 -0
- package/dist/dockable-panel/index.d.ts +41 -0
- package/dist/dockable-panel/index.js +253 -0
- package/dist/dockable-panel/model.d.ts +21 -0
- package/dist/dockable-panel/model.js +61 -0
- package/dist/drawer/index.d.ts +63 -0
- package/dist/drawer/index.js +86 -0
- package/dist/dropdown-menu/index.d.ts +34 -0
- package/dist/dropdown-menu/index.js +40 -0
- package/dist/editable/index.d.ts +53 -0
- package/dist/editable/index.js +162 -0
- package/dist/empty/index.d.ts +21 -0
- package/dist/empty/index.js +23 -0
- package/dist/flip/index.d.ts +49 -0
- package/dist/flip/index.js +138 -0
- package/dist/form/EditablePencilText.d.ts +13 -0
- package/dist/form/EditablePencilText.js +323 -0
- package/dist/form/Field.d.ts +13 -0
- package/dist/form/Field.js +26 -0
- package/dist/form/Input.d.ts +27 -0
- package/dist/form/Input.js +97 -0
- package/dist/form/InputAction.d.ts +7 -0
- package/dist/form/InputAction.js +24 -0
- package/dist/form/Textarea.d.ts +17 -0
- package/dist/form/Textarea.js +102 -0
- package/dist/form/context.d.ts +12 -0
- package/dist/form/context.js +10 -0
- package/dist/form/controlProps.d.ts +23 -0
- package/dist/form/controlProps.js +20 -0
- package/dist/form/index.d.ts +7 -0
- package/dist/form/index.js +3 -0
- package/dist/form/types.d.ts +6 -0
- package/dist/form/types.js +1 -0
- package/dist/heatmap/index.d.ts +59 -0
- package/dist/heatmap/index.js +248 -0
- package/dist/hotkey/index.d.ts +37 -0
- package/dist/hotkey/index.js +130 -0
- package/dist/hotkey/model.d.ts +27 -0
- package/dist/hotkey/model.js +97 -0
- package/dist/hover-card/index.d.ts +33 -0
- package/dist/hover-card/index.js +141 -0
- package/dist/image-cropper/index.d.ts +82 -0
- package/dist/image-cropper/index.js +261 -0
- package/dist/image-cropper/model.d.ts +42 -0
- package/dist/image-cropper/model.js +94 -0
- package/dist/image-viewer/index.d.ts +79 -0
- package/dist/image-viewer/index.js +220 -0
- package/dist/image-viewer/model.d.ts +9 -0
- package/dist/image-viewer/model.js +18 -0
- package/dist/images/layers-2x.png +0 -0
- package/dist/images/layers.png +0 -0
- package/dist/images/marker-icon.png +0 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +69 -0
- package/dist/input-number/index.d.ts +16 -0
- package/dist/input-number/index.js +77 -0
- package/dist/internal/InputActionContext.d.ts +1 -0
- package/dist/internal/InputActionContext.js +3 -0
- package/dist/internal/cache.d.ts +14 -0
- package/dist/internal/cache.js +54 -0
- package/dist/internal/line.d.ts +4 -0
- package/dist/internal/line.js +21 -0
- package/dist/internal/paper.d.ts +1 -0
- package/dist/internal/paper.js +65 -0
- package/dist/internal/random.d.ts +3 -0
- package/dist/internal/random.js +30 -0
- package/dist/internal/recovery.d.ts +1 -0
- package/dist/internal/recovery.js +52 -0
- package/dist/internal/renderTokens.d.ts +10 -0
- package/dist/internal/renderTokens.js +13 -0
- package/dist/internal/stroke.d.ts +24 -0
- package/dist/internal/stroke.js +144 -0
- package/dist/internal/surfaceRenderer.d.ts +41 -0
- package/dist/internal/surfaceRenderer.js +178 -0
- package/dist/internal/textQueue.d.ts +1 -0
- package/dist/internal/textQueue.js +37 -0
- package/dist/internal/textRenderer.d.ts +28 -0
- package/dist/internal/textRenderer.js +239 -0
- package/dist/internal/travelIcons.d.ts +24 -0
- package/dist/internal/travelIcons.js +14 -0
- package/dist/internal/useBrowserLayoutEffect.d.ts +2 -0
- package/dist/internal/useBrowserLayoutEffect.js +3 -0
- package/dist/internal/usePencilSurface.d.ts +3 -0
- package/dist/internal/usePencilSurface.js +116 -0
- package/dist/internal/useSelectionDrawing.d.ts +2 -0
- package/dist/internal/useSelectionDrawing.js +90 -0
- package/dist/internal/wash.d.ts +16 -0
- package/dist/internal/wash.js +149 -0
- package/dist/journey-map/MapScale.d.ts +4 -0
- package/dist/journey-map/MapScale.js +54 -0
- package/dist/journey-map/index.d.ts +39 -0
- package/dist/journey-map/index.js +197 -0
- package/dist/journey-map/terrain.d.ts +8 -0
- package/dist/journey-map/terrain.js +127 -0
- package/dist/keypad/index.d.ts +23 -0
- package/dist/keypad/index.js +63 -0
- package/dist/keypad/model.d.ts +20 -0
- package/dist/keypad/model.js +33 -0
- package/dist/layout/index.d.ts +63 -0
- package/dist/layout/index.js +62 -0
- package/dist/log/index.d.ts +45 -0
- package/dist/log/index.js +159 -0
- package/dist/log/model.d.ts +16 -0
- package/dist/log/model.js +13 -0
- package/dist/node-graph/index.d.ts +123 -0
- package/dist/node-graph/index.js +304 -0
- package/dist/number-ticker/index.d.ts +29 -0
- package/dist/number-ticker/index.js +132 -0
- package/dist/number-ticker/model.d.ts +8 -0
- package/dist/number-ticker/model.js +34 -0
- package/dist/overflow-list/index.d.ts +39 -0
- package/dist/overflow-list/index.js +230 -0
- package/dist/overflow-list/layout.d.ts +19 -0
- package/dist/overflow-list/layout.js +44 -0
- package/dist/overlay/Overlay.d.ts +65 -0
- package/dist/overlay/Overlay.js +77 -0
- package/dist/overlay/Popover.d.ts +43 -0
- package/dist/overlay/Popover.js +45 -0
- package/dist/overlay/index.d.ts +4 -0
- package/dist/overlay/index.js +2 -0
- package/dist/overlay/themeBridge.d.ts +8 -0
- package/dist/overlay/themeBridge.js +98 -0
- package/dist/pagination/index.d.ts +23 -0
- package/dist/pagination/index.js +41 -0
- package/dist/pagination/model.d.ts +4 -0
- package/dist/pagination/model.js +42 -0
- package/dist/pan-zoom/index.d.ts +71 -0
- package/dist/pan-zoom/index.js +175 -0
- package/dist/pencil.d.ts +10 -0
- package/dist/pencil.js +39 -0
- package/dist/radar/index.d.ts +54 -0
- package/dist/radar/index.js +220 -0
- package/dist/range-slider/index.d.ts +16 -0
- package/dist/range-slider/index.js +22 -0
- package/dist/rating/index.d.ts +28 -0
- package/dist/rating/index.js +127 -0
- package/dist/resizable/index.d.ts +32 -0
- package/dist/resizable/index.js +50 -0
- package/dist/scroll-area/index.d.ts +36 -0
- package/dist/scroll-area/index.js +44 -0
- package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
- package/dist/segmented-progress/SegmentedProgress.js +42 -0
- package/dist/select/MultipleSelect.d.ts +44 -0
- package/dist/select/MultipleSelect.js +124 -0
- package/dist/select/SearchableSelect.d.ts +45 -0
- package/dist/select/SearchableSelect.js +50 -0
- package/dist/select/Select.d.ts +109 -0
- package/dist/select/Select.js +205 -0
- package/dist/select/index.d.ts +4 -0
- package/dist/select/index.js +2 -0
- package/dist/selection/Checkbox.d.ts +9 -0
- package/dist/selection/Checkbox.js +37 -0
- package/dist/selection/RadioGroup.d.ts +23 -0
- package/dist/selection/RadioGroup.js +82 -0
- package/dist/selection/Switch.d.ts +8 -0
- package/dist/selection/Switch.js +36 -0
- package/dist/selection/index.d.ts +6 -0
- package/dist/selection/index.js +3 -0
- package/dist/selection/useNativeChecked.d.ts +13 -0
- package/dist/selection/useNativeChecked.js +27 -0
- package/dist/slider/index.d.ts +11 -0
- package/dist/slider/index.js +45 -0
- package/dist/slot/index.d.ts +24 -0
- package/dist/slot/index.js +29 -0
- package/dist/sortable/index.d.ts +50 -0
- package/dist/sortable/index.js +278 -0
- package/dist/sortable/model.d.ts +30 -0
- package/dist/sortable/model.js +64 -0
- package/dist/sparkline/index.d.ts +19 -0
- package/dist/sparkline/index.js +240 -0
- package/dist/sparkline/model.d.ts +16 -0
- package/dist/sparkline/model.js +28 -0
- package/dist/steps/index.d.ts +16 -0
- package/dist/steps/index.js +37 -0
- package/dist/styles.css +8226 -0
- package/dist/tabs/index.d.ts +22 -0
- package/dist/tabs/index.js +42 -0
- package/dist/tag-input/index.d.ts +69 -0
- package/dist/tag-input/index.js +325 -0
- package/dist/tag-input/model.d.ts +14 -0
- package/dist/tag-input/model.js +38 -0
- package/dist/time-picker/index.d.ts +15 -0
- package/dist/time-picker/index.js +168 -0
- package/dist/timeline/index.d.ts +14 -0
- package/dist/timeline/index.js +16 -0
- package/dist/toast/index.d.ts +27 -0
- package/dist/toast/index.js +36 -0
- package/dist/toggle-group/index.d.ts +17 -0
- package/dist/toggle-group/index.js +26 -0
- package/dist/tokens.css +356 -0
- package/dist/tooltip/Tooltip.d.ts +24 -0
- package/dist/tooltip/Tooltip.js +32 -0
- package/dist/tooltip/index.d.ts +2 -0
- package/dist/tooltip/index.js +1 -0
- package/dist/transfer/index.d.ts +64 -0
- package/dist/transfer/index.js +174 -0
- package/dist/transfer/model.d.ts +12 -0
- package/dist/transfer/model.js +33 -0
- package/dist/tree/index.d.ts +39 -0
- package/dist/tree/index.js +36 -0
- package/dist/tree-select/index.d.ts +59 -0
- package/dist/tree-select/index.js +142 -0
- package/dist/tree-select/model.d.ts +31 -0
- package/dist/tree-select/model.js +135 -0
- package/dist/typewriter/index.d.ts +21 -0
- package/dist/typewriter/index.js +101 -0
- package/dist/typewriter/model.d.ts +7 -0
- package/dist/typewriter/model.js +45 -0
- package/dist/upload/index.d.ts +59 -0
- package/dist/upload/index.js +230 -0
- package/dist/virtual-list/index.d.ts +44 -0
- package/dist/virtual-list/index.js +128 -0
- package/dist/virtual-list/model.d.ts +7 -0
- package/dist/virtual-list/model.js +27 -0
- package/package.json +119 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const modifierKeys = new Set([
|
|
2
|
+
'Alt', 'AltGraph', 'Control', 'Fn', 'FnLock', 'Hyper', 'Meta', 'OS', 'Shift', 'Super', 'Symbol', 'SymbolLock',
|
|
3
|
+
]);
|
|
4
|
+
const ignoredKeys = new Set(['Dead', 'Process', 'Unidentified']);
|
|
5
|
+
const modifierOrder = ['control', 'alt', 'shift', 'meta'];
|
|
6
|
+
const keyLabels = {
|
|
7
|
+
' ': 'Space',
|
|
8
|
+
ArrowDown: '↓',
|
|
9
|
+
ArrowLeft: '←',
|
|
10
|
+
ArrowRight: '→',
|
|
11
|
+
ArrowUp: '↑',
|
|
12
|
+
Backspace: 'Backspace',
|
|
13
|
+
CapsLock: 'Caps Lock',
|
|
14
|
+
Delete: 'Delete',
|
|
15
|
+
End: 'End',
|
|
16
|
+
Enter: 'Enter',
|
|
17
|
+
Escape: 'Esc',
|
|
18
|
+
Home: 'Home',
|
|
19
|
+
PageDown: 'Page Down',
|
|
20
|
+
PageUp: 'Page Up',
|
|
21
|
+
Tab: 'Tab',
|
|
22
|
+
};
|
|
23
|
+
const macKeyLabels = {
|
|
24
|
+
Backspace: '⌫',
|
|
25
|
+
CapsLock: '⇪',
|
|
26
|
+
Delete: '⌦',
|
|
27
|
+
End: '↘',
|
|
28
|
+
Enter: '↩',
|
|
29
|
+
Escape: 'Esc',
|
|
30
|
+
Home: '↖',
|
|
31
|
+
PageDown: '⇟',
|
|
32
|
+
PageUp: '⇞',
|
|
33
|
+
Tab: '⇥',
|
|
34
|
+
};
|
|
35
|
+
const modifierLabels = {
|
|
36
|
+
mac: { control: '⌃', alt: '⌥', shift: '⇧', meta: '⌘' },
|
|
37
|
+
windows: { control: 'Ctrl', alt: 'Alt', shift: 'Shift', meta: 'Win' },
|
|
38
|
+
};
|
|
39
|
+
export function detectHotkeyPlatform(platform) {
|
|
40
|
+
return /mac|iphone|ipad|ipod/i.test(platform ?? '') ? 'mac' : 'windows';
|
|
41
|
+
}
|
|
42
|
+
export function normalizeHotkey(value) {
|
|
43
|
+
const modifiers = modifierOrder.filter(modifier => value.modifiers?.includes(modifier));
|
|
44
|
+
return {
|
|
45
|
+
key: normalizeHotkeyKey(value.key),
|
|
46
|
+
...(value.code ? { code: value.code } : {}),
|
|
47
|
+
...(modifiers.length ? { modifiers } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function normalizeHotkeyKey(key) {
|
|
51
|
+
if (key === 'Spacebar')
|
|
52
|
+
return ' ';
|
|
53
|
+
if (key === 'Esc')
|
|
54
|
+
return 'Escape';
|
|
55
|
+
if (key.length === 1 && /[a-z]/i.test(key))
|
|
56
|
+
return key.toUpperCase();
|
|
57
|
+
return key;
|
|
58
|
+
}
|
|
59
|
+
export function hotkeyFromKeyboardEvent(event) {
|
|
60
|
+
if (event.isComposing || event.keyCode === 229 || modifierKeys.has(event.key) || ignoredKeys.has(event.key))
|
|
61
|
+
return null;
|
|
62
|
+
const modifiers = modifierOrder.filter(modifier => {
|
|
63
|
+
if (modifier === 'control')
|
|
64
|
+
return Boolean(event.ctrlKey);
|
|
65
|
+
if (modifier === 'alt')
|
|
66
|
+
return Boolean(event.altKey);
|
|
67
|
+
if (modifier === 'shift')
|
|
68
|
+
return Boolean(event.shiftKey);
|
|
69
|
+
return Boolean(event.metaKey);
|
|
70
|
+
});
|
|
71
|
+
return normalizeHotkey({ key: event.key, code: event.code, modifiers });
|
|
72
|
+
}
|
|
73
|
+
export function hotkeySignature(value) {
|
|
74
|
+
if (!value)
|
|
75
|
+
return '';
|
|
76
|
+
const normalized = normalizeHotkey(value);
|
|
77
|
+
return `${normalized.modifiers?.join('+') ?? ''}|${normalized.code ?? ''}|${normalized.key}`;
|
|
78
|
+
}
|
|
79
|
+
export function getHotkeyTokens(value, platform) {
|
|
80
|
+
const normalized = normalizeHotkey(value);
|
|
81
|
+
const modifiers = normalized.modifiers?.map(modifier => modifierLabels[platform][modifier]) ?? [];
|
|
82
|
+
const key = (platform === 'mac' ? macKeyLabels[normalized.key] : undefined)
|
|
83
|
+
?? keyLabels[normalized.key]
|
|
84
|
+
?? normalized.key;
|
|
85
|
+
return [...modifiers, key];
|
|
86
|
+
}
|
|
87
|
+
export function formatHotkey(value, platform) {
|
|
88
|
+
const tokens = getHotkeyTokens(value, platform);
|
|
89
|
+
return platform === 'mac' ? tokens.join('') : tokens.join(' + ');
|
|
90
|
+
}
|
|
91
|
+
export function isHotkeyEscape(event) {
|
|
92
|
+
return !event.isComposing && event.keyCode !== 229 && normalizeHotkeyKey(event.key) === 'Escape';
|
|
93
|
+
}
|
|
94
|
+
export function isHotkeyClear(event) {
|
|
95
|
+
return !event.isComposing && event.keyCode !== 229 && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey
|
|
96
|
+
&& (event.key === 'Backspace' || event.key === 'Delete');
|
|
97
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PopoverCloseProps, PopoverContentProps, PopoverDescriptionProps, PopoverRootProps, PopoverTitleProps, PopoverTriggerProps } from '../overlay/index.js';
|
|
2
|
+
export interface HoverCardRootProps extends Omit<PopoverRootProps, 'defaultOpen' | 'modal' | 'onOpenChange' | 'open'> {
|
|
3
|
+
/** Delay in milliseconds before hover or focus opens the card. */
|
|
4
|
+
openDelay?: number;
|
|
5
|
+
/** Delay in milliseconds before pointer or focus departure closes the card. */
|
|
6
|
+
closeDelay?: number;
|
|
7
|
+
defaultOpen?: boolean;
|
|
8
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
|
+
open?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function HoverCardRoot({ children, closeDelay, defaultOpen, onOpenChange, open, openDelay, ...props }: HoverCardRootProps): import("react").JSX.Element;
|
|
12
|
+
export type HoverCardTriggerProps = PopoverTriggerProps;
|
|
13
|
+
export declare const HoverCardTrigger: import("react").ForwardRefExoticComponent<import("@radix-ui/react-popover").PopoverTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
14
|
+
export type HoverCardFocusOnOpen = 'always' | 'focus-and-press' | 'never' | 'press';
|
|
15
|
+
export interface HoverCardContentProps extends PopoverContentProps {
|
|
16
|
+
/** Controls which activation reasons may move focus into the card. */
|
|
17
|
+
focusOnOpen?: HoverCardFocusOnOpen;
|
|
18
|
+
}
|
|
19
|
+
export declare const HoverCardContent: import("react").ForwardRefExoticComponent<HoverCardContentProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
20
|
+
export type HoverCardTitleProps = PopoverTitleProps;
|
|
21
|
+
export declare const HoverCardTitle: import("react").ForwardRefExoticComponent<PopoverTitleProps & import("react").RefAttributes<HTMLHeadingElement>>;
|
|
22
|
+
export type HoverCardDescriptionProps = PopoverDescriptionProps;
|
|
23
|
+
export declare const HoverCardDescription: import("react").ForwardRefExoticComponent<PopoverDescriptionProps & import("react").RefAttributes<HTMLParagraphElement>>;
|
|
24
|
+
export type HoverCardCloseProps = PopoverCloseProps;
|
|
25
|
+
export declare const HoverCardClose: import("react").ForwardRefExoticComponent<Omit<import("@radix-ui/react-popover").PopoverCloseProps & import("react").RefAttributes<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
|
|
26
|
+
export declare const HoverCard: Readonly<{
|
|
27
|
+
Root: typeof HoverCardRoot;
|
|
28
|
+
Trigger: import("react").ForwardRefExoticComponent<import("@radix-ui/react-popover").PopoverTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
29
|
+
Content: import("react").ForwardRefExoticComponent<HoverCardContentProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
30
|
+
Title: import("react").ForwardRefExoticComponent<PopoverTitleProps & import("react").RefAttributes<HTMLHeadingElement>>;
|
|
31
|
+
Description: import("react").ForwardRefExoticComponent<PopoverDescriptionProps & import("react").RefAttributes<HTMLParagraphElement>>;
|
|
32
|
+
Close: import("react").ForwardRefExoticComponent<Omit<import("@radix-ui/react-popover").PopoverCloseProps & import("react").RefAttributes<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, forwardRef, useContext, useEffect, useRef, useState, } from 'react';
|
|
3
|
+
import { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger, } from '../overlay/index.js';
|
|
4
|
+
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
5
|
+
const HoverCardContext = createContext(null);
|
|
6
|
+
function useHoverCardContext(part) {
|
|
7
|
+
const context = useContext(HoverCardContext);
|
|
8
|
+
if (!context)
|
|
9
|
+
throw new Error(`${part} must be used inside HoverCard.Root.`);
|
|
10
|
+
return context;
|
|
11
|
+
}
|
|
12
|
+
export function HoverCardRoot({ children, closeDelay = 300, defaultOpen = false, onOpenChange, open, openDelay = 700, ...props }) {
|
|
13
|
+
const controlled = open !== undefined;
|
|
14
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
15
|
+
const currentOpen = controlled ? open : uncontrolledOpen;
|
|
16
|
+
const currentOpenRef = useRef(currentOpen);
|
|
17
|
+
const openReasonRef = useRef('programmatic');
|
|
18
|
+
const timerRef = useRef(null);
|
|
19
|
+
currentOpenRef.current = currentOpen;
|
|
20
|
+
const cancelPending = () => {
|
|
21
|
+
if (timerRef.current === null)
|
|
22
|
+
return;
|
|
23
|
+
clearTimeout(timerRef.current);
|
|
24
|
+
timerRef.current = null;
|
|
25
|
+
};
|
|
26
|
+
const commitOpen = (nextOpen) => {
|
|
27
|
+
cancelPending();
|
|
28
|
+
if (nextOpen === currentOpenRef.current)
|
|
29
|
+
return;
|
|
30
|
+
if (!controlled) {
|
|
31
|
+
currentOpenRef.current = nextOpen;
|
|
32
|
+
setUncontrolledOpen(nextOpen);
|
|
33
|
+
}
|
|
34
|
+
onOpenChange?.(nextOpen);
|
|
35
|
+
if (!nextOpen)
|
|
36
|
+
openReasonRef.current = 'programmatic';
|
|
37
|
+
};
|
|
38
|
+
const schedule = (nextOpen, delay, reason) => {
|
|
39
|
+
cancelPending();
|
|
40
|
+
if (nextOpen === currentOpenRef.current)
|
|
41
|
+
return;
|
|
42
|
+
openReasonRef.current = reason;
|
|
43
|
+
const safeDelay = Number.isFinite(delay) ? Math.max(0, delay) : 0;
|
|
44
|
+
if (safeDelay === 0) {
|
|
45
|
+
commitOpen(nextOpen);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
timerRef.current = setTimeout(() => {
|
|
49
|
+
timerRef.current = null;
|
|
50
|
+
commitOpen(nextOpen);
|
|
51
|
+
}, safeDelay);
|
|
52
|
+
};
|
|
53
|
+
useEffect(() => cancelPending, []);
|
|
54
|
+
return (_jsx(HoverCardContext.Provider, { value: {
|
|
55
|
+
cancelPending,
|
|
56
|
+
getOpenReason: () => openReasonRef.current,
|
|
57
|
+
markExplicitOpen: () => {
|
|
58
|
+
cancelPending();
|
|
59
|
+
openReasonRef.current = 'press';
|
|
60
|
+
},
|
|
61
|
+
scheduleClose: () => schedule(false, closeDelay, 'programmatic'),
|
|
62
|
+
scheduleOpen: reason => schedule(true, openDelay, reason),
|
|
63
|
+
}, children: _jsx(Popover.Root, { ...props, modal: false, open: currentOpen, onOpenChange: commitOpen, children: children }) }));
|
|
64
|
+
}
|
|
65
|
+
export const HoverCardTrigger = forwardRef(function HoverCardTrigger({ onBlur, onClick, onFocus, onPointerDown, onPointerEnter, onPointerLeave, ...props }, ref) {
|
|
66
|
+
const context = useHoverCardContext('HoverCard.Trigger');
|
|
67
|
+
return (_jsx(Popover.Trigger, { ...props, ref: ref, onBlur: event => {
|
|
68
|
+
onBlur?.(event);
|
|
69
|
+
if (!event.defaultPrevented)
|
|
70
|
+
context.scheduleClose();
|
|
71
|
+
}, onFocus: event => {
|
|
72
|
+
onFocus?.(event);
|
|
73
|
+
if (!event.defaultPrevented)
|
|
74
|
+
context.scheduleOpen('focus');
|
|
75
|
+
}, onClick: event => {
|
|
76
|
+
onClick?.(event);
|
|
77
|
+
if (!event.defaultPrevented)
|
|
78
|
+
context.markExplicitOpen();
|
|
79
|
+
}, onPointerDown: event => {
|
|
80
|
+
onPointerDown?.(event);
|
|
81
|
+
if (!event.defaultPrevented)
|
|
82
|
+
context.cancelPending();
|
|
83
|
+
}, onPointerEnter: event => {
|
|
84
|
+
onPointerEnter?.(event);
|
|
85
|
+
if (!event.defaultPrevented && event.pointerType !== 'touch')
|
|
86
|
+
context.scheduleOpen('hover');
|
|
87
|
+
}, onPointerLeave: event => {
|
|
88
|
+
onPointerLeave?.(event);
|
|
89
|
+
if (!event.defaultPrevented && event.pointerType !== 'touch')
|
|
90
|
+
context.scheduleClose();
|
|
91
|
+
} }));
|
|
92
|
+
});
|
|
93
|
+
export const HoverCardContent = forwardRef(function HoverCardContent({ avoidCollisions = true, children, className, collisionPadding = 16, focusOnOpen = 'press', onBlurCapture, onFocusCapture, onOpenAutoFocus, onPointerDown, onPointerEnter, onPointerLeave, sideOffset = 8, ...props }, ref) {
|
|
94
|
+
const context = useHoverCardContext('HoverCard.Content');
|
|
95
|
+
return (_jsx(Popover.Portal, { children: _jsx(Popover.Content, { ...props, avoidCollisions: avoidCollisions, className: classes('inkra-hover-card__content', className), collisionPadding: collisionPadding, ref: ref, sideOffset: sideOffset, onBlurCapture: event => {
|
|
96
|
+
onBlurCapture?.(event);
|
|
97
|
+
if (event.defaultPrevented)
|
|
98
|
+
return;
|
|
99
|
+
const nextTarget = event.relatedTarget;
|
|
100
|
+
if (!(nextTarget instanceof Node) || !event.currentTarget.contains(nextTarget)) {
|
|
101
|
+
context.scheduleClose();
|
|
102
|
+
}
|
|
103
|
+
}, onFocusCapture: event => {
|
|
104
|
+
onFocusCapture?.(event);
|
|
105
|
+
if (!event.defaultPrevented)
|
|
106
|
+
context.cancelPending();
|
|
107
|
+
}, onOpenAutoFocus: event => {
|
|
108
|
+
onOpenAutoFocus?.(event);
|
|
109
|
+
if (event.defaultPrevented)
|
|
110
|
+
return;
|
|
111
|
+
const reason = context.getOpenReason();
|
|
112
|
+
const shouldFocus = focusOnOpen === 'always'
|
|
113
|
+
|| focusOnOpen === 'press' && reason === 'press'
|
|
114
|
+
|| focusOnOpen === 'focus-and-press' && (reason === 'focus' || reason === 'press');
|
|
115
|
+
if (!shouldFocus)
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
}, onPointerDown: event => {
|
|
118
|
+
onPointerDown?.(event);
|
|
119
|
+
if (!event.defaultPrevented)
|
|
120
|
+
context.cancelPending();
|
|
121
|
+
}, onPointerEnter: event => {
|
|
122
|
+
onPointerEnter?.(event);
|
|
123
|
+
if (!event.defaultPrevented && event.pointerType !== 'touch')
|
|
124
|
+
context.cancelPending();
|
|
125
|
+
}, onPointerLeave: event => {
|
|
126
|
+
onPointerLeave?.(event);
|
|
127
|
+
if (!event.defaultPrevented && event.pointerType !== 'touch')
|
|
128
|
+
context.scheduleClose();
|
|
129
|
+
}, children: children }) }));
|
|
130
|
+
});
|
|
131
|
+
export const HoverCardTitle = PopoverTitle;
|
|
132
|
+
export const HoverCardDescription = PopoverDescription;
|
|
133
|
+
export const HoverCardClose = PopoverClose;
|
|
134
|
+
export const HoverCard = Object.freeze({
|
|
135
|
+
Root: HoverCardRoot,
|
|
136
|
+
Trigger: HoverCardTrigger,
|
|
137
|
+
Content: HoverCardContent,
|
|
138
|
+
Title: HoverCardTitle,
|
|
139
|
+
Description: HoverCardDescription,
|
|
140
|
+
Close: HoverCardClose,
|
|
141
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { HTMLAttributeReferrerPolicy, HTMLAttributes } from 'react';
|
|
2
|
+
import { ImageCropperExportError } from './model.js';
|
|
3
|
+
import type { ImageCropperArea, ImageCropperExportOptions, ImageCropperExportType, ImageCropperPoint, ImageCropperSize } from './model.js';
|
|
4
|
+
export type ImageCropperAspect = number | 'free';
|
|
5
|
+
export type ImageCropperLoadState = 'loading' | 'ready' | 'error';
|
|
6
|
+
export type ImageCropperInteractionSource = 'mouse' | 'touch' | 'wheel' | 'keyboard';
|
|
7
|
+
export type ImageCropperChangeReason = ImageCropperInteractionSource | 'control' | 'reset' | 'source-change';
|
|
8
|
+
export interface ImageCropperChangeDetails {
|
|
9
|
+
reason: ImageCropperChangeReason;
|
|
10
|
+
}
|
|
11
|
+
export interface ImageCropperSelection {
|
|
12
|
+
/** Percentage coordinates relative to the original image. */
|
|
13
|
+
percentages: ImageCropperArea;
|
|
14
|
+
/** Integer coordinates in the original image's natural pixel space. */
|
|
15
|
+
pixels: ImageCropperArea;
|
|
16
|
+
naturalSize: ImageCropperSize;
|
|
17
|
+
}
|
|
18
|
+
export interface ImageCropperExportResult {
|
|
19
|
+
blob: Blob;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
type: ImageCropperExportType;
|
|
23
|
+
selection: ImageCropperSelection;
|
|
24
|
+
}
|
|
25
|
+
export interface ImageCropperLabels {
|
|
26
|
+
cropper: string;
|
|
27
|
+
instructions: string;
|
|
28
|
+
loading: string;
|
|
29
|
+
failedTitle: string;
|
|
30
|
+
failedDescription: string;
|
|
31
|
+
retry: string;
|
|
32
|
+
zoom: string;
|
|
33
|
+
cropWidth: string;
|
|
34
|
+
cropHeight: string;
|
|
35
|
+
reset: string;
|
|
36
|
+
export: string;
|
|
37
|
+
exporting: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ImageCropperHandle {
|
|
40
|
+
exportCrop(options?: ImageCropperExportOptions): Promise<ImageCropperExportResult>;
|
|
41
|
+
getSelection(): ImageCropperSelection | null;
|
|
42
|
+
reset(): void;
|
|
43
|
+
}
|
|
44
|
+
export interface ImageCropperProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onError'> {
|
|
45
|
+
source: string | Blob;
|
|
46
|
+
alt: string;
|
|
47
|
+
aspect?: ImageCropperAspect;
|
|
48
|
+
crop?: ImageCropperPoint;
|
|
49
|
+
defaultCrop?: ImageCropperPoint;
|
|
50
|
+
onCropChange?: (crop: ImageCropperPoint, details: ImageCropperChangeDetails) => void;
|
|
51
|
+
zoom?: number;
|
|
52
|
+
defaultZoom?: number;
|
|
53
|
+
minZoom?: number;
|
|
54
|
+
maxZoom?: number;
|
|
55
|
+
zoomStep?: number;
|
|
56
|
+
onZoomChange?: (zoom: number, details: ImageCropperChangeDetails) => void;
|
|
57
|
+
cropAreaSize?: ImageCropperSize;
|
|
58
|
+
defaultCropAreaSize?: ImageCropperSize;
|
|
59
|
+
onCropAreaSizeChange?: (size: ImageCropperSize, details: ImageCropperChangeDetails) => void;
|
|
60
|
+
onSelectionChange?: (selection: ImageCropperSelection) => void;
|
|
61
|
+
onSelectionCommit?: (selection: ImageCropperSelection) => void;
|
|
62
|
+
onLoadStateChange?: (state: ImageCropperLoadState) => void;
|
|
63
|
+
onMediaLoaded?: (naturalSize: ImageCropperSize) => void;
|
|
64
|
+
onLoadError?: (error: Error) => void;
|
|
65
|
+
onExport?: (result: ImageCropperExportResult) => void;
|
|
66
|
+
onExportError?: (error: ImageCropperExportError) => void;
|
|
67
|
+
exportOptions?: ImageCropperExportOptions;
|
|
68
|
+
showControls?: boolean;
|
|
69
|
+
showExport?: boolean;
|
|
70
|
+
showGrid?: boolean;
|
|
71
|
+
zoomWithScroll?: boolean;
|
|
72
|
+
resetOnSourceChange?: boolean;
|
|
73
|
+
disabled?: boolean;
|
|
74
|
+
readOnly?: boolean;
|
|
75
|
+
crossOrigin?: 'anonymous' | 'use-credentials';
|
|
76
|
+
referrerPolicy?: HTMLAttributeReferrerPolicy;
|
|
77
|
+
labels?: Partial<ImageCropperLabels>;
|
|
78
|
+
nonce?: string;
|
|
79
|
+
}
|
|
80
|
+
export declare const ImageCropper: import("react").ForwardRefExoticComponent<ImageCropperProps & import("react").RefAttributes<ImageCropperHandle>>;
|
|
81
|
+
export { IMAGE_CROPPER_DEFAULT_OUTPUT_PIXELS, IMAGE_CROPPER_MAX_OUTPUT_EDGE, IMAGE_CROPPER_MAX_OUTPUT_PIXELS, ImageCropperExportError, clampImageCropperArea, exportImageCrop, resolveImageCropperOutputSize, } from './model.js';
|
|
82
|
+
export type { ImageCropperArea, ImageCropperExportErrorCode, ImageCropperExportOptions, ImageCropperExportType, ImageCropperPoint, ImageCropperSize, } from './model.js';
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { __iconData as downloadIcon } from 'lucide-react/dist/esm/icons/download.mjs';
|
|
3
|
+
import { __iconData as imageOffIcon } from 'lucide-react/dist/esm/icons/image-off.mjs';
|
|
4
|
+
import { __iconData as rotateCcwIcon } from 'lucide-react/dist/esm/icons/rotate-ccw.mjs';
|
|
5
|
+
import Cropper from 'react-easy-crop';
|
|
6
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useId, useRef, useState, } from 'react';
|
|
7
|
+
import { Button } from '../Button.js';
|
|
8
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
9
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
10
|
+
import { PencilText } from '../PencilText.js';
|
|
11
|
+
import { Slider } from '../slider/index.js';
|
|
12
|
+
import { ImageCropperExportError, exportImageCrop, } from './model.js';
|
|
13
|
+
const downloadPaths = downloadIcon.node.map(([, attributes]) => ({ d: attributes.d }));
|
|
14
|
+
const imageOffPaths = imageOffIcon.node.map(([, attributes]) => ({ d: attributes.d }));
|
|
15
|
+
const resetPaths = rotateCcwIcon.node.map(([, attributes]) => ({ d: attributes.d }));
|
|
16
|
+
// Lucide LoaderCircle geometry, rendered through the shared PencilIcon engine.
|
|
17
|
+
const loadingPaths = [{ d: 'M21 12a9 9 0 1 1-6.219-8.56' }];
|
|
18
|
+
const DEFAULT_CROP = Object.freeze({ x: 0, y: 0 });
|
|
19
|
+
const MIN_FREE_CROP_SIZE = 48;
|
|
20
|
+
const defaultLabels = {
|
|
21
|
+
cropper: 'Image crop area',
|
|
22
|
+
instructions: 'Use pointer gestures or arrow keys to move the image. Use the zoom control. In free aspect mode, use the size controls or Alt plus arrow keys to resize the crop area.',
|
|
23
|
+
loading: 'Loading image',
|
|
24
|
+
failedTitle: 'Image unavailable',
|
|
25
|
+
failedDescription: 'The image could not be loaded. Check the source and try again.',
|
|
26
|
+
retry: 'Retry image',
|
|
27
|
+
zoom: 'Zoom',
|
|
28
|
+
cropWidth: 'Crop width',
|
|
29
|
+
cropHeight: 'Crop height',
|
|
30
|
+
reset: 'Reset crop',
|
|
31
|
+
export: 'Export crop',
|
|
32
|
+
exporting: 'Exporting crop',
|
|
33
|
+
};
|
|
34
|
+
function samePoint(left, right) {
|
|
35
|
+
return left.x === right.x && left.y === right.y;
|
|
36
|
+
}
|
|
37
|
+
function sameSize(left, right) {
|
|
38
|
+
return left === right || Boolean(left && right && left.width === right.width && left.height === right.height);
|
|
39
|
+
}
|
|
40
|
+
function validAspect(aspect) {
|
|
41
|
+
return aspect === 'free' || !Number.isFinite(aspect) || aspect <= 0 ? 1 : aspect;
|
|
42
|
+
}
|
|
43
|
+
function exportFailure(error) {
|
|
44
|
+
return error instanceof ImageCropperExportError
|
|
45
|
+
? error
|
|
46
|
+
: new ImageCropperExportError('canvas', 'The crop could not be exported.', { cause: error });
|
|
47
|
+
}
|
|
48
|
+
export const ImageCropper = forwardRef(function ImageCropper({ alt, aspect = 1, className, crop: cropProp, cropAreaSize: cropAreaSizeProp, crossOrigin, defaultCrop = DEFAULT_CROP, defaultCropAreaSize, defaultZoom = 1, disabled = false, exportOptions, labels: labelOverrides, maxZoom = 3, minZoom = 1, nonce, onCropAreaSizeChange, onCropChange, onExport, onExportError, onLoadError, onLoadStateChange, onMediaLoaded, onSelectionChange, onSelectionCommit, onZoomChange, readOnly = false, referrerPolicy, resetOnSourceChange = true, showControls = true, showExport = Boolean(onExport), showGrid = true, source, zoom: zoomProp, zoomStep = 0.1, zoomWithScroll = true, ...props }, forwardedRef) {
|
|
49
|
+
const labels = { ...defaultLabels, ...labelOverrides };
|
|
50
|
+
const generatedId = useId();
|
|
51
|
+
const resolvedId = props.id ?? `inkra-image-cropper-${generatedId}`;
|
|
52
|
+
const stageRef = useRef(null);
|
|
53
|
+
const imageRef = useRef(null);
|
|
54
|
+
const selectionRef = useRef(null);
|
|
55
|
+
const interactionSourceRef = useRef('mouse');
|
|
56
|
+
const initialSourceRef = useRef(source);
|
|
57
|
+
const [blobUrl, setBlobUrl] = useState(null);
|
|
58
|
+
const [attempt, setAttempt] = useState(0);
|
|
59
|
+
const [loadState, setLoadState] = useState('loading');
|
|
60
|
+
const [internalCrop, setInternalCrop] = useState(defaultCrop);
|
|
61
|
+
const [internalZoom, setInternalZoom] = useState(defaultZoom);
|
|
62
|
+
const [internalCropAreaSize, setInternalCropAreaSize] = useState(defaultCropAreaSize ?? null);
|
|
63
|
+
const [naturalSize, setNaturalSize] = useState(null);
|
|
64
|
+
const [stageSize, setStageSize] = useState({ width: 640, height: 400 });
|
|
65
|
+
const [exporting, setExporting] = useState(false);
|
|
66
|
+
const crop = cropProp ?? internalCrop;
|
|
67
|
+
const zoom = zoomProp ?? internalZoom;
|
|
68
|
+
const cropAreaSize = cropAreaSizeProp ?? internalCropAreaSize;
|
|
69
|
+
const freeCropAreaSize = cropAreaSize ?? (aspect === 'free'
|
|
70
|
+
? { width: Math.min(stageSize.width, stageSize.height), height: Math.min(stageSize.width, stageSize.height) }
|
|
71
|
+
: null);
|
|
72
|
+
const sourceUrl = typeof source === 'string' ? source : blobUrl;
|
|
73
|
+
const interactionDisabled = disabled || readOnly || loadState !== 'ready';
|
|
74
|
+
const announceLoadState = useCallback((state) => {
|
|
75
|
+
setLoadState(state);
|
|
76
|
+
onLoadStateChange?.(state);
|
|
77
|
+
}, [onLoadStateChange]);
|
|
78
|
+
const commitCrop = useCallback((next, reason) => {
|
|
79
|
+
if (cropProp === undefined)
|
|
80
|
+
setInternalCrop(next);
|
|
81
|
+
onCropChange?.(next, { reason });
|
|
82
|
+
}, [cropProp, onCropChange]);
|
|
83
|
+
const commitZoom = useCallback((next, reason) => {
|
|
84
|
+
const bounded = Math.min(maxZoom, Math.max(minZoom, next));
|
|
85
|
+
if (zoomProp === undefined)
|
|
86
|
+
setInternalZoom(bounded);
|
|
87
|
+
onZoomChange?.(bounded, { reason });
|
|
88
|
+
}, [maxZoom, minZoom, onZoomChange, zoomProp]);
|
|
89
|
+
const commitCropAreaSize = useCallback((next, reason) => {
|
|
90
|
+
const bounded = {
|
|
91
|
+
width: Math.round(Math.min(Math.max(MIN_FREE_CROP_SIZE, next.width), Math.max(MIN_FREE_CROP_SIZE, stageSize.width))),
|
|
92
|
+
height: Math.round(Math.min(Math.max(MIN_FREE_CROP_SIZE, next.height), Math.max(MIN_FREE_CROP_SIZE, stageSize.height))),
|
|
93
|
+
};
|
|
94
|
+
if (cropAreaSizeProp === undefined)
|
|
95
|
+
setInternalCropAreaSize(bounded);
|
|
96
|
+
onCropAreaSizeChange?.(bounded, { reason });
|
|
97
|
+
}, [cropAreaSizeProp, onCropAreaSizeChange, stageSize.height, stageSize.width]);
|
|
98
|
+
const reset = useCallback((reason = 'reset') => {
|
|
99
|
+
if (!samePoint(crop, defaultCrop))
|
|
100
|
+
commitCrop(defaultCrop, reason);
|
|
101
|
+
if (zoom !== defaultZoom)
|
|
102
|
+
commitZoom(defaultZoom, reason);
|
|
103
|
+
const nextSize = defaultCropAreaSize ?? null;
|
|
104
|
+
if (!sameSize(cropAreaSize, nextSize)) {
|
|
105
|
+
if (nextSize)
|
|
106
|
+
commitCropAreaSize(nextSize, reason);
|
|
107
|
+
else if (cropAreaSizeProp === undefined)
|
|
108
|
+
setInternalCropAreaSize(null);
|
|
109
|
+
}
|
|
110
|
+
selectionRef.current = null;
|
|
111
|
+
}, [commitCrop, commitCropAreaSize, commitZoom, crop, cropAreaSize, cropAreaSizeProp, defaultCrop, defaultCropAreaSize, defaultZoom, zoom]);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (typeof source === 'string') {
|
|
114
|
+
setBlobUrl(null);
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const nextUrl = URL.createObjectURL(source);
|
|
118
|
+
setBlobUrl(nextUrl);
|
|
119
|
+
return () => URL.revokeObjectURL(nextUrl);
|
|
120
|
+
}, [source]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const stage = stageRef.current;
|
|
123
|
+
if (!stage || typeof ResizeObserver === 'undefined')
|
|
124
|
+
return undefined;
|
|
125
|
+
const update = () => {
|
|
126
|
+
const rect = stage.getBoundingClientRect();
|
|
127
|
+
if (rect.width > 0 && rect.height > 0)
|
|
128
|
+
setStageSize({ width: rect.width, height: rect.height });
|
|
129
|
+
};
|
|
130
|
+
update();
|
|
131
|
+
const observer = new ResizeObserver(update);
|
|
132
|
+
observer.observe(stage);
|
|
133
|
+
return () => observer.disconnect();
|
|
134
|
+
}, []);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (initialSourceRef.current === source)
|
|
137
|
+
return;
|
|
138
|
+
initialSourceRef.current = source;
|
|
139
|
+
setAttempt(0);
|
|
140
|
+
setNaturalSize(null);
|
|
141
|
+
selectionRef.current = null;
|
|
142
|
+
announceLoadState('loading');
|
|
143
|
+
if (resetOnSourceChange)
|
|
144
|
+
reset('source-change');
|
|
145
|
+
}, [announceLoadState, reset, resetOnSourceChange, source]);
|
|
146
|
+
const makeSelection = useCallback((percentages, pixels) => {
|
|
147
|
+
if (!naturalSize)
|
|
148
|
+
return null;
|
|
149
|
+
return {
|
|
150
|
+
percentages: { ...percentages },
|
|
151
|
+
pixels: { ...pixels },
|
|
152
|
+
naturalSize,
|
|
153
|
+
};
|
|
154
|
+
}, [naturalSize]);
|
|
155
|
+
const exportCurrent = useCallback(async (options = exportOptions ?? {}) => {
|
|
156
|
+
const image = imageRef.current;
|
|
157
|
+
const selection = selectionRef.current;
|
|
158
|
+
if (!selection)
|
|
159
|
+
throw new ImageCropperExportError('invalid-selection', 'No completed crop selection is available.');
|
|
160
|
+
if (!image)
|
|
161
|
+
throw new ImageCropperExportError('image-not-ready', 'The image element is not available.');
|
|
162
|
+
const result = await exportImageCrop(image, selection.pixels, options);
|
|
163
|
+
return { ...result, selection };
|
|
164
|
+
}, [exportOptions]);
|
|
165
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
166
|
+
exportCrop: exportCurrent,
|
|
167
|
+
getSelection: () => selectionRef.current,
|
|
168
|
+
reset: () => reset(),
|
|
169
|
+
}), [exportCurrent, reset]);
|
|
170
|
+
const handleExport = async () => {
|
|
171
|
+
setExporting(true);
|
|
172
|
+
try {
|
|
173
|
+
onExport?.(await exportCurrent());
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
onExportError?.(exportFailure(error));
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
setExporting(false);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const resizeFreeCrop = (axis, delta) => {
|
|
183
|
+
if (aspect !== 'free' || interactionDisabled)
|
|
184
|
+
return;
|
|
185
|
+
const base = cropAreaSize ?? {
|
|
186
|
+
width: Math.min(stageSize.width, stageSize.height),
|
|
187
|
+
height: Math.min(stageSize.width, stageSize.height),
|
|
188
|
+
};
|
|
189
|
+
commitCropAreaSize({ ...base, [axis]: base[axis] + delta }, 'control');
|
|
190
|
+
};
|
|
191
|
+
const handleRootKeyDown = (event) => {
|
|
192
|
+
props.onKeyDown?.(event);
|
|
193
|
+
if (event.defaultPrevented || !event.altKey || aspect !== 'free')
|
|
194
|
+
return;
|
|
195
|
+
if (!stageRef.current?.contains(event.target))
|
|
196
|
+
return;
|
|
197
|
+
const step = event.shiftKey ? 24 : 8;
|
|
198
|
+
if (event.key === 'ArrowLeft')
|
|
199
|
+
resizeFreeCrop('width', -step);
|
|
200
|
+
else if (event.key === 'ArrowRight')
|
|
201
|
+
resizeFreeCrop('width', step);
|
|
202
|
+
else if (event.key === 'ArrowUp')
|
|
203
|
+
resizeFreeCrop('height', -step);
|
|
204
|
+
else if (event.key === 'ArrowDown')
|
|
205
|
+
resizeFreeCrop('height', step);
|
|
206
|
+
else
|
|
207
|
+
return;
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
};
|
|
210
|
+
const engineAspect = validAspect(aspect);
|
|
211
|
+
const engineCropSize = aspect === 'free' && freeCropAreaSize ? freeCropAreaSize : undefined;
|
|
212
|
+
const rootProps = { ...props };
|
|
213
|
+
delete rootProps.onKeyDown;
|
|
214
|
+
return _jsxs("div", { ...rootProps, className: ['inkra-image-cropper', className].filter(Boolean).join(' '), "data-disabled": disabled || undefined, "data-read-only": readOnly || undefined, "data-state": loadState, onKeyDown: handleRootKeyDown, children: [_jsxs("div", { "aria-busy": loadState === 'loading' || undefined, "aria-describedby": `${resolvedId}-instructions`, "aria-disabled": disabled || undefined, "aria-label": labels.cropper, className: "inkra-image-cropper__stage", ref: stageRef, children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-image-cropper__stage-surface", radius: 8, variant: "outline" }), sourceUrl ? _jsx(Cropper, { aspect: engineAspect, classes: {
|
|
215
|
+
containerClassName: 'inkra-image-cropper__engine',
|
|
216
|
+
mediaClassName: 'inkra-image-cropper__media',
|
|
217
|
+
cropAreaClassName: 'inkra-image-cropper__area',
|
|
218
|
+
}, crop: crop, cropSize: engineCropSize, image: sourceUrl, keyboardStep: 1, maxZoom: maxZoom, mediaProps: { alt, crossOrigin, referrerPolicy }, minZoom: minZoom, nonce: nonce, onCropAreaChange: (percentages, pixels) => {
|
|
219
|
+
const selection = makeSelection(percentages, pixels);
|
|
220
|
+
if (!selection)
|
|
221
|
+
return;
|
|
222
|
+
selectionRef.current = selection;
|
|
223
|
+
onSelectionChange?.(selection);
|
|
224
|
+
}, onCropChange: next => {
|
|
225
|
+
if (!interactionDisabled)
|
|
226
|
+
commitCrop(next, interactionSourceRef.current);
|
|
227
|
+
}, onCropComplete: (percentages, pixels) => {
|
|
228
|
+
const selection = makeSelection(percentages, pixels);
|
|
229
|
+
if (!selection)
|
|
230
|
+
return;
|
|
231
|
+
selectionRef.current = selection;
|
|
232
|
+
onSelectionCommit?.(selection);
|
|
233
|
+
}, onCropSizeChange: next => {
|
|
234
|
+
if (aspect === 'free' && !cropAreaSize)
|
|
235
|
+
commitCropAreaSize(next, 'control');
|
|
236
|
+
}, onInteractionStart: ({ source: interactionSource }) => {
|
|
237
|
+
interactionSourceRef.current = interactionSource;
|
|
238
|
+
}, onMediaLoaded: (media) => {
|
|
239
|
+
const nextNaturalSize = { width: media.naturalWidth, height: media.naturalHeight };
|
|
240
|
+
setNaturalSize(nextNaturalSize);
|
|
241
|
+
announceLoadState('ready');
|
|
242
|
+
onMediaLoaded?.(nextNaturalSize);
|
|
243
|
+
}, onZoomChange: next => {
|
|
244
|
+
if (!interactionDisabled)
|
|
245
|
+
commitZoom(next, interactionSourceRef.current);
|
|
246
|
+
}, disableAutomaticStylesInjection: true, restrictPosition: true, roundCropAreaPixels: true, setImageRef: (ref) => {
|
|
247
|
+
imageRef.current = ref.current;
|
|
248
|
+
}, showGrid: showGrid, zoom: zoom, zoomWithScroll: zoomWithScroll && !interactionDisabled }, `${sourceUrl}:${attempt}`) : null, loadState === 'loading' ? _jsxs("div", { className: "inkra-image-cropper__state", role: "status", children: [_jsx(PencilIcon, { "aria-hidden": "true", className: "inkra-image-cropper__spinner", paths: loadingPaths, seed: `${resolvedId}-loading`, size: 40, sourceSize: 24 }), _jsx(PencilText, { children: labels.loading })] }) : null, loadState === 'error' ? _jsxs("div", { className: "inkra-image-cropper__state", role: "alert", children: [_jsx(PencilIcon, { decorative: true, paths: imageOffPaths, size: 48, sourceSize: 24 }), _jsx(PencilText, { as: "h3", children: labels.failedTitle }), _jsx(PencilText, { as: "p", children: labels.failedDescription }), _jsx(Button, { variant: "outline", onClick: () => {
|
|
249
|
+
setAttempt(current => current + 1);
|
|
250
|
+
announceLoadState('loading');
|
|
251
|
+
}, children: labels.retry })] }) : null, sourceUrl && loadState !== 'error' ? _jsx("img", { alt: "", "aria-hidden": "true", className: "inkra-image-cropper__load-probe", crossOrigin: crossOrigin, onError: () => {
|
|
252
|
+
const error = new Error('The crop image could not be loaded.');
|
|
253
|
+
announceLoadState('error');
|
|
254
|
+
onLoadError?.(error);
|
|
255
|
+
}, referrerPolicy: referrerPolicy, src: sourceUrl }, `probe:${sourceUrl}:${attempt}`) : null] }), _jsx("span", { className: "inkra-image-cropper__instructions", id: `${resolvedId}-instructions`, children: labels.instructions }), showControls ? _jsxs("div", { className: "inkra-image-cropper__controls", children: [_jsxs("label", { className: "inkra-image-cropper__control", children: [_jsxs("span", { className: "inkra-image-cropper__control-label", children: [_jsx(PencilText, { children: labels.zoom }), _jsxs("output", { children: [Math.round(zoom * 100), "%"] })] }), _jsx(Slider, { "aria-label": labels.zoom, disabled: interactionDisabled, getValueLabel: value => `${Math.round(value * 100)}%`, max: maxZoom, min: minZoom, onValueChange: values => commitZoom(values[0], 'control'), step: zoomStep, value: [zoom] })] }), aspect === 'free' && freeCropAreaSize ? _jsx("div", { className: "inkra-image-cropper__free-controls", children: ['width', 'height'].map(axis => {
|
|
256
|
+
const label = axis === 'width' ? labels.cropWidth : labels.cropHeight;
|
|
257
|
+
const maximum = Math.max(MIN_FREE_CROP_SIZE, axis === 'width' ? stageSize.width : stageSize.height);
|
|
258
|
+
return _jsxs("label", { className: "inkra-image-cropper__control", children: [_jsxs("span", { className: "inkra-image-cropper__control-label", children: [_jsx(PencilText, { children: label }), _jsxs("output", { children: [Math.round(freeCropAreaSize[axis]), "px"] })] }), _jsx(Slider, { "aria-label": label, disabled: interactionDisabled, max: maximum, min: MIN_FREE_CROP_SIZE, onValueChange: values => commitCropAreaSize({ ...freeCropAreaSize, [axis]: values[0] }, 'control'), step: 4, value: [Math.min(maximum, freeCropAreaSize[axis])] })] }, axis);
|
|
259
|
+
}) }) : null, _jsxs("div", { className: "inkra-image-cropper__actions", children: [_jsx(Button, { "aria-label": labels.reset, disabled: interactionDisabled, onClick: () => reset(), size: "icon-sm", title: labels.reset, variant: "outline", children: _jsx(PencilIcon, { decorative: true, paths: resetPaths, size: 22, sourceSize: 24 }) }), showExport ? _jsx(Button, { "aria-label": labels.export, disabled: interactionDisabled || selectionRef.current === null, loading: exporting, onClick: () => void handleExport(), startIcon: _jsx(PencilIcon, { decorative: true, paths: downloadPaths, size: 22, sourceSize: 24 }), variant: "primary", children: exporting ? labels.exporting : labels.export }) : null] })] }) : null] });
|
|
260
|
+
});
|
|
261
|
+
export { IMAGE_CROPPER_DEFAULT_OUTPUT_PIXELS, IMAGE_CROPPER_MAX_OUTPUT_EDGE, IMAGE_CROPPER_MAX_OUTPUT_PIXELS, ImageCropperExportError, clampImageCropperArea, exportImageCrop, resolveImageCropperOutputSize, } from './model.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ImageCropperPoint {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ImageCropperSize {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ImageCropperArea extends ImageCropperPoint, ImageCropperSize {
|
|
10
|
+
}
|
|
11
|
+
export type ImageCropperExportType = 'image/png' | 'image/jpeg' | 'image/webp';
|
|
12
|
+
export interface ImageCropperExportOptions {
|
|
13
|
+
/** Maximum output width. The crop aspect ratio is always preserved. */
|
|
14
|
+
maxWidth?: number;
|
|
15
|
+
/** Maximum output height. The crop aspect ratio is always preserved. */
|
|
16
|
+
maxHeight?: number;
|
|
17
|
+
/** Maximum output pixel count. Defaults to 16,777,216 and never exceeds the hard 33,554,432 limit. */
|
|
18
|
+
maxPixels?: number;
|
|
19
|
+
/** Permit enlarging beyond the selected original pixels. Defaults to false. */
|
|
20
|
+
allowUpscale?: boolean;
|
|
21
|
+
type?: ImageCropperExportType;
|
|
22
|
+
quality?: number;
|
|
23
|
+
}
|
|
24
|
+
export type ImageCropperExportErrorCode = 'canvas' | 'cors' | 'image-not-ready' | 'invalid-selection' | 'unsupported-format';
|
|
25
|
+
export declare class ImageCropperExportError extends Error {
|
|
26
|
+
readonly code: ImageCropperExportErrorCode;
|
|
27
|
+
readonly cause?: unknown;
|
|
28
|
+
constructor(code: ImageCropperExportErrorCode, message: string, options?: {
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export declare const IMAGE_CROPPER_MAX_OUTPUT_EDGE = 8192;
|
|
33
|
+
export declare const IMAGE_CROPPER_MAX_OUTPUT_PIXELS = 33554432;
|
|
34
|
+
export declare const IMAGE_CROPPER_DEFAULT_OUTPUT_PIXELS = 16777216;
|
|
35
|
+
export declare function clampImageCropperArea(area: ImageCropperArea, naturalSize: ImageCropperSize): ImageCropperArea;
|
|
36
|
+
export declare function resolveImageCropperOutputSize(area: ImageCropperSize, options?: ImageCropperExportOptions): ImageCropperSize;
|
|
37
|
+
export declare function exportImageCrop(image: HTMLImageElement, area: ImageCropperArea, options?: ImageCropperExportOptions): Promise<{
|
|
38
|
+
blob: Blob;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
type: ImageCropperExportType;
|
|
42
|
+
}>;
|