@inkra/react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +213 -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 +212 -0
- package/dist/PencilLine.d.ts +10 -0
- package/dist/PencilLine.js +85 -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 +205 -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/assets/ui-hand-yozai.woff2 +0 -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/content/index.d.ts +106 -0
- package/dist/content/index.js +80 -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/index.d.ts +32 -0
- package/dist/display/index.js +59 -0
- package/dist/dropdown-menu/index.d.ts +34 -0
- package/dist/dropdown-menu/index.js +40 -0
- package/dist/form/EditablePencilText.d.ts +13 -0
- package/dist/form/EditablePencilText.js +312 -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 +5 -0
- package/dist/form/Textarea.js +23 -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/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 +28 -0
- package/dist/index.js +22 -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/stroke.d.ts +24 -0
- package/dist/internal/stroke.js +144 -0
- package/dist/internal/surfaceRenderer.d.ts +33 -0
- package/dist/internal/surfaceRenderer.js +175 -0
- package/dist/internal/textQueue.d.ts +1 -0
- package/dist/internal/textQueue.js +37 -0
- package/dist/internal/textRenderer.d.ts +27 -0
- package/dist/internal/textRenderer.js +213 -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 +108 -0
- package/dist/internal/useSelectionDrawing.d.ts +2 -0
- package/dist/internal/useSelectionDrawing.js +86 -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 +52 -0
- package/dist/journey-map/index.d.ts +39 -0
- package/dist/journey-map/index.js +195 -0
- package/dist/journey-map/terrain.d.ts +8 -0
- package/dist/journey-map/terrain.js +127 -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/pencil.d.ts +10 -0
- package/dist/pencil.js +39 -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/styles.css +3663 -0
- package/dist/tabs/index.d.ts +22 -0
- package/dist/tabs/index.js +42 -0
- package/dist/tokens.css +355 -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/package.json +100 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as Primitive from '@radix-ui/react-dropdown-menu';
|
|
3
|
+
import { forwardRef, useId } from 'react';
|
|
4
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
5
|
+
import { PencilText } from '../PencilText.js';
|
|
6
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
7
|
+
import { PencilLine } from '../PencilLine.js';
|
|
8
|
+
import { PortalThemeBridge, PortalThemeProvider, usePortalTriggerRef } from '../overlay/themeBridge.js';
|
|
9
|
+
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
10
|
+
const copy = (children) => typeof children === 'string' ? _jsx(PencilText, { children: children }) : children;
|
|
11
|
+
function Root(props) { return _jsx(PortalThemeProvider, { children: _jsx(Primitive.Root, { ...props }) }); }
|
|
12
|
+
const Trigger = forwardRef(function Trigger(props, ref) {
|
|
13
|
+
return _jsx(Primitive.Trigger, { ...props, ref: usePortalTriggerRef(ref) });
|
|
14
|
+
});
|
|
15
|
+
function Portal({ children, ...props }) { return _jsx(Primitive.Portal, { ...props, children: _jsx(PortalThemeBridge, { children: children }) }); }
|
|
16
|
+
const Content = forwardRef(function Content({ className, children, seed, sideOffset = 6, collisionPadding = 8, ...props }, ref) {
|
|
17
|
+
const id = useId();
|
|
18
|
+
return _jsx(Primitive.Content, { ...props, ref: ref, sideOffset: sideOffset, collisionPadding: collisionPadding, className: classes('inkra-menu', className), children: _jsx(PencilSurface, { variant: "paper", radius: 8, seed: seed ?? `menu-${id}`, className: "inkra-menu__paper", children: children }) });
|
|
19
|
+
});
|
|
20
|
+
function ItemBody({ children, icon }) {
|
|
21
|
+
return _jsxs(_Fragment, { children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", variant: "selection", className: "inkra-menu__highlight" }), _jsx("span", { className: "inkra-menu__icon", "aria-hidden": "true", children: icon }), _jsx("span", { className: "inkra-menu__label", children: copy(children) })] });
|
|
22
|
+
}
|
|
23
|
+
const Item = forwardRef(function Item({ className, children, destructive = false, icon, ...props }, ref) {
|
|
24
|
+
return _jsx(Primitive.Item, { ...props, ref: ref, "data-destructive": destructive || undefined, className: classes('inkra-menu__item', className), children: _jsx(ItemBody, { icon: icon, children: children }) });
|
|
25
|
+
});
|
|
26
|
+
const CheckboxItem = forwardRef(function CheckboxItem({ className, children, checked, ...props }, ref) {
|
|
27
|
+
const indicator = _jsx(Primitive.ItemIndicator, { children: _jsx(PencilIcon, { size: 16, sourceSize: 24, paths: [{ d: checked === 'indeterminate' ? 'M5 12h14' : 'M5 12.5L10 17L19 7' }] }) });
|
|
28
|
+
return _jsx(Primitive.CheckboxItem, { ...props, checked: checked, ref: ref, className: classes('inkra-menu__item', className), children: _jsx(ItemBody, { icon: indicator, children: children }) });
|
|
29
|
+
});
|
|
30
|
+
const RadioItem = forwardRef(function RadioItem({ className, children, ...props }, ref) {
|
|
31
|
+
const indicator = _jsx(Primitive.ItemIndicator, { children: _jsx(PencilIcon, { size: 16, sourceSize: 24, paths: [{ d: 'M5 12.5L10 17L19 7' }] }) });
|
|
32
|
+
return _jsx(Primitive.RadioItem, { ...props, ref: ref, className: classes('inkra-menu__item', className), children: _jsx(ItemBody, { icon: indicator, children: children }) });
|
|
33
|
+
});
|
|
34
|
+
const Label = forwardRef(function Label({ className, children, ...props }, ref) {
|
|
35
|
+
return _jsx(Primitive.Label, { ...props, ref: ref, className: classes('inkra-menu__group-label', className), children: copy(children) });
|
|
36
|
+
});
|
|
37
|
+
const Separator = forwardRef(function Separator({ className, ...props }, ref) {
|
|
38
|
+
return _jsx(Primitive.Separator, { ...props, ref: ref, className: classes('inkra-menu__separator', className), children: _jsx(PencilLine, {}) });
|
|
39
|
+
});
|
|
40
|
+
export const DropdownMenu = { Root, Trigger, Portal, Content, Item, CheckboxItem, RadioItem, Label, Separator, Group: Primitive.Group, RadioGroup: Primitive.RadioGroup };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
type EditableControl = HTMLInputElement | HTMLTextAreaElement;
|
|
3
|
+
interface EditablePencilTextProps {
|
|
4
|
+
controlRef: RefObject<EditableControl | null>;
|
|
5
|
+
defaultValue?: string | number | readonly string[];
|
|
6
|
+
multiline?: boolean;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
seed: string | number;
|
|
9
|
+
value?: string | number | readonly string[];
|
|
10
|
+
wrap?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function EditablePencilText({ controlRef, defaultValue, multiline, placeholder, seed, value, }: EditablePencilTextProps): import("react").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
4
|
+
import { effectiveDpr } from '../internal/cache.js';
|
|
5
|
+
import { observeCanvasRecovery } from '../internal/recovery.js';
|
|
6
|
+
import { seedToNumber } from '../internal/random.js';
|
|
7
|
+
import { renderPencilText } from '../internal/textRenderer.js';
|
|
8
|
+
import { queueTextPaint } from '../internal/textQueue.js';
|
|
9
|
+
const MAX_EDITABLE_TEXT = 4096;
|
|
10
|
+
const MAX_EDITABLE_LINE = 256;
|
|
11
|
+
const complexScript = /[\u0590-\u08ff\u0900-\u0dff\u0e00-\u0fff\u1780-\u18af\u200c\u200d]/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 visibleText(text, context, scrollLeft, viewportWidth) {
|
|
21
|
+
const characters = Array.from(text);
|
|
22
|
+
const overscan = Math.max(16, viewportWidth * 0.2);
|
|
23
|
+
const startEdge = Math.max(0, scrollLeft - overscan);
|
|
24
|
+
const endEdge = scrollLeft + viewportWidth + overscan;
|
|
25
|
+
let offset = 0;
|
|
26
|
+
let start = 0;
|
|
27
|
+
let end = characters.length;
|
|
28
|
+
for (let index = 0; index < characters.length; index += 1) {
|
|
29
|
+
const width = context.measureText(characters[index]).width;
|
|
30
|
+
if (offset + width < startEdge)
|
|
31
|
+
start = index + 1;
|
|
32
|
+
if (offset <= endEdge)
|
|
33
|
+
end = index + 1;
|
|
34
|
+
offset += width;
|
|
35
|
+
if (offset > endEdge)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
const visible = characters.slice(start, end).join('');
|
|
39
|
+
if (Array.from(visible).length > MAX_EDITABLE_LINE)
|
|
40
|
+
return null;
|
|
41
|
+
return { text: visible, offset: context.measureText(characters.slice(0, start).join('')).width };
|
|
42
|
+
}
|
|
43
|
+
function copyMirrorStyle(mirror, control, style) {
|
|
44
|
+
Object.assign(mirror.style, {
|
|
45
|
+
boxSizing: 'border-box',
|
|
46
|
+
position: 'fixed',
|
|
47
|
+
left: '-100000px',
|
|
48
|
+
top: '0',
|
|
49
|
+
visibility: 'hidden',
|
|
50
|
+
pointerEvents: 'none',
|
|
51
|
+
width: `${control.clientWidth}px`,
|
|
52
|
+
minHeight: `${control.scrollHeight}px`,
|
|
53
|
+
paddingTop: style.paddingTop,
|
|
54
|
+
paddingRight: style.paddingRight,
|
|
55
|
+
paddingBottom: style.paddingBottom,
|
|
56
|
+
paddingLeft: style.paddingLeft,
|
|
57
|
+
border: '0',
|
|
58
|
+
font: style.font,
|
|
59
|
+
fontFamily: style.fontFamily,
|
|
60
|
+
fontSize: style.fontSize,
|
|
61
|
+
fontWeight: style.fontWeight,
|
|
62
|
+
fontStyle: style.fontStyle,
|
|
63
|
+
fontKerning: style.fontKerning,
|
|
64
|
+
letterSpacing: style.letterSpacing,
|
|
65
|
+
lineHeight: style.lineHeight,
|
|
66
|
+
textAlign: style.textAlign,
|
|
67
|
+
textIndent: style.textIndent,
|
|
68
|
+
textTransform: style.textTransform,
|
|
69
|
+
direction: style.direction,
|
|
70
|
+
unicodeBidi: style.unicodeBidi,
|
|
71
|
+
tabSize: style.tabSize,
|
|
72
|
+
overflowWrap: style.overflowWrap,
|
|
73
|
+
wordBreak: style.wordBreak,
|
|
74
|
+
whiteSpace: control.wrap === 'off' ? 'pre' : 'pre-wrap',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function measureTextareaLines(control, text, style) {
|
|
78
|
+
const mirror = document.createElement('div');
|
|
79
|
+
copyMirrorStyle(mirror, control, style);
|
|
80
|
+
const source = document.createTextNode(text);
|
|
81
|
+
mirror.appendChild(source);
|
|
82
|
+
document.body.appendChild(mirror);
|
|
83
|
+
const mirrorBox = mirror.getBoundingClientRect();
|
|
84
|
+
const range = document.createRange();
|
|
85
|
+
const lines = [];
|
|
86
|
+
let offset = 0;
|
|
87
|
+
try {
|
|
88
|
+
for (const character of text) {
|
|
89
|
+
range.setStart(source, offset);
|
|
90
|
+
offset += character.length;
|
|
91
|
+
range.setEnd(source, offset);
|
|
92
|
+
const rectangle = range.getBoundingClientRect();
|
|
93
|
+
let line = lines[lines.length - 1];
|
|
94
|
+
if (!line || Math.abs(line.top - rectangle.top) > 1) {
|
|
95
|
+
line = { text: '', left: rectangle.left - mirrorBox.left, top: rectangle.top - mirrorBox.top };
|
|
96
|
+
lines.push(line);
|
|
97
|
+
}
|
|
98
|
+
line.text += character;
|
|
99
|
+
if (Array.from(line.text).length > MAX_EDITABLE_LINE)
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
range.detach();
|
|
106
|
+
mirror.remove();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function textStart(direction, textAlign, controlWidth, paddingStart, paddingEnd, advance) {
|
|
110
|
+
const available = Math.max(0, controlWidth - paddingStart - paddingEnd);
|
|
111
|
+
const resolved = textAlign === 'start'
|
|
112
|
+
? (direction === 'rtl' ? 'right' : 'left')
|
|
113
|
+
: textAlign === 'end'
|
|
114
|
+
? (direction === 'rtl' ? 'left' : 'right')
|
|
115
|
+
: textAlign;
|
|
116
|
+
if (resolved === 'center')
|
|
117
|
+
return paddingStart + (available - advance) / 2;
|
|
118
|
+
if (resolved === 'right')
|
|
119
|
+
return controlWidth - paddingEnd - advance;
|
|
120
|
+
return paddingStart;
|
|
121
|
+
}
|
|
122
|
+
function paintEditableText(canvas, control, seed, multiline) {
|
|
123
|
+
const width = control.clientWidth;
|
|
124
|
+
const height = control.clientHeight;
|
|
125
|
+
const context = canvas.getContext('2d');
|
|
126
|
+
if (!context || context.isContextLost?.() || width < 2 || height < 2)
|
|
127
|
+
return false;
|
|
128
|
+
Object.assign(canvas.style, {
|
|
129
|
+
left: `${control.offsetLeft}px`,
|
|
130
|
+
top: `${control.offsetTop}px`,
|
|
131
|
+
right: 'auto',
|
|
132
|
+
bottom: 'auto',
|
|
133
|
+
width: `${width}px`,
|
|
134
|
+
height: `${height}px`,
|
|
135
|
+
});
|
|
136
|
+
const content = editableText(control);
|
|
137
|
+
if (Array.from(content.text).length > MAX_EDITABLE_TEXT)
|
|
138
|
+
return false;
|
|
139
|
+
const style = getComputedStyle(control);
|
|
140
|
+
if (style.direction === 'rtl' || complexScript.test(content.text))
|
|
141
|
+
return false;
|
|
142
|
+
const fontSize = Number.parseFloat(style.fontSize);
|
|
143
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0)
|
|
144
|
+
return false;
|
|
145
|
+
const dpr = effectiveDpr(2);
|
|
146
|
+
canvas.width = Math.ceil(width * dpr);
|
|
147
|
+
canvas.height = Math.ceil(height * dpr);
|
|
148
|
+
context.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
149
|
+
context.clearRect(0, 0, width, height);
|
|
150
|
+
if (!content.text)
|
|
151
|
+
return true;
|
|
152
|
+
const family = style.fontFamily;
|
|
153
|
+
const color = content.placeholder
|
|
154
|
+
? style.getPropertyValue('--inkra-editable-placeholder-color').trim() || style.color
|
|
155
|
+
: style.getPropertyValue('--inkra-editable-text-color').trim() || style.color;
|
|
156
|
+
const paddingStart = numeric(style, 'padding-inline-start') || numeric(style, 'padding-left');
|
|
157
|
+
const paddingEnd = numeric(style, 'padding-inline-end') || numeric(style, 'padding-right');
|
|
158
|
+
const measure = document.createElement('canvas').getContext('2d');
|
|
159
|
+
if (!measure)
|
|
160
|
+
return false;
|
|
161
|
+
measure.font = `400 ${fontSize}px ${family}`;
|
|
162
|
+
measure.fontKerning = 'none';
|
|
163
|
+
if (!multiline && Array.from(content.text).length > MAX_EDITABLE_LINE)
|
|
164
|
+
return false;
|
|
165
|
+
if (!multiline && style.direction === 'rtl'
|
|
166
|
+
&& measure.measureText(content.text).width > control.clientWidth - paddingStart - paddingEnd)
|
|
167
|
+
return false;
|
|
168
|
+
const measuredLines = multiline
|
|
169
|
+
? measureTextareaLines(control, content.text, style)
|
|
170
|
+
: [{ text: content.text, left: 0, top: 0 }];
|
|
171
|
+
if (!measuredLines)
|
|
172
|
+
return false;
|
|
173
|
+
const baseSeed = seedToNumber(seed);
|
|
174
|
+
context.save();
|
|
175
|
+
context.beginPath();
|
|
176
|
+
context.rect(0, 0, width, height);
|
|
177
|
+
context.clip();
|
|
178
|
+
for (let index = 0; index < measuredLines.length; index += 1) {
|
|
179
|
+
const line = measuredLines[index];
|
|
180
|
+
const visible = multiline && control.wrap !== 'off'
|
|
181
|
+
? { text: line.text, offset: 0 }
|
|
182
|
+
: visibleText(line.text, measure, control.scrollLeft, width - paddingStart - paddingEnd);
|
|
183
|
+
if (!visible?.text)
|
|
184
|
+
continue;
|
|
185
|
+
const sprite = renderPencilText(visible.text, {
|
|
186
|
+
size: fontSize,
|
|
187
|
+
family,
|
|
188
|
+
color,
|
|
189
|
+
seed: baseSeed + index * 131,
|
|
190
|
+
dpr,
|
|
191
|
+
cache: false,
|
|
192
|
+
});
|
|
193
|
+
const start = multiline
|
|
194
|
+
? line.left
|
|
195
|
+
: textStart(style.direction, style.textAlign, control.clientWidth, paddingStart, paddingEnd, sprite.advance);
|
|
196
|
+
const x = start + visible.offset - control.scrollLeft - sprite.padding;
|
|
197
|
+
const y = multiline
|
|
198
|
+
? line.top - control.scrollTop - sprite.padding
|
|
199
|
+
: (control.clientHeight - sprite.height) / 2;
|
|
200
|
+
if (y > height || y + sprite.height < 0)
|
|
201
|
+
continue;
|
|
202
|
+
context.drawImage(sprite.ink, x, y, sprite.width, sprite.height);
|
|
203
|
+
}
|
|
204
|
+
context.restore();
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
export function EditablePencilText({ controlRef, defaultValue, multiline = false, placeholder, seed, value, }) {
|
|
208
|
+
const canvasRef = useRef(null);
|
|
209
|
+
const scheduleRef = useRef(() => { });
|
|
210
|
+
const composingRef = useRef(false);
|
|
211
|
+
useBrowserLayoutEffect(() => {
|
|
212
|
+
const control = controlRef.current;
|
|
213
|
+
const canvas = canvasRef.current;
|
|
214
|
+
if (!control || !canvas)
|
|
215
|
+
return undefined;
|
|
216
|
+
const root = control.closest('.inkra-field-control-hit');
|
|
217
|
+
let frame = 0;
|
|
218
|
+
let pending = null;
|
|
219
|
+
let disposed = false;
|
|
220
|
+
const paint = () => {
|
|
221
|
+
pending = null;
|
|
222
|
+
if (disposed || composingRef.current)
|
|
223
|
+
return;
|
|
224
|
+
try {
|
|
225
|
+
const ready = paintEditableText(canvas, control, seed, multiline);
|
|
226
|
+
if (ready)
|
|
227
|
+
root?.setAttribute('data-pencil-editable-ready', 'true');
|
|
228
|
+
else
|
|
229
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const schedule = () => {
|
|
236
|
+
if (disposed || composingRef.current || frame || pending)
|
|
237
|
+
return;
|
|
238
|
+
frame = requestAnimationFrame(() => {
|
|
239
|
+
frame = 0;
|
|
240
|
+
pending = queueTextPaint(paint);
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
scheduleRef.current = schedule;
|
|
244
|
+
const invalidate = () => {
|
|
245
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
246
|
+
schedule();
|
|
247
|
+
};
|
|
248
|
+
const showNativeComposition = () => {
|
|
249
|
+
composingRef.current = true;
|
|
250
|
+
pending?.();
|
|
251
|
+
pending = null;
|
|
252
|
+
root?.setAttribute('data-composing', 'true');
|
|
253
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
254
|
+
};
|
|
255
|
+
const finishComposition = () => {
|
|
256
|
+
composingRef.current = false;
|
|
257
|
+
root?.removeAttribute('data-composing');
|
|
258
|
+
schedule();
|
|
259
|
+
};
|
|
260
|
+
const reset = () => window.setTimeout(invalidate, 0);
|
|
261
|
+
const form = control.form;
|
|
262
|
+
const resize = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(invalidate);
|
|
263
|
+
resize?.observe(control);
|
|
264
|
+
const appearance = new MutationObserver(invalidate);
|
|
265
|
+
appearance.observe(document.documentElement, {
|
|
266
|
+
attributes: true,
|
|
267
|
+
subtree: true,
|
|
268
|
+
attributeFilter: ['data-theme', 'data-inkra-theme'],
|
|
269
|
+
});
|
|
270
|
+
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
|
271
|
+
colorScheme.addEventListener('change', invalidate);
|
|
272
|
+
control.addEventListener('input', invalidate);
|
|
273
|
+
control.addEventListener('scroll', invalidate, { passive: true });
|
|
274
|
+
control.addEventListener('compositionstart', showNativeComposition);
|
|
275
|
+
control.addEventListener('compositionend', finishComposition);
|
|
276
|
+
form?.addEventListener('reset', reset);
|
|
277
|
+
document.fonts?.addEventListener('loadingdone', invalidate);
|
|
278
|
+
document.fonts?.addEventListener('loadingerror', invalidate);
|
|
279
|
+
document.fonts?.ready.then(() => { if (!disposed)
|
|
280
|
+
invalidate(); });
|
|
281
|
+
const stopRecovery = observeCanvasRecovery(invalidate);
|
|
282
|
+
invalidate();
|
|
283
|
+
return () => {
|
|
284
|
+
disposed = true;
|
|
285
|
+
stopRecovery();
|
|
286
|
+
pending?.();
|
|
287
|
+
resize?.disconnect();
|
|
288
|
+
appearance.disconnect();
|
|
289
|
+
colorScheme.removeEventListener('change', invalidate);
|
|
290
|
+
control.removeEventListener('input', invalidate);
|
|
291
|
+
control.removeEventListener('scroll', invalidate);
|
|
292
|
+
control.removeEventListener('compositionstart', showNativeComposition);
|
|
293
|
+
control.removeEventListener('compositionend', finishComposition);
|
|
294
|
+
form?.removeEventListener('reset', reset);
|
|
295
|
+
document.fonts?.removeEventListener('loadingdone', invalidate);
|
|
296
|
+
document.fonts?.removeEventListener('loadingerror', invalidate);
|
|
297
|
+
cancelAnimationFrame(frame);
|
|
298
|
+
root?.removeAttribute('data-pencil-editable-ready');
|
|
299
|
+
root?.removeAttribute('data-composing');
|
|
300
|
+
composingRef.current = false;
|
|
301
|
+
scheduleRef.current = () => { };
|
|
302
|
+
};
|
|
303
|
+
}, [controlRef, multiline, seed]);
|
|
304
|
+
useBrowserLayoutEffect(() => {
|
|
305
|
+
if (composingRef.current)
|
|
306
|
+
return;
|
|
307
|
+
controlRef.current?.closest('.inkra-field-control-hit')
|
|
308
|
+
?.removeAttribute('data-pencil-editable-ready');
|
|
309
|
+
scheduleRef.current();
|
|
310
|
+
}, [controlRef, defaultValue, placeholder, value]);
|
|
311
|
+
return _jsx("canvas", { "aria-hidden": "true", className: "inkra-editable-text", "data-renderer": "inkra-editable-lettering", ref: canvasRef });
|
|
312
|
+
}
|
|
@@ -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,5 @@
|
|
|
1
|
+
import type { TextareaHTMLAttributes } from 'react';
|
|
2
|
+
import type { FieldControlStateProps } from './types.js';
|
|
3
|
+
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement>, FieldControlStateProps {
|
|
4
|
+
}
|
|
5
|
+
export declare const Textarea: import("react").ForwardRefExoticComponent<TextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useId, useImperativeHandle, useRef } from 'react';
|
|
3
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
4
|
+
import { useResolvedControlProps } from './controlProps.js';
|
|
5
|
+
import { EditablePencilText } from './EditablePencilText.js';
|
|
6
|
+
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', ...props }, ref) {
|
|
7
|
+
const generatedId = useId();
|
|
8
|
+
const textareaRef = useRef(null);
|
|
9
|
+
useImperativeHandle(ref, () => textareaRef.current);
|
|
10
|
+
const state = useResolvedControlProps({
|
|
11
|
+
id,
|
|
12
|
+
required,
|
|
13
|
+
disabled,
|
|
14
|
+
readOnly,
|
|
15
|
+
invalid,
|
|
16
|
+
'aria-describedby': ariaDescribedBy,
|
|
17
|
+
'aria-errormessage': ariaErrorMessage,
|
|
18
|
+
'aria-invalid': ariaInvalid,
|
|
19
|
+
});
|
|
20
|
+
const resolvedSeed = seed ?? `inkra-textarea-${state.id ?? generatedId}`;
|
|
21
|
+
const classes = ['inkra-textarea', className].filter(Boolean).join(' ');
|
|
22
|
+
return (_jsx("span", { 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, { as: "span", className: "inkra-field-control inkra-field-control--textarea", seed: resolvedSeed, variant: state.invalid ? 'danger' : 'outline', children: [_jsx("textarea", { ...props, "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, className: classes, disabled: state.disabled, id: state.id, readOnly: state.readOnly, ref: textareaRef, required: state.required }), _jsx(EditablePencilText, { controlRef: textareaRef, defaultValue: props.defaultValue, multiline: true, placeholder: props.placeholder, seed: `${resolvedSeed}-editable`, value: props.value })] }) }));
|
|
23
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface FieldContextValue {
|
|
2
|
+
controlId: string;
|
|
3
|
+
descriptionId?: string;
|
|
4
|
+
errorId?: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
invalid: boolean;
|
|
7
|
+
disabled: boolean;
|
|
8
|
+
readOnly: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const FieldContext: import("react").Context<FieldContextValue | null>;
|
|
11
|
+
export declare function useFieldContext(): FieldContextValue | null;
|
|
12
|
+
export declare function mergeAriaIds(...values: Array<string | undefined>): string | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const FieldContext = createContext(null);
|
|
3
|
+
export function useFieldContext() {
|
|
4
|
+
return useContext(FieldContext);
|
|
5
|
+
}
|
|
6
|
+
export function mergeAriaIds(...values) {
|
|
7
|
+
const ids = values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []);
|
|
8
|
+
const merged = Array.from(new Set(ids));
|
|
9
|
+
return merged.length ? merged.join(' ') : undefined;
|
|
10
|
+
}
|