@design-edito/tools 0.5.1 → 0.5.3

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 (156) hide show
  1. package/agnostic/arrays/index.d.ts +1 -1
  2. package/agnostic/arrays/index.js +1 -1
  3. package/agnostic/colors/index.d.ts +1 -1
  4. package/agnostic/colors/index.js +1 -1
  5. package/agnostic/css/index.d.ts +1 -1
  6. package/agnostic/css/index.js +1 -1
  7. package/agnostic/html/deep-select/index.js +5 -3
  8. package/agnostic/html/hyper-json/smart-tags/coalesced/index.d.ts +9 -9
  9. package/agnostic/html/hyper-json/smart-tags/coalesced/index.js +9 -9
  10. package/agnostic/html/hyper-json/smart-tags/isolated/index.d.ts +1 -1
  11. package/agnostic/html/hyper-json/smart-tags/isolated/index.js +1 -1
  12. package/agnostic/html/index.d.ts +3 -3
  13. package/agnostic/html/index.js +3 -3
  14. package/agnostic/index.d.ts +3 -3
  15. package/agnostic/index.js +3 -3
  16. package/agnostic/misc/index.d.ts +1 -1
  17. package/agnostic/misc/index.js +1 -1
  18. package/agnostic/misc/logs/index.d.ts +1 -1
  19. package/agnostic/misc/logs/index.js +1 -1
  20. package/agnostic/numbers/index.d.ts +3 -3
  21. package/agnostic/numbers/index.js +3 -3
  22. package/agnostic/objects/index.d.ts +3 -3
  23. package/agnostic/objects/index.js +3 -3
  24. package/agnostic/random/index.d.ts +1 -1
  25. package/agnostic/random/index.js +1 -1
  26. package/agnostic/strings/index.d.ts +3 -3
  27. package/agnostic/strings/index.js +3 -3
  28. package/agnostic/time/dates/format-date/index.d.ts +4 -2
  29. package/agnostic/time/dates/format-date/index.js +43 -94
  30. package/agnostic/time/dates/format-date/index.test.js +154 -0
  31. package/agnostic/time/dates/format-date/parts.d.ts +28 -0
  32. package/agnostic/time/dates/format-date/parts.js +136 -0
  33. package/agnostic/time/dates/format-date/types.d.ts +84 -0
  34. package/agnostic/time/dates/format-date/types.js +1 -0
  35. package/agnostic/time/duration/format-duration/index.d.ts +42 -0
  36. package/agnostic/time/duration/format-duration/index.js +94 -0
  37. package/agnostic/time/duration/format-duration/index.test.d.ts +1 -0
  38. package/agnostic/time/duration/format-duration/index.test.js +148 -0
  39. package/agnostic/time/duration/format-duration/parts.d.ts +32 -0
  40. package/agnostic/time/duration/format-duration/parts.js +107 -0
  41. package/agnostic/time/duration/format-duration/types.d.ts +93 -0
  42. package/agnostic/time/duration/format-duration/types.js +1 -0
  43. package/agnostic/time/duration/index.d.ts +6 -1
  44. package/agnostic/time/duration/index.js +2 -2
  45. package/agnostic/time/duration/index.test.d.ts +1 -0
  46. package/agnostic/time/duration/index.test.js +168 -0
  47. package/components/BeforeAfter/index.controlled.d.ts +25 -25
  48. package/components/BeforeAfter/index.controlled.js +47 -53
  49. package/components/BeforeAfter/index.d.ts +20 -23
  50. package/components/BeforeAfter/index.js +48 -40
  51. package/components/Button/index.js +1 -1
  52. package/components/Clippable/index.d.ts +17 -26
  53. package/components/Clippable/index.js +21 -23
  54. package/components/Disclaimer/index.d.ts +29 -22
  55. package/components/Disclaimer/index.js +29 -24
  56. package/components/Drawer/index.d.ts +45 -25
  57. package/components/Drawer/index.js +61 -52
  58. package/components/EventListener/index.d.ts +4 -5
  59. package/components/Gallery/index.d.ts +14 -16
  60. package/components/Gallery/index.js +25 -30
  61. package/components/Iframe/index.js +2 -2
  62. package/components/Image/index.d.ts +5 -12
  63. package/components/Image/index.js +11 -32
  64. package/components/Input/index.d.ts +23 -19
  65. package/components/Input/index.js +30 -20
  66. package/components/JsonEditor/index.d.ts +177 -9
  67. package/components/JsonEditor/index.js +133 -75
  68. package/components/JsonEditor/types.d.ts +11 -0
  69. package/components/JsonEditor/types.js +1 -0
  70. package/components/JsonEditor/utils.d.ts +8 -0
  71. package/components/JsonEditor/utils.js +23 -0
  72. package/components/ListLoader/index.controlled.d.ts +78 -0
  73. package/components/ListLoader/index.controlled.js +99 -0
  74. package/components/ListLoader/index.d.ts +69 -0
  75. package/components/ListLoader/index.js +146 -0
  76. package/components/ListLoader/utils.d.ts +8 -0
  77. package/components/ListLoader/utils.js +10 -0
  78. package/components/Overlayer/index.js +4 -4
  79. package/components/Paginator/index.d.ts +7 -12
  80. package/components/Paginator/index.js +7 -11
  81. package/components/ResizeObserver/index.d.ts +14 -3
  82. package/components/ResizeObserver/index.js +49 -24
  83. package/components/ResizeObserver/styles.module.css +0 -0
  84. package/components/Scrllgngn/index.d.ts +17 -13
  85. package/components/Scrllgngn/index.js +25 -84
  86. package/components/Scrllgngn/utils.d.ts +52 -0
  87. package/components/Scrllgngn/utils.js +102 -0
  88. package/components/ScrollListener/index.d.ts +43 -28
  89. package/components/ScrollListener/index.js +59 -79
  90. package/components/ScrollListener/utils.d.ts +37 -16
  91. package/components/ScrollListener/utils.js +141 -28
  92. package/components/Select/index.d.ts +24 -20
  93. package/components/Select/index.js +30 -20
  94. package/components/Sequencer/index.controlled.d.ts +16 -26
  95. package/components/Sequencer/index.controlled.js +15 -12
  96. package/components/Sequencer/index.d.ts +25 -30
  97. package/components/Sequencer/index.js +20 -34
  98. package/components/Subtitles/index.d.ts +29 -5
  99. package/components/Subtitles/index.js +90 -12
  100. package/components/Textarea/index.d.ts +26 -20
  101. package/components/Textarea/index.js +40 -21
  102. package/components/Theatre/index.d.ts +35 -39
  103. package/components/Theatre/index.js +46 -58
  104. package/components/UIModule/index.d.ts +25 -29
  105. package/components/UIModule/index.js +81 -110
  106. package/components/Video/index.controlled.d.ts +51 -51
  107. package/components/Video/index.controlled.js +75 -72
  108. package/components/Video/index.d.ts +12 -14
  109. package/components/Video/index.js +41 -86
  110. package/components/Video/utils.d.ts +35 -3
  111. package/components/Video/utils.js +37 -14
  112. package/components/index.d.ts +5 -4
  113. package/components/index.js +5 -4
  114. package/components/public-classnames.d.ts +1 -0
  115. package/components/public-classnames.js +1 -0
  116. package/components/utils/index.d.ts +22 -0
  117. package/components/utils/index.js +31 -0
  118. package/node/@google-cloud/storage/directory/index.d.ts +2 -2
  119. package/node/@google-cloud/storage/directory/index.js +2 -2
  120. package/node/@google-cloud/storage/file/index.d.ts +2 -2
  121. package/node/@google-cloud/storage/file/index.js +2 -2
  122. package/node/@google-cloud/storage/index.d.ts +1 -1
  123. package/node/@google-cloud/storage/index.js +1 -1
  124. package/node/cloud-storage/operations/index.d.ts +2 -2
  125. package/node/cloud-storage/operations/index.js +2 -2
  126. package/node/ftps/directory/index.d.ts +1 -1
  127. package/node/ftps/directory/index.js +1 -1
  128. package/node/ftps/file/index.d.ts +2 -2
  129. package/node/ftps/file/index.js +2 -2
  130. package/node/images/index.d.ts +1 -1
  131. package/node/images/index.js +1 -1
  132. package/node/images/transform/operations/index.d.ts +3 -3
  133. package/node/images/transform/operations/index.js +3 -3
  134. package/node/index.d.ts +2 -2
  135. package/node/index.js +2 -2
  136. package/node/process/index.d.ts +1 -1
  137. package/node/process/index.js +1 -1
  138. package/node/sftp/directory/index.d.ts +1 -1
  139. package/node/sftp/directory/index.js +1 -1
  140. package/node/sftp/file/index.d.ts +1 -1
  141. package/node/sftp/file/index.js +1 -1
  142. package/package.json +27 -5
  143. package/TODO.md +0 -269
  144. package/components/BeforeAfter/utils.d.ts +0 -4
  145. package/components/BeforeAfter/utils.js +0 -9
  146. package/components/Input/index.controlled.d.ts +0 -42
  147. package/components/Input/index.controlled.js +0 -54
  148. package/components/Select/index.controlled.d.ts +0 -43
  149. package/components/Select/index.controlled.js +0 -54
  150. package/components/Subtitles/types.d.ts +0 -24
  151. package/components/Subtitles/utils.d.ts +0 -32
  152. package/components/Subtitles/utils.js +0 -107
  153. package/components/Textarea/index.controlled.d.ts +0 -50
  154. package/components/Textarea/index.controlled.js +0 -75
  155. /package/{components/Subtitles/types.js → agnostic/time/dates/format-date/index.test.d.ts} +0 -0
  156. /package/components/{ResizeObserver/style.module.css → ListLoader/styles.module.css} +0 -0
