@inkra/react 0.6.0 → 0.9.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 (72) hide show
  1. package/README.md +166 -1
  2. package/dist/Button.js +1 -1
  3. package/dist/PencilIcon.d.ts +14 -1
  4. package/dist/PencilIcon.js +88 -52
  5. package/dist/PencilText.js +8 -0
  6. package/dist/form/Textarea.d.ts +4 -0
  7. package/dist/form/Textarea.js +14 -8
  8. package/dist/form/index.d.ts +1 -1
  9. package/dist/form/textareaSizing.d.ts +29 -0
  10. package/dist/form/textareaSizing.js +153 -0
  11. package/dist/guide/index.d.ts +77 -0
  12. package/dist/guide/index.js +379 -0
  13. package/dist/i18n/catalog.d.ts +1 -1
  14. package/dist/i18n/catalog.js +15 -0
  15. package/dist/image-viewer/index.d.ts +2 -0
  16. package/dist/image-viewer/index.js +28 -28
  17. package/dist/index.d.ts +6 -0
  18. package/dist/index.js +5 -0
  19. package/dist/infinite-scroll/gesture.d.ts +19 -0
  20. package/dist/infinite-scroll/gesture.js +218 -0
  21. package/dist/infinite-scroll/index.d.ts +55 -0
  22. package/dist/infinite-scroll/index.js +543 -0
  23. package/dist/infinite-scroll/model.d.ts +33 -0
  24. package/dist/infinite-scroll/model.js +168 -0
  25. package/dist/internal/iconFill.d.ts +16 -0
  26. package/dist/internal/iconFill.js +275 -0
  27. package/dist/inventory-grid/index.d.ts +35 -0
  28. package/dist/inventory-grid/index.js +215 -0
  29. package/dist/inventory-grid/model.d.ts +56 -0
  30. package/dist/inventory-grid/model.js +93 -0
  31. package/dist/inventory-grid/outline.d.ts +9 -0
  32. package/dist/inventory-grid/outline.js +54 -0
  33. package/dist/layout/GridBoard.d.ts +22 -0
  34. package/dist/layout/GridBoard.js +113 -0
  35. package/dist/layout/GridResizable.d.ts +31 -0
  36. package/dist/layout/GridResizable.js +35 -0
  37. package/dist/layout/index.d.ts +19 -4
  38. package/dist/layout/index.js +45 -9
  39. package/dist/layout/swipeNavigation.d.ts +15 -0
  40. package/dist/layout/swipeNavigation.js +121 -0
  41. package/dist/loading/index.d.ts +58 -0
  42. package/dist/loading/index.js +188 -0
  43. package/dist/loading/locales.d.ts +15 -0
  44. package/dist/loading/locales.js +34 -0
  45. package/dist/loading/model.d.ts +6 -0
  46. package/dist/loading/model.js +29 -0
  47. package/dist/masonry/index.d.ts +25 -0
  48. package/dist/masonry/index.js +227 -0
  49. package/dist/masonry/layout.d.ts +35 -0
  50. package/dist/masonry/layout.js +67 -0
  51. package/dist/number-ticker/RollingValue.d.ts +6 -0
  52. package/dist/number-ticker/RollingValue.js +10 -0
  53. package/dist/number-ticker/index.d.ts +4 -0
  54. package/dist/number-ticker/index.js +55 -7
  55. package/dist/number-ticker/rolling.d.ts +35 -0
  56. package/dist/number-ticker/rolling.js +87 -0
  57. package/dist/pagination/index.d.ts +17 -0
  58. package/dist/pagination/index.js +30 -6
  59. package/dist/styles/base.css +9 -1
  60. package/dist/styles/form.css +10 -0
  61. package/dist/styles/guide.css +202 -0
  62. package/dist/styles/image-viewer.css +20 -0
  63. package/dist/styles/infinite-scroll.css +171 -0
  64. package/dist/styles/inventory-grid.css +43 -0
  65. package/dist/styles/layout.css +85 -0
  66. package/dist/styles/loading.css +348 -0
  67. package/dist/styles/masonry.css +68 -0
  68. package/dist/styles/number-ticker.css +46 -0
  69. package/dist/styles/pagination.css +10 -0
  70. package/dist/styles.css +1162 -146
  71. package/dist/toast/manager.d.ts +4 -4
  72. package/package.json +33 -1
package/README.md CHANGED
@@ -17,6 +17,52 @@ Inkra components require a React client boundary. Next.js and React Server Compo
17
17
 
18
18
  ## Usage
19
19
 
20
+ ### Guided And Asynchronous Experiences
21
+
22
+ `Guide.Root`, `Guide.Trigger` and `Guide.Step` compose a targeted walkthrough.
23
+ `showSkip` exposes skipping; `onSkip` is separate from `onFinish`, so leaving
24
+ early never reports completion. Controlled `open` and `step`, target resolution,
25
+ focus restoration and localized labels are available.
26
+
27
+ `Loading` supports `inline`, `region` and `fullscreen` carriers with six layout
28
+ presets: `brand-center`, `scene-bottom`, `intel`, `staged`, `edge` and `skeleton`.
29
+ Supply real progress or `null` for indeterminate progress. The application owns
30
+ `status` and the cancel/retry/continue actions. Fullscreen blocking and scroll
31
+ locking can be configured independently; the edge preset is non-blocking by
32
+ default. Backgrounds, logos, stages, six logical content slots and rotating
33
+ ReactNode tips share the same component. Tips support manual navigation, pause,
34
+ controlled indices and sequential or non-repeating random rotation.
35
+
36
+ `InfiniteScroll` loads at `top`, `bottom`, `left` or `right` boundaries. Configure
37
+ automatic boundary loading and touch pull-and-release separately per direction;
38
+ buttons remain available for keyboard use. `onLoad` receives an abort signal,
39
+ direction and source. Stable item identities support anchored prepends, while
40
+ errors, retry, exhausted directions and in-flight requests remain explicit.
41
+
42
+ `Masonry` arranges measured items in shortest-column or sequential order while
43
+ retaining DOM order. It supports adaptive or fixed columns, independent gaps,
44
+ RTL and dynamic item sizes. Supply unique stable keys, image alternatives and
45
+ your own data fetching, optionally through `InfiniteScroll`:
46
+
47
+ ```tsx
48
+ <InfiniteScroll
49
+ directions="bottom"
50
+ hasMore={hasMore}
51
+ onLoad={({ signal }) => fetchNextPage(signal)}
52
+ style={{ maxHeight: 640, overflow: 'auto' }}
53
+ >
54
+ <Masonry
55
+ items={items}
56
+ getItemKey={item => item.id}
57
+ minColumnWidth={220}
58
+ renderItem={({ item }) => <article>{item.content}</article>}
59
+ />
60
+ </InfiniteScroll>
61
+ ```
62
+
63
+ These APIs are included in this source candidate. Local browser checks are not
64
+ real-device certification or a claim that this candidate has been published.
65
+
20
66
  Import the component styles once near the application entry point:
