@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
@@ -9,15 +9,14 @@ import cssModule from './styles.module.css';
9
9
  /**
10
10
  * Controlled before/after comparison component.
11
11
  *
12
- * Renders two content panels separated by a draggable divider whose position
13
- * is expressed as a ratio between `0` and `1`. Supports both mouse and touch
14
- * interactions, distinguishing clicks from drags.
12
+ * Renders two content panels separated by a divider whose position is expressed
13
+ * as a ratio between `0` and `1`. Handles both mouse and touch input, telling a
14
+ * click apart from a drag.
15
15
  *
16
16
  * The active ratio is exposed as:
17
- * - CSS custom properties:
18
- * - `--{prefix}-ratio`
19
- * - `--{prefix}-ratio-percent`
20
- * - A `data-ratio` attribute on the root element.
17
+ * - CSS custom properties `--lm-before-after-ratio` and
18
+ * `--lm-before-after-ratio-percent`,
19
+ * - a `data-ratio` attribute on the root element.
21
20
  *
22
21
  * ### CSS modifiers
23
22
  * - `horizontal` — applied when `mode` is `'horizontal'`.
@@ -26,14 +25,18 @@ import cssModule from './styles.module.css';
26
25
  * ### CSS elements
27
26
  * - `before`
28
27
  * - `after`
28
+ * - `separator`
29
29
  *
30
30
  * @param props - Component properties.
31
31
  * @see {@link Props}
32
- * @returns A split-panel container with pointer interaction handlers and ratio state applied.
32
+ * @returns A split-panel container reporting pointer interactions as ratios.
33
+ *
34
+ * @remarks
35
+ * The component holds no ratio state: it only reports where the pointer is, and
36
+ * renders whatever `ratio` it is given. Turning one into the other is the
37
+ * uncontrolled wrapper's job.
33
38
  */