@@ -1,43 +1,42 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback, Children, useRef } from 'react';
2
+ import { useState, useEffect, useCallback, Children } from 'react';
3
3
  import { absoluteModulo } from '../../agnostic/numbers/absolute-modulo/index.js';
4
4
  import { clamp } from '../../agnostic/numbers/clamp/index.js';
5
5
  import { IntersectionObserverComponent } from '../IntersectionObserver/index.js';
6
- import { SequencerControlled } from './index.controlled.js';
6
+ import { useChangeDispatch } from '../utils/index.js';
7
+ import { ControlledSequencer } from './index.controlled.js';
7
8
  /**
8
9
  * Uncontrolled, self-advancing sequencer component. Drives a
9
- * {@link SequencerControlled} instance with an internal tempo-based interval,
10
+ * {@link ControlledSequencer} instance with an internal tempo-based interval,
10
11
  * optional loop/clamp boundary behaviour, and viewport-driven play/reset triggers
11
12
  * via an {@link IntersectionObserverComponent}.
12
13
  *
13
14
  * Supports mixed controlled/uncontrolled usage: passing `step` disables the
14
15
  * internal interval while still applying loop/clamp arithmetic before forwarding
15
16
  * to the controlled layer. Passing `play` disables internal play state management
16
- * while still allowing viewport handlers to fire `actionHandlers.intersected`.
17
+ * while still allowing viewport handlers to fire `onIntersected`.
17
18
  *
18
- * ### Forwarded modifiers to {@link SequencerControlled}
19
- * The following `_modifiers` are computed and injected automatically:
20
- * - `playing` — `true` when the effective play state is active.
21
- * - `at-start` — `true` when the forwarded step is `0`.
22
- * - `at-end` — `true` when the forwarded step equals `stepsCount - 1`.
19
+ * ### Forwarded to {@link ControlledSequencer}
20
+ * - `step` the effective step, after loop/clamp arithmetic.
21
+ * - `isPlaying` — the effective play state, controlled or internal.
22
+ * - `tempo` — the current tempo, which the controlled layer exposes as `data-tempo`.
23
23
  *
24
- * ### Forwarded data attributes to {@link SequencerControlled}
25
- * - `data-tempo` the current `tempo` value.
24
+ * The `--at-start` and `--at-end` modifiers are derived by the controlled layer
25
+ * from `step` and the children count.
26
26
  *
27
27
  * @param props - Component properties.
28
28
  * @see {@link Props}
29
- * @see {@link SequencerControlled}
29
+ * @see {@link ControlledSequencer}
30
30
  * @returns An {@link IntersectionObserverComponent} wrapping a
31
- * {@link SequencerControlled} with the computed step and modifiers applied.
31
+ * {@link ControlledSequencer} with the computed step and modifiers applied.
32
32
  */