21
67
 
22
68
  ```tsx
@@ -176,6 +222,39 @@ All standard `button` props, including `disabled`, `name`, `value`, `form`, `ari
176
222
 
177
223
  `Input` defaults to an underline appearance, with explicit `outline`, `soft` and `ghost` variants. It supports `startIcon`, `endIcon`, `prefix`, `suffix`, `clearable`, `loading`, `startAction`, `endAction`, `fullWidth` and logical `textAlign`. Use `onValueChange` for edits and clearing; native `onChange` does not receive fabricated clear events. `Field` connects labels, descriptions and errors.
178
224
 
225
+ `Textarea` respects an explicit native `rows`, including `rows={1}`. Omit both
226
+ `rows` and `autoSize` to retain the existing sm/md/lg minimum heights. Enable
227
+ `autoSize` to grow and shrink with content, including lines wrapped by the actual
228
+ container width:
229
+
230
+ ```tsx
231
+ import { Textarea } from '@inkra/react/form';
232
+ import type { TextareaAutoSizeOptions } from '@inkra/react/form';
233
+
234
+ <Textarea rows={1} aria-label="Single-line initial height" />
235
+ <Textarea autoSize={{ minRows: 1, maxRows: 6 }} aria-label="Note" />
236
+ <Textarea autoSize rows={2} aria-label="At least two rows, unlimited growth" />
237
+ ```
238
+
239
+ `autoSize` accepts `boolean | TextareaAutoSizeOptions` and defaults to `false`.
240
+ Its `minRows` falls back to `rows`, then 1; omit `maxRows` for unlimited growth.
241
+ Both limits accept positive safe integers; invalid values use these defaults and
242
+ `maxRows` below `minRows` is raised to `minRows`. The limit counts visible text
243
+ rows, including soft wraps, rather than newline characters or input length.
244
+ Above the maximum, the native editor scrolls internally; deleting text shrinks it.
245
+ Controlled value updates, native form reset, width and font changes remeasure
246
+ without emitting change events or replacing the native control. Empty editors
247
+ size from their minimum rows rather than their placeholder text.
248
+
249
+ Automatic sizing owns `height`, `minHeight`, `maxHeight` and vertical overflow;
250
+ it takes precedence over inline height and `resize="vertical"`, hiding the manual
251
+ handle. With automatic sizing disabled, manual resizing keeps its existing
252
+ behavior (an explicit `rows` supplies its minimum). Row limits apply only to the
253
+ editor: the character counter retains its separate footer, even while its
254
+ automatic indicator is hidden. On the server, native `rows` provides the initial
255
+ height; existing multi-line content is measured after hydration. Avoid CSS
256
+ `!important` height constraints when enabling automatic sizing.
257
+
179
258
  `InputNumber` places decrease and increase buttons on either side of a centered numeric input, with no clear button. Keyboard deletion produces `null`; decimal stepping and out-of-range validation remain native. `Slider` supports single/range values, horizontal/vertical layout and `standard` or `inset` appearance.
180
259
 
181
260
  `InputNumber` supports Shift-click and Shift-arrow increments of 10 by default.
@@ -328,7 +407,7 @@ working examples, live property tabs and per-component API/accessibility notes.
328
407
  | Drawer | Dialog parts plus section layout, four `Content.direction` values and token/custom `size`; focus trap, Escape and return focus, no swipe gesture API |
329
408
  | DatePicker | Calendar/Popover composition; single/range local dates, controlled/default values, clear, form fields and localized labels; no UTC conversion |
330
409
  | TimePicker | Native time input with clear, disabled/readOnly, min/max/step and optional seconds; browser/OS controls the native presentation |
331
- | Pagination | Page count/current page, boundary actions and ellipses; buttons change local state rather than pretending to navigate URLs |
410
+ | Pagination | Page count/current page, boundary actions and ellipses; optional page-size selection, button variants, logical alignment and wrapping |
332
411
  | Empty | Optional `Root`, `Icon`, `Title`, `Description`, `Actions`; no mandatory card or live-region role |
333
412
  | ToggleGroup | `Root` and `Item`; array values for single/multiple toggles, roving focus and `aria-pressed`, not tabs |
334
413
  | Command | cmdk-backed search, grouped commands, empty/disabled states and optional modal composition |
@@ -338,6 +417,49 @@ working examples, live property tabs and per-component API/accessibility notes.
338
417
  | Steps | Ordered `items`, `current`, orientation and optional `onStepChange`; status and disabled steps |
339
418
  | Timeline | Ordered `items` with date/title/description/content, semantic `dateTime` and horizontal/vertical orientation |
340
419
 
