@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.
Files changed (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +316 -2
  3. package/THIRD_PARTY_NOTICES.md +39 -0
  4. package/Yozai-OFL.txt +94 -0
  5. package/dist/Button.d.ts +36 -0
  6. package/dist/Button.js +89 -0
  7. package/dist/PencilIcon.d.ts +22 -0
  8. package/dist/PencilIcon.js +210 -0
  9. package/dist/PencilLine.d.ts +10 -0
  10. package/dist/PencilLine.js +91 -0
  11. package/dist/PencilSurface.d.ts +19 -0
  12. package/dist/PencilSurface.js +19 -0
  13. package/dist/PencilText.d.ts +16 -0
  14. package/dist/PencilText.js +338 -0
  15. package/dist/accordion/Accordion.d.ts +27 -0
  16. package/dist/accordion/Accordion.js +40 -0
  17. package/dist/accordion/index.d.ts +2 -0
  18. package/dist/accordion/index.js +1 -0
  19. package/dist/alert-dialog/index.d.ts +64 -0
  20. package/dist/alert-dialog/index.js +109 -0
  21. package/dist/assets/ui-hand-yozai.woff2 +0 -0
  22. package/dist/audio-player/index.d.ts +60 -0
  23. package/dist/audio-player/index.js +101 -0
  24. package/dist/breadcrumb/index.d.ts +16 -0
  25. package/dist/breadcrumb/index.js +25 -0
  26. package/dist/calendar/index.d.ts +21 -0
  27. package/dist/calendar/index.js +41 -0
  28. package/dist/carousel/index.d.ts +51 -0
  29. package/dist/carousel/index.js +290 -0
  30. package/dist/choice-grid/index.d.ts +38 -0
  31. package/dist/choice-grid/index.js +85 -0
  32. package/dist/color-picker/color.d.ts +15 -0
  33. package/dist/color-picker/color.js +55 -0
  34. package/dist/color-picker/index.d.ts +31 -0
  35. package/dist/color-picker/index.js +69 -0
  36. package/dist/command/index.d.ts +23 -0
  37. package/dist/command/index.js +39 -0
  38. package/dist/content/index.d.ts +106 -0
  39. package/dist/content/index.js +80 -0
  40. package/dist/context-menu/index.d.ts +76 -0
  41. package/dist/context-menu/index.js +240 -0
  42. package/dist/data-table/index.d.ts +47 -0
  43. package/dist/data-table/index.js +91 -0
  44. package/dist/date-picker/index.d.ts +50 -0
  45. package/dist/date-picker/index.js +180 -0
  46. package/dist/day-agenda/index.d.ts +43 -0
  47. package/dist/day-agenda/index.js +47 -0
  48. package/dist/day-agenda/layout.d.ts +20 -0
  49. package/dist/day-agenda/layout.js +60 -0
  50. package/dist/display/ProgressRing.d.ts +6 -0
  51. package/dist/display/ProgressRing.js +146 -0
  52. package/dist/display/index.d.ts +59 -0
  53. package/dist/display/index.js +75 -0
  54. package/dist/dockable-panel/index.d.ts +41 -0
  55. package/dist/dockable-panel/index.js +253 -0
  56. package/dist/dockable-panel/model.d.ts +21 -0
  57. package/dist/dockable-panel/model.js +61 -0
  58. package/dist/drawer/index.d.ts +63 -0
  59. package/dist/drawer/index.js +86 -0
  60. package/dist/dropdown-menu/index.d.ts +34 -0
  61. package/dist/dropdown-menu/index.js +40 -0
  62. package/dist/editable/index.d.ts +53 -0
  63. package/dist/editable/index.js +162 -0
  64. package/dist/empty/index.d.ts +21 -0
  65. package/dist/empty/index.js +23 -0
  66. package/dist/flip/index.d.ts +49 -0
  67. package/dist/flip/index.js +138 -0
  68. package/dist/form/EditablePencilText.d.ts +13 -0
  69. package/dist/form/EditablePencilText.js +323 -0
  70. package/dist/form/Field.d.ts +13 -0
  71. package/dist/form/Field.js +26 -0
  72. package/dist/form/Input.d.ts +27 -0
  73. package/dist/form/Input.js +97 -0
  74. package/dist/form/InputAction.d.ts +7 -0
  75. package/dist/form/InputAction.js +24 -0
  76. package/dist/form/Textarea.d.ts +17 -0
  77. package/dist/form/Textarea.js +102 -0
  78. package/dist/form/context.d.ts +12 -0
  79. package/dist/form/context.js +10 -0
  80. package/dist/form/controlProps.d.ts +23 -0
  81. package/dist/form/controlProps.js +20 -0
  82. package/dist/form/index.d.ts +7 -0
  83. package/dist/form/index.js +3 -0
  84. package/dist/form/types.d.ts +6 -0
  85. package/dist/form/types.js +1 -0
  86. package/dist/heatmap/index.d.ts +59 -0
  87. package/dist/heatmap/index.js +248 -0
  88. package/dist/hotkey/index.d.ts +37 -0
  89. package/dist/hotkey/index.js +130 -0
  90. package/dist/hotkey/model.d.ts +27 -0
  91. package/dist/hotkey/model.js +97 -0
  92. package/dist/hover-card/index.d.ts +33 -0
  93. package/dist/hover-card/index.js +141 -0
  94. package/dist/image-cropper/index.d.ts +82 -0
  95. package/dist/image-cropper/index.js +261 -0
  96. package/dist/image-cropper/model.d.ts +42 -0
  97. package/dist/image-cropper/model.js +94 -0
  98. package/dist/image-viewer/index.d.ts +79 -0
  99. package/dist/image-viewer/index.js +220 -0
  100. package/dist/image-viewer/model.d.ts +9 -0
  101. package/dist/image-viewer/model.js +18 -0
  102. package/dist/images/layers-2x.png +0 -0
  103. package/dist/images/layers.png +0 -0
  104. package/dist/images/marker-icon.png +0 -0
  105. package/dist/index.d.ts +75 -0
  106. package/dist/index.js +69 -0
  107. package/dist/input-number/index.d.ts +16 -0
  108. package/dist/input-number/index.js +77 -0
  109. package/dist/internal/InputActionContext.d.ts +1 -0
  110. package/dist/internal/InputActionContext.js +3 -0
  111. package/dist/internal/cache.d.ts +14 -0
  112. package/dist/internal/cache.js +54 -0
  113. package/dist/internal/line.d.ts +4 -0
  114. package/dist/internal/line.js +21 -0
  115. package/dist/internal/paper.d.ts +1 -0
  116. package/dist/internal/paper.js +65 -0
  117. package/dist/internal/random.d.ts +3 -0
  118. package/dist/internal/random.js +30 -0
  119. package/dist/internal/recovery.d.ts +1 -0
  120. package/dist/internal/recovery.js +52 -0
  121. package/dist/internal/renderTokens.d.ts +10 -0
  122. package/dist/internal/renderTokens.js +13 -0
  123. package/dist/internal/stroke.d.ts +24 -0
  124. package/dist/internal/stroke.js +144 -0
  125. package/dist/internal/surfaceRenderer.d.ts +41 -0
  126. package/dist/internal/surfaceRenderer.js +178 -0
  127. package/dist/internal/textQueue.d.ts +1 -0
  128. package/dist/internal/textQueue.js +37 -0
  129. package/dist/internal/textRenderer.d.ts +28 -0
  130. package/dist/internal/textRenderer.js +239 -0
  131. package/dist/internal/travelIcons.d.ts +24 -0
  132. package/dist/internal/travelIcons.js +14 -0
  133. package/dist/internal/useBrowserLayoutEffect.d.ts +2 -0
  134. package/dist/internal/useBrowserLayoutEffect.js +3 -0
  135. package/dist/internal/usePencilSurface.d.ts +3 -0
  136. package/dist/internal/usePencilSurface.js +116 -0
  137. package/dist/internal/useSelectionDrawing.d.ts +2 -0
  138. package/dist/internal/useSelectionDrawing.js +90 -0
  139. package/dist/internal/wash.d.ts +16 -0
  140. package/dist/internal/wash.js +149 -0
  141. package/dist/journey-map/MapScale.d.ts +4 -0
  142. package/dist/journey-map/MapScale.js +54 -0
  143. package/dist/journey-map/index.d.ts +39 -0
  144. package/dist/journey-map/index.js +197 -0
  145. package/dist/journey-map/terrain.d.ts +8 -0
  146. package/dist/journey-map/terrain.js +127 -0
  147. package/dist/keypad/index.d.ts +23 -0
  148. package/dist/keypad/index.js +63 -0
  149. package/dist/keypad/model.d.ts +20 -0
  150. package/dist/keypad/model.js +33 -0
  151. package/dist/layout/index.d.ts +63 -0
  152. package/dist/layout/index.js +62 -0
  153. package/dist/log/index.d.ts +45 -0
  154. package/dist/log/index.js +159 -0
  155. package/dist/log/model.d.ts +16 -0
  156. package/dist/log/model.js +13 -0
  157. package/dist/node-graph/index.d.ts +123 -0
  158. package/dist/node-graph/index.js +304 -0
  159. package/dist/number-ticker/index.d.ts +29 -0
  160. package/dist/number-ticker/index.js +132 -0
  161. package/dist/number-ticker/model.d.ts +8 -0
  162. package/dist/number-ticker/model.js +34 -0
  163. package/dist/overflow-list/index.d.ts +39 -0
  164. package/dist/overflow-list/index.js +230 -0
  165. package/dist/overflow-list/layout.d.ts +19 -0
  166. package/dist/overflow-list/layout.js +44 -0
  167. package/dist/overlay/Overlay.d.ts +65 -0
  168. package/dist/overlay/Overlay.js +77 -0
  169. package/dist/overlay/Popover.d.ts +43 -0
  170. package/dist/overlay/Popover.js +45 -0
  171. package/dist/overlay/index.d.ts +4 -0
  172. package/dist/overlay/index.js +2 -0
  173. package/dist/overlay/themeBridge.d.ts +8 -0
  174. package/dist/overlay/themeBridge.js +98 -0
  175. package/dist/pagination/index.d.ts +23 -0
  176. package/dist/pagination/index.js +41 -0
  177. package/dist/pagination/model.d.ts +4 -0
  178. package/dist/pagination/model.js +42 -0
  179. package/dist/pan-zoom/index.d.ts +71 -0
  180. package/dist/pan-zoom/index.js +175 -0
  181. package/dist/pencil.d.ts +10 -0
  182. package/dist/pencil.js +39 -0
  183. package/dist/radar/index.d.ts +54 -0
  184. package/dist/radar/index.js +220 -0
  185. package/dist/range-slider/index.d.ts +16 -0
  186. package/dist/range-slider/index.js +22 -0
  187. package/dist/rating/index.d.ts +28 -0
  188. package/dist/rating/index.js +127 -0
  189. package/dist/resizable/index.d.ts +32 -0
  190. package/dist/resizable/index.js +50 -0
  191. package/dist/scroll-area/index.d.ts +36 -0
  192. package/dist/scroll-area/index.js +44 -0
  193. package/dist/segmented-progress/SegmentedProgress.d.ts +17 -0
  194. package/dist/segmented-progress/SegmentedProgress.js +42 -0
  195. package/dist/select/MultipleSelect.d.ts +44 -0
  196. package/dist/select/MultipleSelect.js +124 -0
  197. package/dist/select/SearchableSelect.d.ts +45 -0
  198. package/dist/select/SearchableSelect.js +50 -0
  199. package/dist/select/Select.d.ts +109 -0
  200. package/dist/select/Select.js +205 -0
  201. package/dist/select/index.d.ts +4 -0
  202. package/dist/select/index.js +2 -0
  203. package/dist/selection/Checkbox.d.ts +9 -0
  204. package/dist/selection/Checkbox.js +37 -0
  205. package/dist/selection/RadioGroup.d.ts +23 -0
  206. package/dist/selection/RadioGroup.js +82 -0
  207. package/dist/selection/Switch.d.ts +8 -0
  208. package/dist/selection/Switch.js +36 -0
  209. package/dist/selection/index.d.ts +6 -0
  210. package/dist/selection/index.js +3 -0
  211. package/dist/selection/useNativeChecked.d.ts +13 -0
  212. package/dist/selection/useNativeChecked.js +27 -0
  213. package/dist/slider/index.d.ts +11 -0
  214. package/dist/slider/index.js +45 -0
  215. package/dist/slot/index.d.ts +24 -0
  216. package/dist/slot/index.js +29 -0
  217. package/dist/sortable/index.d.ts +50 -0
  218. package/dist/sortable/index.js +278 -0
  219. package/dist/sortable/model.d.ts +30 -0
  220. package/dist/sortable/model.js +64 -0
  221. package/dist/sparkline/index.d.ts +19 -0
  222. package/dist/sparkline/index.js +240 -0
  223. package/dist/sparkline/model.d.ts +16 -0
  224. package/dist/sparkline/model.js +28 -0
  225. package/dist/steps/index.d.ts +16 -0
  226. package/dist/steps/index.js +37 -0
  227. package/dist/styles.css +8226 -0
  228. package/dist/tabs/index.d.ts +22 -0
  229. package/dist/tabs/index.js +42 -0
  230. package/dist/tag-input/index.d.ts +69 -0
  231. package/dist/tag-input/index.js +325 -0
  232. package/dist/tag-input/model.d.ts +14 -0
  233. package/dist/tag-input/model.js +38 -0
  234. package/dist/time-picker/index.d.ts +15 -0
  235. package/dist/time-picker/index.js +168 -0
  236. package/dist/timeline/index.d.ts +14 -0
  237. package/dist/timeline/index.js +16 -0
  238. package/dist/toast/index.d.ts +27 -0
  239. package/dist/toast/index.js +36 -0
  240. package/dist/toggle-group/index.d.ts +17 -0
  241. package/dist/toggle-group/index.js +26 -0
  242. package/dist/tokens.css +356 -0
  243. package/dist/tooltip/Tooltip.d.ts +24 -0
  244. package/dist/tooltip/Tooltip.js +32 -0
  245. package/dist/tooltip/index.d.ts +2 -0
  246. package/dist/tooltip/index.js +1 -0
  247. package/dist/transfer/index.d.ts +64 -0
  248. package/dist/transfer/index.js +174 -0
  249. package/dist/transfer/model.d.ts +12 -0
  250. package/dist/transfer/model.js +33 -0
  251. package/dist/tree/index.d.ts +39 -0
  252. package/dist/tree/index.js +36 -0
  253. package/dist/tree-select/index.d.ts +59 -0
  254. package/dist/tree-select/index.js +142 -0
  255. package/dist/tree-select/model.d.ts +31 -0
  256. package/dist/tree-select/model.js +135 -0
  257. package/dist/typewriter/index.d.ts +21 -0
  258. package/dist/typewriter/index.js +101 -0
  259. package/dist/typewriter/model.d.ts +7 -0
  260. package/dist/typewriter/model.js +45 -0
  261. package/dist/upload/index.d.ts +59 -0
  262. package/dist/upload/index.js +230 -0
  263. package/dist/virtual-list/index.d.ts +44 -0
  264. package/dist/virtual-list/index.js +128 -0
  265. package/dist/virtual-list/model.d.ts +7 -0
  266. package/dist/virtual-list/model.js +27 -0
  267. 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,7 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function InputAction({ children, disabled, seed, position }: {
3
+ children: ReactNode;
4
+ disabled: boolean;
5
+ seed: string;
6
+ position?: 'start' | 'end';
7
+ }): import("react").JSX.Element;
@@ -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
+ });