@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,323 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
4
|
+
import { BoundedCache, canvasByteCost, canvasIsUsable, effectiveDpr } from '../internal/cache.js';
|
|
5
|
+
import { observeCanvasRecovery } from '../internal/recovery.js';
|
|
6
|
+
import { seedToNumber } from '../internal/random.js';
|
|
7
|
+
import { pencilLettering, renderPencilText } from '../internal/textRenderer.js';
|
|
8
|
+
import { readRenderToken } from '../internal/renderTokens.js';
|
|
9
|
+
const MAX_EDITABLE_TEXT = 4096;
|
|
10
|
+
const MAX_EDITABLE_LINE = 256;
|
|
11
|
+
const complexScript = /[\u0300-\u036f\u0590-\u08ff\u0900-\u0dff\u0e00-\u0fff\u1780-\u18af\u200c\u200d\ufe00-\ufe0f]/u;
|
|
12
|
+
function numeric(style, property) {
|
|
13
|
+
const value = Number.parseFloat(style.getPropertyValue(property));
|
|
14
|
+
return Number.isFinite(value) ? value : 0;
|
|
15
|
+
}
|
|
16
|
+
function editableText(control) {
|
|
17
|
+
const placeholder = control.value.length === 0;
|
|
18
|
+
return { text: placeholder ? control.placeholder : control.value, placeholder };
|
|
19
|
+
}
|
|
20
|
+
function copyMirrorStyle(mirror, control, style) {
|
|
21
|
+
Object.assign(mirror.style, {
|
|
22
|
+
boxSizing: 'border-box',
|
|
23
|
+
position: 'fixed',
|
|
24
|
+
left: '-100000px',
|
|
25
|
+
top: '0',
|
|
26
|
+
visibility: 'hidden',
|
|
27
|
+
pointerEvents: 'none',
|
|
28
|
+
width: `${control.clientWidth}px`,
|
|
29
|
+
minHeight: `${control.scrollHeight}px`,
|
|
30
|
+
paddingTop: style.paddingTop,
|
|
31
|
+
paddingRight: style.paddingRight,
|
|
32
|
+
paddingBottom: style.paddingBottom,
|
|
33
|
+
paddingLeft: style.paddingLeft,
|
|
34
|
+
border: '0',
|
|
35
|
+
font: style.font,
|
|
36
|
+
fontFamily: style.fontFamily,
|
|
37
|
+
fontSize: style.fontSize,
|
|
38
|
+
fontWeight: style.fontWeight,
|
|
39
|
+
fontStyle: style.fontStyle,
|
|
40
|
+
fontKerning: style.fontKerning,
|
|
41
|
+
letterSpacing: style.letterSpacing,
|
|
42
|
+
lineHeight: style.lineHeight,
|
|
43
|
+
textAlign: style.textAlign,
|
|
44
|
+
textIndent: style.textIndent,
|
|
45
|
+
textTransform: style.textTransform,
|
|
46
|
+
direction: style.direction,
|
|
47
|
+
unicodeBidi: style.unicodeBidi,
|
|
48
|
+
tabSize: style.tabSize,
|
|
49
|
+
overflowWrap: style.overflowWrap,
|
|
50
|
+
wordBreak: style.wordBreak,
|
|
51
|
+
whiteSpace: control.wrap === 'off' ? 'pre' : 'pre-wrap',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function measureTextareaLines(control, text, style) {
|
|
55
|
+
const mirror = document.createElement('div');
|
|
56
|
+
copyMirrorStyle(mirror, control, style);
|
|
57
|
+
const source = document.createTextNode(text);
|
|
58
|
+
mirror.appendChild(source);
|
|
59
|
+
document.body.appendChild(mirror);
|
|
60
|
+
const mirrorBox = mirror.getBoundingClientRect();
|
|
61
|
+
const range = document.createRange();
|
|
62
|
+
const lines = [];
|
|
63
|
+
let offset = 0;
|
|
64
|
+
try {
|
|
65
|
+
for (const character of text) {
|
|
66
|
+
range.setStart(source, offset);
|
|
67
|
+
offset += character.length;
|
|
68
|
+
range.setEnd(source, offset);
|
|
69
|
+
const rectangle = range.getBoundingClientRect();
|
|
70
|
+
let line = lines[lines.length - 1];
|
|
71
|
+
if (!line || Math.abs(line.top - rectangle.top) > 1) {
|
|
72
|
+
line = { text: '', left: rectangle.left - mirrorBox.left, top: rectangle.top - mirrorBox.top, start: offset - character.length };
|
|
73
|
+
lines.push(line);
|
|
74
|
+
}
|
|
75
|
+
line.text += character;
|
|
76
|
+
if (Array.from(line.text).length > MAX_EDITABLE_LINE)
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
range.detach();
|
|
83
|
+
mirror.remove();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function textStart(direction, textAlign, controlWidth, paddingStart, paddingEnd, advance) {
|
|
87
|
+
const available = Math.max(0, controlWidth - paddingStart - paddingEnd);
|
|
88
|
+
const resolved = textAlign === 'start'
|
|
89
|
+
? (direction === 'rtl' ? 'right' : 'left')
|
|
90
|
+
: textAlign === 'end'
|
|
91
|
+
? (direction === 'rtl' ? 'left' : 'right')
|
|
92
|
+
: textAlign;
|
|
93
|
+
if (resolved === 'center')
|
|
94
|
+
return paddingStart + (available - advance) / 2;
|
|
95
|
+
if (resolved === 'right')
|
|
96
|
+
return controlWidth - paddingEnd - advance;
|
|
97
|
+
return paddingStart;
|
|
98
|
+
}
|
|
99
|
+
function paintEditableText(canvas, buffer, glyphs, control, seed, multiline, composition) {
|
|
100
|
+
const width = control.clientWidth;
|
|
101
|
+
const height = control.clientHeight;
|
|
102
|
+
const target = canvas.getContext('2d');
|
|
103
|
+
const context = buffer.getContext('2d');
|
|
104
|
+
const dpr = effectiveDpr(2);
|
|
105
|
+
if (!target || target.isContextLost?.() || !context || context.isContextLost?.()
|
|
106
|
+
|| width < 2 || height < 2 || width * height * dpr ** 2 > 2 * 1024 * 1024)
|
|
107
|
+
return false;
|
|
108
|
+
const content = editableText(control);
|
|
109
|
+
if (content.text.length > MAX_EDITABLE_TEXT)
|
|
110
|
+
return false;
|
|
111
|
+
const style = getComputedStyle(control);
|
|
112
|
+
if (style.direction === 'rtl' || complexScript.test(content.text))
|
|
113
|
+
return false;
|
|
114
|
+
const fontSize = Number.parseFloat(style.fontSize);
|
|
115
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0)
|
|
116
|
+
return false;
|
|
117
|
+
const family = style.fontFamily;
|
|
118
|
+
const color = content.placeholder
|
|
119
|
+
? style.getPropertyValue('--inkra-editable-placeholder-color').trim() || style.color
|
|
120
|
+
: style.getPropertyValue('--inkra-editable-text-color').trim() || style.color;
|
|
121
|
+
const settings = {
|
|
122
|
+
variation: readRenderToken(style, '--inkra-letter-variation', pencilLettering.variation, 0, 1),
|
|
123
|
+
breaks: readRenderToken(style, '--inkra-letter-breaks', pencilLettering.breaks, 0, 1),
|
|
124
|
+
outline: readRenderToken(style, '--inkra-letter-outline', pencilLettering.outline, 0, 1),
|
|
125
|
+
fill: readRenderToken(style, '--inkra-letter-fill', pencilLettering.fill, 0, 1),
|
|
126
|
+
grain: readRenderToken(style, '--inkra-letter-grain', pencilLettering.grain, 0, 1),
|
|
127
|
+
};
|
|
128
|
+
buffer.width = Math.ceil(width * dpr);
|
|
129
|
+
buffer.height = Math.ceil(height * dpr);
|
|
130
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
131
|
+
context.font = `400 ${fontSize}px ${family}`;
|
|
132
|
+
context.fontKerning = 'none';
|
|
133
|
+
context.fillStyle = color;
|
|
134
|
+
const metrics = context.measureText('Mg');
|
|
135
|
+
const ascent = metrics.fontBoundingBoxAscent ?? fontSize;
|
|
136
|
+
const descent = metrics.fontBoundingBoxDescent ?? fontSize * 0.25;
|
|
137
|
+
const paddingStart = numeric(style, 'padding-inline-start') || numeric(style, 'padding-left');
|
|
138
|
+
const paddingEnd = numeric(style, 'padding-inline-end') || numeric(style, 'padding-right');
|
|
139
|
+
if (!multiline && content.text.length > MAX_EDITABLE_LINE)
|
|
140
|
+
return false;
|
|
141
|
+
const lines = multiline
|
|
142
|
+
? measureTextareaLines(control, content.text, style)
|
|
143
|
+
: [{ text: content.text, left: 0, top: 0, start: 0 }];
|
|
144
|
+
if (!lines)
|
|
145
|
+
return false;
|
|
146
|
+
const baseSeed = seedToNumber(seed);
|
|
147
|
+
const glyphAppearance = JSON.stringify([fontSize, family, color, baseSeed, dpr, settings]);
|
|
148
|
+
const composingStart = composition?.start ?? -1;
|
|
149
|
+
const composingEnd = composition
|
|
150
|
+
? Math.max(composition.start, control.value.length - (composition.value.length - composition.end))
|
|
151
|
+
: -1;
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
const baseline = multiline
|
|
154
|
+
? line.top - control.scrollTop + ascent
|
|
155
|
+
: (height - ascent - descent) / 2 + ascent;
|
|
156
|
+
if (baseline - ascent > height || baseline + descent < 0)
|
|
157
|
+
continue;
|
|
158
|
+
let left = (multiline ? line.left : textStart(style.direction, style.textAlign, width, paddingStart, paddingEnd, context.measureText(line.text).width)) - control.scrollLeft;
|
|
159
|
+
let offset = line.start;
|
|
160
|
+
for (const character of line.text) {
|
|
161
|
+
const advance = character === '\n' || character === '\r' ? 0
|
|
162
|
+
: character === '\t' ? numeric(style, 'tab-size') * context.measureText(' ').width
|
|
163
|
+
: context.measureText(character).width;
|
|
164
|
+
if (advance && left + advance >= 0 && left <= width) {
|
|
165
|
+
if (composition && !content.placeholder && offset >= composingStart && offset < composingEnd) {
|
|
166
|
+
// Keep preedit visibly underlined without changing the native IME session.
|
|
167
|
+
context.fillText(character, left, baseline);
|
|
168
|
+
context.fillRect(left, baseline + 2, advance, 1 / dpr);
|
|
169
|
+
}
|
|
170
|
+
else if (character.trim()) {
|
|
171
|
+
const key = glyphAppearance + character;
|
|
172
|
+
let glyph = glyphs.get(key);
|
|
173
|
+
if (!glyph || !canvasIsUsable(glyph.ink)) {
|
|
174
|
+
glyph = renderPencilText(character, {
|
|
175
|
+
size: fontSize, family, color, seed: baseSeed ^ seedToNumber(character),
|
|
176
|
+
dpr, settings, cache: false,
|
|
177
|
+
});
|
|
178
|
+
glyphs.set(key, glyph, canvasByteCost(glyph.ink));
|
|
179
|
+
}
|
|
180
|
+
context.drawImage(glyph.ink, left - glyph.padding, baseline - glyph.ascent - glyph.padding, glyph.width, glyph.height);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
left += advance;
|
|
184
|
+
offset += character.length;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Commit only a complete frame. The visible canvas never waits empty for idle work.
|
|
188
|
+
if (canvas.width !== buffer.width)
|
|
189
|
+
canvas.width = buffer.width;
|
|
190
|
+
if (canvas.height !== buffer.height)
|
|
191
|
+
canvas.height = buffer.height;
|
|
192
|
+
target.setTransform(1, 0, 0, 1, 0, 0);
|
|
193
|
+
target.clearRect(0, 0, canvas.width, canvas.height);
|
|
194
|
+
target.drawImage(buffer, 0, 0);
|
|
195
|
+
Object.assign(canvas.style, {
|
|
196
|
+
left: `${control.offsetLeft}px`, top: `${control.offsetTop}px`,
|
|
197
|
+
right: 'auto', bottom: 'auto', width: `${width}px`, height: `${height}px`,
|
|
198
|
+
});
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
export function EditablePencilText({ controlRef, defaultValue, multiline = false, placeholder, seed, value, }) {
|
|
202
|
+
const canvasRef = useRef(null);
|
|
203
|
+
const scheduleRef = useRef(() => { });
|
|
204
|
+
const composingRef = useRef(null);
|
|
205
|
+
useBrowserLayoutEffect(() => {
|
|
206
|
+
const control = controlRef.current;
|
|
207
|
+
const canvas = canvasRef.current;
|
|
208
|
+
if (!control || !canvas)
|
|
209
|
+
return undefined;
|
|
210
|
+
const root = control.closest('.inkra-field-control-hit');
|
|
211
|
+
const buffer = document.createElement('canvas');
|
|
212
|
+
// Private, bounded, per-editor glyphs. Never enter the shared text cache.
|
|
213
|
+
let glyphs = new BoundedCache(128, 4 * 1024 * 1024);
|
|
214
|
+
let disposed = false;
|
|
215
|
+
let lastAppearance = '';
|
|
216
|
+
const appearanceKey = () => {
|
|
217
|
+
const style = getComputedStyle(control);
|
|
218
|
+
return JSON.stringify([
|
|
219
|
+
control.clientWidth, control.clientHeight, control.offsetLeft, control.offsetTop,
|
|
220
|
+
control.scrollLeft, control.scrollTop, control.value, control.placeholder, effectiveDpr(2),
|
|
221
|
+
composingRef.current?.start, composingRef.current?.end, composingRef.current?.value.length,
|
|
222
|
+
...['font-family', 'font-size', 'line-height', 'text-align', 'text-indent',
|
|
223
|
+
'letter-spacing', 'white-space', 'word-break', 'overflow-wrap', 'direction', 'tab-size',
|
|
224
|
+
'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
|
|
225
|
+
'--inkra-letter-variation', '--inkra-letter-breaks', '--inkra-letter-outline',
|
|
226
|
+
'--inkra-letter-fill', '--inkra-letter-grain',
|
|
227
|
+
'--inkra-editable-text-color', '--inkra-editable-placeholder-color'].map(name => style.getPropertyValue(name)),
|
|
228
|
+
]);
|
|
229
|
+
};
|
|
230
|
+
const paint = () => {
|
|
231
|
+
if (disposed)
|
|
232
|
+
return;
|
|
233
|
+
const key = appearanceKey();
|
|
234
|
+
if (key === lastAppearance && root?.hasAttribute('data-pencil-editable-ready'))
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
if (paintEditableText(canvas, buffer, glyphs, control, seed, multiline, composingRef.current)) {
|
|
238
|
+
root?.setAttribute('data-pencil-editable-ready', 'true');
|
|
239
|
+
lastAppearance = key;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
243
|
+
lastAppearance = '';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
248
|
+
lastAppearance = '';
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
scheduleRef.current = paint;
|
|
252
|
+
const startComposition = () => {
|
|
253
|
+
composingRef.current = {
|
|
254
|
+
value: control.value,
|
|
255
|
+
start: control.selectionStart ?? control.value.length,
|
|
256
|
+
end: control.selectionEnd ?? control.value.length,
|
|
257
|
+
};
|
|
258
|
+
root?.setAttribute('data-composing', 'true');
|
|
259
|
+
paint();
|
|
260
|
+
};
|
|
261
|
+
const finishComposition = () => {
|
|
262
|
+
composingRef.current = null;
|
|
263
|
+
root?.removeAttribute('data-composing');
|
|
264
|
+
paint();
|
|
265
|
+
};
|
|
266
|
+
const refreshFonts = () => {
|
|
267
|
+
glyphs = new BoundedCache(128, 4 * 1024 * 1024);
|
|
268
|
+
lastAppearance = '';
|
|
269
|
+
paint();
|
|
270
|
+
};
|
|
271
|
+
const reset = () => queueMicrotask(() => {
|
|
272
|
+
if (disposed)
|
|
273
|
+
return;
|
|
274
|
+
composingRef.current = null;
|
|
275
|
+
root?.removeAttribute('data-composing');
|
|
276
|
+
refreshFonts();
|
|
277
|
+
});
|
|
278
|
+
const form = control.form;
|
|
279
|
+
const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(paint);
|
|
280
|
+
resize?.observe(control);
|
|
281
|
+
const appearance = new MutationObserver(paint);
|
|
282
|
+
for (let element = control; element; element = element.parentElement) {
|
|
283
|
+
appearance.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'dir', 'data-theme', 'data-inkra-theme'] });
|
|
284
|
+
}
|
|
285
|
+
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
|
286
|
+
colorScheme.addEventListener('change', paint);
|
|
287
|
+
control.addEventListener('input', paint);
|
|
288
|
+
control.addEventListener('scroll', paint, { passive: true });
|
|
289
|
+
control.addEventListener('compositionstart', startComposition);
|
|
290
|
+
control.addEventListener('compositionend', finishComposition);
|
|
291
|
+
form?.addEventListener('reset', reset);
|
|
292
|
+
document.fonts?.addEventListener('loadingdone', refreshFonts);
|
|
293
|
+
document.fonts?.addEventListener('loadingerror', refreshFonts);
|
|
294
|
+
document.fonts?.ready.then(() => { if (!disposed)
|
|
295
|
+
refreshFonts(); });
|
|
296
|
+
const stopRecovery = observeCanvasRecovery(refreshFonts);
|
|
297
|
+
paint();
|
|
298
|
+
return () => {
|
|
299
|
+
disposed = true;
|
|
300
|
+
stopRecovery();
|
|
301
|
+
resize?.disconnect();
|
|
302
|
+
appearance.disconnect();
|
|
303
|
+
colorScheme.removeEventListener('change', paint);
|
|
304
|
+
control.removeEventListener('input', paint);
|
|
305
|
+
control.removeEventListener('scroll', paint);
|
|
306
|
+
control.removeEventListener('compositionstart', startComposition);
|
|
307
|
+
control.removeEventListener('compositionend', finishComposition);
|
|
308
|
+
form?.removeEventListener('reset', reset);
|
|
309
|
+
document.fonts?.removeEventListener('loadingdone', refreshFonts);
|
|
310
|
+
document.fonts?.removeEventListener('loadingerror', refreshFonts);
|
|
311
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
312
|
+
root?.removeAttribute('data-composing');
|
|
313
|
+
composingRef.current = null;
|
|
314
|
+
glyphs = new BoundedCache(0, 0);
|
|
315
|
+
buffer.width = buffer.height = 0;
|
|
316
|
+
scheduleRef.current = () => { };
|
|
317
|
+
};
|
|
318
|
+
}, [controlRef, multiline, seed]);
|
|
319
|
+
useBrowserLayoutEffect(() => {
|
|
320
|
+
scheduleRef.current();
|
|
321
|
+
}, [controlRef, defaultValue, placeholder, value]);
|
|
322
|
+
return _jsx("canvas", { "aria-hidden": "true", className: "inkra-editable-text", "data-renderer": "inkra-editable-lettering", ref: canvasRef });
|
|
323
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
export interface FieldProps extends Omit<HTMLAttributes<HTMLDivElement>, 'id'> {
|
|
3
|
+
id?: string;
|
|
4
|
+
label: ReactNode;
|
|
5
|
+
description?: ReactNode;
|
|
6
|
+
error?: ReactNode;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
invalid?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function Field({ children, className, description, disabled, error, id, invalid, label, readOnly, required, ...props }: FieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import { PencilText } from '../PencilText.js';
|
|
4
|
+
import { FieldContext } from './context.js';
|
|
5
|
+
function fieldCopy(value, seed) {
|
|
6
|
+
return typeof value === 'string' ? _jsx(PencilText, { seed: seed, children: value }) : value;
|
|
7
|
+
}
|
|
8
|
+
export function Field({ children, className, description, disabled = false, error, id, invalid = false, label, readOnly = false, required = false, ...props }) {
|
|
9
|
+
const generatedId = useId();
|
|
10
|
+
const controlId = id ?? `inkra-field-${generatedId}`;
|
|
11
|
+
const hasDescription = description !== undefined && description !== null;
|
|
12
|
+
const hasError = error !== undefined && error !== null;
|
|
13
|
+
const resolvedInvalid = invalid || hasError;
|
|
14
|
+
const descriptionId = hasDescription ? `${controlId}-description` : undefined;
|
|
15
|
+
const errorId = hasError ? `${controlId}-error` : undefined;
|
|
16
|
+
const classes = ['inkra-field', className].filter(Boolean).join(' ');
|
|
17
|
+
return (_jsx(FieldContext.Provider, { value: {
|
|
18
|
+
controlId,
|
|
19
|
+
descriptionId,
|
|
20
|
+
errorId,
|
|
21
|
+
required,
|
|
22
|
+
invalid: resolvedInvalid,
|
|
23
|
+
disabled,
|
|
24
|
+
readOnly,
|
|
25
|
+
}, children: _jsxs("div", { ...props, className: classes, "data-disabled": disabled || undefined, "data-invalid": resolvedInvalid || undefined, "data-readonly": readOnly || undefined, "data-required": required || undefined, children: [_jsxs("label", { className: "inkra-field__label", htmlFor: controlId, children: [_jsx("span", { children: fieldCopy(label, `${controlId}-label`) }), required ? _jsx("span", { "aria-hidden": "true", className: "inkra-field__required", children: "*" }) : null] }), _jsx("div", { className: "inkra-field__control", children: children }), hasDescription ? (_jsx("div", { className: "inkra-field__description", id: descriptionId, children: fieldCopy(description, `${controlId}-description`) })) : null, hasError ? (_jsx("div", { className: "inkra-field__error", id: errorId, children: fieldCopy(error, `${controlId}-error`) })) : null] }) }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, MouseEvent, ReactNode } from 'react';
|
|
2
|
+
import type { FieldControlSize, FieldControlStateProps } from './types.js';
|
|
3
|
+
export type InputVariant = 'underline' | 'outline' | 'soft' | 'ghost';
|
|
4
|
+
export type InputTextAlign = 'start' | 'center' | 'end';
|
|
5
|
+
export interface InputValueChangeDetails {
|
|
6
|
+
reason: 'input' | 'clear';
|
|
7
|
+
}
|
|
8
|
+
export type InputClassNames = Partial<Record<'root' | 'control' | 'input' | 'startIcon' | 'endIcon' | 'prefix' | 'suffix' | 'actions', string>>;
|
|
9
|
+
export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'>, FieldControlStateProps {
|
|
10
|
+
startIcon?: ReactNode;
|
|
11
|
+
endIcon?: ReactNode;
|
|
12
|
+
prefix?: ReactNode;
|
|
13
|
+
suffix?: ReactNode;
|
|
14
|
+
clearable?: boolean;
|
|
15
|
+
clearButtonLabel?: string;
|
|
16
|
+
onClear?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
17
|
+
onValueChange?: (value: string, details: InputValueChangeDetails) => void;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
startAction?: ReactNode;
|
|
20
|
+
endAction?: ReactNode;
|
|
21
|
+
variant?: InputVariant;
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
textAlign?: InputTextAlign;
|
|
24
|
+
classNames?: InputClassNames;
|
|
25
|
+
}
|
|
26
|
+
export declare const Input: import("react").ForwardRefExoticComponent<InputProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
27
|
+
export type { FieldControlSize };
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { PencilLine } from '../PencilLine.js';
|
|
5
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
6
|
+
import { PencilText } from '../PencilText.js';
|
|
7
|
+
import { InputAction } from './InputAction.js';
|
|
8
|
+
import { useResolvedControlProps } from './controlProps.js';
|
|
9
|
+
import { EditablePencilText } from './EditablePencilText.js';
|
|
10
|
+
const editablePencilInputTypes = new Set(['text', 'search', 'email', 'url', 'tel']);
|
|
11
|
+
const clearableInputTypes = new Set(['text', 'search', 'email', 'url', 'tel', 'password', 'number']);
|
|
12
|
+
// Lucide X / LoaderCircle ISC geometry, rendered by the shared pencil engine.
|
|
13
|
+
const clearPaths = [{ d: 'M18 6 6 18' }, { d: 'm6 6 12 12' }];
|
|
14
|
+
const loadingPaths = [{ d: 'M21 12a9 9 0 1 1-6.219-8.56' }];
|
|
15
|
+
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
16
|
+
function affixContent(content, seed) {
|
|
17
|
+
return typeof content === 'string' || typeof content === 'number'
|
|
18
|
+
? _jsx(PencilText, { seed: seed, children: String(content) })
|
|
19
|
+
: content;
|
|
20
|
+
}
|
|
21
|
+
export const Input = forwardRef(function Input({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, className, classNames = {}, clearable = false, clearButtonLabel = 'Clear input', defaultValue, dir, disabled, endAction, endIcon, fullWidth = true, id, invalid = false, loading, onChange, onClear, onCompositionEnd, onCompositionStart, onValueChange, prefix, readOnly, required, seed, size = 'md', startAction, startIcon, style, suffix, textAlign = 'start', value, variant = 'underline', ...props }, ref) {
|
|
22
|
+
const generatedId = useId();
|
|
23
|
+
const inputRef = useRef(null);
|
|
24
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(() => String(defaultValue ?? ''));
|
|
25
|
+
const [composing, setComposing] = useState(false);
|
|
26
|
+
const [focusWithin, setFocusWithin] = useState(false);
|
|
27
|
+
const isControlled = value != null;
|
|
28
|
+
useImperativeHandle(ref, () => inputRef.current);
|
|
29
|
+
const state = useResolvedControlProps({
|
|
30
|
+
id,
|
|
31
|
+
required,
|
|
32
|
+
disabled,
|
|
33
|
+
readOnly,
|
|
34
|
+
invalid,
|
|
35
|
+
'aria-describedby': ariaDescribedBy,
|
|
36
|
+
'aria-errormessage': ariaErrorMessage,
|
|
37
|
+
'aria-invalid': ariaInvalid,
|
|
38
|
+
});
|
|
39
|
+
const resolvedSeed = seed ?? `inkra-input-${state.id ?? generatedId}`;
|
|
40
|
+
const resolvedType = props.type ?? 'text';
|
|
41
|
+
const enhanceEditableText = editablePencilInputTypes.has(resolvedType);
|
|
42
|
+
const hasClearSlot = clearable && clearableInputTypes.has(resolvedType);
|
|
43
|
+
const currentValue = isControlled ? String(value) : uncontrolledValue;
|
|
44
|
+
const canClear = hasClearSlot && currentValue.length > 0 && !state.disabled && !state.readOnly && !composing;
|
|
45
|
+
const hasStart = startIcon != null || prefix != null || startAction != null;
|
|
46
|
+
const hasEnd = endIcon != null || suffix != null || loading !== undefined || hasClearSlot || endAction != null;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const input = inputRef.current;
|
|
49
|
+
if (!input || isControlled)
|
|
50
|
+
return;
|
|
51
|
+
let disposed = false;
|
|
52
|
+
let resetTimer;
|
|
53
|
+
const synchronize = () => { if (!disposed)
|
|
54
|
+
setUncontrolledValue(input.value); };
|
|
55
|
+
const reset = (event) => {
|
|
56
|
+
// The reset event precedes the browser's default value restoration.
|
|
57
|
+
clearTimeout(resetTimer);
|
|
58
|
+
resetTimer = setTimeout(() => { if (!event.defaultPrevented)
|
|
59
|
+
synchronize(); }, 0);
|
|
60
|
+
};
|
|
61
|
+
synchronize();
|
|
62
|
+
input.addEventListener('input', synchronize);
|
|
63
|
+
input.addEventListener('change', synchronize);
|
|
64
|
+
input.addEventListener('focus', synchronize);
|
|
65
|
+
const form = input.form;
|
|
66
|
+
form?.addEventListener('reset', reset);
|
|
67
|
+
return () => {
|
|
68
|
+
disposed = true;
|
|
69
|
+
clearTimeout(resetTimer);
|
|
70
|
+
input.removeEventListener('input', synchronize);
|
|
71
|
+
input.removeEventListener('change', synchronize);
|
|
72
|
+
input.removeEventListener('focus', synchronize);
|
|
73
|
+
form?.removeEventListener('reset', reset);
|
|
74
|
+
};
|
|
75
|
+
}, [isControlled, props.form, defaultValue]);
|
|
76
|
+
const clear = (event) => {
|
|
77
|
+
const input = inputRef.current;
|
|
78
|
+
if (!canClear || !input || input.matches(':disabled'))
|
|
79
|
+
return;
|
|
80
|
+
if (!isControlled) {
|
|
81
|
+
input.value = '';
|
|
82
|
+
setUncontrolledValue('');
|
|
83
|
+
}
|
|
84
|
+
input.focus({ preventScroll: true });
|
|
85
|
+
onValueChange?.('', { reason: 'clear' });
|
|
86
|
+
onClear?.(event);
|
|
87
|
+
};
|
|
88
|
+
return (_jsx("span", { className: classes('inkra-field-control-hit', 'inkra-input-root', classNames.root), dir: dir, "data-full-width": fullWidth, "data-variant": variant, "data-focus-within": focusWithin || undefined, "data-clearable": hasClearSlot || undefined, "data-loading": loading || undefined, "data-start-content": hasStart || undefined, "data-end-content": hasEnd || undefined, "data-disabled": state.disabled || undefined, "data-invalid": state.invalid || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, onFocusCapture: () => setFocusWithin(true), onBlurCapture: event => {
|
|
89
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
90
|
+
setFocusWithin(false);
|
|
91
|
+
}, children: _jsxs(PencilSurface, { as: "span", clipContent: true, className: classes('inkra-field-control inkra-field-control--input', classNames.control), seed: resolvedSeed, variant: variant === 'underline' ? 'ghost' : state.invalid ? 'danger' : variant === 'soft' ? 'quiet' : variant === 'ghost' && focusWithin ? 'outline' : variant, children: [startAction == null ? null : (_jsx(InputAction, { position: "start", disabled: state.disabled, seed: `${resolvedSeed}-start-action`, children: startAction })), startIcon == null ? null : (_jsx("span", { "aria-hidden": "true", className: classes('inkra-field-control__icon inkra-field-control__icon--start', classNames.startIcon), children: startIcon })), prefix == null ? null : _jsx("span", { className: classes('inkra-input__affix inkra-input__affix--prefix', classNames.prefix), children: affixContent(prefix, `${resolvedSeed}-prefix`) }), _jsx("input", { ...props, "aria-busy": loading || props['aria-busy'], "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, className: classes('inkra-input', className, classNames.input), defaultValue: defaultValue, disabled: state.disabled, id: state.id, readOnly: state.readOnly, ref: inputRef, required: state.required, style: { textAlign, ...style }, value: value, onChange: event => {
|
|
92
|
+
if (!isControlled)
|
|
93
|
+
setUncontrolledValue(event.currentTarget.value);
|
|
94
|
+
onChange?.(event);
|
|
95
|
+
onValueChange?.(event.currentTarget.value, { reason: 'input' });
|
|
96
|
+
}, onCompositionStart: event => { setComposing(true); onCompositionStart?.(event); }, onCompositionEnd: event => { setComposing(false); onCompositionEnd?.(event); } }), suffix == null ? null : _jsx("span", { className: classes('inkra-input__affix inkra-input__affix--suffix', classNames.suffix), children: affixContent(suffix, `${resolvedSeed}-suffix`) }), endIcon == null ? null : (_jsx("span", { "aria-hidden": "true", className: classes('inkra-field-control__icon inkra-field-control__icon--end', classNames.endIcon), children: endIcon })), loading === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "inkra-input__loading", "data-active": loading, children: _jsx(PencilIcon, { className: "inkra-input__spinner", paths: loadingPaths, sourceSize: 24, size: 30, seed: `${resolvedSeed}-loading` }) })), hasClearSlot || endAction != null ? (_jsxs("span", { className: classes('inkra-input__actions', classNames.actions), children: [hasClearSlot ? (_jsx("button", { type: "button", className: "inkra-input__clear", "aria-label": clearButtonLabel, title: clearButtonLabel, "aria-hidden": !canClear || undefined, disabled: !canClear, tabIndex: canClear ? 0 : -1, "data-visible": canClear, onPointerDown: event => event.preventDefault(), onClick: clear, children: _jsx(PencilIcon, { paths: clearPaths, sourceSize: 24, size: 30, seed: `${resolvedSeed}-clear` }) })) : null, endAction == null ? null : (_jsx(InputAction, { disabled: state.disabled, seed: `${resolvedSeed}-action`, children: endAction }))] })) : null, variant === 'underline' ? (_jsx(PencilLine, { className: "inkra-input__underline", seed: `${resolvedSeed}-underline`, weight: (focusWithin ? 1.5 : 1) + (state.invalid ? 0.1 : 0) })) : null, enhanceEditableText ? (_jsx(EditablePencilText, { controlRef: inputRef, defaultValue: defaultValue, placeholder: props.placeholder, seed: `${resolvedSeed}-editable`, value: isControlled ? value : uncontrolledValue }, `${textAlign}-${dir ?? 'inherit'}`)) : null] }) }));
|
|
97
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { PencilLine } from '../PencilLine.js';
|
|
4
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
5
|
+
import { InputActionContext } from '../internal/InputActionContext.js';
|
|
6
|
+
import { useSelectionDrawing } from '../internal/useSelectionDrawing.js';
|
|
7
|
+
export function InputAction({ children, disabled, seed, position = 'end' }) {
|
|
8
|
+
const [hovered, setHovered] = useState(false);
|
|
9
|
+
const [focused, setFocused] = useState(false);
|
|
10
|
+
const washRef = useRef(null);
|
|
11
|
+
const active = !disabled && (hovered || focused);
|
|
12
|
+
useSelectionDrawing(washRef, active);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (disabled) {
|
|
15
|
+
setHovered(false);
|
|
16
|
+
setFocused(false);
|
|
17
|
+
}
|
|
18
|
+
}, [disabled]);
|
|
19
|
+
return _jsxs("span", { className: `inkra-input__end-action${position === 'start' ? ' inkra-input__start-action' : ''}`, inert: disabled || undefined, "data-active": active || undefined, onPointerEnter: event => { if (event.pointerType !== 'touch')
|
|
20
|
+
setHovered(true); }, onPointerLeave: () => setHovered(false), onFocusCapture: () => setFocused(true), onBlurCapture: event => {
|
|
21
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
22
|
+
setFocused(false);
|
|
23
|
+
}, children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-input__action-wash", variant: "selection", seed: `${seed}-selection`, ref: washRef }), _jsx(PencilLine, { className: "inkra-input__action-divider", orientation: "vertical", seed: `${seed}-divider`, weight: 0.9 }), _jsx(InputActionContext.Provider, { value: true, children: children })] });
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TextareaHTMLAttributes } from 'react';
|
|
2
|
+
import type { FieldControlStateProps } from './types.js';
|
|
3
|
+
export interface TextareaCountDetails {
|
|
4
|
+
count: number;
|
|
5
|
+
maxLength: number;
|
|
6
|
+
remaining: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, FieldControlStateProps {
|
|
9
|
+
countMode?: 'remaining' | 'fraction' | 'progress' | 'none';
|
|
10
|
+
countDisplay?: 'auto' | 'always';
|
|
11
|
+
/** Ratio of maxLength at which the automatic counter appears. */
|
|
12
|
+
countThreshold?: number;
|
|
13
|
+
getCountLabel?: (details: TextareaCountDetails) => string;
|
|
14
|
+
resize?: 'none' | 'vertical';
|
|
15
|
+
resizeLabel?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare const Textarea: import("react").ForwardRefExoticComponent<TextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
|
|
3
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
4
|
+
import { PencilText } from '../PencilText.js';
|
|
5
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
6
|
+
import { Button } from '../Button.js';
|
|
7
|
+
import { Progress } from '../display/index.js';
|
|
8
|
+
import { useResolvedControlProps } from './controlProps.js';
|
|
9
|
+
import { EditablePencilText } from './EditablePencilText.js';
|
|
10
|
+
const resizePaths = [{ d: 'M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4' }];
|
|
11
|
+
const defaultCountLabel = ({ remaining }) => remaining < 0 ? `${-remaining} over limit` : `${remaining} remaining`;
|
|
12
|
+
export const Textarea = forwardRef(function Textarea({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, className, disabled, id, invalid = false, readOnly, required, seed, size = 'md', countMode = 'remaining', countDisplay = 'auto', countThreshold = 0.8, getCountLabel = defaultCountLabel, resize = 'none', resizeLabel = 'Resize text area', ...props }, ref) {
|
|
13
|
+
const generatedId = useId();
|
|
14
|
+
const textareaRef = useRef(null);
|
|
15
|
+
const [uncontrolledCount, setUncontrolledCount] = useState(() => String(props.defaultValue ?? '').length);
|
|
16
|
+
const [height, setHeight] = useState();
|
|
17
|
+
const [measuredHeight, setMeasuredHeight] = useState();
|
|
18
|
+
const drag = useRef(null);
|
|
19
|
+
const controlled = props.value != null;
|
|
20
|
+
const count = controlled ? String(props.value).length : uncontrolledCount;
|
|
21
|
+
const maximum = props.maxLength;
|
|
22
|
+
const hasCount = countMode !== 'none' && typeof maximum === 'number' && Number.isFinite(maximum) && maximum >= 0;
|
|
23
|
+
const threshold = Number.isFinite(countThreshold) ? Math.max(0, Math.min(1, countThreshold)) : 0.8;
|
|
24
|
+
const showCount = hasCount && (countDisplay === 'always' || count >= maximum * threshold);
|
|
25
|
+
const countId = `inkra-textarea-${generatedId}-count`;
|
|
26
|
+
const controlId = id ?? `inkra-textarea-${generatedId}`;
|
|
27
|
+
const remaining = hasCount ? maximum - count : 0;
|
|
28
|
+
const countLabel = hasCount ? getCountLabel({ count, maxLength: maximum, remaining }) : '';
|
|
29
|
+
const minimumHeight = size === 'sm' ? 80 : size === 'lg' ? 112 : 96;
|
|
30
|
+
const resizeTo = (next) => setHeight(Math.max(minimumHeight, Math.min(1200, next)));
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const control = textareaRef.current;
|
|
33
|
+
if (resize !== 'vertical' || !control)
|
|
34
|
+
return;
|
|
35
|
+
const measure = () => setMeasuredHeight(control.getBoundingClientRect().height);
|
|
36
|
+
measure();
|
|
37
|
+
if (typeof ResizeObserver === 'undefined')
|
|
38
|
+
return;
|
|
39
|
+
const observer = new ResizeObserver(measure);
|
|
40
|
+
observer.observe(control);
|
|
41
|
+
return () => observer.disconnect();
|
|
42
|
+
}, [resize]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const control = textareaRef.current;
|
|
45
|
+
const form = control?.form;
|
|
46
|
+
let frame = 0;
|
|
47
|
+
const reset = () => {
|
|
48
|
+
cancelAnimationFrame(frame);
|
|
49
|
+
frame = requestAnimationFrame(() => setUncontrolledCount(control?.value.length ?? 0));
|
|
50
|
+
};
|
|
51
|
+
form?.addEventListener('reset', reset);
|
|
52
|
+
if (!controlled)
|
|
53
|
+
setUncontrolledCount(control?.value.length ?? 0);
|
|
54
|
+
return () => {
|
|
55
|
+
cancelAnimationFrame(frame);
|
|
56
|
+
form?.removeEventListener('reset', reset);
|
|
57
|
+
};
|
|
58
|
+
}, [controlled, props.defaultValue, props.form]);
|
|
59
|
+
useImperativeHandle(ref, () => textareaRef.current);
|
|
60
|
+
const state = useResolvedControlProps({
|
|
61
|
+
id: controlId,
|
|
62
|
+
required,
|
|
63
|
+
disabled,
|
|
64
|
+
readOnly,
|
|
65
|
+
invalid,
|
|
66
|
+
'aria-describedby': ariaDescribedBy,
|
|
67
|
+
'aria-errormessage': ariaErrorMessage,
|
|
68
|
+
'aria-invalid': ariaInvalid,
|
|
69
|
+
});
|
|
70
|
+
const resolvedSeed = seed ?? `inkra-textarea-${state.id ?? generatedId}`;
|
|
71
|
+
const classes = ['inkra-textarea', className].filter(Boolean).join(' ');
|
|
72
|
+
return (_jsx("div", { className: "inkra-field-control-hit inkra-field-control-hit--textarea", "data-disabled": state.disabled || undefined, "data-invalid": state.invalid || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, children: _jsxs(PencilSurface, { className: "inkra-field-control inkra-field-control--textarea", seed: resolvedSeed, variant: state.invalid ? 'danger' : 'outline', children: [_jsx("textarea", { ...props, "aria-describedby": [state.describedBy, showCount && countId].filter(Boolean).join(' ') || undefined, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, className: classes, disabled: state.disabled, id: state.id, readOnly: state.readOnly, ref: textareaRef, required: state.required, style: { ...props.style, ...(resize === 'vertical' && height !== undefined ? { height } : {}), resize: 'none' }, onInput: event => {
|
|
73
|
+
if (!controlled)
|
|
74
|
+
setUncontrolledCount(event.currentTarget.value.length);
|
|
75
|
+
props.onInput?.(event);
|
|
76
|
+
} }), _jsx(EditablePencilText, { controlRef: textareaRef, defaultValue: props.defaultValue, multiline: true, placeholder: props.placeholder, seed: `${resolvedSeed}-editable`, value: props.value }), (hasCount || resize === 'vertical') && _jsxs("div", { className: "inkra-textarea__footer", children: [hasCount && _jsxs("div", { className: "inkra-textarea__count", id: countId, "data-visible": showCount, "data-limit": remaining <= 0 ? 'reached' : 'near', "aria-hidden": !showCount || undefined, children: [countMode === 'progress' && _jsx(Progress, { size: "sm", value: maximum === 0 ? 1 : count, max: maximum === 0 ? 1 : maximum, "aria-label": countLabel, "aria-valuetext": countLabel }), _jsx(PencilText, { children: countMode === 'fraction' ? `${count} / ${maximum}` : countLabel })] }), resize === 'vertical' && _jsx(Button, { className: "inkra-textarea__resize", variant: "ghost", size: "icon", role: "slider", "aria-label": resizeLabel, title: resizeLabel, "aria-controls": state.id, "aria-orientation": "vertical", "aria-valuemin": minimumHeight, "aria-valuemax": 1200, "aria-valuenow": Math.round(measuredHeight ?? minimumHeight), disabled: state.disabled, onPointerDown: event => {
|
|
77
|
+
if (event.button !== 0)
|
|
78
|
+
return;
|
|
79
|
+
drag.current = { y: event.clientY, height: textareaRef.current?.getBoundingClientRect().height ?? minimumHeight };
|
|
80
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
81
|
+
}, onPointerMove: event => {
|
|
82
|
+
if (drag.current)
|
|
83
|
+
resizeTo(drag.current.height + event.clientY - drag.current.y);
|
|
84
|
+
}, onPointerUp: event => {
|
|
85
|
+
drag.current = null;
|
|
86
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId))
|
|
87
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
88
|
+
}, onPointerCancel: () => { drag.current = null; }, onLostPointerCapture: () => { drag.current = null; }, onKeyDown: event => {
|
|
89
|
+
const current = textareaRef.current?.getBoundingClientRect().height ?? minimumHeight;
|
|
90
|
+
if (event.key === 'ArrowUp')
|
|
91
|
+
resizeTo(current - 20);
|
|
92
|
+
else if (event.key === 'ArrowDown')
|
|
93
|
+
resizeTo(current + 20);
|
|
94
|
+
else if (event.key === 'Home')
|
|
95
|
+
resizeTo(minimumHeight);
|
|
96
|
+
else if (event.key === 'End')
|
|
97
|
+
resizeTo(1200);
|
|
98
|
+
else
|
|
99
|
+
return;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
}, children: _jsx(PencilIcon, { paths: resizePaths, size: 22 }) })] })] }) }));
|
|
102
|
+
});
|