34
- export const BeforeAfterControlled = ({ mode = 'horizontal', ratio = 0, before, after, className, children, actionHandlers }) => {
35
- // eslint-disable-next-line no-param-reassign
36
- ratio = clamp(ratio, 0, 1);
39
+ export const ControlledBeforeAfter = ({ mode = 'horizontal', ratio = 0, before, after, className, children, onDragged, onClicked }) => {
37
40
  // State & refs
38
41
  const rootRef = useRef(null);
39
42
  const isPointerDown = useRef(false);
@@ -48,71 +51,62 @@ export const BeforeAfterControlled = ({ mode = 'horizontal', ratio = 0, before,
48
51
  yRatio: (clientY - rect.top) / rect.height
49
52
  };
50
53
  };
51
- // Handlers
52
- const handleMouseDown = event => {
54
+ const reportDrag = (clientX, clientY) => {
55
+ const ratios = getRatios(clientX, clientY);
56
+ if (ratios !== null)
57
+ onDragged?.(ratios.xRatio, ratios.yRatio);
58
+ };
59
+ const reportClick = (clientX, clientY) => {
60
+ const ratios = getRatios(clientX, clientY);
61
+ if (ratios !== null)
62
+ onClicked?.(ratios.xRatio, ratios.yRatio);
63
+ };
64
+ // User action handlers
65
+ const handlePointerDown = () => {
53
66
  isPointerDown.current = true;
54
67
  hasDragged.current = false;
55
68
  };
69
+ const handlePointerUp = () => {
70
+ isPointerDown.current = false;
71
+ hasDragged.current = false;
72
+ };
56
73
  const handleMouseMove = event => {
57
74
  if (!isPointerDown.current)
58
75
  return;
59
76
  hasDragged.current = true;
60
- const ratios = getRatios(event.clientX, event.clientY);
61
- if (ratios !== null)
62
- actionHandlers?.dragged?.(ratios.xRatio, ratios.yRatio);
77
+ reportDrag(event.clientX, event.clientY);
63
78
  };
64
79
  const handleMouseUp = event => {
65
- if (!hasDragged.current) {
66
- const ratios = getRatios(event.clientX, event.clientY);
67
- if (ratios !== null)
68
- actionHandlers?.clicked?.(ratios.xRatio, ratios.yRatio);
69
- }
70
- isPointerDown.current = false;
71
- hasDragged.current = false;
72
- };
73
- const handleTouchStart = event => {
74
- isPointerDown.current = true;
75
- hasDragged.current = false;
80
+ if (!hasDragged.current)
81
+ reportClick(event.clientX, event.clientY);
82
+ handlePointerUp();
76
83
  };
77
84
  const handleTouchMove = event => {
78
- const t = event.touches[0];
79
- if (t === undefined)
85
+ const touch = event.touches[0];
86
+ if (touch === undefined)
80
87
  return;
81
88
  if (!isPointerDown.current)
82
89
  return;
83
90
  hasDragged.current = true;
84
- const ratios = getRatios(t.clientX, t.clientY);
85
- if (ratios !== null)
86
- actionHandlers?.dragged?.(ratios.xRatio, ratios.yRatio);
91
+ reportDrag(touch.clientX, touch.clientY);
87
92
  };
88
93
  const handleTouchEnd = event => {
89
- if (!hasDragged.current) {
90
- const t = event.changedTouches[0];
91
- if (t !== undefined) {
92
- const ratios = getRatios(t.clientX, t.clientY);
93
- if (ratios !== null)
94
- actionHandlers?.clicked?.(ratios.xRatio, ratios.yRatio);
95
- }
96
- }
97
- isPointerDown.current = false;
98
- hasDragged.current = false;
94
+ const touch = event.changedTouches[0];
95
+ if (!hasDragged.current && touch !== undefined)
96
+ reportClick(touch.clientX, touch.clientY);
97
+ handlePointerUp();
99
98
  };
100
- // Classes & attributes
99
+ // Rendering
100
+ const clampedRatio = clamp(ratio, 0, 1);
101
101
  const c = clss(publicClassName, { cssModule });
102
102
  const rootClss = mergeClassNames(c(null, {
103
103
  horizontal: mode === 'horizontal',
104
104
  vertical: mode === 'vertical'
105
105
  }), className);
106
- const beforeClass = c('before');
107
- const afterClass = c('after');
108
- const separatorClass = c('separator');
106
+ const roundedRatio = round(clampedRatio, 4);
109
107
  const customProps = {
110
- [`--${publicClassName}-ratio`]: `${round(ratio, 4)}`,
111
- [`--${publicClassName}-ratio-percent`]: `${round(ratio * 100, 2)}%`
108
+ '--lm-before-after-ratio': `${roundedRatio}`,
109
+ '--lm-before-after-ratio-percent': `${round(clampedRatio * 100, 2)}%`
112
110
  };
113
- const dataAttributes = {
114
- 'data-ratio': `${round(ratio, 4)}`
115
- };
116
- // Rendering
117
- return _jsxs("div", { ref: rootRef, className: rootClss, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: { ...customProps }, ...dataAttributes, children: [_jsx("div", { className: beforeClass, children: before }), _jsx("div", { className: afterClass, children: after }), _jsx("div", { className: separatorClass }), children] });
111
+ return _jsxs("div", { ref: rootRef, className: rootClss, onMouseDown: handlePointerDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onTouchStart: handlePointerDown, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: { ...customProps }, "data-ratio": roundedRatio, children: [_jsx("div", { className: c('before'), children: before }), _jsx("div", { className: c('after'), children: after }), _jsx("div", { className: c('separator') }), children] });
118
112
  };
@@ -3,39 +3,36 @@ import { type Props as ControlledProps } from './index.controlled.js';
3
3
  /**
4
4
  * Props for the {@link BeforeAfter} component.
5
5
  *
6
- * Extends {@link ControlledProps} with uncontrolled defaults and state callbacks.
7
- * When `ratio` is provided (inherited from {@link ControlledProps}), the component
8
- * operates in controlled mode and internal state is ignored.
6
+ * Extends {@link ControlledProps} with uncontrolled divider positioning. When
7
+ * `ratio` is provided, the component operates in controlled mode.
9
8
  *
10
- * @property defaultRatio - Initial divider position in uncontrolled mode, as a value
9
+ * @property defaultRatio - Initial divider position in uncontrolled mode,
11
10
  * between `0` and `1`. Ignored when `ratio` is provided. Defaults to `0.5`.
12
- * @property stateHandlers - Optional callbacks invoked when derived state changes:
13
- * - `ratioChanged` — called after the internal ratio has been updated, with the new ratio value.
11
+ * @property onRatioChanged - Called after the divider position changed, with the
12
+ * new ratio.
14
13
  */
15
14
  export type Props = ControlledProps & {
16
15
  defaultRatio?: number;
17
- stateHandlers?: {
18
- ratioChanged?: (ratio: number) => void;
19
- };
16
+ onRatioChanged?: (ratio: number) => void;
20
17
  };
21
18
  /**
22
- * Before/after comparison component with optional controlled and uncontrolled behavior.
19
+ * Before/after comparison component supporting controlled and uncontrolled usage.
23
20
  *
24
- * Wraps {@link BeforeAfterControlled} and manages internal divider position when
25
- * operating in uncontrolled mode. The active axis used to derive the ratio from
26
- * pointer position depends on `mode`: horizontal interactions use the x ratio,
27
- * vertical interactions use the y ratio.
28
- *
29
- * @remarks
30
- * - In controlled mode (`ratio` defined), divider position is fully driven by the prop
31
- * and internal state is never updated.
32
- * - In uncontrolled mode, internal state is initialized from `defaultRatio` and updated
33
- * on both drag and click interactions.
34
- * - `actionHandlers.dragged` and `actionHandlers.clicked` are always forwarded to the
35
- * underlying controlled component, regardless of mode.
21
+ * Turns the pointer ratios reported by {@link ControlledBeforeAfter} into a
22
+ * divider position, reading the axis that matches `mode`: the x ratio when
23
+ * horizontal, the y ratio when vertical.
36
24
  *
37
25
  * @param props - Component properties.
38
26
  * @see {@link Props}
39
- * @returns A {@link BeforeAfterControlled} instance with ratio state managed internally when uncontrolled.
27
+ * @see {@link ControlledBeforeAfter} for the rendered markup and CSS elements.
28
+ * @returns A {@link ControlledBeforeAfter} with the divider position managed
29
+ * internally when uncontrolled.
30
+ *
31
+ * @remarks
32
+ * - In controlled mode (`ratio` defined), the divider position is fully driven
33
+ * by the parent and internal state is never updated.
34
+ * - `onDragged` and `onClicked` fire in both modes — a controlled parent needs
35
+ * them to know where the pointer went.
36
+ * - `onRatioChanged` fires in both modes too, and never on mount.
40
37
  */
41
38
  export declare const BeforeAfter: FunctionComponent<Props>;
@@ -1,54 +1,62 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
- import { BeforeAfterControlled } from './index.controlled.js';
3
+ import { useChangeDispatch } from '../utils/index.js';
4
+ import { ControlledBeforeAfter } from './index.controlled.js';
4
5
  /**
5
- * Before/after comparison component with optional controlled and uncontrolled behavior.
6
- *
7
- * Wraps {@link BeforeAfterControlled} and manages internal divider position when
8
- * operating in uncontrolled mode. The active axis used to derive the ratio from
9
- * pointer position depends on `mode`: horizontal interactions use the x ratio,
10
- * vertical interactions use the y ratio.
6
+ * Pointer ratios are stretched by this much on each side before becoming the
7
+ * divider position, so the very edges stay reachable without having to land the
8
+ * pointer exactly on the component's border.
9
+ */
10
+ const edgeOvershoot = 0.01;
11
+ /** Turns a raw pointer ratio into a divider position, edges included. */
12
+ function toDividerRatio(pointerRatio) {
13
+ return pointerRatio * (1 + 2 * edgeOvershoot) - edgeOvershoot;
14
+ }
15
+ /**
16
+ * Before/after comparison component supporting controlled and uncontrolled usage.
11
17
  *
12
- * @remarks
13
- * - In controlled mode (`ratio` defined), divider position is fully driven by the prop
14
- * and internal state is never updated.
15
- * - In uncontrolled mode, internal state is initialized from `defaultRatio` and updated
16
- * on both drag and click interactions.
17
- * - `actionHandlers.dragged` and `actionHandlers.clicked` are always forwarded to the
18
- * underlying controlled component, regardless of mode.
18
+ * Turns the pointer ratios reported by {@link ControlledBeforeAfter} into a
19
+ * divider position, reading the axis that matches `mode`: the x ratio when
20
+ * horizontal, the y ratio when vertical.
19
21
  *
20
22
  * @param props - Component properties.
21
23
  * @see {@link Props}
22
- * @returns A {@link BeforeAfterControlled} instance with ratio state managed internally when uncontrolled.
24
+ * @see {@link ControlledBeforeAfter} for the rendered markup and CSS elements.
25
+ * @returns A {@link ControlledBeforeAfter} with the divider position managed
26
+ * internally when uncontrolled.
27
+ *
28
+ * @remarks
29
+ * - In controlled mode (`ratio` defined), the divider position is fully driven
30
+ * by the parent and internal state is never updated.
31
+ * - `onDragged` and `onClicked` fire in both modes — a controlled parent needs
32
+ * them to know where the pointer went.
33
+ * - `onRatioChanged` fires in both modes too, and never on mount.
23
34
  */
24
- export const BeforeAfter = ({ mode = 'horizontal', actionHandlers, stateHandlers, ratio, defaultRatio = 0.5, ...controlledProps }) => {
35
+ export const BeforeAfter = ({ mode = 'horizontal', ratio, defaultRatio = 0.5, onDragged, onClicked, onRatioChanged, ...controlledProps }) => {
36
+ // State
25
37
  const [internalRatio, setInternalRatio] = useState(defaultRatio);
26
38
  const isControlled = ratio !== undefined;
27
39
  const effectiveRatio = isControlled ? ratio : internalRatio;
28
- const handleDrag = (x, y) => {
29
- // eslint-disable-next-line no-param-reassign
30
- x = 1.02 * x - 0.01;
31
- // eslint-disable-next-line no-param-reassign
32
- y = 1.02 * y - 0.01;
33
- actionHandlers?.dragged?.(x, y);
34
- if (!isControlled) {
35
- setInternalRatio(mode === 'horizontal' ? x : y);
36
- stateHandlers?.ratioChanged?.(mode === 'horizontal' ? x : y);
37
- }
40
+ // State dispatch
41
+ useChangeDispatch(effectiveRatio, onRatioChanged);
42
+ // User action handlers
43
+ const moveDividerTo = (xRatio, yRatio) => {
44
+ if (isControlled)
45
+ return;
46
+ setInternalRatio(mode === 'horizontal' ? xRatio : yRatio);
47
+ };
48
+ const handleDrag = (xRatio, yRatio) => {
49
+ const x = toDividerRatio(xRatio);
50
+ const y = toDividerRatio(yRatio);
51
+ onDragged?.(x, y);
52
+ moveDividerTo(x, y);
38
53
  };
39
- const handleClick = (x, y) => {
40
- // eslint-disable-next-line no-param-reassign
41
- x = 1.02 * x - 0.01;
42
- // eslint-disable-next-line no-param-reassign
43
- y = 1.02 * y - 0.01;
44
- actionHandlers?.clicked?.(x, y);
45
- if (!isControlled) {
46
- setInternalRatio(mode === 'horizontal' ? x : y);
47
- stateHandlers?.ratioChanged?.(mode === 'horizontal' ? x : y);
48
- }
54
+ const handleClick = (xRatio, yRatio) => {
55
+ const x = toDividerRatio(xRatio);
56
+ const y = toDividerRatio(yRatio);
57
+ onClicked?.(x, y);
58
+ moveDividerTo(x, y);
49
59
  };
50
- return _jsx(BeforeAfterControlled, { ...controlledProps, mode: mode, ratio: effectiveRatio, actionHandlers: {
51
- dragged: handleDrag,
52
- clicked: handleClick
53
- } });
60
+ // Rendering
61
+ return _jsx(ControlledBeforeAfter, { ...controlledProps, mode: mode, ratio: effectiveRatio, onDragged: handleDrag, onClicked: handleClick });
54
62
  };
@@ -15,6 +15,6 @@ import cssModule from './styles.module.css';
15
15
  */
16
16
  export const Button = ({ className, ...rest }) => {
17
17
  const c = clss(publicClassName, { cssModule });
18
- const rootClss = mergeClassNames(c(null), className);
18
+ const rootClss = mergeClassNames(c(), className);
19
19
  return _jsx("button", { ...rest, className: rootClss });
20
20
  };
@@ -3,40 +3,27 @@ import type { WithClassName } from '../utils/types.js';
3
3
  /**
4
4
  * Props for the {@link Clippable} component.
5
5
  *
6
- * Extends {@link WithClassName} with clipboard-related configuration and callbacks.
7
- *
8
- * @property toClip - Content written to the clipboard. When omitted, the current
9
- * content container's `innerHTML` is used. A function may be provided to transform
10
- * the current content before it is written.
11
- * @property actionHandlers - Optional user action callbacks:
12
- * - `clicked` called when the copy button is clicked, before clipboard content is resolved.
13
- * @property stateHandlers - Optional callbacks invoked when derived state changes:
14
- * - `clipped` — called after content has been successfully written to the clipboard.
6
+ * @property toClip - Content written to the clipboard. When omitted, the
7
+ * content container's `innerHTML` is used. A function may be provided to
8
+ * transform that current content before it is written.
9
+ * @property onCopyClicked - Called when the copy button is clicked, before the
10
+ * clipboard content is resolved, with the container's raw HTML.
11
+ * @property onClipped - Called once content has been written to the clipboard.
12
+ * Not called when the write fails.
15
13
  * @property className - Additional class name(s) applied to the root element.
16
14
  * @property children - Content rendered inside the copyable container.
17
15
  */
18
16
  export type Props = PropsWithChildren<WithClassName<{
19
17
  toClip?: string | ((curr: string | undefined) => string | undefined);
20
- actionHandlers?: {
21
- clicked?: (e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>, rawContent: string | undefined) => void;
22
- };
23
- stateHandlers?: {
24
- clipped?: (content: string) => void;
25
- };
18
+ onCopyClicked?: (e: MouseEvent<HTMLButtonElement, globalThis.MouseEvent>, rawContent: string | undefined) => void;
19
+ onClipped?: (content: string) => void;
26
20
  }>>;
27
21
  /**
28
- * Clipboard-enabled container component.
29
- *
30
- * Renders arbitrary content alongside a copy control. When activated,
31
- * the component writes HTML content to the clipboard using the
32
- * `text/html` MIME type.
33
- *
34
- * Supports content overriding and transformation through the `toClip`
35
- * prop, as well as action and state callbacks.
22
+ * Clipboard-enabled container. Renders arbitrary content alongside a copy
23
+ * control that writes it to the clipboard, as both `text/html` and `text/plain`.
36
24
  *
37
25
  * ### CSS modifiers
38
- * The following modifiers are applied automatically:
39
- * - `clipped` — `true` during the 3 seconds following a successful clipboard write.
26
+ * - `clipped` — on during the 3 seconds following a successful write.
40
27
  *
41
28
  * ### CSS elements
42
29
  * - `copy`
@@ -44,6 +31,10 @@ export type Props = PropsWithChildren<WithClassName<{
44
31
  *
45
32
  * @param props - Component properties.
46
33
  * @see {@link Props}
47
- * @returns A copy-enabled content container with clipboard state modifiers applied.
34
+ * @returns A copy-enabled content container.
35
+ *
36
+ * @remarks
37
+ * A failed clipboard write is logged and leaves the component untouched:
38
+ * neither `onClipped` nor the `clipped` modifier fires.
48
39
  */
49
40
  export declare const Clippable: FunctionComponent<Props>;
@@ -4,19 +4,14 @@ import { clss } from '../../agnostic/css/clss/index.js';
4
4
  import { mergeClassNames } from '../utils/index.js';
5
5
  import { clippable as publicClassName } from '../public-classnames.js';
6
6
  import cssModule from './styles.module.css';
7
+ /** How long the `clipped` modifier stays on after a successful write. */
8
+ const clippedModifierDurationMs = 3000;
7
9
  /**
8
- * Clipboard-enabled container component.
9
- *
10
- * Renders arbitrary content alongside a copy control. When activated,
11
- * the component writes HTML content to the clipboard using the
12
- * `text/html` MIME type.
13
- *
14
- * Supports content overriding and transformation through the `toClip`
15
- * prop, as well as action and state callbacks.
10
+ * Clipboard-enabled container. Renders arbitrary content alongside a copy
11
+ * control that writes it to the clipboard, as both `text/html` and `text/plain`.
16
12
  *
17
13
  * ### CSS modifiers
18
- * The following modifiers are applied automatically:
19
- * - `clipped` — `true` during the 3 seconds following a successful clipboard write.
14
+ * - `clipped` — on during the 3 seconds following a successful write.
20
15
  *
21
16
  * ### CSS elements
22
17
  * - `copy`
@@ -24,22 +19,26 @@ import cssModule from './styles.module.css';
24
19
  *
25
20
  * @param props - Component properties.
26
21
  * @see {@link Props}
27
- * @returns A copy-enabled content container with clipboard state modifiers applied.
22
+ * @returns A copy-enabled content container.
23
+ *
24
+ * @remarks
25
+ * A failed clipboard write is logged and leaves the component untouched:
26
+ * neither `onClipped` nor the `clipped` modifier fires.
28
27
  */
29
- export const Clippable = ({ className, children, toClip, actionHandlers, stateHandlers }) => {
28
+ export const Clippable = ({ className, children, toClip, onCopyClicked, onClipped }) => {
30
29
  // State & refs
31
- const [beenRecentlyClipped, setBeenRecentlyClipped] = useState(false);
30
+ const [hasBeenRecentlyClipped, setHasBeenRecentlyClipped] = useState(false);
32
31
  const contentRef = useRef(null);
33
32
  const clippedTimeoutRef = useRef(null);
34
33
  // User action handlers
35
34
  const handleCopyClick = async (e) => {
36
- const rawhHtml = contentRef.current?.innerHTML;
37
- actionHandlers?.clicked?.(e, rawhHtml);
35
+ const rawHtml = contentRef.current?.innerHTML;
36
+ onCopyClicked?.(e, rawHtml);
38
37
  const html = typeof toClip === 'string'
39
38
  ? toClip
40
39
  : typeof toClip === 'function'
41
- ? toClip(rawhHtml)
42
- : rawhHtml;
40
+ ? toClip(rawHtml)
41
+ : rawHtml;
43
42
  if (html === undefined)
44
43
  return;
45
44
  try {
@@ -53,19 +52,18 @@ export const Clippable = ({ className, children, toClip, actionHandlers, stateHa
53
52
  catch (err) {
54
53
  // eslint-disable-next-line no-console
55
54
  console.error(err);
55
+ return;
56
56
  }
57
- stateHandlers?.clipped?.(html);
58
- setBeenRecentlyClipped(true);
57
+ onClipped?.(html);
58
+ setHasBeenRecentlyClipped(true);
59
59
  if (clippedTimeoutRef.current !== null)
60
60
  clearTimeout(clippedTimeoutRef.current);
61
- clippedTimeoutRef.current = setTimeout(() => {
62
- setBeenRecentlyClipped(false);
63
- }, 3000);
61
+ clippedTimeoutRef.current = setTimeout(() => setHasBeenRecentlyClipped(false), clippedModifierDurationMs);
64
62
  };
65
63
  // Rendering
66
64
  const c = clss(publicClassName, { cssModule });
67
65
  const rootClss = mergeClassNames(c(null, {
68
- clipped: beenRecentlyClipped
66
+ clipped: hasBeenRecentlyClipped
69
67
  }), className);
70
68
  const copyClss = c('copy');
71
69
  const contentClss = c('content');
@@ -1,45 +1,52 @@
1
1
  import { type ReactNode, type PropsWithChildren, type FunctionComponent } from 'react';
2
2
  import type { WithClassName } from '../utils/types.js';
3
3
  /**
4
- * Props for the Disclaimer component.
4
+ * Props for the {@link Disclaimer} component.
5
5
  *
6
6
  * @property content - Content displayed inside the disclaimer panel.
7
7
  * @property togglerContent - Content rendered inside the dismiss toggler.
8
- * If not provided, the toggler is not rendered.
9
- * @property isOn - Controls the visibility state. When defined, the component
10
- * behaves as a controlled component.
11
- * @property defaultIsOn - Default visibility state for uncontrolled mode.
12
- * @property stateHandlers - Callbacks invoked after state changes.
13
- * @property stateHandlers.toggled - Callback invoked after the disclaimer state changes.
14
- * @property actionHandlers - Callbacks invoked before actions are committed.
15
- * @property actionHandlers.dismissClick - Callback invoked before the disclaimer is dismissed.
8
+ * When omitted, the toggler is not rendered.
9
+ * @property isOn - Controlled visibility state. When defined, the component
10
+ * behaves as a controlled component and internal state is never updated.
11
+ * @property defaultIsOn - Initial visibility state in uncontrolled mode.
12
+ * Ignored when `isOn` is provided. Defaults to `true`.
13
+ * @property onDismissClicked - Called when the toggler is clicked, before the
14
+ * disclaimer reacts, with the visibility state as it was.
15
+ * @property onIsOnChanged - Called after the visibility state changed, with the
16
+ * new value.
16
17
  * @property className - Optional additional class name(s) applied to the root element.
17
- * @property children - Additional content rendered below the disclaimer panel.
18
+ * @property children - The content the disclaimer gates, rendered below the panel.
18
19
  */
19
20
  export type Props = PropsWithChildren<WithClassName<{
20
21
  content?: ReactNode;
21
22
  togglerContent?: ReactNode;
22
23
  isOn?: boolean;
23
24
  defaultIsOn?: boolean;
24
- stateHandlers?: {
25
- toggled?: (isOn: boolean) => void;
26
- };
27
- actionHandlers?: {
28
- dismissClick?: (prevIsOn: boolean) => void;
29
- };
25
+ onDismissClicked?: (isOn: boolean) => void;
26
+ onIsOnChanged?: (isOn: boolean) => void;
30
27
  }>>;
31
28
  /**
32
- * Component that displays a dismissible disclaimer panel.
29
+ * Dismissible disclaimer panel, gating the content it wraps.
33
30
  *
34
- * Supports both controlled (`isOn` provided) and uncontrolled modes.
31
+ * ### CSS modifiers
32
+ * - `on` — the disclaimer is showing.
33
+ * - `off` — the disclaimer has been dismissed.
34
+ *
35
+ * ### CSS elements
36
+ * - `panel`
37
+ * - `content`
38
+ * - `toggler`
39
+ * - `sensitive` — wraps `children`.
35
40
  *
36
41
  * @param props - Component properties.
37
42
  * @see {@link Props}
43
+ * @returns A root `<div>` holding the disclaimer panel and the gated content.
38
44
  *
39
45
  * @remarks
40
- * - In controlled mode, visibility is driven by `isOn` and internal state
41
- * does not toggle automatically.
42
- * - In uncontrolled mode, the component manages its own visibility state.
43
- * - Applies `on` and `off` modifier classes depending on visibility state.
46
+ * - In controlled mode (`isOn` defined), visibility is fully driven by the
47
+ * parent and internal state is never updated.
48
+ * - `onDismissClicked` fires in both modes a controlled parent needs it to
49
+ * know a click happened at all.
50
+ * - `onIsOnChanged` fires in both modes too, and never on mount.
44
51
  */
45
52
  export declare const Disclaimer: FunctionComponent<Props>;
@@ -1,39 +1,44 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useRef } from 'react';
2
+ import { useState } from 'react';
3
3
  import { clss } from '../../agnostic/css/clss/index.js';
4
- import { mergeClassNames } from '../utils/index.js';
4
+ import { mergeClassNames, useChangeDispatch } from '../utils/index.js';
5
5
  import { disclaimer as publicClassName } from '../public-classnames.js';
6
6
  import cssModule from './styles.module.css';
7
7
  /**
8
- * Component that displays a dismissible disclaimer panel.
8
+ * Dismissible disclaimer panel, gating the content it wraps.
9
9
  *
10
- * Supports both controlled (`isOn` provided) and uncontrolled modes.
10
+ * ### CSS modifiers
11
+ * - `on` — the disclaimer is showing.
12
+ * - `off` — the disclaimer has been dismissed.
13
+ *
14
+ * ### CSS elements
15
+ * - `panel`
16
+ * - `content`
17
+ * - `toggler`
18
+ * - `sensitive` — wraps `children`.
11
19
  *
12
20
  * @param props - Component properties.
13
21
  * @see {@link Props}
22
+ * @returns A root `<div>` holding the disclaimer panel and the gated content.
14
23
  *
15
24
  * @remarks
16
- * - In controlled mode, visibility is driven by `isOn` and internal state
17
- * does not toggle automatically.
18
- * - In uncontrolled mode, the component manages its own visibility state.
19
- * - Applies `on` and `off` modifier classes depending on visibility state.
25
+ * - In controlled mode (`isOn` defined), visibility is fully driven by the
26
+ * parent and internal state is never updated.
27
+ * - `onDismissClicked` fires in both modes a controlled parent needs it to
28
+ * know a click happened at all.
29
+ * - `onIsOnChanged` fires in both modes too, and never on mount.
20
30
  */
21
- export const Disclaimer = ({ content, togglerContent, isOn: isOnProp, defaultIsOn: defaultIsOnProp, stateHandlers, actionHandlers, children, className }) => {
22
- // State & refs
23
- const [internalIsOn, setInternalIsOn] = useState(isOnProp ?? defaultIsOnProp ?? true);
31
+ export const Disclaimer = ({ content, togglerContent, isOn: isOnProp, defaultIsOn = true, onDismissClicked, onIsOnChanged, children, className }) => {
32
+ // State
33
+ const [internalIsOn, setInternalIsOn] = useState(defaultIsOn);
34
+ const isControlled = isOnProp !== undefined;
24
35
  const isOn = isOnProp ?? internalIsOn;
25
- const pIsOn = useRef(isOn);
26
- // State change handlers
27
- useEffect(() => {
28
- if (pIsOn.current === isOn)
29
- return;
30
- stateHandlers?.toggled?.(isOn);
31
- pIsOn.current = isOn;
32
- }, [isOn]);
33
- // User actions handlers
36
+ // State dispatch
37
+ useChangeDispatch(isOn, onIsOnChanged);
38
+ // User action handlers
34
39
  const handleDismissClick = () => {
35
- actionHandlers?.dismissClick?.(isOn);
36
- if (isOnProp !== undefined)
40
+ onDismissClicked?.(isOn);
41
+ if (isControlled)
37
42
  return;
38
43
  setInternalIsOn(false);
39
44
  };
@@ -45,7 +50,7 @@ export const Disclaimer = ({ content, togglerContent, isOn: isOnProp, defaultIsO
45
50
  }), className);
46
51
  const panelClss = c('panel');
47
52
  const contentClss = c('content');
48
- const btnClss = c('toggler');
53
+ const togglerClss = c('toggler');
49
54
  const sensitiveClss = c('sensitive');
50
- return _jsxs("div", { className: rootClss, children: [_jsxs("div", { className: panelClss, children: [content !== undefined && _jsx("div", { className: contentClss, children: content }), togglerContent !== undefined && _jsx("div", { className: btnClss, onClick: handleDismissClick, children: togglerContent })] }), _jsx("div", { className: sensitiveClss, children: children })] });
55
+ return _jsxs("div", { className: rootClss, children: [_jsxs("div", { className: panelClss, children: [content !== undefined && _jsx("div", { className: contentClss, children: content }), togglerContent !== undefined && _jsx("div", { className: togglerClss, onClick: handleDismissClick, children: togglerContent })] }), _jsx("div", { className: sensitiveClss, children: children })] });
51
56
  };