33
- export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, clampLast, resetOnVisible, resetOnHidden, playOnVisible, pauseOnHidden, actionHandlers, stateHandlers, ...controlledProps }) => {
33
+ export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, clampLast, resetOnVisible, resetOnHidden, playOnVisible, pauseOnHidden, onIntersected, onIsPlayingChanged, onStepChanged, ...controlledProps }) => {
34
34
  // State
35
35
  const { step, activateOnStep, children } = controlledProps;
36
36
  const [internalPlay, setInternalPlay] = useState(play ?? false);
37
37
  const [internalStep, setInternalStep] = useState(step ?? defaultStep ?? 0);
38
38
  const actualPlay = play ?? internalPlay;
39
39
  const actualStep = step ?? internalStep;
40
- const actualPlayRef = useRef(actualPlay);
41
40
  // Effects
42
41
  useEffect(() => {
43
42
  const clampedTempo = Math.max(tempo, 1);
@@ -61,24 +60,15 @@ export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, cla
61
60
  const rightClamp = clampLast === true ? stepsCount - 1 : Infinity;
62
61
  forwardedStep = clamp(actualStep, leftClamp, rightClamp);
63
62
  }
64
- const forwardedStepRef = useRef(forwardedStep);
65
- // State change handlers
66
- useEffect(() => {
67
- if (actualPlay !== actualPlayRef.current) {
68
- actualPlayRef.current = actualPlay;
69
- stateHandlers?.isPlaying?.(actualPlay);
70
- }
71
- if (forwardedStep !== forwardedStepRef.current) {
72
- forwardedStepRef.current = forwardedStep;
73
- stateHandlers?.stepChanged?.(forwardedStep);
74
- }
75
- }, [actualPlay, forwardedStep]);
63
+ // State dispatch
64
+ useChangeDispatch(actualPlay, onIsPlayingChanged);
65
+ useChangeDispatch(forwardedStep, onStepChanged);
76
66
  // Action handlers
77
67
  const handleIntersection = useCallback(({ ioEntry, observer }) => {
68
+ onIntersected?.({ ioEntry, observer });
78
69
  if (play === true || step !== undefined)
79
70
  return;
80
71
  const { isIntersecting } = ioEntry ?? {};
81
- actionHandlers?.intersected?.({ ioEntry, observer });
82
72
  if (isIntersecting === true) {
83
73
  if (resetOnVisible === true)
84
74
  setInternalStep(0);
@@ -98,12 +88,8 @@ export const Sequencer = ({ defaultStep, tempo = 60, play, loop, clampFirst, cla
98
88
  pauseOnHidden,
99
89
  play,
100
90
  step,
101
- actionHandlers
91
+ onIntersected
102
92
  ]);
103
93
  // Rendering
104
- return _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: _jsx(SequencerControlled, { ...controlledProps, step: forwardedStep, _modifiers: {
105
- playing: actualPlay,
106
- 'at-start': forwardedStep === 0,
107
- 'at-end': forwardedStep === stepsCount - 1
108
- }, _dataAttributes: { tempo } }) });
94
+ return _jsx(IntersectionObserverComponent, { onIntersected: handleIntersection, children: _jsx(ControlledSequencer, { ...controlledProps, step: forwardedStep, isPlaying: actualPlay, tempo: tempo }) });
109
95
  };
@@ -1,6 +1,29 @@
1
1
  import { type FunctionComponent, type PropsWithChildren } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
- import type { ParsedSub } from './types.js';
3
+ /**
4
+ * A single parsed subtitle entry from an SRT file.
5
+ *
6
+ * @property id - Sequential subtitle number.
7
+ * @property start - Start time in milliseconds.
8
+ * @property end - End time in milliseconds.
9
+ * @property content - Subtitle text content.
10
+ */
11
+ export type ParsedSub = {
12
+ id: number;
13
+ start?: number;
14
+ end?: number;
15
+ content?: string;
16
+ };
17
+ /**
18
+ * The inclusive ID boundaries of a subtitle group.
19
+ *
20
+ * @property startId - ID of the first subtitle in the group.
21
+ * @property endId - ID of the last subtitle in the group.
22
+ */
23
+ export type SubGroupBoundaries = {
24
+ startId: number;
25
+ endId: number;
26
+ };
4
27
  /**
5
28
  * Props for the {@link Subtitles} component.
6
29
  *
@@ -16,10 +39,11 @@ import type { ParsedSub } from './types.js';
16
39
  * @property isEnded - When `true`, forces the last group to be treated as current,
17
40
  * regardless of `timecodeMs`. Useful to keep the final subtitle group visible after
18
41
  * media playback finishes.
19
- * @property onLoaded - Callback invoked with the raw SRT string after a successful
20
- * fetch and parse. Not called when `srtFileContent` is used directly.
21
- * @property onParsed - Callback invoked with the raw SRT string has been parsed.
22
- * @property onLoadFailed - Callback invoked with an `Error` if the fetch or parse step fails.
42
+ * @property onLoaded - Called with the raw SRT string once fetched. Not called
43
+ * when `srtFileContent` is used directly, since nothing is fetched then.
44
+ * @property onParsed - Called after the subtitles have been parsed, with the
45
+ * resulting entries.
46
+ * @property onLoadFailed - Called with an `Error` when the fetch or the parse fails.
23
47
  * @property className - Optional additional class name(s) applied to the root element.
24
48
  * @property children - React children rendered inside the root element, after the subtitle groups.
25
49
  */
@@ -1,12 +1,95 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
4
  import { toError } from '../../agnostic/misc/cast/index.js';
5
5
  import { unknownToString } from '../../agnostic/errors/unknown-to-string/index.js';
6
- import { mergeClassNames } from '../utils/index.js';
6
+ import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
7
7
  import { subtitles as publicClassName } from '../public-classnames.js';
8
- import { computeSubGroupsWithBoundaries, getCurrentGroup, parseSubs } from './utils.js';
9
8
  import cssModule from './styles.module.css';
9
+ /** Converts an SRT timecode (hh:mm:ss,ms) to milliseconds. */
10
+ const getTimecodeToMs = (timecode) => {
11
+ const [hours = '0', minutes = '0', secondsAndMs = '0,0'] = timecode.split(':');
12
+ const [seconds = '0', milliseconds = '0'] = secondsAndMs.split(',');
13
+ let result = parseInt(hours) * 60 * 60 * 1000;
14
+ result += parseInt(minutes) * 60 * 1000;
15
+ result += parseInt(seconds) * 1000;
16
+ result += parseInt(milliseconds);
17
+ return result;
18
+ };
19
+ /** Parses a raw SRT subtitle text into a list of {@link ParsedSub} entries. */
20
+ const parseSubs = (rawSubs) => {
21
+ const numberRegex = /^\d+$/v;
22
+ const timecodeRegex = /^[0-9]+:[0-9]+:[0-9]+,[0-9]+\s*-->\s*[0-9]+:[0-9]+:[0-9]+,[0-9]+$/v;
23
+ const parsedSubs = [];
24
+ rawSubs.split('\n').forEach(line => {
25
+ if (line.trim() === '')
26
+ return;
27
+ const lastParsedSub = parsedSubs[parsedSubs.length - 1];
28
+ const matchId = line.match(numberRegex);
29
+ const matchTimecode = line.match(timecodeRegex);
30
+ // id
31
+ if (matchId !== null) {
32
+ if (lastParsedSub === undefined
33
+ || lastParsedSub.content !== undefined) {
34
+ const parsedSub = { id: parseInt(line) };
35
+ parsedSubs.push(parsedSub);
36
+ return;
37
+ }
38
+ }
39
+ // timecode
40
+ if (matchTimecode !== null) {
41
+ if (lastParsedSub?.id !== undefined) {
42
+ const [rawStart = '', rawEnd = ''] = line.split('-->');
43
+ const startTime = rawStart.trim();
44
+ const endTime = rawEnd.trim();
45
+ lastParsedSub.start = getTimecodeToMs(startTime);
46
+ lastParsedSub.end = getTimecodeToMs(endTime);
47
+ return;
48
+ }
49
+ }
50
+ // content
51
+ if (lastParsedSub?.id !== undefined
52
+ && lastParsedSub.start !== undefined
53
+ && lastParsedSub.end !== undefined) {
54
+ if (lastParsedSub.content !== undefined) {
55
+ lastParsedSub.content += `\n${line}`;
56
+ return;
57
+ }
58
+ lastParsedSub.content = line;
59
+ }
60
+ });
61
+ return parsedSubs;
62
+ };
63
+ /** Computes subtitle groups with their inclusive `startId` / `endId` boundaries. */
64
+ const computeSubGroupsWithBoundaries = (subsGroups, highestSubId) => {
65
+ const fallback = [{ startId: 1, endId: highestSubId }];
66
+ if (subsGroups === undefined || subsGroups.length === 0)
67
+ return fallback;
68
+ const emptySubGroupBoundaries = [];
69
+ return subsGroups.reduce((acc, curr, currIndex) => {
70
+ const lastInAcc = acc[acc.length - 1];
71
+ const startId = lastInAcc === undefined ? 1 : lastInAcc.endId + 1;
72
+ const endId = curr;
73
+ if (currIndex === subsGroups.length - 1
74
+ && endId !== highestSubId) {
75
+ return [
76
+ ...acc,
77
+ { startId, endId },
78
+ { startId: endId + 1, endId: highestSubId }
79
+ ];
80
+ }
81
+ return [...acc, { startId, endId }];
82
+ }, emptySubGroupBoundaries);
83
+ };
84
+ /** Returns the group holding the last elapsed subtitle, or the last group once playback ended. */
85
+ const getCurrentGroup = (subsGroupsWithBoundaries, lastPrevSubId, isEnded) => {
86
+ const previousGroups = subsGroupsWithBoundaries.filter(group => group.startId <= (lastPrevSubId ?? 0));
87
+ if (previousGroups.length === 0)
88
+ return isEnded === true
89
+ ? subsGroupsWithBoundaries[subsGroupsWithBoundaries.length - 1]
90
+ : subsGroupsWithBoundaries[0];
91
+ return previousGroups[previousGroups.length - 1];
92
+ };
10
93
  /**
11
94
  * Subtitle synchronization component. Fetches or receives an SRT source, parses it,
12
95
  * and renders subtitle groups whose individual spans are styled according to the
@@ -34,19 +117,14 @@ export const Subtitles = ({ src, srtFileContent, subsGroups, timecodeMs, isEnded
34
117
  const [isLoading, setIsLoading] = useState(false);
35
118
  const [loadError, setLoadError] = useState(null);
36
119
  const [parsedSubs, setParsedSubs] = useState([]);
37
- const pParsedSubs = useRef(parsedSubs);
38
- // State change handlers
39
- useEffect(() => {
40
- if (pParsedSubs.current === parsedSubs)
41
- return;
42
- onParsed?.(parsedSubs);
43
- }, [parsedSubs]);
120
+ // State dispatch
121
+ useChangeDispatch(parsedSubs, onParsed);
44
122
  // Effects
45
123
  const fetchAndParseSubs = useCallback(async (src, srtFileContent) => {
46
- if (src === undefined)
47
- return;
48
124
  if (srtFileContent !== undefined)
49
125
  return setParsedSubs(parseSubs(srtFileContent));
126
+ if (src === undefined)
127
+ return;
50
128
  setIsLoading(true);
51
129
  setLoadError(null);
52
130
  try {
@@ -1,20 +1,30 @@
1
- import { type FunctionComponent } from 'react';
2
- import { type Props as ControlledProps } from './index.controlled.js';
1
+ import { type FunctionComponent, type ReactNode, type TextareaHTMLAttributes } from 'react';
2
+ import type { WithClassName } from '../utils/types.js';
3
3
  /**
4
4
  * Props for the {@link Textarea} component.
5
5
  *
6
- * Alias of {@link ControlledProps}.
6
+ * Extends all native {@link TextareaHTMLAttributes} and {@link WithClassName}
7
+ * with optional label, error content, and automatic height adjustment.
7
8
  *
8
- * All native textarea attributes remain available, including `value`,
9
- * `defaultValue`, and `onChange`, allowing the component to operate
10
- * in either controlled or hybrid mode.
9
+ * @property label - Content rendered as an associated `<label>`. When omitted, no label is rendered.
10
+ * @property error - Content rendered as an error message below the textarea. When omitted, no error is rendered.
11
+ * @property autoHeight - When `true`, grows and shrinks the textarea to fit its
12
+ * content on every value change. Defaults to `false`.
13
+ * @property className - Additional class name(s) applied to the textarea element.
11
14
  */
12
- export type Props = ControlledProps;
15
+ export type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & WithClassName<{
16
+ label?: ReactNode;
17
+ error?: ReactNode;
18
+ autoHeight?: boolean;
19
+ }>;
13
20
  /**
14
- * Textarea field component supporting controlled and hybrid usage.
21
+ * Textarea field supporting controlled and uncontrolled usage.
15
22
  *
16
- * Wraps {@link ControlledTextarea} and automatically manages the textarea
17
- * value when no `value` prop is provided.
23
+ * Renders a native `<textarea>` with optional label and error feedback. All
24
+ * standard textarea attributes are forwarded to the underlying element.
25
+ *
26
+ * A stable auto-generated `id` is created on mount and used to associate the
27
+ * rendered label through the `htmlFor` attribute.
18
28
  *
19
29
  * ### CSS elements
20
30
  * - `label`
@@ -22,17 +32,13 @@ export type Props = ControlledProps;
22
32
  *
23
33
  * @param props - Component properties.
24
34
  * @see {@link Props}
25
- *
26
- * @returns A labelled textarea with optional internal value management.
35
+ * @returns A labelled textarea with optional error feedback.
27
36
  *
28
37
  * @remarks
29
- * - In controlled mode (`value` defined), the textarea value is fully driven
30
- * by the parent component and internal state is never updated.
31
- * - In hybrid mode, internal state is initialized from `defaultValue` and
32
- * subsequently manages value updates itself.
33
- * - In hybrid mode, the internal value is updated before forwarding the
34
- * `onChange` callback.
35
- * - `defaultValue` is only used to initialize internal state and is not
36
- * forwarded to the underlying controlled component.
38
+ * - In controlled mode (`value` defined), the displayed value is fully driven
39
+ * by the parent and internal state is never updated.
40
+ * - In uncontrolled mode, internal state is initialized from `defaultValue` and
41
+ * updated before `onChange` is forwarded.
42
+ * - `onChange` fires in both modes.
37
43
  */
38
44
  export declare const Textarea: FunctionComponent<Props>;
@@ -1,11 +1,19 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { ControlledTextarea } from './index.controlled.js';
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef, useState } from 'react';
3
+ import { clss } from '../../agnostic/css/clss/index.js';
4
+ import { isNotFalsy } from '../../agnostic/booleans/is-falsy/index.js';
5
+ import { randomHash } from '../../agnostic/random/uuid/index.js';
6
+ import { mergeClassNames } from '../utils/index.js';
7
+ import { textarea as publicClassName } from '../public-classnames.js';
8
+ import cssModule from './styles.module.css';
4
9
  /**
5
- * Textarea field component supporting controlled and hybrid usage.
10
+ * Textarea field supporting controlled and uncontrolled usage.
6
11
  *
7
- * Wraps {@link ControlledTextarea} and automatically manages the textarea
8
- * value when no `value` prop is provided.
12
+ * Renders a native `<textarea>` with optional label and error feedback. All
13
+ * standard textarea attributes are forwarded to the underlying element.
14
+ *
15
+ * A stable auto-generated `id` is created on mount and used to associate the
16
+ * rendered label through the `htmlFor` attribute.
9
17
  *
10
18
  * ### CSS elements
11
19
  * - `label`
@@ -13,27 +21,38 @@ import { ControlledTextarea } from './index.controlled.js';
13
21
  *
14
22
  * @param props - Component properties.
15
23
  * @see {@link Props}
16
- *
17
- * @returns A labelled textarea with optional internal value management.
24
+ * @returns A labelled textarea with optional error feedback.
18
25
  *
19
26
  * @remarks
20
- * - In controlled mode (`value` defined), the textarea value is fully driven
21
- * by the parent component and internal state is never updated.
22
- * - In hybrid mode, internal state is initialized from `defaultValue` and
23
- * subsequently manages value updates itself.
24
- * - In hybrid mode, the internal value is updated before forwarding the
25
- * `onChange` callback.
26
- * - `defaultValue` is only used to initialize internal state and is not
27
- * forwarded to the underlying controlled component.
27
+ * - In controlled mode (`value` defined), the displayed value is fully driven
28
+ * by the parent and internal state is never updated.
29
+ * - In uncontrolled mode, internal state is initialized from `defaultValue` and
30
+ * updated before `onChange` is forwarded.
31
+ * - `onChange` fires in both modes.
28
32
  */
29
- export const Textarea = ({ defaultValue, value, onChange, ...rest }) => {
33
+ export const Textarea = ({ label, error, autoHeight = false, value, defaultValue, onChange, className, ...rest }) => {
34
+ // State & refs
35
+ const [id] = useState(`_${randomHash(12)}`);
36
+ const [internalValue, setInternalValue] = useState(defaultValue ?? '');
37
+ const textareaRef = useRef(null);
30
38
  const isControlled = value !== undefined;
31
- const [internal, setInternal] = useState(defaultValue ?? '');
32
- const currentValue = isControlled ? value : internal;
39
+ const currentValue = isControlled ? value : internalValue;
40
+ // Fx. dep. `autoHeight`, `currentValue` - fit the height to the content
41
+ useLayoutEffect(() => {
42
+ const textarea = textareaRef.current;
43
+ if (!autoHeight || textarea === null)
44
+ return;
45
+ textarea.style.height = 'auto';
46
+ textarea.style.height = `${textarea.scrollHeight}px`;
47
+ }, [autoHeight, currentValue]);
48
+ // User action handlers
33
49
  const handleChange = (e) => {
34
50
  if (!isControlled)
35
- setInternal(e.target.value);
51
+ setInternalValue(e.target.value);
36
52
  onChange?.(e);
37
53
  };
38
- return _jsx(ControlledTextarea, { ...rest, value: currentValue, onChange: handleChange });
54
+ // Rendering
55
+ const c = clss(publicClassName, { cssModule });
56
+ const rootClss = mergeClassNames(c(), className);
57
+ return _jsxs(_Fragment, { children: [isNotFalsy(label) && _jsx("label", { className: c('label'), htmlFor: id, children: label }), _jsx("textarea", { ...rest, ref: textareaRef, id: id, className: rootClss, value: currentValue, onChange: handleChange }), isNotFalsy(error) && _jsx("span", { className: c('error'), children: error })] });
39
58
  };
@@ -3,23 +3,24 @@ import type { WithClassName } from '../utils/types.js';
3
3
  /**
4
4
  * Props for the {@link Theatre} component.
5
5
  *
6
- * @property closeBtnContent - Custom content rendered inside the close/exit button.
7
- * @property openBtnContent - Custom content rendered inside the open/enter button.
8
- * @property isOn - Controlled theatre mode state. When provided, overrides the
9
- * internal state. Use together with {@link Props.onToggleClick} for fully
10
- * controlled usage.
11
- * @property defaultIsOn - Default state for the theatre mode.
12
- * @property exitOnEscape When uncontrolled and on, toggles internal state to off when 'esc' key is pressed
13
- * @property exitOnBgClick When uncontrolled and on, toggles internal state to off when the background is clicked
14
- * @property stateHandlers - Callbacks called after the internal state changed
15
- * @property stateHandlers.toggled - Callback invoked after the state changed
16
- * @property actionHandlers - Callbacks called after a user action on children elements
17
- * @property actionHandlers.toggleClick - Callback invoked when either the open or close
18
- * button is clicked, the 'esc' key pressed or the background clicked. Receives the theatre state value (`isOn`) at the time of the click,
19
- * i.e. the previous state before the toggle.
6
+ * @property closeBtnContent - Content rendered inside the close/exit button.
7
+ * @property openBtnContent - Content rendered inside the open/enter button.
8
+ * @property isOn - Controlled theatre mode state. When defined, the component
9
+ * behaves as a controlled component and internal state is never updated.
10
+ * @property defaultIsOn - Initial theatre mode state in uncontrolled mode.
11
+ * Ignored when `isOn` is provided. Defaults to `false`.
12
+ * @property exitOnEscape - When `true`, pressing `Escape` while the stage is
13
+ * open counts as a toggle.
14
+ * @property exitOnBgClick - When `true`, clicking the stage background — and
15
+ * not its content counts as a toggle.
16
+ * @property onToggleClicked - Called whenever a toggle is requested: the open
17
+ * or close button, the `Escape` key, or the stage background. Fires before the
18
+ * theatre reacts, with the state as it was.
19
+ * @property onIsOnChanged - Called after the theatre mode changed, with the new
20
+ * value.
20
21
  * @property className - Optional additional class name(s) applied to the root element.
21
- * @property children - Content rendered both in the default slot and, when theatre
22
- * mode is active, duplicated inside the stage element.
22
+ * @property children - Content rendered both in the default slot and, when
23
+ * theatre mode is active, duplicated inside the stage element.
23
24
  */
24
25
  export type Props = PropsWithChildren<WithClassName<{
25
26
  closeBtnContent?: ReactNode;
@@ -28,37 +29,32 @@ export type Props = PropsWithChildren<WithClassName<{
28
29
  defaultIsOn?: boolean;
29
30
  exitOnEscape?: boolean;
30
31
  exitOnBgClick?: boolean;
31
- stateHandlers?: {
32
- toggled?: (isOn: boolean) => void;
33
- };
34
- actionHandlers?: {
35
- toggleClick?: (prevIsOn: boolean) => void;
36
- };
32
+ onToggleClicked?: (isOn: boolean) => void;
33
+ onIsOnChanged?: (isOn: boolean) => void;
37
34
  }>>;
38
35
  /**
39
36
  * Theatre mode component. Wraps content in a toggleable fullscreen-like "stage"
40
- * overlay. Supports both controlled and uncontrolled usage.
37
+ * overlay.
41
38
  *
42
- * When `isOn` is not provided the component manages its own open/closed state
43
- * internally. When `isOn` is provided it acts as the source of truth and the
44
- * internal state is ignored.
39
+ * ### CSS modifiers
40
+ * - `on` theatre mode is active.
41
+ * - `off` — theatre mode is inactive.
45
42
  *
46
- * ### Root element modifiers
47
- * The root `<div>` receives the public class name defined by `theatre` and the
48
- * following BEM-style modifier classes:
49
- * - `--on` — when theatre mode is active.
50
- * - `--off` — when theatre mode is inactive.
51
- *
52
- * ### Child elements
53
- * - `__stage` — container rendered inside the root that holds the duplicated
54
- * `children` when theatre mode is active. Only mounted when `isOn` is `true`.
55
- * - `__open-btn` — clickable element that activates theatre mode.
56
- * - `__close-btn` — clickable element that deactivates theatre mode.
43
+ * ### CSS elements
44
+ * - `stage` holds the duplicated `children`, mounted only when `isOn`.
45
+ * - `open-btn`
46
+ * - `close-btn`
57
47
  *
58
48
  * @param props - Component properties.
59
49
  * @see {@link Props}
60
50
  * @returns A root `<div>` containing the children in their original position,
61
- * a stage overlay with the duplicated children (when active), and the open/close
62
- * toggle buttons.
51
+ * a stage overlay with the duplicated children when active, and both toggles.
52
+ *
53
+ * @remarks
54
+ * - In controlled mode (`isOn` defined), the state is fully driven by the
55
+ * parent and internal state is never updated.
56
+ * - `onToggleClicked` fires in both modes, for all four toggle sources — a
57
+ * controlled parent needs it to know a toggle was requested at all.
58
+ * - `onIsOnChanged` fires in both modes too, and never on mount.
63
59
  */
64
60
  export declare const Theatre: FunctionComponent<Props>;