@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,12 @@
|
|
|
1
|
+
export interface FieldContextValue {
|
|
2
|
+
controlId: string;
|
|
3
|
+
descriptionId?: string;
|
|
4
|
+
errorId?: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
invalid: boolean;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
readOnly: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const FieldContext: import("react").Context<FieldContextValue | null>;
|
|
11
|
+
export declare function useFieldContext(): FieldContextValue | null;
|
|
12
|
+
export declare function mergeAriaIds(...values: Array<string | undefined>): string | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const FieldContext = createContext(null);
|
|
3
|
+
export function useFieldContext() {
|
|
4
|
+
return useContext(FieldContext);
|
|
5
|
+
}
|
|
6
|
+
export function mergeAriaIds(...values) {
|
|
7
|
+
const ids = values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []);
|
|
8
|
+
const merged = Array.from(new Set(ids));
|
|
9
|
+
return merged.length ? merged.join(' ') : undefined;
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AriaAttributes } from 'react';
|
|
2
|
+
interface NativeControlState {
|
|
3
|
+
id?: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
readOnly?: boolean;
|
|
7
|
+
'aria-describedby'?: string;
|
|
8
|
+
'aria-errormessage'?: string;
|
|
9
|
+
'aria-invalid'?: AriaAttributes['aria-invalid'];
|
|
10
|
+
}
|
|
11
|
+
export declare function useResolvedControlProps(props: NativeControlState & {
|
|
12
|
+
invalid?: boolean;
|
|
13
|
+
}): {
|
|
14
|
+
id: string | undefined;
|
|
15
|
+
required: boolean;
|
|
16
|
+
disabled: boolean;
|
|
17
|
+
readOnly: boolean;
|
|
18
|
+
invalid: boolean;
|
|
19
|
+
describedBy: string | undefined;
|
|
20
|
+
errorMessage: string | undefined;
|
|
21
|
+
ariaInvalid: boolean | "true" | "false" | "grammar" | "spelling" | undefined;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { mergeAriaIds, useFieldContext } from './context.js';
|
|
2
|
+
export function useResolvedControlProps(props) {
|
|
3
|
+
const field = useFieldContext();
|
|
4
|
+
const resolvedInvalid = Boolean(props.invalid
|
|
5
|
+
|| field?.invalid
|
|
6
|
+
|| props['aria-invalid'] === true
|
|
7
|
+
|| props['aria-invalid'] === 'true');
|
|
8
|
+
const describedBy = mergeAriaIds(props['aria-describedby'], field?.descriptionId, field?.errorId);
|
|
9
|
+
const errorMessage = mergeAriaIds(props['aria-errormessage'], resolvedInvalid ? field?.errorId : undefined);
|
|
10
|
+
return {
|
|
11
|
+
id: field?.controlId ?? props.id,
|
|
12
|
+
required: Boolean(props.required || field?.required),
|
|
13
|
+
disabled: Boolean(props.disabled || field?.disabled),
|
|
14
|
+
readOnly: Boolean(props.readOnly || field?.readOnly),
|
|
15
|
+
invalid: resolvedInvalid,
|
|
16
|
+
describedBy,
|
|
17
|
+
errorMessage,
|
|
18
|
+
ariaInvalid: resolvedInvalid ? true : props['aria-invalid'],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Field } from './Field.js';
|
|
2
|
+
export type { FieldProps } from './Field.js';
|
|
3
|
+
export { Input } from './Input.js';
|
|
4
|
+
export type { InputProps, InputVariant, InputTextAlign, InputValueChangeDetails, InputClassNames } from './Input.js';
|
|
5
|
+
export { Textarea } from './Textarea.js';
|
|
6
|
+
export type { TextareaProps, TextareaCountDetails } from './Textarea.js';
|
|
7
|
+
export type { FieldControlSize, FieldControlStateProps } from './types.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
export type HeatmapKey = string | number;
|
|
3
|
+
export type HeatmapScaleType = 'continuous' | 'quantize';
|
|
4
|
+
export interface HeatmapAxisItem {
|
|
5
|
+
id: HeatmapKey;
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
ariaLabel?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HeatmapCell {
|
|
10
|
+
rowId: HeatmapKey;
|
|
11
|
+
columnId: HeatmapKey;
|
|
12
|
+
value?: number | null;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
data?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface HeatmapSelection {
|
|
17
|
+
rowId: HeatmapKey;
|
|
18
|
+
columnId: HeatmapKey;
|
|
19
|
+
}
|
|
20
|
+
export interface HeatmapCellContext {
|
|
21
|
+
row: HeatmapAxisItem;
|
|
22
|
+
column: HeatmapAxisItem;
|
|
23
|
+
color: string | null;
|
|
24
|
+
missing: boolean;
|
|
25
|
+
selected: boolean;
|
|
26
|
+
active: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface HeatmapDetailsContext extends HeatmapCellContext {
|
|
29
|
+
valueLabel: string;
|
|
30
|
+
}
|
|
31
|
+
export interface HeatmapProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
|
|
32
|
+
rows: readonly HeatmapAxisItem[];
|
|
33
|
+
columns: readonly HeatmapAxisItem[];
|
|
34
|
+
cells: readonly HeatmapCell[];
|
|
35
|
+
label: ReactNode;
|
|
36
|
+
description?: ReactNode;
|
|
37
|
+
scaleType?: HeatmapScaleType;
|
|
38
|
+
domain?: readonly [number, number];
|
|
39
|
+
colorRange?: readonly string[];
|
|
40
|
+
binCount?: number;
|
|
41
|
+
showLegend?: boolean;
|
|
42
|
+
showValues?: boolean;
|
|
43
|
+
legendLabel?: ReactNode;
|
|
44
|
+
missingLabel?: ReactNode;
|
|
45
|
+
missingValueLabel?: string;
|
|
46
|
+
selectionMode?: 'single' | 'none';
|
|
47
|
+
selectedCell?: HeatmapSelection | null;
|
|
48
|
+
defaultSelectedCell?: HeatmapSelection | null;
|
|
49
|
+
onSelectedCellChange?: (selection: HeatmapSelection | null, cell: HeatmapCell | null) => void;
|
|
50
|
+
getValueLabel?: (value: number) => string;
|
|
51
|
+
getCellAriaLabel?: (cell: HeatmapCell, context: HeatmapDetailsContext) => string;
|
|
52
|
+
renderCell?: (cell: HeatmapCell, context: HeatmapCellContext) => ReactNode;
|
|
53
|
+
renderDetails?: (cell: HeatmapCell, context: HeatmapDetailsContext) => ReactNode;
|
|
54
|
+
cellSize?: number | string;
|
|
55
|
+
rowLabelWidth?: number | string;
|
|
56
|
+
rowHeaderLabel?: string;
|
|
57
|
+
emptyMessage?: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
export declare const Heatmap: import("react").ForwardRefExoticComponent<HeatmapProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { scaleLinear, scaleQuantize } from 'd3-scale';
|
|
3
|
+
import { forwardRef, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
4
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
5
|
+
import { PencilText } from '../PencilText.js';
|
|
6
|
+
const defaultColors = ['#f0dda0', '#b6d09b', '#70b19c', '#43869a', '#365f82'];
|
|
7
|
+
function classes(...values) {
|
|
8
|
+
return values.filter(Boolean).join(' ');
|
|
9
|
+
}
|
|
10
|
+
function keyPart(value) {
|
|
11
|
+
return JSON.stringify([typeof value, value]);
|
|
12
|
+
}
|
|
13
|
+
function cellKey(rowId, columnId) {
|
|
14
|
+
return JSON.stringify([typeof rowId, rowId, typeof columnId, columnId]);
|
|
15
|
+
}
|
|
16
|
+
function sameSelection(a, b) {
|
|
17
|
+
return a != null && b != null && a.rowId === b.rowId && a.columnId === b.columnId;
|
|
18
|
+
}
|
|
19
|
+
function axisLabel(item) {
|
|
20
|
+
if (item.ariaLabel)
|
|
21
|
+
return item.ariaLabel;
|
|
22
|
+
if (typeof item.label === 'string' || typeof item.label === 'number')
|
|
23
|
+
return String(item.label);
|
|
24
|
+
return String(item.id);
|
|
25
|
+
}
|
|
26
|
+
function isMissing(cell) {
|
|
27
|
+
return typeof cell.value !== 'number' || !Number.isFinite(cell.value);
|
|
28
|
+
}
|
|
29
|
+
function normalizeCssSize(value, fallback) {
|
|
30
|
+
return typeof value === 'number'
|
|
31
|
+
? Number.isFinite(value) ? `${Math.max(1, value)}px` : fallback
|
|
32
|
+
: value || fallback;
|
|
33
|
+
}
|
|
34
|
+
function normalizeDomain(values, requested) {
|
|
35
|
+
let minimum;
|
|
36
|
+
let maximum;
|
|
37
|
+
if (requested && Number.isFinite(requested[0]) && Number.isFinite(requested[1])) {
|
|
38
|
+
minimum = Math.min(requested[0], requested[1]);
|
|
39
|
+
maximum = Math.max(requested[0], requested[1]);
|
|
40
|
+
}
|
|
41
|
+
else if (values.length > 0) {
|
|
42
|
+
minimum = values[0];
|
|
43
|
+
maximum = values[0];
|
|
44
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
45
|
+
minimum = Math.min(minimum, values[index]);
|
|
46
|
+
maximum = Math.max(maximum, values[index]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return [0, 1];
|
|
51
|
+
}
|
|
52
|
+
if (minimum !== maximum)
|
|
53
|
+
return [minimum, maximum];
|
|
54
|
+
const padding = Math.abs(minimum) * 0.5 || 1;
|
|
55
|
+
return [minimum - padding, maximum + padding];
|
|
56
|
+
}
|
|
57
|
+
function paletteDomain(domain, count) {
|
|
58
|
+
const [minimum, maximum] = domain;
|
|
59
|
+
if (count <= 1)
|
|
60
|
+
return [minimum];
|
|
61
|
+
return Array.from({ length: count }, (_, index) => minimum + ((maximum - minimum) * index) / (count - 1));
|
|
62
|
+
}
|
|
63
|
+
function clampBinCount(value) {
|
|
64
|
+
if (value === undefined || !Number.isFinite(value))
|
|
65
|
+
return 5;
|
|
66
|
+
return Math.max(2, Math.min(9, Math.round(value ?? 5)));
|
|
67
|
+
}
|
|
68
|
+
function resolvePosition(selection, rows, columns) {
|
|
69
|
+
if (!selection)
|
|
70
|
+
return null;
|
|
71
|
+
const rowIndex = rows.findIndex(row => row.id === selection.rowId);
|
|
72
|
+
const columnIndex = columns.findIndex(column => column.id === selection.columnId);
|
|
73
|
+
return rowIndex < 0 || columnIndex < 0 ? null : { rowIndex, columnIndex };
|
|
74
|
+
}
|
|
75
|
+
function positionEquals(a, b) {
|
|
76
|
+
return a?.rowIndex === b?.rowIndex && a?.columnIndex === b?.columnIndex;
|
|
77
|
+
}
|
|
78
|
+
function defaultValueLabel(value) {
|
|
79
|
+
return String(value);
|
|
80
|
+
}
|
|
81
|
+
function paintStyle(color) {
|
|
82
|
+
return {
|
|
83
|
+
'--inkra-accent': color,
|
|
84
|
+
'--inkra-accent-soft': color,
|
|
85
|
+
'--inkra-accent-strong': color,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export const Heatmap = forwardRef(function Heatmap({ rows, columns, cells, label, description, scaleType = 'continuous', domain: requestedDomain, colorRange = defaultColors, binCount, showLegend = true, showValues = true, legendLabel = 'Intensity', missingLabel = '—', missingValueLabel = 'Missing', selectionMode = 'single', selectedCell, defaultSelectedCell = null, onSelectedCellChange, getValueLabel = defaultValueLabel, getCellAriaLabel, renderCell, renderDetails, cellSize = 56, rowLabelWidth = '8rem', rowHeaderLabel = 'Rows', emptyMessage = 'No heatmap data', className, style, ...props }, forwardedRef) {
|
|
89
|
+
const generatedId = useId();
|
|
90
|
+
const labelId = `inkra-heatmap-${generatedId}-label`;
|
|
91
|
+
const descriptionId = description == null ? undefined : `inkra-heatmap-${generatedId}-description`;
|
|
92
|
+
const detailsId = `inkra-heatmap-${generatedId}-details`;
|
|
93
|
+
const isControlled = selectedCell !== undefined;
|
|
94
|
+
const [internalSelection, setInternalSelection] = useState(defaultSelectedCell);
|
|
95
|
+
const resolvedSelection = isControlled ? selectedCell ?? null : internalSelection;
|
|
96
|
+
const initialPosition = resolvePosition(resolvedSelection, rows, columns) ?? (rows.length > 0 && columns.length > 0 ? { rowIndex: 0, columnIndex: 0 } : null);
|
|
97
|
+
const [activePosition, setActivePosition] = useState(initialPosition);
|
|
98
|
+
const [hoveredPosition, setHoveredPosition] = useState(null);
|
|
99
|
+
const [focusedPosition, setFocusedPosition] = useState(null);
|
|
100
|
+
const cellRefs = useRef(new Map());
|
|
101
|
+
const rowSignature = JSON.stringify(rows.map(row => [typeof row.id, row.id]));
|
|
102
|
+
const columnSignature = JSON.stringify(columns.map(column => [typeof column.id, column.id]));
|
|
103
|
+
const cellsByKey = useMemo(() => {
|
|
104
|
+
const map = new Map();
|
|
105
|
+
for (const cell of cells)
|
|
106
|
+
map.set(cellKey(cell.rowId, cell.columnId), cell);
|
|
107
|
+
return map;
|
|
108
|
+
}, [cells]);
|
|
109
|
+
const matrix = useMemo(() => rows.map(row => columns.map(column => (cellsByKey.get(cellKey(row.id, column.id)) ?? { rowId: row.id, columnId: column.id, value: null }))), [cellsByKey, columns, rows]);
|
|
110
|
+
const values = useMemo(() => matrix.flat().filter(cell => !isMissing(cell)).map(cell => cell.value), [matrix]);
|
|
111
|
+
const domain = useMemo(() => normalizeDomain(values, requestedDomain), [requestedDomain, values]);
|
|
112
|
+
const colors = colorRange.length >= 2 ? colorRange : defaultColors;
|
|
113
|
+
const continuousScale = useMemo(() => scaleLinear()
|
|
114
|
+
.domain(paletteDomain(domain, colors.length))
|
|
115
|
+
.range([...colors])
|
|
116
|
+
.clamp(true), [colors, domain]);
|
|
117
|
+
const resolvedBinCount = clampBinCount(binCount);
|
|
118
|
+
const quantizedColors = useMemo(() => Array.from({ length: resolvedBinCount }, (_, index) => continuousScale(domain[0] + ((domain[1] - domain[0]) * index) / Math.max(1, resolvedBinCount - 1))), [continuousScale, domain, resolvedBinCount]);
|
|
119
|
+
const quantizeScale = useMemo(() => scaleQuantize()
|
|
120
|
+
.domain(domain)
|
|
121
|
+
.range(quantizedColors), [domain, quantizedColors]);
|
|
122
|
+
const colorForValue = (value) => scaleType === 'quantize' ? quantizeScale(value) : continuousScale(value);
|
|
123
|
+
const selectedPosition = resolvePosition(resolvedSelection, rows, columns);
|
|
124
|
+
const inspectedPosition = focusedPosition ?? hoveredPosition ?? selectedPosition ?? activePosition;
|
|
125
|
+
const inspectedCell = inspectedPosition ? matrix[inspectedPosition.rowIndex]?.[inspectedPosition.columnIndex] : undefined;
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const next = resolvePosition(resolvedSelection, rows, columns);
|
|
128
|
+
if (next)
|
|
129
|
+
setActivePosition(current => positionEquals(current, next) ? current : next);
|
|
130
|
+
}, [columnSignature, resolvedSelection?.columnId, resolvedSelection?.rowId, rowSignature]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (rows.length === 0 || columns.length === 0) {
|
|
133
|
+
setActivePosition(null);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
setActivePosition(current => current && current.rowIndex < rows.length && current.columnIndex < columns.length
|
|
137
|
+
? current
|
|
138
|
+
: { rowIndex: 0, columnIndex: 0 });
|
|
139
|
+
}, [columns.length, rows.length]);
|
|
140
|
+
const focusPosition = (position) => {
|
|
141
|
+
setActivePosition(position);
|
|
142
|
+
const cell = matrix[position.rowIndex]?.[position.columnIndex];
|
|
143
|
+
if (!cell)
|
|
144
|
+
return;
|
|
145
|
+
cellRefs.current.get(cellKey(cell.rowId, cell.columnId))?.focus();
|
|
146
|
+
};
|
|
147
|
+
const select = (cell) => {
|
|
148
|
+
if (selectionMode === 'none' || cell.disabled)
|
|
149
|
+
return;
|
|
150
|
+
const next = { rowId: cell.rowId, columnId: cell.columnId };
|
|
151
|
+
if (!isControlled)
|
|
152
|
+
setInternalSelection(next);
|
|
153
|
+
onSelectedCellChange?.(next, cell);
|
|
154
|
+
};
|
|
155
|
+
const onCellKeyDown = (event, position, cell) => {
|
|
156
|
+
let next = position;
|
|
157
|
+
if (event.key === 'ArrowLeft')
|
|
158
|
+
next = { ...position, columnIndex: Math.max(0, position.columnIndex - 1) };
|
|
159
|
+
else if (event.key === 'ArrowRight')
|
|
160
|
+
next = { ...position, columnIndex: Math.min(columns.length - 1, position.columnIndex + 1) };
|
|
161
|
+
else if (event.key === 'ArrowUp')
|
|
162
|
+
next = { ...position, rowIndex: Math.max(0, position.rowIndex - 1) };
|
|
163
|
+
else if (event.key === 'ArrowDown')
|
|
164
|
+
next = { ...position, rowIndex: Math.min(rows.length - 1, position.rowIndex + 1) };
|
|
165
|
+
else if (event.key === 'Home')
|
|
166
|
+
next = event.ctrlKey ? { rowIndex: 0, columnIndex: 0 } : { ...position, columnIndex: 0 };
|
|
167
|
+
else if (event.key === 'End')
|
|
168
|
+
next = event.ctrlKey
|
|
169
|
+
? { rowIndex: rows.length - 1, columnIndex: columns.length - 1 }
|
|
170
|
+
: { ...position, columnIndex: columns.length - 1 };
|
|
171
|
+
else if (event.key === 'Enter' || event.key === ' ') {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
select(cell);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
return;
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
focusPosition(next);
|
|
180
|
+
};
|
|
181
|
+
const rootStyle = {
|
|
182
|
+
...style,
|
|
183
|
+
'--inkra-heatmap-cell-size': normalizeCssSize(cellSize, '56px'),
|
|
184
|
+
'--inkra-heatmap-row-label-width': normalizeCssSize(rowLabelWidth, '8rem'),
|
|
185
|
+
'--inkra-heatmap-column-count': columns.length,
|
|
186
|
+
};
|
|
187
|
+
const renderCellContent = (cell, context) => {
|
|
188
|
+
if (renderCell)
|
|
189
|
+
return renderCell(cell, context);
|
|
190
|
+
if (context.missing)
|
|
191
|
+
return missingLabel;
|
|
192
|
+
return showValues ? getValueLabel(cell.value) : _jsx("span", { "aria-hidden": "true", className: "inkra-heatmap__value-mark" });
|
|
193
|
+
};
|
|
194
|
+
const detailsFor = (cell, position) => {
|
|
195
|
+
const row = rows[position.rowIndex];
|
|
196
|
+
const column = columns[position.columnIndex];
|
|
197
|
+
const missing = isMissing(cell);
|
|
198
|
+
const valueLabel = missing ? missingValueLabel : getValueLabel(cell.value);
|
|
199
|
+
const context = {
|
|
200
|
+
row,
|
|
201
|
+
column,
|
|
202
|
+
color: missing ? null : colorForValue(cell.value),
|
|
203
|
+
missing,
|
|
204
|
+
selected: sameSelection(resolvedSelection, cell),
|
|
205
|
+
active: positionEquals(inspectedPosition, position),
|
|
206
|
+
valueLabel,
|
|
207
|
+
};
|
|
208
|
+
return { context, valueLabel };
|
|
209
|
+
};
|
|
210
|
+
return (_jsxs("div", { ...props, className: classes('inkra-heatmap', className), ref: forwardedRef, style: rootStyle, children: [_jsxs("div", { className: "inkra-heatmap__heading", children: [_jsx("div", { className: "inkra-heatmap__label", id: labelId, children: typeof label === 'string' ? _jsx(PencilText, { seed: `${generatedId}-label`, children: label }) : label }), description == null ? null : _jsx("div", { className: "inkra-heatmap__description", id: descriptionId, children: description })] }), rows.length === 0 || columns.length === 0 ? (_jsx("div", { className: "inkra-heatmap__empty", children: emptyMessage })) : (_jsx("div", { className: "inkra-heatmap__viewport", children: _jsxs("div", { "aria-describedby": classes(descriptionId, detailsId), "aria-labelledby": labelId, "aria-rowcount": rows.length + 1, "aria-colcount": columns.length + 1, className: "inkra-heatmap__grid", role: "grid", children: [_jsxs("div", { "aria-rowindex": 1, className: "inkra-heatmap__row inkra-heatmap__row--header", role: "row", children: [_jsx("span", { "aria-colindex": 1, "aria-label": rowHeaderLabel, className: "inkra-heatmap__corner", role: "columnheader" }), columns.map((column, columnIndex) => (_jsx("span", { "aria-colindex": columnIndex + 2, className: "inkra-heatmap__column-label", role: "columnheader", children: column.label }, keyPart(column.id))))] }), matrix.map((rowCells, rowIndex) => {
|
|
211
|
+
const row = rows[rowIndex];
|
|
212
|
+
return (_jsxs("div", { "aria-rowindex": rowIndex + 2, className: "inkra-heatmap__row", role: "row", children: [_jsx("span", { "aria-colindex": 1, className: "inkra-heatmap__row-label", role: "rowheader", children: row.label }), rowCells.map((cell, columnIndex) => {
|
|
213
|
+
const position = { rowIndex, columnIndex };
|
|
214
|
+
const column = columns[columnIndex];
|
|
215
|
+
const missing = isMissing(cell);
|
|
216
|
+
const selected = sameSelection(resolvedSelection, cell);
|
|
217
|
+
const active = positionEquals(inspectedPosition, position);
|
|
218
|
+
const color = missing ? null : colorForValue(cell.value);
|
|
219
|
+
const valueLabel = missing ? missingValueLabel : getValueLabel(cell.value);
|
|
220
|
+
const context = { row, column, color, missing, selected, active, valueLabel };
|
|
221
|
+
const ariaLabel = getCellAriaLabel?.(cell, context)
|
|
222
|
+
?? `${axisLabel(row)}, ${axisLabel(column)}: ${valueLabel}`;
|
|
223
|
+
const resolvedPaintStyle = color ? paintStyle(color) : undefined;
|
|
224
|
+
return (_jsxs("button", { "aria-colindex": columnIndex + 2, "aria-disabled": cell.disabled || undefined, "aria-label": ariaLabel, "aria-selected": selectionMode === 'single' ? selected : undefined, className: "inkra-heatmap__cell", "data-active": active || undefined, "data-disabled": cell.disabled || undefined, "data-missing": missing || undefined, "data-selected": selected || undefined, "data-value": missing ? undefined : cell.value, onBlur: () => setFocusedPosition(current => positionEquals(current, position) ? null : current), onClick: () => select(cell), onFocus: () => {
|
|
225
|
+
setActivePosition(position);
|
|
226
|
+
setFocusedPosition(position);
|
|
227
|
+
}, onKeyDown: event => onCellKeyDown(event, position, cell), onMouseEnter: () => setHoveredPosition(position), onMouseLeave: () => setHoveredPosition(current => positionEquals(current, position) ? null : current), ref: node => {
|
|
228
|
+
const key = cellKey(cell.rowId, cell.columnId);
|
|
229
|
+
if (node)
|
|
230
|
+
cellRefs.current.set(key, node);
|
|
231
|
+
else
|
|
232
|
+
cellRefs.current.delete(key);
|
|
233
|
+
}, role: "gridcell", tabIndex: positionEquals(activePosition, position) ? 0 : -1, type: "button", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-heatmap__cell-paint", radius: 6, seed: `heatmap-${cellKey(cell.rowId, cell.columnId)}-paint`, style: resolvedPaintStyle, variant: missing ? 'outline' : 'action' }), selected ? _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-heatmap__cell-selection", radius: 6, seed: `heatmap-${cellKey(cell.rowId, cell.columnId)}-selection`, variant: "selection" }) : null, _jsx("span", { className: "inkra-heatmap__cell-content", children: renderCellContent(cell, context) })] }, cellKey(cell.rowId, cell.columnId)));
|
|
234
|
+
})] }, keyPart(row.id)));
|
|
235
|
+
})] }) })), showLegend && rows.length > 0 && columns.length > 0 ? (_jsxs("div", { "aria-label": typeof legendLabel === 'string' ? legendLabel : undefined, className: "inkra-heatmap__legend", role: "group", children: [_jsx("div", { className: "inkra-heatmap__legend-label", children: legendLabel }), scaleType === 'continuous' ? (_jsxs("div", { className: "inkra-heatmap__legend-continuous", children: [_jsx("div", { "aria-hidden": "true", className: "inkra-heatmap__legend-strip", children: Array.from({ length: 7 }, (_, index) => {
|
|
236
|
+
const value = domain[0] + ((domain[1] - domain[0]) * index) / 6;
|
|
237
|
+
const color = continuousScale(value);
|
|
238
|
+
return _jsx(PencilSurface, { as: "span", className: "inkra-heatmap__legend-swatch", seed: `heatmap-legend-${index}`, style: paintStyle(color), variant: "action" }, index);
|
|
239
|
+
}) }), _jsxs("div", { className: "inkra-heatmap__legend-values", children: [_jsx("span", { children: getValueLabel(domain[0]) }), _jsx("span", { children: getValueLabel((domain[0] + domain[1]) / 2) }), _jsx("span", { children: getValueLabel(domain[1]) })] })] })) : (_jsx("div", { className: "inkra-heatmap__legend-bins", role: "list", children: quantizedColors.map((color, index) => {
|
|
240
|
+
const [from, to] = quantizeScale.invertExtent(color);
|
|
241
|
+
return _jsxs("div", { className: "inkra-heatmap__legend-bin", role: "listitem", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-heatmap__legend-swatch", seed: `heatmap-bin-${index}`, style: paintStyle(color), variant: "action" }), _jsxs("span", { children: [getValueLabel(from), "\u2013", getValueLabel(to)] })] }, `${color}-${index}`);
|
|
242
|
+
}) })), _jsxs("div", { className: "inkra-heatmap__legend-missing", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-heatmap__legend-swatch", seed: "heatmap-missing", variant: "outline" }), _jsx("span", { children: missingValueLabel })] })] })) : null, _jsx("div", { "aria-live": "polite", className: "inkra-heatmap__details", id: detailsId, children: inspectedCell && inspectedPosition ? (() => {
|
|
243
|
+
const { context, valueLabel } = detailsFor(inspectedCell, inspectedPosition);
|
|
244
|
+
return renderDetails
|
|
245
|
+
? renderDetails(inspectedCell, context)
|
|
246
|
+
: _jsxs(_Fragment, { children: [_jsxs("strong", { children: [axisLabel(context.row), " \u00B7 ", axisLabel(context.column)] }), _jsx("span", { children: valueLabel })] });
|
|
247
|
+
})() : null })] }));
|
|
248
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { FieldControlSize, FieldControlStateProps } from '../form/types.js';
|
|
3
|
+
import { detectHotkeyPlatform, formatHotkey, getHotkeyTokens, hotkeyFromKeyboardEvent, hotkeySignature, normalizeHotkey } from './model.js';
|
|
4
|
+
import type { HotkeyModifier, HotkeyPlatform, HotkeyValue, ResolvedHotkeyPlatform } from './model.js';
|
|
5
|
+
export type HotkeyValidationResult = boolean | string | null | undefined;
|
|
6
|
+
export type HotkeyInputChangeReason = 'record' | 'clear';
|
|
7
|
+
export interface HotkeyInputChangeDetails {
|
|
8
|
+
reason: HotkeyInputChangeReason;
|
|
9
|
+
conflict?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HotkeyProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
12
|
+
value: HotkeyValue;
|
|
13
|
+
platform?: HotkeyPlatform;
|
|
14
|
+
separator?: ReactNode;
|
|
15
|
+
size?: FieldControlSize;
|
|
16
|
+
}
|
|
17
|
+
export interface HotkeyInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'children' | 'defaultValue' | 'onChange' | 'readOnly' | 'size' | 'value'>, FieldControlStateProps {
|
|
18
|
+
value?: HotkeyValue | null;
|
|
19
|
+
defaultValue?: HotkeyValue | null;
|
|
20
|
+
onValueChange?: (value: HotkeyValue | null, details: HotkeyInputChangeDetails) => void;
|
|
21
|
+
onCancel?: () => void;
|
|
22
|
+
validateHotkey?: (value: HotkeyValue) => HotkeyValidationResult;
|
|
23
|
+
onConflict?: (value: HotkeyValue, message: string) => void;
|
|
24
|
+
conflict?: boolean | string;
|
|
25
|
+
conflictMessage?: string;
|
|
26
|
+
platform?: HotkeyPlatform;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
clearable?: boolean;
|
|
29
|
+
clearButtonLabel?: string;
|
|
30
|
+
blurOnCapture?: boolean;
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
classNames?: Partial<Record<'root' | 'control' | 'input' | 'value' | 'message', string>>;
|
|
33
|
+
}
|
|
34
|
+
export declare const Hotkey: import("react").ForwardRefExoticComponent<HotkeyProps & import("react").RefAttributes<HTMLElement>>;
|
|
35
|
+
export declare const HotkeyInput: import("react").ForwardRefExoticComponent<HotkeyInputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
36
|
+
export type { FieldControlSize, HotkeyModifier, HotkeyPlatform, HotkeyValue, ResolvedHotkeyPlatform, };
|
|
37
|
+
export { detectHotkeyPlatform, formatHotkey, getHotkeyTokens, hotkeyFromKeyboardEvent, hotkeySignature, normalizeHotkey, };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState, } from 'react';
|
|
3
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
4
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
5
|
+
import { PencilText } from '../PencilText.js';
|
|
6
|
+
import { useResolvedControlProps } from '../form/controlProps.js';
|
|
7
|
+
import { detectHotkeyPlatform, formatHotkey, getHotkeyTokens, hotkeyFromKeyboardEvent, hotkeySignature, isHotkeyClear, isHotkeyEscape, normalizeHotkey, } from './model.js';
|
|
8
|
+
const clearPaths = [{ d: 'M18 6 6 18' }, { d: 'm6 6 12 12' }];
|
|
9
|
+
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
10
|
+
function browserPlatform() {
|
|
11
|
+
if (typeof navigator === 'undefined')
|
|
12
|
+
return '';
|
|
13
|
+
const userAgentData = navigator.userAgentData;
|
|
14
|
+
return userAgentData?.platform ?? navigator.platform ?? navigator.userAgent;
|
|
15
|
+
}
|
|
16
|
+
function useHotkeyPlatform(platform) {
|
|
17
|
+
const explicit = platform === 'auto' ? null : platform;
|
|
18
|
+
const [detected, setDetected] = useState('windows');
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!explicit)
|
|
21
|
+
setDetected(detectHotkeyPlatform(browserPlatform()));
|
|
22
|
+
}, [explicit]);
|
|
23
|
+
return explicit ?? detected;
|
|
24
|
+
}
|
|
25
|
+
function conflictText(result, fallback) {
|
|
26
|
+
if (typeof result === 'string')
|
|
27
|
+
return result || undefined;
|
|
28
|
+
return result ? fallback : undefined;
|
|
29
|
+
}
|
|
30
|
+
export const Hotkey = forwardRef(function Hotkey({ 'aria-label': ariaLabel, className, platform = 'auto', separator, size = 'md', value, ...props }, ref) {
|
|
31
|
+
const resolvedPlatform = useHotkeyPlatform(platform);
|
|
32
|
+
const normalized = normalizeHotkey(value);
|
|
33
|
+
const tokens = getHotkeyTokens(normalized, resolvedPlatform);
|
|
34
|
+
const label = formatHotkey(normalized, resolvedPlatform);
|
|
35
|
+
return (_jsx("kbd", { ...props, "aria-label": ariaLabel ?? label, className: classes('inkra-hotkey', className), "data-platform": resolvedPlatform, "data-size": size, ref: ref, children: tokens.map((token, index) => (_jsxs("span", { className: "inkra-hotkey__part", children: [index > 0 && resolvedPlatform !== 'mac' ? (_jsx("span", { "aria-hidden": "true", className: "inkra-hotkey__separator", children: separator ?? '+' })) : null, _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-hotkey__key", radius: 4, seed: `${hotkeySignature(normalized)}-${resolvedPlatform}-${index}`, variant: "quiet", children: _jsx(PencilText, { seed: `${hotkeySignature(normalized)}-${resolvedPlatform}-${index}-label`, children: token }) })] }, `${token}-${index}`))) }));
|
|
36
|
+
});
|
|
37
|
+
export const HotkeyInput = forwardRef(function HotkeyInput({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, blurOnCapture = true, className, classNames = {}, clearable = true, clearButtonLabel = 'Clear shortcut', conflict, conflictMessage = 'Shortcut already assigned', defaultValue = null, disabled, id, invalid = false, onBlur, onCancel, onCompositionEnd, onCompositionStart, onConflict, onFocus, onKeyDown, onValueChange, placeholder = 'Press shortcut', platform = 'auto', readOnly, required, seed, size = 'md', validateHotkey, value, ...props }, forwardedRef) {
|
|
38
|
+
const generatedId = useId();
|
|
39
|
+
const inputRef = useRef(null);
|
|
40
|
+
const composingRef = useRef(false);
|
|
41
|
+
const isControlled = value !== undefined;
|
|
42
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(() => defaultValue ? normalizeHotkey(defaultValue) : null);
|
|
43
|
+
const [focused, setFocused] = useState(false);
|
|
44
|
+
const [localConflict, setLocalConflict] = useState(null);
|
|
45
|
+
const currentValue = isControlled ? (value ? normalizeHotkey(value) : null) : uncontrolledValue;
|
|
46
|
+
const resolvedPlatform = useHotkeyPlatform(platform);
|
|
47
|
+
const externalConflict = conflictText(conflict, conflictMessage);
|
|
48
|
+
const currentSignature = hotkeySignature(currentValue);
|
|
49
|
+
const validationMessage = externalConflict ?? (localConflict?.signature === currentSignature ? localConflict.message : undefined);
|
|
50
|
+
const messageId = validationMessage ? `${id ?? generatedId}-hotkey-conflict` : undefined;
|
|
51
|
+
const state = useResolvedControlProps({
|
|
52
|
+
id,
|
|
53
|
+
required,
|
|
54
|
+
disabled,
|
|
55
|
+
readOnly,
|
|
56
|
+
invalid: invalid || Boolean(validationMessage),
|
|
57
|
+
'aria-describedby': [ariaDescribedBy, messageId].filter(Boolean).join(' ') || undefined,
|
|
58
|
+
'aria-errormessage': validationMessage ? messageId : ariaErrorMessage,
|
|
59
|
+
'aria-invalid': ariaInvalid,
|
|
60
|
+
});
|
|
61
|
+
const resolvedSeed = seed ?? `inkra-hotkey-input-${state.id ?? generatedId}`;
|
|
62
|
+
const displayValue = currentValue ? formatHotkey(currentValue, resolvedPlatform) : '';
|
|
63
|
+
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
64
|
+
const updateValue = (nextValue, details) => {
|
|
65
|
+
if (!isControlled)
|
|
66
|
+
setUncontrolledValue(nextValue);
|
|
67
|
+
onValueChange?.(nextValue, details);
|
|
68
|
+
};
|
|
69
|
+
const clear = () => {
|
|
70
|
+
if (state.disabled || state.readOnly || !currentValue)
|
|
71
|
+
return;
|
|
72
|
+
setLocalConflict(null);
|
|
73
|
+
updateValue(null, { reason: 'clear' });
|
|
74
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
75
|
+
};
|
|
76
|
+
const handleKeyDown = (event) => {
|
|
77
|
+
onKeyDown?.(event);
|
|
78
|
+
if (event.defaultPrevented || state.disabled || state.readOnly)
|
|
79
|
+
return;
|
|
80
|
+
const nativeEvent = event.nativeEvent;
|
|
81
|
+
const eventLike = {
|
|
82
|
+
key: event.key,
|
|
83
|
+
code: event.code,
|
|
84
|
+
ctrlKey: event.ctrlKey,
|
|
85
|
+
altKey: event.altKey,
|
|
86
|
+
shiftKey: event.shiftKey,
|
|
87
|
+
metaKey: event.metaKey,
|
|
88
|
+
isComposing: composingRef.current || nativeEvent.isComposing,
|
|
89
|
+
keyCode: nativeEvent.keyCode,
|
|
90
|
+
};
|
|
91
|
+
if (isHotkeyEscape(eventLike)) {
|
|
92
|
+
onCancel?.();
|
|
93
|
+
event.currentTarget.blur();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (isHotkeyClear(eventLike)) {
|
|
97
|
+
clear();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (event.key === 'Tab')
|
|
101
|
+
return;
|
|
102
|
+
const nextValue = hotkeyFromKeyboardEvent(eventLike);
|
|
103
|
+
if (!nextValue)
|
|
104
|
+
return;
|
|
105
|
+
const result = validateHotkey?.(nextValue);
|
|
106
|
+
const message = conflictText(result, conflictMessage);
|
|
107
|
+
setLocalConflict(message ? { signature: hotkeySignature(nextValue), message } : null);
|
|
108
|
+
if (message)
|
|
109
|
+
onConflict?.(nextValue, message);
|
|
110
|
+
updateValue(nextValue, { reason: 'record', ...(message ? { conflict: message } : {}) });
|
|
111
|
+
if (blurOnCapture)
|
|
112
|
+
event.currentTarget.blur();
|
|
113
|
+
};
|
|
114
|
+
const handleFocus = (event) => {
|
|
115
|
+
setFocused(true);
|
|
116
|
+
onFocus?.(event);
|
|
117
|
+
};
|
|
118
|
+
const handleBlur = (event) => {
|
|
119
|
+
setFocused(false);
|
|
120
|
+
onBlur?.(event);
|
|
121
|
+
};
|
|
122
|
+
return (_jsxs("span", { className: classes('inkra-hotkey-input', classNames.root), "data-disabled": state.disabled || undefined, "data-focus-within": focused || undefined, "data-invalid": state.invalid || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, children: [_jsxs(PencilSurface, { as: "span", className: classes('inkra-hotkey-input__control', classNames.control), clipContent: true, seed: resolvedSeed, variant: state.invalid ? 'danger' : 'outline', children: [_jsx("input", { ...props, "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, "aria-label": ariaLabel, autoComplete: "off", className: classes('inkra-hotkey-input__native', className, classNames.input), disabled: state.disabled, id: state.id, inputMode: "none", onBlur: handleBlur, onChange: () => { }, onCompositionEnd: event => {
|
|
123
|
+
composingRef.current = false;
|
|
124
|
+
onCompositionEnd?.(event);
|
|
125
|
+
}, onCompositionStart: event => {
|
|
126
|
+
composingRef.current = true;
|
|
127
|
+
onCompositionStart?.(event);
|
|
128
|
+
}, onFocus: handleFocus, onKeyDown: handleKeyDown, readOnly: state.readOnly, ref: inputRef, required: state.required, spellCheck: false, value: displayValue }), _jsx("span", { "aria-hidden": "true", className: classes('inkra-hotkey-input__value', classNames.value), children: currentValue ? _jsx(Hotkey, { value: currentValue, platform: resolvedPlatform, size: size }) : _jsx(PencilText, { className: "inkra-hotkey-input__placeholder", seed: `${resolvedSeed}-placeholder`, children: placeholder }) }), clearable && currentValue ? (_jsx("button", { "aria-label": clearButtonLabel, className: "inkra-hotkey-input__clear", disabled: state.disabled || state.readOnly, onClick: clear, onPointerDown: event => event.preventDefault(), title: clearButtonLabel, type: "button", children: _jsx(PencilIcon, { paths: clearPaths, seed: `${resolvedSeed}-clear`, size: size === 'sm' ? 18 : size === 'lg' ? 26 : 22, sourceSize: 24 }) })) : null] }), validationMessage ? (_jsx(PencilText, { as: "span", className: classes('inkra-hotkey-input__message', classNames.message), id: messageId, role: "alert", seed: `${resolvedSeed}-message`, children: validationMessage })) : null] }));
|
|
129
|
+
});
|
|
130
|
+
export { detectHotkeyPlatform, formatHotkey, getHotkeyTokens, hotkeyFromKeyboardEvent, hotkeySignature, normalizeHotkey, };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type HotkeyPlatform = 'auto' | 'mac' | 'windows';
|
|
2
|
+
export type ResolvedHotkeyPlatform = Exclude<HotkeyPlatform, 'auto'>;
|
|
3
|
+
export type HotkeyModifier = 'control' | 'alt' | 'shift' | 'meta';
|
|
4
|
+
export interface HotkeyValue {
|
|
5
|
+
key: string;
|
|
6
|
+
code?: string;
|
|
7
|
+
modifiers?: readonly HotkeyModifier[];
|
|
8
|
+
}
|
|
9
|
+
export interface HotkeyKeyboardEventLike {
|
|
10
|
+
key: string;
|
|
11
|
+
code?: string;
|
|
12
|
+
ctrlKey?: boolean;
|
|
13
|
+
altKey?: boolean;
|
|
14
|
+
shiftKey?: boolean;
|
|
15
|
+
metaKey?: boolean;
|
|
16
|
+
isComposing?: boolean;
|
|
17
|
+
keyCode?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function detectHotkeyPlatform(platform?: string): ResolvedHotkeyPlatform;
|
|
20
|
+
export declare function normalizeHotkey(value: HotkeyValue): HotkeyValue;
|
|
21
|
+
export declare function normalizeHotkeyKey(key: string): string;
|
|
22
|
+
export declare function hotkeyFromKeyboardEvent(event: HotkeyKeyboardEventLike): HotkeyValue | null;
|
|
23
|
+
export declare function hotkeySignature(value: HotkeyValue | null | undefined): string;
|
|
24
|
+
export declare function getHotkeyTokens(value: HotkeyValue, platform: ResolvedHotkeyPlatform): string[];
|
|
25
|
+
export declare function formatHotkey(value: HotkeyValue, platform: ResolvedHotkeyPlatform): string;
|
|
26
|
+
export declare function isHotkeyEscape(event: HotkeyKeyboardEventLike): boolean;
|
|
27
|
+
export declare function isHotkeyClear(event: HotkeyKeyboardEventLike): boolean;
|