@inkra/react 0.1.0 → 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/README.md +104 -1
- package/dist/PencilIcon.js +14 -16
- package/dist/PencilLine.js +18 -12
- package/dist/PencilText.js +192 -59
- package/dist/alert-dialog/index.d.ts +64 -0
- package/dist/alert-dialog/index.js +109 -0
- package/dist/audio-player/index.d.ts +60 -0
- package/dist/audio-player/index.js +101 -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/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/display/ProgressRing.d.ts +6 -0
- package/dist/display/ProgressRing.js +146 -0
- package/dist/display/index.d.ts +27 -0
- package/dist/display/index.js +18 -2
- 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/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.js +157 -146
- package/dist/form/Textarea.d.ts +12 -0
- package/dist/form/Textarea.js +83 -4
- package/dist/form/index.d.ts +1 -1
- 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/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/internal/cache.d.ts +1 -1
- package/dist/internal/cache.js +1 -1
- package/dist/internal/renderTokens.d.ts +10 -0
- package/dist/internal/renderTokens.js +13 -0
- package/dist/internal/surfaceRenderer.d.ts +14 -6
- package/dist/internal/surfaceRenderer.js +15 -12
- package/dist/internal/textRenderer.d.ts +1 -0
- package/dist/internal/textRenderer.js +33 -7
- package/dist/internal/usePencilSurface.js +16 -8
- package/dist/internal/useSelectionDrawing.js +15 -11
- package/dist/journey-map/MapScale.js +3 -1
- package/dist/journey-map/index.js +3 -1
- 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/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/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/slider/index.js +1 -1
- 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 +4850 -287
- 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 +10 -9
- 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 +22 -3
|
@@ -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;
|
|
@@ -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
|
+
}>;
|