420
+ ### Pagination page sizes
421
+
422
+ ```tsx
423
+ const [page, setPage] = useState(1);
424
+ const [pageSize, setPageSize] = useState(20);
425
+ <Pagination totalPages={Math.ceil(itemCount / pageSize)}
426
+ page={page} onPageChange={setPage}
427
+ pageSize={pageSize} onPageSizeChange={setPageSize}
428
+ pageSizeOptions={[10, 20, 50]} hideOnSinglePage
429
+ variant="outline" align="end" wrap />
430
+ ```
431
+
432
+ The caller computes `totalPages` and slices or fetches its data. `pageSize` is
433
+ controlled; `defaultPageSize` initializes an uncontrolled size (default `10`).
434
+ Only positive safe integers are accepted. Invalid sizes fall back to `10`;
435
+ invalid options are ignored, duplicates removed and options sorted numerically.
436
+ The current size is retained in a nonempty option list. Omit `pageSizeOptions`
437
+ or provide no valid options to hide the selector.
438
+
439
+ Selecting a different size first calls `onPageSizeChange(size)`, then requests
440
+ `onPageChange(1)` when the current page exceeds one. Each uncontrolled state
441
+ updates independently; controlled state changes only when the parent accepts
442
+ the request. Rendering, changing totals or options, and clamping do not emit
443
+ user callbacks. `hideOnSinglePage` hides page controls for zero or one page while
444
+ retaining the size selector.
445
+
446
+ `variant` uses the Button variants (default `ghost`). `align` accepts logical
447
+ `start | center | end` (default `start`); `wrap` defaults to `false` and retains
448
+ local horizontal scrolling. The size selector reuses Select, inherits disabled
449
+ state and direction, and uses InkraProvider labels. Override
450
+ `labels.pageSize` (accessible name) and `labels.pageSizeOption(size)` (visible
451
+ option text) for application-specific copy. `simple`, `renderPage` and
452
+ `renderSummary` compose with these options.
453
+
454
+ 分页容量由调用方计算总页数并切分或请求数据。`pageSize` 为受控值,
455
+ `defaultPageSize` 为非受控初值,默认 10;只接收正安全整数。非法容量回退为
456
+ 10,非法选项忽略,选项去重并排序,有效列表保留当前容量。省略或提供空的有效
457
+ 选项列表时不显示选择器。切换容量先通知 `onPageSizeChange`,当前页大于 1 时
458
+ 再请求 `onPageChange(1)`;受控值由父组件决定是否接受。渲染、总页数及选项变化
459
+ 不触发交互回调。`hideOnSinglePage` 隐藏翻页按钮但保留容量选择器;`align`
460
+ 按逻辑方向对齐,`wrap` 允许换行,默认仍为局部横滚。容量选择复用 Select,
461
+ 继承禁用、方向和语言设置,可用 `labels.pageSize/pageSizeOption` 覆盖文案。
462
+
341
463
  ## Utility Components (Unreleased)
342
464
 
343
465
  `Slot` is a fixed square visual container for arbitrary icons or content. Static
@@ -359,6 +481,22 @@ motion and SSR show deterministic final values. Intermediate frames are visual;
359
481
  the polite announcement updates only at the final value. Use `getValueLabel`
360
482
  when units or custom rendering need a complete accessible phrase.
361
483
 
484
+ Use `animation="roll"` for vertical, place-aligned digit reels. Integers align
485
+ from the right and fractions from the decimal point; carry, multiplication,
486
+ shrinking values and sign changes expand or collapse the affected columns.
487
+ Grouping, signs, currency and compact units remain formatted text, not digit
488
+ reels. `rollDirection="auto"` rolls up for increasing values and down for
489
+ decreasing values; `up` or `down` fixes the direction. `duration` and `easing`
490
+ apply to both modes. The default `animation="count"` retains value interpolation.
491
+ `renderValue` takes precedence over the built-in reels. Changing the formatting
492
+ or animation mode settles immediately, as do disabled/reduced motion, invalid
493
+ durations, and transitions with more than 128 formatted tokens. Exact strings
494
+ and bigint values retain their original precision in either mode.
495
+
496
+ ```tsx
497
+ <NumberTicker value={amount} animation="roll" rollDirection="auto" duration={650} />
498
+ ```
499
+
362
500
  Set `compact` to follow `locale` (Chinese units for `zh`, English units otherwise),
363
501
  or use `compact="en"` / `compact="zh"` to select a unit table explicitly.
364
502
  English uses this library's short-scale suffix convention: K, M, B, T, Qa, Qi,
@@ -470,6 +608,33 @@ The matching inherited tokens are `--inkra-icon-stroke-width`,
470
608
  and `--inkra-icon-break-count`. Finite explicit props override those tokens.
471
609
  See `docs/pencil-contract.md` in the workspace for the material contract.
472
610
 
611
+ Icons support `variant="outline"` (default) and `variant="filled"`. Filled icons
612
+ keep holes and internal marks transparent, join overlapping silhouettes and do
613
+ not close open arrows or checks into polygons. Set `fillColor` independently of
614
+ the contour's CSS `color`; it defaults to `currentColor` and accepts CSS variables.
615
+ The silhouette stays continuous while stroke controls affect its exterior.
616
+
617
+ ```tsx
618
+ <PencilIcon paths={folderPaths} variant="filled" fillColor="var(--folder-color)"
619
+ sourceSize={24} size={32} style={{ color: 'var(--inkra-ink)' }} />
620
+ ```
621
+
622
+ Supply `filledPaths` when a shape needs a dedicated filled silhouette. Each path
623
+ can use `mode: 'fill'` with an optional `color` for independently colored regions,
624
+ or `mode: 'cutout'` for transparent details. Closed cutouts remove an area; open
625
+ cutouts remove a stroke without implicitly closing it. Explicit fills accept
626
+ `fillRule: 'nonzero' | 'evenodd'` (default `nonzero`). These authored regions do
627
+ not add their own contour stroke. For lighter colored-pencil regions, use
628
+ `mode: 'wash'` with the same optional `color` and `fillRule`: paper remains visible
629
+ between pigment strokes, with a light pencil edge in the icon's CSS `color`.
630
+ Dense `fill` regions are unchanged.
631
+ Explicit `mode: 'stroke'` keeps a line symbol
632
+ even when its path closes; omitted mode detects filled silhouettes automatically.
633
+ An empty `filledPaths` intentionally hides the
634
+ base shape in filled mode. `badge` accepts the same three props and inherits the
635
+ base variant and fill color unless overridden. Intensity applies once to the
636
+ completed image, including its fills, exterior strokes and badge.
637
+
473
638
  `PencilLine` is an aria-hidden canvas decoration using the same pressure, grain, breaks, and repeated-pass renderer as surfaces and icons. It defaults to horizontal and supports a native vertical stroke without rotating a horizontal bitmap:
474
639
 
475
640
  ```tsx
package/dist/Button.js CHANGED
@@ -36,7 +36,7 @@ function ButtonVisual({ children, endIcon, loading, loadingPosition, loadingIndi
36
36
  const label = typeof children === 'string'
37
37
  ? _jsx(PencilText, { seed: `${seed}-label`, children: children })
38
38
  : children == null ? null : _jsx("span", { children: children });
39
- return (_jsxs(_Fragment, { children: [_jsx(PencilSurface, { as: "span", className: "inkra-button__surface", clipContent: sharedOutline, radius: sharedOutline ? 0 : undefined, seed: seed, variant: inputAction ? 'ghost' : surfaceVariant(variant), children: _jsxs("span", { className: "inkra-button__content", children: [loading && loadingPosition === 'center' ? indicator : null, iconSlot('start', startIcon), label == null ? null : _jsxs("span", { className: "inkra-button__label", children: [label, variant === 'link' ? _jsx(PencilLine, { className: "inkra-button__underline", seed: `${seed}-underline`, weight: 0.9 }) : null] }), iconSlot('end', endIcon)] }) }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-button__focus-ring", variant: "outline", seed: `${seed}-focus` }), group === null ? null : _jsx(PencilLine, { className: "inkra-button-group__divider", orientation: group.orientation === 'horizontal' ? 'vertical' : 'horizontal', seed: `${seed}-group-divider` })] }));
39
+ return (_jsxs(_Fragment, { children: [_jsx(PencilSurface, { as: "span", className: "inkra-button__surface", clipContent: sharedOutline || variant === 'ghost', radius: sharedOutline ? 0 : undefined, seed: seed, variant: inputAction ? 'ghost' : surfaceVariant(variant), children: _jsxs("span", { className: "inkra-button__content", children: [loading && loadingPosition === 'center' ? indicator : null, iconSlot('start', startIcon), label == null ? null : _jsxs("span", { className: "inkra-button__label", children: [label, variant === 'link' ? _jsx(PencilLine, { className: "inkra-button__underline", seed: `${seed}-underline`, weight: 0.9 }) : null] }), iconSlot('end', endIcon)] }) }), _jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-button__focus-ring", variant: "outline", seed: `${seed}-focus` }), group === null ? null : _jsx(PencilLine, { className: "inkra-button-group__divider", orientation: group.orientation === 'horizontal' ? 'vertical' : 'horizontal', seed: `${seed}-group-divider` })] }));
40
40
  }
41
41
  export const Button = forwardRef(function Button({ children, 'aria-label': ariaLabel, className, disabled = false, endIcon, loading = false, loadingPosition = 'center', loadingIndicator, fullWidth = false, align = 'center', seed, size: sizeProp, startIcon, type = 'button', variant: variantProp, ...props }, ref) {
42
42
  const id = useId();
@@ -1,10 +1,17 @@
1
1
  import type { CanvasHTMLAttributes } from 'react';
2
2
  export interface PencilIconPath {
3
3
  d: string;
4
- mode?: 'stroke' | 'wash' | 'scuff';
4
+ /** Filled icons: stroke stays a line, wash exposes paper, fill is dense; omitted mode detects closed silhouettes. */
5
+ mode?: 'stroke' | 'wash' | 'scuff' | 'fill' | 'cutout';
6
+ /** Optional pigment for an explicit fill region, including CSS custom properties. */
7
+ color?: string;
8
+ fillRule?: CanvasFillRule;
5
9
  }
6
10
  export interface PencilIconBadge {
7
11
  paths: readonly PencilIconPath[];
12
+ filledPaths?: readonly PencilIconPath[];
13
+ variant?: 'outline' | 'filled';
14
+ fillColor?: string;
8
15
  sourceSize?: number;
9
16
  position?: 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end';
10
17
  /** Relative canvas size, clamped to 0.35-0.65. Defaults to 0.55. */
@@ -12,6 +19,12 @@ export interface PencilIconBadge {
12
19
  }
13
20
  export interface PencilIconProps extends Omit<CanvasHTMLAttributes<HTMLCanvasElement>, 'children' | 'height' | 'width'> {
14
21
  paths: readonly PencilIconPath[];
22
+ /** Filled icons retain open symbols and use negative space for interior details. */
23
+ variant?: 'outline' | 'filled';
24
+ /** Optional authored geometry for shapes whose filled silhouette differs from the outline. */
25
+ filledPaths?: readonly PencilIconPath[];
26
+ /** Body pigment, independent of the CSS color used for the contour. Defaults to currentColor. */
27
+ fillColor?: string;
15
28
  badge?: PencilIconBadge;
16
29
  seed?: string | number;
17
30
  size?: number;
@@ -6,6 +6,7 @@ import { seedToNumber } from './internal/random.js';
6
6
  import { pencilStroke } from './internal/stroke.js';
7
7
  import { maxStrokeRoughness, pencilRenderDefaults, readRenderToken } from './internal/renderTokens.js';
8
8
  import { cachedPathParts } from './internal/iconGeometry.js';
9
+ import { paintFilledIcon } from './internal/iconFill.js';
9
10
  const imageCache = new BoundedCache(128, 24 * 1024 * 1024);
10
11
  function drawFallbackPath(context, data, color, mode, weightScale = 1) {
11
12
  try {
@@ -47,56 +48,62 @@ function renderIcon(paths, options) {
47
48
  const offset = (40 - sourceSize) / 2;
48
49
  context.translate(offset, offset);
49
50
  let sampledPoints = 0;
50
- paths.forEach((path, index) => {
51
- if (path.d.length > 65536)
52
- return;
53
- const mode = path.mode ?? 'stroke';
54
- const pathSeed = seed + index * 137;
55
- if (mode === 'wash') {
51
+ if (options.variant === 'filled') {
52
+ paintFilledIcon(context, paths, { color, fillColor: options.fillColor ?? color, sourceSize, seed, weightScale, roughnessScale, breaks, breakCount, pressureVariation, coordinateScale });
53
+ }
54
+ else
55
+ paths.forEach((path, index) => {
56
+ if (path.d.length > 65536)
57
+ return;
58
+ const mode = path.mode ?? 'stroke';
59
+ if (mode === 'cutout')
60
+ return;
61
+ const pathSeed = seed + index * 137;
62
+ if (mode === 'wash' || mode === 'fill') {
63
+ try {
64
+ context.save();
65
+ context.clip(new Path2D(path.d));
66
+ context.globalAlpha = active ? 0.76 : 0.34;
67
+ for (let y = sourceSize * 0.18; y < sourceSize; y += 1.6) {
68
+ pencilStroke(context, [[sourceSize * 0.15, y + 3], [sourceSize * 0.85, y - 3]], color, 1.1 * weightScale / (pencilRenderDefaults.strokeWidth / 1.2), pathSeed + Math.round(y * 17), 0.25, 2, false, { variation: Math.min(1, 0.85 * variationScale), fitPressure: true, breaks, breakCount, breakScale: coordinateScale, continuousBreaks: true, grain: 0.7, gain: 2.2, step: 0.35 });
69
+ }
70
+ context.restore();
71
+ }
72
+ catch {
73
+ drawFallbackPath(context, path.d, color, mode, weightScale);
74
+ }
75
+ return;
76
+ }
56
77
  try {
57
- context.save();
58
- context.clip(new Path2D(path.d));
59
- context.globalAlpha = active ? 0.76 : 0.34;
60
- for (let y = sourceSize * 0.18; y < sourceSize; y += 1.6) {
61
- pencilStroke(context, [[sourceSize * 0.15, y + 3], [sourceSize * 0.85, y - 3]], color, 1.1 * weightScale / (pencilRenderDefaults.strokeWidth / 1.2), pathSeed + Math.round(y * 17), 0.25, 2, false, { variation: Math.min(1, 0.85 * variationScale), fitPressure: true, breaks, breakCount, breakScale: coordinateScale, continuousBreaks: true, grain: 0.7, gain: 2.2, step: 0.35 });
78
+ if (sampledPoints >= 65536) {
79
+ drawFallbackPath(context, path.d, color, mode, weightScale);
80
+ return;
81
+ }
82
+ const parts = cachedPathParts(path.d);
83
+ sampledPoints += parts.reduce((total, part) => total + part.length, 0);
84
+ if (sampledPoints > 65536) {
85
+ drawFallbackPath(context, path.d, color, mode, weightScale);
86
+ return;
87
+ }
88
+ for (const points of parts) {
89
+ pencilStroke(context, points, color, (mode === 'scuff' ? 0.55 : 1.65) * weightScale, pathSeed, 0.3 * roughnessScale, mode === 'scuff' ? 1 : 3, false, {
90
+ variation: pressureVariation,
91
+ fitPressure: true,
92
+ breaks,
93
+ breakCount,
94
+ breakScale: coordinateScale,
95
+ continuousBreaks: true,
96
+ grain: 0.68,
97
+ gain: mode === 'scuff' ? 1.5 : 3,
98
+ step: 0.35,
99
+ taperLength: 0.65,
100
+ });
62
101
  }
63
- context.restore();
64
102
  }
65
103
  catch {
66
104
  drawFallbackPath(context, path.d, color, mode, weightScale);
67
105
  }
68
- return;
69
- }
70
- try {
71
- if (sampledPoints >= 65536) {
72
- drawFallbackPath(context, path.d, color, mode, weightScale);
73
- return;
74
- }
75
- const parts = cachedPathParts(path.d);
76
- sampledPoints += parts.reduce((total, part) => total + part.length, 0);
77
- if (sampledPoints > 65536) {
78
- drawFallbackPath(context, path.d, color, mode, weightScale);
79
- return;
80
- }
81
- for (const points of parts) {
82
- pencilStroke(context, points, color, (mode === 'scuff' ? 0.55 : 1.65) * weightScale, pathSeed, 0.3 * roughnessScale, mode === 'scuff' ? 1 : 3, false, {
83
- variation: pressureVariation,
84
- fitPressure: true,
85
- breaks,
86
- breakCount,
87
- breakScale: coordinateScale,
88
- continuousBreaks: true,
89
- grain: 0.68,
90
- gain: mode === 'scuff' ? 1.5 : 3,
91
- step: 0.35,
92
- taperLength: 0.65,
93
- });
94
- }
95
- }
96
- catch {
97
- drawFallbackPath(context, path.d, color, mode, weightScale);
98
- }
99
- });
106
+ });
100
107
  if (active) {
101
108
  pencilStroke(context, [[sourceSize * 0.2, sourceSize - 2], [sourceSize * 0.5, sourceSize - 1.5], [sourceSize * 0.8, sourceSize - 2.2]], color, 1.1 * weightScale, seed + 977, 0.25 * roughnessScale, 3, false, { variation: 0.8 * variationScale, fitPressure: true, breaks, breakCount, breakScale: coordinateScale, continuousBreaks: true, grain: 0.7, gain: 2.8, step: 0.35 });
102
109
  }
@@ -109,8 +116,10 @@ function renderIcon(paths, options) {
109
116
  const extent = 40 * scale;
110
117
  const x = onLeft ? 0 : 40 - extent;
111
118
  const y = position.startsWith('top') ? 0 : 40 - extent;
112
- const badgeImage = renderIcon(badge.paths, {
119
+ const badgeVariant = badge.variant ?? options.variant;
120
+ const badgeImage = renderIcon(badgeVariant === 'filled' ? badge.filledPaths ?? badge.paths : badge.paths, {
113
121
  seed: seed + 1543, size: size * scale, sourceSize: badgeSourceSize, active: false, color, dpr,
122
+ variant: badgeVariant, fillColor: badge.fillColor ?? options.fillColor,
114
123
  weightScale: weightScale / scale, roughnessScale, breaks, breakCount, pressureVariation,
115
124
  });
116
125
  const mask = document.createElement('canvas');
@@ -140,17 +149,18 @@ function renderIcon(paths, options) {
140
149
  }
141
150
  return canvas;
142
151
  }
143
- export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge, className = '', decorative = true, paths, seed, size = 40, sourceSize = 32, strokeWidth, intensity, breaks, breakCount, pressureVariation, ...props }, forwardedRef) {
152
+ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge, className = '', decorative = true, paths, variant = 'outline', filledPaths, fillColor, seed, size = 40, sourceSize = 32, strokeWidth, intensity, breaks, breakCount, pressureVariation, ...props }, forwardedRef) {
144
153
  const canvasRef = useRef(null);
145
154
  const lastPaintRef = useRef('');
146
155
  useImperativeHandle(forwardedRef, () => canvasRef.current);
147
156
  // Compare values, not caller object identity. Serializing each render also
148
157
  // detects in-place edits; the effect receives a stable snapshot of that value.
149
- const geometryKey = JSON.stringify(paths);
158
+ const originalGeometryKey = JSON.stringify(paths);
159
+ const geometryKey = variant === 'filled' && filledPaths ? JSON.stringify(filledPaths) : originalGeometryKey;
150
160
  const badgeKey = badge ? JSON.stringify(badge) : '';
151
161
  const stablePaths = useMemo(() => JSON.parse(geometryKey), [geometryKey]);
152
162
  const stableBadge = useMemo(() => badgeKey ? JSON.parse(badgeKey) : undefined, [badgeKey]);
153
- const resolvedSeed = seedToNumber(seed ?? geometryKey);
163
+ const resolvedSeed = seedToNumber(seed ?? originalGeometryKey);
154
164
  useEffect(() => {
155
165
  const canvas = canvasRef.current;
156
166
  if (!canvas)
@@ -162,6 +172,28 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
162
172
  const dpr = effectiveDpr(3);
163
173
  const computed = getComputedStyle(canvas);
164
174
  const color = computed.color;
175
+ const resolveColor = (value) => {
176
+ if (!value || value === 'currentColor')
177
+ return color;
178
+ const probe = document.createElement('span');
179
+ probe.style.color = value;
180
+ // Canvas fallback children inherit its own color and custom properties, not just its parent's.
181
+ canvas.appendChild(probe);
182
+ try {
183
+ return getComputedStyle(probe).color || color;
184
+ }
185
+ finally {
186
+ probe.remove();
187
+ }
188
+ };
189
+ const resolvedFill = variant === 'filled' ? resolveColor(fillColor) : color;
190
+ const resolvePaths = (items) => items.map(path => path.color ? { ...path, color: resolveColor(path.color) } : path);
191
+ const renderedPaths = variant === 'filled' ? resolvePaths(stablePaths) : stablePaths;
192
+ const renderedBadge = stableBadge ? { ...stableBadge,
193
+ fillColor: resolveColor(stableBadge.fillColor ?? fillColor),
194
+ paths: resolvePaths(stableBadge.paths),
195
+ filledPaths: stableBadge.filledPaths ? resolvePaths(stableBadge.filledPaths) : undefined,
196
+ } : undefined;
165
197
  const rtl = computed.direction === 'rtl';
166
198
  const iconWeight = readRenderToken(computed, '--inkra-icon-stroke-width', pencilRenderDefaults.iconStrokeWidth, 0.3, 3);
167
199
  const weightScale = (typeof strokeWidth === 'number' && Number.isFinite(strokeWidth) ? Math.min(3, Math.max(0.3, strokeWidth)) : iconWeight) / 1.2;
@@ -180,9 +212,10 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
180
212
  : readRenderToken(computed, '--inkra-icon-break-count', pencilRenderDefaults.iconBreakCount, 0, 12));
181
213
  const key = JSON.stringify([
182
214
  PENCIL_RENDERER_VERSION,
183
- 'icon-path-pressure-gaps-v4',
215
+ 'icon-path-paper-wash-v8',
184
216
  geometryKey,
185
217
  badgeKey,
218
+ variant, resolvedFill, renderedPaths, renderedBadge,
186
219
  rtl,
187
220
  resolvedSeed,
188
221
  size,
@@ -202,7 +235,7 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
202
235
  image = undefined;
203
236
  }
204
237
  if (!image) {
205
- image = renderIcon(stablePaths, { seed: resolvedSeed, size, sourceSize, active, color, dpr, badge: stableBadge, rtl, weightScale, roughnessScale, breaks: strokeBreaks, breakCount: strokeBreakCount, pressureVariation: strokeVariation });
238
+ image = renderIcon(renderedPaths, { seed: resolvedSeed, size, sourceSize, active, color, dpr, variant, fillColor: resolvedFill, badge: renderedBadge, rtl, weightScale, roughnessScale, breaks: strokeBreaks, breakCount: strokeBreakCount, pressureVariation: strokeVariation });
206
239
  imageCache.set(key, image, canvasByteCost(image));
207
240
  }
208
241
  canvas.width = image.width;
@@ -236,7 +269,9 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
236
269
  inheritedColor.observe(element, { attributes: true, attributeFilter: ['style', 'class', 'dir', 'data-theme', 'data-inkra-theme'] });
237
270
  }
238
271
  const colorScheme = window.matchMedia('(prefers-color-scheme: dark)');
272
+ const forcedColors = window.matchMedia('(forced-colors: active)');
239
273
  colorScheme.addEventListener('change', paint);
274
+ forcedColors.addEventListener('change', paint);
240
275
  const stopRecovery = observeCanvasRecovery(() => { lastPaintRef.current = ''; paint(); });
241
276
  paint();
242
277
  return () => {
@@ -245,10 +280,11 @@ export const PencilIcon = forwardRef(function PencilIcon({ active = false, badge
245
280
  stopRecovery();
246
281
  inheritedColor.disconnect();
247
282
  colorScheme.removeEventListener('change', paint);
283
+ forcedColors.removeEventListener('change', paint);
248
284
  };
249
- }, [active, stableBadge, badgeKey, geometryKey, stablePaths, resolvedSeed, size, sourceSize, strokeWidth, intensity, breaks, breakCount, pressureVariation]);
285
+ }, [active, stableBadge, badgeKey, geometryKey, stablePaths, resolvedSeed, size, sourceSize, strokeWidth, intensity, breaks, breakCount, pressureVariation, variant, fillColor]);
250
286
  const accessibilityProps = decorative
251
287
  ? { 'aria-hidden': true }
252
288
  : { role: props.role ?? 'img' };
253
- return (_jsx("canvas", { ...props, ...accessibilityProps, className: ['inkra-pencil-icon', className].filter(Boolean).join(' '), "data-active": active || undefined, "data-composite": Boolean(badge?.paths.length) || undefined, "data-renderer": "inkra-pressure-pencil", ref: canvasRef, style: { width: size, height: size, ...props.style } }));
289
+ return (_jsx("canvas", { ...props, ...accessibilityProps, className: ['inkra-pencil-icon', className].filter(Boolean).join(' '), "data-active": active || undefined, "data-icon-variant": variant, "data-composite": Boolean(badge?.paths.length) || undefined, "data-renderer": "inkra-pressure-pencil", ref: canvasRef, style: { width: size, height: size, ...props.style } }));
254
290
  });
@@ -324,6 +324,14 @@ const PencilTextImplementation = forwardRef(function PencilText({ as: Element =
324
324
  markDirty();
325
325
  });
326
326
  schedule();
327
+ // Ancestor layout effects can reveal a clipped label during the same commit.
328
+ if (!visible)
329
+ queueMicrotask(() => {
330
+ if (!disposed && dirty && inViewport()) {
331
+ visible = true;
332
+ schedule();
333
+ }
334
+ });
327
335
  return () => {
328
336
  disposed = true;
329
337
  stopRecovery();
@@ -1,5 +1,7 @@
1
1
  import type { ReactNode, TextareaHTMLAttributes } from 'react';
2
2
  import type { FieldControlStateProps } from './types.js';
3
+ import type { TextareaAutoSizeOptions } from './textareaSizing.js';
4
+ export type { TextareaAutoSizeOptions } from './textareaSizing.js';
3
5
  export interface TextareaCountDetails {
4
6
  count: number;
5
7
  maxLength: number;
@@ -15,5 +17,7 @@ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElemen
15
17
  getCountLabel?: (details: TextareaCountDetails) => string;
16
18
  resize?: 'none' | 'vertical';
17
19
  resizeLabel?: string;
20
+ /** Grow and shrink with content. Takes precedence over manual resize and inline height. */
21
+ autoSize?: boolean | TextareaAutoSizeOptions;
18
22
  }
19
23
  export declare const Textarea: import("react").ForwardRefExoticComponent<TextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
@@ -10,16 +10,19 @@ import { Progress } from '../display/index.js';
10
10
  import { useResolvedControlProps } from './controlProps.js';
11
11
  import { EditablePencilText } from './EditablePencilText.js';
12
12
  import { InputAction } from './InputAction.js';
13
+ import { textareaResizeHeight, useTextareaSizing } from './textareaSizing.js';
13
14
  const resizePaths = [{ d: 'M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4' }];
14
15
  export const Textarea = forwardRef(function Textarea(inkraProps, ref) {
15
16
  const { t } = useInkraLocale();
16
- const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, className, disabled, id, invalid = false, readOnly, required, seed, size = 'md', variant = 'outline', endAction, countMode = 'remaining', countDisplay = 'auto', countThreshold = 0.8, getCountLabel = ({ remaining }) => t(remaining < 0 ? 'textarea.overLimit' : 'textarea.countRemaining', { count: Math.abs(remaining) }), resize = 'none', resizeLabel = t('textarea.resize'), ...props } = inkraProps;
17
+ const { 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, className, disabled, id, invalid = false, readOnly, required, seed, size = 'md', variant = 'outline', endAction, countMode = 'remaining', countDisplay = 'auto', countThreshold = 0.8, getCountLabel = ({ remaining }) => t(remaining < 0 ? 'textarea.overLimit' : 'textarea.countRemaining', { count: Math.abs(remaining) }), resize = 'none', resizeLabel = t('textarea.resize'), autoSize = false, ...props } = inkraProps;
17
18
  const generatedId = useId();
18
19
  const textareaRef = useRef(null);
19
20
  const [uncontrolledCount, setUncontrolledCount] = useState(() => String(props.defaultValue ?? '').length);
20
21
  const [height, setHeight] = useState();
21
22
  const [measuredHeight, setMeasuredHeight] = useState();
22
23
  const drag = useRef(null);
24
+ const sizing = useTextareaSizing(textareaRef, autoSize, props.rows, props.form, resize === 'vertical');
25
+ const manualResize = resize === 'vertical' && !sizing.enabled;
23
26
  const controlled = props.value != null;
24
27
  const count = controlled ? String(props.value).length : uncontrolledCount;
25
28
  const maximum = props.maxLength;
@@ -30,11 +33,12 @@ export const Textarea = forwardRef(function Textarea(inkraProps, ref) {
30
33
  const controlId = id ?? `inkra-textarea-${generatedId}`;
31
34
  const remaining = hasCount ? maximum - count : 0;
32
35
  const countLabel = hasCount ? getCountLabel({ count, maxLength: maximum, remaining }) : '';
33
- const minimumHeight = size === 'sm' ? 80 : size === 'lg' ? 112 : 96;
34
- const resizeTo = (next) => setHeight(Math.max(minimumHeight, Math.min(1200, next)));
36
+ const minimumHeight = (sizing.minimumHeight ?? (size === 'sm' ? 80 : size === 'lg' ? 112 : 96)) + sizing.boxAdjustment;
37
+ const maximumHeight = Math.max(1200, minimumHeight);
38
+ const resizeTo = (next) => setHeight(textareaResizeHeight(next, minimumHeight, maximumHeight, sizing.boxAdjustment));
35
39
  useEffect(() => {
36
40
  const control = textareaRef.current;
37
- if (resize !== 'vertical' || !control)
41
+ if (!manualResize || !control)
38
42
  return;
39
43
  const measure = () => setMeasuredHeight(control.getBoundingClientRect().height);
40
44
  measure();
@@ -43,7 +47,7 @@ export const Textarea = forwardRef(function Textarea(inkraProps, ref) {
43
47
  const observer = new ResizeObserver(measure);
44
48
  observer.observe(control);
45
49
  return () => observer.disconnect();
46
- }, [resize]);
50
+ }, [manualResize]);
47
51
  useEffect(() => {
48
52
  const control = textareaRef.current;
49
53
  const form = control?.form;
@@ -73,11 +77,13 @@ export const Textarea = forwardRef(function Textarea(inkraProps, ref) {
73
77
  });
74
78
  const resolvedSeed = seed ?? `inkra-textarea-${state.id ?? generatedId}`;
75
79
  const classes = ['inkra-textarea', className].filter(Boolean).join(' ');
76
- 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, "data-variant": variant, "data-end-action": endAction != null || undefined, children: _jsxs(PencilSurface, { className: "inkra-field-control inkra-field-control--textarea", seed: resolvedSeed, variant: variant === 'underline' ? 'ghost' : 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 => {
80
+ 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, "data-variant": variant, "data-end-action": endAction != null || undefined, "data-row-sizing": sizing.rowSizing || undefined, children: _jsxs(PencilSurface, { className: "inkra-field-control inkra-field-control--textarea", seed: resolvedSeed, variant: variant === 'underline' ? 'ghost' : 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, rows: sizing.enabled ? sizing.minRows : props.rows, style: { ...props.style, ...(manualResize && height !== undefined ? { height: Math.max(height, sizing.minimumHeight ?? height) } : {}), ...sizing.style, resize: 'none' }, onInput: event => {
77
81
  if (!controlled)
78
82
  setUncontrolledCount(event.currentTarget.value.length);
79
83
  props.onInput?.(event);
80
- } }), _jsx(EditablePencilText, { controlRef: textareaRef, defaultValue: props.defaultValue, multiline: true, placeholder: props.placeholder, seed: `${resolvedSeed}-editable`, value: props.value }), endAction != null && _jsx("div", { className: "inkra-textarea__end-action", children: _jsx(InputAction, { disabled: Boolean(state.disabled), seed: `${resolvedSeed}-action`, children: endAction }) }), (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 => {
84
+ // Wait for React to accept or restore a controlled value before measuring.
85
+ sizing.schedule();
86
+ } }), _jsx(EditablePencilText, { controlRef: textareaRef, defaultValue: props.defaultValue, multiline: true, placeholder: props.placeholder, seed: `${resolvedSeed}-editable`, value: props.value }), endAction != null && _jsx("div", { className: "inkra-textarea__end-action", children: _jsx(InputAction, { disabled: Boolean(state.disabled), seed: `${resolvedSeed}-action`, children: endAction }) }), (hasCount || manualResize) && _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 })] }), manualResize && _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": maximumHeight, "aria-valuenow": Math.round(Math.max(measuredHeight ?? minimumHeight, minimumHeight)), disabled: state.disabled, onPointerDown: event => {
81
87
  if (event.button !== 0)
82
88
  return;
83
89
  drag.current = { y: event.clientY, height: textareaRef.current?.getBoundingClientRect().height ?? minimumHeight };
@@ -98,7 +104,7 @@ export const Textarea = forwardRef(function Textarea(inkraProps, ref) {
98
104
  else if (event.key === 'Home')
99
105
  resizeTo(minimumHeight);
100
106
  else if (event.key === 'End')
101
- resizeTo(1200);
107
+ resizeTo(maximumHeight);
102
108
  else
103
109
  return;
104
110
  event.preventDefault();
@@ -3,5 +3,5 @@ export type { FieldProps } from './Field.js';
3
3
  export { Input } from './Input.js';
4
4
  export type { InputProps, InputVariant, InputTextAlign, InputValueChangeDetails, InputClassNames } from './Input.js';
5
5
  export { Textarea } from './Textarea.js';
6
- export type { TextareaProps, TextareaCountDetails } from './Textarea.js';
6
+ export type { TextareaProps, TextareaCountDetails, TextareaAutoSizeOptions } from './Textarea.js';
7
7
  export type { FieldControlSize, FieldControlStateProps } from './types.js';
@@ -0,0 +1,29 @@
1
+ import type { CSSProperties, RefObject } from 'react';
2
+ export interface TextareaAutoSizeOptions {
3
+ /** Minimum visible text rows. Falls back to rows, then 1. */
4
+ minRows?: number;
5
+ /** Maximum visible text rows, including soft wraps. Omit for unlimited growth. */
6
+ maxRows?: number;
7
+ }
8
+ export declare function resolveTextareaRows(autoSize: boolean | TextareaAutoSizeOptions | undefined, rows?: number): {
9
+ minRows: number;
10
+ maxRows: number;
11
+ };
12
+ /** Measure a native textarea without changing its value, selection, scroll position or height. */
13
+ export declare function measureTextareaSize(control: HTMLTextAreaElement, minRows: number, maxRows: number, measureContent?: boolean): {
14
+ minimumHeight: number;
15
+ boxAdjustment: number;
16
+ height: number;
17
+ overflowY: "hidden" | "auto";
18
+ } | undefined;
19
+ export declare function useTextareaSizing(controlRef: RefObject<HTMLTextAreaElement | null>, autoSize: boolean | TextareaAutoSizeOptions | undefined, rows: number | undefined, form: string | undefined, manualResize?: boolean): {
20
+ enabled: boolean;
21
+ rowSizing: boolean;
22
+ minRows: number;
23
+ minimumHeight: number | undefined;
24
+ boxAdjustment: number;
25
+ style: CSSProperties | undefined;
26
+ schedule: () => void;
27
+ };
28
+ /** Manual handles and ARIA use visible border-box pixels; CSS height may use content-box pixels. */
29
+ export declare function textareaResizeHeight(visibleHeight: number, minimum: number, maximum: number, boxAdjustment: number): number;