@konce-pt/react 0.11.1 → 0.12.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 (203) hide show
  1. package/README.md +8 -2
  2. package/dist/accordion/accordion.js +11 -10
  3. package/dist/alert/alert.js +1 -0
  4. package/dist/alert/llms.txt +1 -0
  5. package/dist/app-shell/app-shell.js +12 -5
  6. package/dist/app-shell/llms.txt +5 -4
  7. package/dist/auth/auth.js +17 -8
  8. package/dist/auth/llms.txt +3 -2
  9. package/dist/autocomplete/autocomplete.d.ts +1 -1
  10. package/dist/autocomplete/autocomplete.js +12 -4
  11. package/dist/autocomplete/llms.txt +3 -0
  12. package/dist/avatar/avatar.js +1 -0
  13. package/dist/avatar-group/avatar-group.js +1 -0
  14. package/dist/backdrop/backdrop.d.ts +0 -19
  15. package/dist/backdrop/backdrop.js +18 -13
  16. package/dist/bottom-sheet/bottom-sheet.d.ts +6 -1
  17. package/dist/bottom-sheet/bottom-sheet.js +3 -2
  18. package/dist/bottom-sheet/llms.txt +3 -0
  19. package/dist/breadcrumb/breadcrumb.d.ts +7 -1
  20. package/dist/breadcrumb/breadcrumb.js +10 -2
  21. package/dist/breadcrumb/llms.txt +8 -0
  22. package/dist/button/button.js +2 -1
  23. package/dist/button-group/button-group.d.ts +15 -2
  24. package/dist/button-group/button-group.js +31 -4
  25. package/dist/button-group/llms.txt +9 -6
  26. package/dist/card/card.js +4 -3
  27. package/dist/carousel/carousel.js +4 -1
  28. package/dist/charts/chart-legend.js +1 -0
  29. package/dist/charts/chart.d.ts +3 -2
  30. package/dist/charts/chart.js +12 -4
  31. package/dist/charts/llms.txt +6 -3
  32. package/dist/chat/chat.js +39 -9
  33. package/dist/chat/llms.txt +9 -0
  34. package/dist/chat-composer/chat-composer.d.ts +5 -2
  35. package/dist/chat-composer/chat-composer.js +10 -1
  36. package/dist/chat-dock/chat-dock.js +1 -0
  37. package/dist/chat-message/chat-message.js +2 -1
  38. package/dist/chat-typing/chat-typing.js +1 -0
  39. package/dist/checkbox/checkbox.d.ts +1 -1
  40. package/dist/checkbox/checkbox.js +9 -2
  41. package/dist/checkbox/llms.txt +3 -0
  42. package/dist/chip/chip.js +1 -0
  43. package/dist/chips-input/chips-input.d.ts +3 -1
  44. package/dist/chips-input/chips-input.js +14 -5
  45. package/dist/chips-input/llms.txt +3 -0
  46. package/dist/clock/clock.d.ts +9 -4
  47. package/dist/clock/clock.js +35 -5
  48. package/dist/clock/llms.txt +6 -2
  49. package/dist/color-picker/color-picker.d.ts +5 -1
  50. package/dist/color-picker/color-picker.js +36 -17
  51. package/dist/color-picker/llms.txt +3 -0
  52. package/dist/confirm/confirm-container.js +1 -0
  53. package/dist/confirm/confirm-store.js +6 -0
  54. package/dist/confirm/llms.txt +2 -1
  55. package/dist/context-menu/context-menu.js +35 -12
  56. package/dist/context-menu/llms.txt +3 -0
  57. package/dist/data-table/data-table.d.ts +12 -1
  58. package/dist/data-table/data-table.js +113 -51
  59. package/dist/data-table/export-csv.d.ts +2 -2
  60. package/dist/data-table/export-csv.js +7 -9
  61. package/dist/data-table/llms.txt +19 -4
  62. package/dist/data-view/data-view.js +1 -0
  63. package/dist/date-range/date-range.d.ts +16 -4
  64. package/dist/date-range/date-range.js +69 -46
  65. package/dist/date-range/llms.txt +17 -3
  66. package/dist/datepicker/datepicker.d.ts +17 -3
  67. package/dist/datepicker/datepicker.js +86 -54
  68. package/dist/datepicker/llms.txt +17 -3
  69. package/dist/dialog/dialog.js +29 -8
  70. package/dist/dialog/llms.txt +3 -0
  71. package/dist/divider/divider.d.ts +7 -1
  72. package/dist/divider/divider.js +3 -2
  73. package/dist/divider/llms.txt +1 -0
  74. package/dist/drawer/drawer.d.ts +6 -1
  75. package/dist/drawer/drawer.js +3 -2
  76. package/dist/drawer/llms.txt +3 -0
  77. package/dist/fieldset/fieldset.js +4 -1
  78. package/dist/file-upload/file-upload.d.ts +7 -1
  79. package/dist/file-upload/file-upload.js +18 -7
  80. package/dist/file-upload/llms.txt +6 -3
  81. package/dist/form-field/form-field.js +1 -0
  82. package/dist/galleria/galleria.js +4 -1
  83. package/dist/i18n/i18n-context.d.ts +3 -3
  84. package/dist/i18n/i18n-context.js +25 -3
  85. package/dist/icon/icon.js +4 -1
  86. package/dist/image/image.js +4 -4
  87. package/dist/input/input.js +1 -0
  88. package/dist/input-mask/input-mask.d.ts +7 -2
  89. package/dist/input-mask/input-mask.js +15 -41
  90. package/dist/input-mask/llms.txt +3 -0
  91. package/dist/input-number/input-number.d.ts +3 -1
  92. package/dist/input-number/input-number.js +15 -11
  93. package/dist/input-number/llms.txt +5 -2
  94. package/dist/input-otp/input-otp.d.ts +16 -3
  95. package/dist/input-otp/input-otp.js +43 -19
  96. package/dist/input-otp/llms.txt +11 -0
  97. package/dist/internal/mask.d.ts +14 -0
  98. package/dist/internal/mask.js +62 -0
  99. package/dist/internal/number-group.d.ts +22 -0
  100. package/dist/internal/number-group.js +53 -0
  101. package/dist/internal/option-key.d.ts +6 -0
  102. package/dist/internal/option-key.js +8 -0
  103. package/dist/internal/overlay.js +37 -7
  104. package/dist/internal/password-strength.d.ts +6 -0
  105. package/dist/internal/password-strength.js +74 -0
  106. package/dist/internal/scroll-lock.d.ts +6 -0
  107. package/dist/internal/scroll-lock.js +35 -0
  108. package/dist/internal/slot-children.d.ts +13 -0
  109. package/dist/internal/slot-children.js +31 -0
  110. package/dist/internal/step.d.ts +12 -0
  111. package/dist/internal/step.js +31 -0
  112. package/dist/internal/tween.js +2 -3
  113. package/dist/internal/use-controllable-state.d.ts +5 -0
  114. package/dist/internal/use-controllable-state.js +25 -4
  115. package/dist/internal/use-modal-focus.d.ts +1 -0
  116. package/dist/internal/use-modal-focus.js +12 -1
  117. package/dist/knob/knob.d.ts +5 -2
  118. package/dist/knob/knob.js +15 -14
  119. package/dist/knob/llms.txt +4 -2
  120. package/dist/listbox/listbox.d.ts +3 -1
  121. package/dist/listbox/listbox.js +30 -10
  122. package/dist/listbox/llms.txt +3 -0
  123. package/dist/map/llms.txt +5 -1
  124. package/dist/map/map-create-dialog.js +30 -22
  125. package/dist/map/map-hours-field.js +1 -0
  126. package/dist/map/map-image-field.js +2 -1
  127. package/dist/map/map-legend.js +1 -0
  128. package/dist/map/map-poi-card.js +2 -1
  129. package/dist/map/map-search.js +54 -30
  130. package/dist/map/map.d.ts +6 -1
  131. package/dist/map/map.js +62 -34
  132. package/dist/megamenu/llms.txt +11 -0
  133. package/dist/megamenu/megamenu.d.ts +7 -1
  134. package/dist/megamenu/megamenu.js +15 -6
  135. package/dist/menu/menu-rows.js +1 -0
  136. package/dist/menu/menu.js +10 -3
  137. package/dist/menubar/llms.txt +11 -0
  138. package/dist/menubar/menubar.d.ts +7 -1
  139. package/dist/menubar/menubar.js +15 -3
  140. package/dist/meter-group/meter-group.js +1 -0
  141. package/dist/order-list/llms.txt +3 -0
  142. package/dist/order-list/order-list.js +21 -6
  143. package/dist/paginator/paginator.js +7 -4
  144. package/dist/panel/panel.js +5 -3
  145. package/dist/password/llms.txt +6 -2
  146. package/dist/password/password.js +4 -14
  147. package/dist/peek/llms.txt +1 -1
  148. package/dist/peek/peek.js +37 -15
  149. package/dist/pick-list/llms.txt +3 -0
  150. package/dist/pick-list/pick-list.js +19 -3
  151. package/dist/popover/llms.txt +3 -0
  152. package/dist/popover/popover.d.ts +3 -1
  153. package/dist/popover/popover.js +10 -3
  154. package/dist/progress/progress.js +1 -0
  155. package/dist/public-api.d.ts +1 -1
  156. package/dist/radio-group/llms.txt +3 -0
  157. package/dist/radio-group/radio-group.d.ts +3 -1
  158. package/dist/radio-group/radio-group.js +13 -2
  159. package/dist/rating/rating.js +1 -0
  160. package/dist/rich-text/llms.txt +14 -0
  161. package/dist/rich-text/rich-text.d.ts +3 -9
  162. package/dist/rich-text/rich-text.js +241 -96
  163. package/dist/rich-text/sanitize.d.ts +9 -0
  164. package/dist/rich-text/sanitize.js +77 -29
  165. package/dist/roadmap/llms.txt +9 -3
  166. package/dist/roadmap/roadmap.js +88 -11
  167. package/dist/scroll-top/scroll-top.js +1 -0
  168. package/dist/select/llms.txt +3 -0
  169. package/dist/select/select.d.ts +3 -1
  170. package/dist/select/select.js +46 -32
  171. package/dist/shared/types.d.ts +21 -0
  172. package/dist/shared/types.js +9 -1
  173. package/dist/sidenav/sidenav.d.ts +1 -2
  174. package/dist/sidenav/sidenav.js +4 -3
  175. package/dist/slider/llms.txt +3 -0
  176. package/dist/slider/slider.d.ts +3 -1
  177. package/dist/slider/slider.js +15 -4
  178. package/dist/speed-dial/speed-dial.js +2 -1
  179. package/dist/spinner/spinner.js +1 -0
  180. package/dist/split-button/split-button.js +10 -3
  181. package/dist/splitter/splitter.js +23 -8
  182. package/dist/stepper/llms.txt +3 -0
  183. package/dist/stepper/stepper.js +9 -5
  184. package/dist/switch/llms.txt +3 -0
  185. package/dist/switch/switch.d.ts +1 -1
  186. package/dist/switch/switch.js +9 -2
  187. package/dist/switch-group/llms.txt +3 -0
  188. package/dist/switch-group/switch-group.d.ts +3 -1
  189. package/dist/switch-group/switch-group.js +5 -3
  190. package/dist/tabs/llms.txt +3 -0
  191. package/dist/tabs/tabs.js +30 -14
  192. package/dist/textarea/textarea.js +1 -0
  193. package/dist/toast/llms.txt +3 -0
  194. package/dist/toast/toast-container.js +1 -0
  195. package/dist/toast/toast-store.d.ts +5 -1
  196. package/dist/toast/toast-store.js +1 -1
  197. package/dist/tooltip/llms.txt +5 -3
  198. package/dist/tooltip/tooltip.d.ts +3 -2
  199. package/dist/tooltip/tooltip.js +148 -55
  200. package/dist/tree/llms.txt +1 -0
  201. package/dist/tree/tree.d.ts +6 -1
  202. package/dist/tree/tree.js +14 -7
  203. package/package.json +15 -14
@@ -1,51 +1,73 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Fragment, useState } from 'react';
2
4
  import { cn } from "../internal/cn.js";
3
5
  import { useControllableState } from "../internal/use-controllable-state.js";
4
6
  import { useKptFieldA11y } from "../form-field/form-field-context.js";
5
7
  import { useKptMessages } from "../i18n/i18n-context.js";
6
8
  /**
7
9
  * Pole kodu jednorazowego (OTP/PIN) — N osobnych pól, automatyczne przejście do następnego
8
- * i wklejanie całego kodu naraz. Wartością jest sklejony string.
10
+ * i wklejanie całego kodu naraz. Wartością są wpisane znaki **bez dziur** (`'13'`, nie `'1 3'`), więc
11
+ * walidator długości i backend widzą tyle znaków, ile naprawdę wpisano.
9
12
  *
10
13
  * @example
11
14
  * <KptInputOtp value={code} onValueChange={setCode} length={6} integerOnly />
15
+ * <KptInputOtp value={code} onValueChange={setCode} length={9} groupSize={3} />
12
16
  */
13
- export function KptInputOtp({ value, defaultValue = '', onValueChange, length = 6, integerOnly = false, id, ariaLabel = null, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
17
+ export function KptInputOtp({ value, defaultValue = '', onValueChange, length = 6, groupSize = 0, groupSeparator = '–', integerOnly = false, id, ariaLabel = null, disabled = false, readOnly = false, required = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
14
18
  const { t } = useKptMessages({ locale, dictionary });
15
19
  const field = useKptFieldA11y(id, 'kpt-input-otp');
16
20
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
17
21
  const showInvalid = invalid && (touched ?? true);
18
- const charAt = (index) => current[index] ?? '';
19
22
  /*
20
- * Wartość jest jednym stringiem, a pól jest N — pozycje pośrodku muszą więc dać się zostawić
21
- * puste. Dopełniamy spacjami, podmieniamy znak i obcinamy końcowe spacje, żeby „a__c" dało się
22
- * zapisać, a „a" nie zamieniło się w „a ".
23
+ * Komórki trzymają układ osobno od wartości: wyczyszczenie środkowej zostawia w niej pustkę,
24
+ * zamiast przesuwać resztę kodu w lewo, a wartość to same znaki. Zmiana wartości z zewnątrz
25
+ * (inna niż złożenie bieżących komórek) układa znaki od początku — wzorzec „poprzednia
26
+ * wartość w stanie”, bez efektu.
23
27
  */
28
+ const toCells = (text) => Array.from({ length }, (_, index) => text[index] ?? '');
29
+ const [cells, setCells] = useState(() => toCells(current));
30
+ const inSync = cells.length === length && cells.join('') === current;
31
+ if (!inSync)
32
+ setCells(toCells(current));
33
+ const shown = inSync ? cells : toCells(current);
34
+ const charAt = (index) => shown[index] ?? '';
35
+ /** Zapis komórek i złożonej z nich wartości — jedno miejsce, żeby układ i model się nie rozjechały. */
36
+ const commitCells = (next) => {
37
+ setCells(next);
38
+ setCurrent(next.join(''));
39
+ };
24
40
  const setChar = (index, char) => {
25
- const chars = current.padEnd(length, ' ').split('');
26
- chars[index] = char || ' ';
27
- setCurrent(chars.join('').replace(/ +$/, ''));
41
+ const next = [...shown];
42
+ next[index] = char;
43
+ commitCells(next);
28
44
  };
45
+ /** Następna/poprzednia komórka — po polach, nie po rodzeństwie, bo między grupami stoi separator. */
29
46
  const focusSibling = (el, direction) => {
30
- const target = direction === 1 ? el.nextElementSibling : el.previousElementSibling;
31
- target?.focus();
47
+ const cells = [...(el.closest('.kpt-input-otp')?.querySelectorAll('input') ?? [])];
48
+ cells[cells.indexOf(el) + direction]?.focus();
32
49
  };
50
+ /** Czy za komórką `index` kończy się grupa (a nie cały kod). */
51
+ const group = Math.floor(groupSize);
52
+ const separatorAfter = (index) => group >= 1 && (index + 1) % group === 0 && index < length - 1;
33
53
  /** Wpisuje `text` od komórki `start` (bez spacji i — przy `integerOnly` — bez nie-cyfr). */
34
54
  const fillFrom = (start, text, el) => {
35
55
  let chars = text.replace(/\s/g, '');
36
56
  if (integerOnly)
37
57
  chars = chars.replace(/\D/g, '');
38
- const arr = current.padEnd(length, ' ').split('');
58
+ const arr = [...shown];
39
59
  for (let k = 0; k < chars.length && start + k < length; k += 1)
40
60
  arr[start + k] = chars[k];
41
- setCurrent(arr.join('').replace(/ +$/, ''));
61
+ commitCells(arr);
42
62
  // Fokus na pierwszą pustą komórkę, a przy komplecie — na ostatnią.
43
- const cells = [...(el.closest('.kpt-input-otp')?.querySelectorAll('input') ?? [])];
44
- const empty = arr.findIndex((char) => char === ' ');
45
- cells[empty >= 0 ? empty : cells.length - 1]?.focus();
63
+ const inputs = [...(el.closest('.kpt-input-otp')?.querySelectorAll('input') ?? [])];
64
+ const empty = arr.findIndex((char) => char === '');
65
+ inputs[empty >= 0 ? empty : inputs.length - 1]?.focus();
46
66
  };
47
67
  const onInput = (index, event) => {
48
68
  const el = event.target;
69
+ if (readOnly)
70
+ return;
49
71
  // Kilka znaków naraz to autouzupełnienie kodu z SMS (`one-time-code`) albo wklejenie przez
50
72
  // klawiaturę ekranową — rozkładamy je po komórkach od bieżącej. Dwa znaki w zajętej komórce
51
73
  // to zwykłe nadpisanie: stary znak + nowy.
@@ -67,7 +89,7 @@ export function KptInputOtp({ value, defaultValue = '', onValueChange, length =
67
89
  };
68
90
  const onKeyDown = (index, event) => {
69
91
  const el = event.currentTarget;
70
- if (event.key === 'Backspace' && !charAt(index))
92
+ if (event.key === 'Backspace' && !charAt(index) && !readOnly)
71
93
  focusSibling(el, -1);
72
94
  else if (event.key === 'ArrowLeft')
73
95
  focusSibling(el, -1);
@@ -76,6 +98,8 @@ export function KptInputOtp({ value, defaultValue = '', onValueChange, length =
76
98
  };
77
99
  const onPaste = (event) => {
78
100
  event.preventDefault();
101
+ if (readOnly)
102
+ return;
79
103
  fillFrom(0, event.clipboardData.getData('text'), event.currentTarget);
80
104
  };
81
105
  /** „Dotknięcie” dopiero przy wyjściu fokusu z całej grupy, nie przy przejściu między komórkami. */
@@ -84,5 +108,5 @@ export function KptInputOtp({ value, defaultValue = '', onValueChange, length =
84
108
  if (!next || !event.currentTarget.contains(next))
85
109
  onTouch?.();
86
110
  };
87
- return (_jsx("div", { className: cn('kpt-input-otp-host', className), ...rest, children: _jsx("div", { className: cn('kpt-input-otp', showInvalid && 'kpt-input-otp--invalid'), role: "group", "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onBlur: onGroupBlur, children: Array.from({ length }, (_, index) => (_jsx("input", { className: "kpt-input-otp__cell", id: index === 0 ? field.id : undefined, "aria-label": t('inputOtp.cell', { index: index + 1, count: length }), "aria-invalid": showInvalid || undefined, type: "text", inputMode: integerOnly ? 'numeric' : 'text', autoComplete: index === 0 ? 'one-time-code' : 'off', value: charAt(index), disabled: disabled, onChange: (event) => onInput(index, event), onKeyDown: (event) => onKeyDown(index, event), onPaste: onPaste, onFocus: (event) => event.target.select() }, index))) }) }));
111
+ return (_jsx("div", { className: cn('kpt-input-otp-host', className), ...rest, children: _jsx("div", { className: cn('kpt-input-otp', showInvalid && 'kpt-input-otp--invalid'), role: "group", "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, onBlur: onGroupBlur, children: Array.from({ length }, (_, index) => (_jsxs(Fragment, { children: [_jsx("input", { className: "kpt-input-otp__cell", id: index === 0 ? field.id : undefined, "aria-label": t('inputOtp.cell', { index: index + 1, count: length }), "aria-invalid": showInvalid || undefined, type: "text", inputMode: integerOnly ? 'numeric' : 'text', autoComplete: index === 0 ? 'one-time-code' : 'off', value: charAt(index), disabled: disabled, readOnly: readOnly, "aria-required": required || undefined, onChange: (event) => onInput(index, event), onKeyDown: (event) => onKeyDown(index, event), onPaste: onPaste, onFocus: (event) => event.target.select() }), separatorAfter(index) ? (_jsx("span", { className: "kpt-input-otp__sep", "aria-hidden": "true", children: groupSeparator })) : null] }, index))) }) }));
88
112
  }
@@ -55,3 +55,14 @@ is added to `aria-describedby`. The cells form a named `role="group"`; `id` goes
55
55
  ## Accessibility
56
56
  Native inputs with `inputmode` matched to the allowed characters, so a phone shows the numeric
57
57
  keypad for a PIN instead of the full keyboard.
58
+
59
+ ## Read-only and required
60
+ `readOnly` shows the code but blocks typing and paste; `required` sets `aria-required` on the cells (`group` does not accept it). The value holds the typed characters **without gaps**: clearing a middle cell gives `"13"`, not `"1 3"`, while the cell itself stays empty on screen.
61
+
62
+ ## Groups
63
+ `groupSize` (default `0` = no groups) inserts a separator after every N boxes, counted from the
64
+ left; `groupSeparator` sets the character (default `–`). With `length` 9 and `groupSize` 3 you get
65
+ `[][][]–[][][]–[][][]`. The separator is purely visual (`aria-hidden`): the value is the bare characters
66
+ (`"123456789"`), each box keeps its name "Character 4 of 9", and arrows/Backspace jump over it.
67
+
68
+ <KptInputOtp value={code} onValueChange={setCode} length={9} groupSize={3} integerOnly />
@@ -0,0 +1,14 @@
1
+ /** Tokeny maski: `9` = cyfra, `a` = litera (także `ą`, `ł`, `ß`), `*` = litera albo cyfra. */
2
+ export declare const KPT_MASK_TOKENS: Readonly<Record<string, RegExp>>;
3
+ /**
4
+ * Maska przepisuje wejście od zera: z tekstu bierze wyłącznie znaki pasujące do kolejnych
5
+ * tokenów, a literały wstawia sama. Dzięki temu wklejenie „48123456789” i wpisanie „(48) 123…”
6
+ * dają ten sam wynik, a usunięcie literału nie zostawia dziury we wzorcu.
7
+ */
8
+ export declare function kptApplyMask(raw: string, mask: string): string;
9
+ /**
10
+ * Pozycja kursora po przeformatowaniu. Liczy znaczące znaki (te, które maska zabrała) przed
11
+ * kursorem w surowym tekście i stawia kursor za tyloma samymi znakami w wyniku — poprawienie
12
+ * cyfry w środku numeru nie przerzuca kursora na koniec pola.
13
+ */
14
+ export declare function kptMaskCaret(raw: string, caret: number, mask: string): number;
@@ -0,0 +1,62 @@
1
+ /*
2
+ * Maska znakowa `kpt-input-mask`. Bliźniak w porcie Angulara (`shared/mask.ts`) — zmiana w obu.
3
+ */
4
+ /** Tokeny maski: `9` = cyfra, `a` = litera (także `ą`, `ł`, `ß`), `*` = litera albo cyfra. */
5
+ export const KPT_MASK_TOKENS = {
6
+ '9': /\d/,
7
+ a: /\p{L}/u,
8
+ '*': /[\p{L}\p{N}]/u,
9
+ };
10
+ /**
11
+ * Maska przepisuje wejście od zera: z tekstu bierze wyłącznie znaki pasujące do kolejnych
12
+ * tokenów, a literały wstawia sama. Dzięki temu wklejenie „48123456789” i wpisanie „(48) 123…”
13
+ * dają ten sam wynik, a usunięcie literału nie zostawia dziury we wzorcu.
14
+ */
15
+ export function kptApplyMask(raw, mask) {
16
+ if (!mask)
17
+ return raw;
18
+ const chars = [...raw];
19
+ let charIndex = 0;
20
+ let out = '';
21
+ for (let maskIndex = 0; maskIndex < mask.length && charIndex < chars.length;) {
22
+ const rule = KPT_MASK_TOKENS[mask[maskIndex]];
23
+ if (rule) {
24
+ while (charIndex < chars.length && !rule.test(chars[charIndex]))
25
+ charIndex++;
26
+ if (charIndex >= chars.length)
27
+ break;
28
+ out += chars[charIndex];
29
+ charIndex++;
30
+ }
31
+ else {
32
+ out += mask[maskIndex];
33
+ }
34
+ maskIndex++;
35
+ }
36
+ return out;
37
+ }
38
+ /**
39
+ * Pozycja kursora po przeformatowaniu. Liczy znaczące znaki (te, które maska zabrała) przed
40
+ * kursorem w surowym tekście i stawia kursor za tyloma samymi znakami w wyniku — poprawienie
41
+ * cyfry w środku numeru nie przerzuca kursora na koniec pola.
42
+ */
43
+ export function kptMaskCaret(raw, caret, mask) {
44
+ const formatted = kptApplyMask(raw, mask);
45
+ if (!mask)
46
+ return Math.min(caret, formatted.length);
47
+ const before = kptApplyMask(raw.slice(0, caret), mask);
48
+ // Ile znaczących znaków jest przed kursorem — liczone na wyniku, więc literały się nie liczą.
49
+ let significant = 0;
50
+ for (let i = 0; i < before.length; i += 1)
51
+ if (KPT_MASK_TOKENS[mask[i]])
52
+ significant += 1;
53
+ let seen = 0;
54
+ for (let i = 0; i < formatted.length; i += 1) {
55
+ if (KPT_MASK_TOKENS[mask[i]]) {
56
+ if (seen === significant)
57
+ return i;
58
+ seen += 1;
59
+ }
60
+ }
61
+ return formatted.length;
62
+ }
@@ -0,0 +1,22 @@
1
+ /** Grupowanie włączone, gdy grupa ma co najmniej jedną cyfrę, a separator nie jest pusty. */
2
+ export interface KptDigitGrouping {
3
+ readonly size: number;
4
+ readonly separator: string;
5
+ }
6
+ /**
7
+ * Zdejmuje separatory grup. Wiodący znak `-`/`+` zostaje nawet wtedy, gdy separatorem jest `-`
8
+ * (`-123-456` → `-123456`), bo na początku to znak liczby, a nie granica grupy.
9
+ */
10
+ export declare function kptStripGroups(text: string, separator: string): string;
11
+ /**
12
+ * Wstawia separator co `size` cyfr części całkowitej, licząc od prawej — tak jak tysiące:
13
+ * `123456789` → `123-456-789`, `1234` → `1-234`. Część ułamkowa (po `.` albo `,`) i znak zostają
14
+ * bez zmian. Tekst, który nie wygląda na liczbę w trakcie pisania, wraca nietknięty.
15
+ */
16
+ export declare function kptGroupDigits(text: string, grouping: KptDigitGrouping): string;
17
+ /**
18
+ * Pozycja kursora po przeformatowaniu: tyle samo „znaczących” znaków (cyfry, znak, przecinek
19
+ * dziesiętny) przed kursorem co w tekście sprzed grupowania. Bez tego wstawiony separator
20
+ * przesuwałby kursor o jedno miejsce przy każdej trzeciej cyfrze.
21
+ */
22
+ export declare function kptCaretAfterGrouping(before: string, caret: number, after: string, separator: string): number;
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Grupowanie cyfr w polu liczbowym (`kpt-input-number`, `groupSize` + `groupSeparator`).
3
+ * Bliźniak: packages/angular/src/lib/shared/number-group.ts — zmiana tutaj wymaga zmiany tam.
4
+ */
5
+ /**
6
+ * Zdejmuje separatory grup. Wiodący znak `-`/`+` zostaje nawet wtedy, gdy separatorem jest `-`
7
+ * (`-123-456` → `-123456`), bo na początku to znak liczby, a nie granica grupy.
8
+ */
9
+ export function kptStripGroups(text, separator) {
10
+ if (!separator)
11
+ return text;
12
+ const sign = /^[-+]/.exec(text)?.[0] ?? '';
13
+ return sign + text.slice(sign.length).split(separator).join('');
14
+ }
15
+ /**
16
+ * Wstawia separator co `size` cyfr części całkowitej, licząc od prawej — tak jak tysiące:
17
+ * `123456789` → `123-456-789`, `1234` → `1-234`. Część ułamkowa (po `.` albo `,`) i znak zostają
18
+ * bez zmian. Tekst, który nie wygląda na liczbę w trakcie pisania, wraca nietknięty.
19
+ */
20
+ export function kptGroupDigits(text, grouping) {
21
+ const { size, separator } = grouping;
22
+ if (size < 1 || !separator)
23
+ return text;
24
+ const match = /^([-+]?)(\d*)([.,]\d*)?$/.exec(kptStripGroups(text, separator));
25
+ if (!match)
26
+ return text;
27
+ const [, sign, integer, fraction = ''] = match;
28
+ const groups = [];
29
+ for (let end = integer.length; end > 0; end -= size)
30
+ groups.unshift(integer.slice(Math.max(0, end - size), end));
31
+ return sign + groups.join(separator) + fraction;
32
+ }
33
+ /**
34
+ * Pozycja kursora po przeformatowaniu: tyle samo „znaczących” znaków (cyfry, znak, przecinek
35
+ * dziesiętny) przed kursorem co w tekście sprzed grupowania. Bez tego wstawiony separator
36
+ * przesuwałby kursor o jedno miejsce przy każdej trzeciej cyfrze.
37
+ */
38
+ export function kptCaretAfterGrouping(before, caret, after, separator) {
39
+ const significant = kptStripGroups(before.slice(0, caret), separator).length;
40
+ let seen = 0;
41
+ let index = 0;
42
+ // Wiodący znak liczby liczy się zawsze, nawet gdy jest identyczny z separatorem.
43
+ if (significant > 0 && /^[-+]/.test(after)) {
44
+ seen = 1;
45
+ index = 1;
46
+ }
47
+ while (index < after.length && seen < significant) {
48
+ if (!after.startsWith(separator, index))
49
+ seen += 1;
50
+ index += after.startsWith(separator, index) ? separator.length : 1;
51
+ }
52
+ return index;
53
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Klucz Reacta dla opcji kontrolki generycznej: wartość prymitywna jako tekst, obiekt — pozycja.
3
+ * `String(obiekt)` dawałby „[object Object]” dla każdej opcji, czyli zdublowane klucze i stan
4
+ * przenoszony między opcjami przy zmianie listy.
5
+ */
6
+ export declare function optionKey(value: unknown, index: number): string;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Klucz Reacta dla opcji kontrolki generycznej: wartość prymitywna jako tekst, obiekt — pozycja.
3
+ * `String(obiekt)` dawałby „[object Object]” dla każdej opcji, czyli zdublowane klucze i stan
4
+ * przenoszony między opcjami przy zmianie listy.
5
+ */
6
+ export function optionKey(value, index) {
7
+ return typeof value === 'object' && value !== null ? `#${index}` : String(value);
8
+ }
@@ -1,7 +1,19 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
4
  import { createPortal } from 'react-dom';
4
5
  import { useKptOverlayContainer } from "./overlay-container.js";
6
+ /*
7
+ * Prymityw nakładki: panel przypięty do wyzwalacza, renderowany w portalu do `document.body`.
8
+ *
9
+ * Port Angulara stoi tu na CDK Overlay, którego w Reakcie nie ma. Odtwarzamy jego zachowanie:
10
+ * pozycjonowanie względem wyzwalacza z odbiciem w górę, gdy pod spodem brakuje miejsca,
11
+ * przezroczysty backdrop zamykający kliknięciem oraz Escape. Style samego panelu pochodzą
12
+ * ze wspólnego arkusza (`.kpt-select__panel`, `.kpt-popover__panel`) — tutaj są wyłącznie
13
+ * współrzędne, więc nic nie duplikuje wyglądu.
14
+ */
15
+ /** Otwarte nakładki od najstarszej — Escape obsługuje tylko ostatnia. */
16
+ const overlayStack = [];
5
17
  /** Ta sama warstwa co `--kpt-z-overlay` w tokenach — panel ma być nad treścią aplikacji. */
6
18
  const Z_INDEX = 'var(--kpt-z-overlay, 1100)';
7
19
  export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, minAnchorWidth = false, offset = 4, align = 'start', withBackdrop = true, preferAbove = false, children, }) {
@@ -27,12 +39,17 @@ export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, mi
27
39
  ...(minAnchorWidth ? { minWidth: rect.width } : {}),
28
40
  });
29
41
  }, [anchor, matchAnchorWidth, minAnchorWidth, offset, align, preferAbove]);
30
- useLayoutEffect(() => {
31
- if (!open) {
42
+ // Zamknięcie zeruje pozycję w tym samym renderze (wzorzec „poprzednia wartość w stanie”), więc
43
+ // ponowne otwarcie nie pokaże panelu na klatkę w starym miejscu — bez setState w efekcie.
44
+ const [wasOpen, setWasOpen] = useState(open);
45
+ if (wasOpen !== open) {
46
+ setWasOpen(open);
47
+ if (!open)
32
48
  setPosition(null);
33
- return;
34
- }
35
- update();
49
+ }
50
+ useLayoutEffect(() => {
51
+ if (open)
52
+ update();
36
53
  }, [open, update]);
37
54
  useEffect(() => {
38
55
  if (!open)
@@ -41,15 +58,28 @@ export function KptOverlay({ open, anchor, onClose, matchAnchorWidth = false, mi
41
58
  // `capture` łapie też przewijanie kontenerów w środku strony, nie tylko okna.
42
59
  window.addEventListener('scroll', onScrollOrResize, true);
43
60
  window.addEventListener('resize', onScrollOrResize);
61
+ /*
62
+ * Escape zamyka tylko najwyższą nakładkę (stos jak `OverlayKeyboardDispatcher` w CDK) i tylko
63
+ * wtedy, gdy komponent nie obsłużył klawisza sam. `preventDefault` jest konieczne: natywny
64
+ * `<dialog>` traktuje ten sam Escape jako żądanie zamknięcia — popover w `KptDialog` zamykał
65
+ * razem ze sobą całe okno.
66
+ */
67
+ const token = {};
68
+ overlayStack.push(token);
44
69
  const onKeyDown = (event) => {
45
- if (event.key === 'Escape')
46
- onClose();
70
+ if (event.key !== 'Escape' || event.defaultPrevented)
71
+ return;
72
+ if (overlayStack[overlayStack.length - 1] !== token)
73
+ return;
74
+ event.preventDefault();
75
+ onClose();
47
76
  };
48
77
  document.addEventListener('keydown', onKeyDown);
49
78
  return () => {
50
79
  window.removeEventListener('scroll', onScrollOrResize, true);
51
80
  window.removeEventListener('resize', onScrollOrResize);
52
81
  document.removeEventListener('keydown', onKeyDown);
82
+ overlayStack.splice(overlayStack.indexOf(token), 1);
53
83
  };
54
84
  }, [open, update, onClose]);
55
85
  if (!open || !container)
@@ -0,0 +1,6 @@
1
+ /** 0 = bardzo słabe … 4 = silne. */
2
+ export type KptPasswordScore = 0 | 1 | 2 | 3 | 4;
3
+ /** Przybliżona entropia w bitach. */
4
+ export declare function kptPasswordBits(value: string): number;
5
+ /** Wynik 0–4 z entropii: <28 bardzo słabe, <36 słabe, <50 średnie, <64 dobre, reszta silne. */
6
+ export declare function kptPasswordScore(value: string): KptPasswordScore;
@@ -0,0 +1,74 @@
1
+ /*
2
+ * Ocena siły hasła dla miernika `kpt-password`. Bliźniak w porcie Angulara (`shared/password-strength.ts`) — zmiana w obu.
3
+ *
4
+ * Wynik idzie przede wszystkim za długością, a nie za regułami składu, które odradza NIST SP 800-63B:
5
+ * `Password1!` spełnia wszystkie „wielka, cyfra, znak specjalny”, a jest jednym z najczęściej
6
+ * zgadywanych haseł, podczas gdy trzydzieści małych liter jest bardzo mocne. Liczymy przybliżoną
7
+ * entropię znak po znaku — z puli klas, które naprawdę wystąpiły — i obcinamy ją za powtórzenia,
8
+ * ciągi (`abcd`, `1234`, `qwerty`) i schemat „Słowo + cyfry + znak”. Litery spoza ASCII
9
+ * (`ą`, `ż`) są literami, nie znakami specjalnymi. Bez słownika: to podpowiedź, nie zabezpieczenie.
10
+ */
11
+ const KEYBOARD_ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm', '1234567890'];
12
+ /** Pula znaków z klas, które wystąpiły w haśle. */
13
+ function poolSize(value) {
14
+ let pool = 0;
15
+ if (/\p{Ll}/u.test(value))
16
+ pool += 26;
17
+ if (/\p{Lu}/u.test(value))
18
+ pool += 26;
19
+ if (/\p{N}/u.test(value))
20
+ pool += 10;
21
+ if (/[^\P{L}\p{Ll}\p{Lu}]/u.test(value))
22
+ pool += 40; // litery bez wielkości (np. CJK)
23
+ if (/[^\p{L}\p{N}]/u.test(value))
24
+ pool += 33;
25
+ return Math.max(pool, 10);
26
+ }
27
+ /** Czy `b` stoi zaraz po `a` w alfabecie, cyfrach albo rzędzie klawiatury (w dowolną stronę). */
28
+ function isSequential(a, b) {
29
+ const diff = Math.abs(b.codePointAt(0) - a.codePointAt(0));
30
+ if (diff === 1 && /[\p{L}\p{N}]/u.test(a + b))
31
+ return true;
32
+ const pair = (a + b).toLowerCase();
33
+ const reversed = (b + a).toLowerCase();
34
+ return KEYBOARD_ROWS.some((row) => row.includes(pair) || row.includes(reversed));
35
+ }
36
+ /** Przybliżona entropia w bitach. */
37
+ export function kptPasswordBits(value) {
38
+ if (!value)
39
+ return 0;
40
+ // „Słowo + cyfry + znak” (`Password1!`, `Kwiatek2024`) to pierwsze, co zgaduje atakujący —
41
+ // liczymy je jak samo słowo pisane małymi literami.
42
+ const pattern = /^(\p{Lu}?\p{Ll}+)(\p{N}{0,4})([^\p{L}\p{N}]{0,2})$/u.exec(value);
43
+ const chars = [...(pattern ? pattern[1] : value)];
44
+ const perChar = Math.log2(pattern ? 26 : poolSize(value));
45
+ let bits = 0;
46
+ for (let i = 0; i < chars.length; i += 1) {
47
+ const prev = chars[i - 1];
48
+ if (prev === undefined)
49
+ bits += perChar;
50
+ else if (chars[i] === prev)
51
+ bits += 0.5; // aaaa
52
+ else if (isSequential(prev, chars[i]))
53
+ bits += 1; // abcd, 1234, qwerty
54
+ else
55
+ bits += perChar;
56
+ }
57
+ // Doklejone cyfry i znak w schemacie to kilka bitów, nie pełna pula.
58
+ if (pattern)
59
+ bits += pattern[2].length * 2 + pattern[3].length * 3;
60
+ return bits;
61
+ }
62
+ /** Wynik 0–4 z entropii: <28 bardzo słabe, <36 słabe, <50 średnie, <64 dobre, reszta silne. */
63
+ export function kptPasswordScore(value) {
64
+ const bits = kptPasswordBits(value);
65
+ if (bits < 28)
66
+ return 0;
67
+ if (bits < 36)
68
+ return 1;
69
+ if (bits < 50)
70
+ return 2;
71
+ if (bits < 64)
72
+ return 3;
73
+ return 4;
74
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Blokuje przewijanie dokumentu i zwraca funkcję zdejmującą tę jedną blokadę. Licznik pozwala
3
+ * zagnieżdżać okna (szuflada → dialog): przewijanie wraca dopiero po zdjęciu ostatniej.
4
+ * `scrollbar-gutter: stable` trzyma miejsce po pasku przewijania, więc treść nie przeskakuje w bok.
5
+ */
6
+ export declare function kptLockScroll(): () => void;
@@ -0,0 +1,35 @@
1
+ /*
2
+ * Blokada przewijania strony pod oknem modalnym. `showModal()` blokuje interakcję z tłem, ale nie
3
+ * jego przewijanie — kółko myszy dalej przesuwa stronę. Bliźniak w porcie Angulara (`shared/scroll-lock.ts`) — zmiana w obu.
4
+ */
5
+ let locks = 0;
6
+ let savedOverflow = '';
7
+ let savedGutter = '';
8
+ /**
9
+ * Blokuje przewijanie dokumentu i zwraca funkcję zdejmującą tę jedną blokadę. Licznik pozwala
10
+ * zagnieżdżać okna (szuflada → dialog): przewijanie wraca dopiero po zdjęciu ostatniej.
11
+ * `scrollbar-gutter: stable` trzyma miejsce po pasku przewijania, więc treść nie przeskakuje w bok.
12
+ */
13
+ export function kptLockScroll() {
14
+ if (typeof document === 'undefined')
15
+ return () => undefined;
16
+ const root = document.documentElement;
17
+ if (locks === 0) {
18
+ savedOverflow = root.style.overflow;
19
+ savedGutter = root.style.scrollbarGutter;
20
+ root.style.overflow = 'hidden';
21
+ root.style.scrollbarGutter = 'stable';
22
+ }
23
+ locks += 1;
24
+ let released = false;
25
+ return () => {
26
+ if (released)
27
+ return;
28
+ released = true;
29
+ locks -= 1;
30
+ if (locks === 0) {
31
+ root.style.overflow = savedOverflow;
32
+ root.style.scrollbarGutter = savedGutter;
33
+ }
34
+ };
35
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Dzieci komponentu ze slotami, z rozwiniętymi fragmentami. `Children.forEach` traktuje
4
+ * `<>…</>` jak jedno dziecko, więc slot owinięty we fragment (np. z warunku
5
+ * `{admin && <><KptTab …/><KptTab …/></>}`) był dla komponentu niewidoczny. Rozwijamy fragmenty
6
+ * rekurencyjnie; `null`, `false` i puste napisy odpadają tak jak w `Children.forEach`.
7
+ */
8
+ export declare function slotChildren(children: ReactNode): ReactNode[];
9
+ /**
10
+ * Ostrzeżenie deweloperskie o dziecku, które komponent przyjmujący wyłącznie sloty pominie.
11
+ * Białe znaki między tagami (`"\n "`) nie są pomyłką — te przemilczamy.
12
+ */
13
+ export declare function warnUnslottedChild(component: string, child: ReactNode, expected: string): void;
@@ -0,0 +1,31 @@
1
+ import { Children, Fragment, isValidElement } from 'react';
2
+ import { isDevBuild } from "./dev.js";
3
+ /**
4
+ * Dzieci komponentu ze slotami, z rozwiniętymi fragmentami. `Children.forEach` traktuje
5
+ * `<>…</>` jak jedno dziecko, więc slot owinięty we fragment (np. z warunku
6
+ * `{admin && <><KptTab …/><KptTab …/></>}`) był dla komponentu niewidoczny. Rozwijamy fragmenty
7
+ * rekurencyjnie; `null`, `false` i puste napisy odpadają tak jak w `Children.forEach`.
8
+ */
9
+ export function slotChildren(children) {
10
+ const out = [];
11
+ Children.forEach(children, (child) => {
12
+ if (isValidElement(child) && child.type === Fragment) {
13
+ out.push(...slotChildren(child.props.children));
14
+ }
15
+ else if (child !== null && child !== undefined && typeof child !== 'boolean') {
16
+ out.push(child);
17
+ }
18
+ });
19
+ return out;
20
+ }
21
+ /**
22
+ * Ostrzeżenie deweloperskie o dziecku, które komponent przyjmujący wyłącznie sloty pominie.
23
+ * Białe znaki między tagami (`"\n "`) nie są pomyłką — te przemilczamy.
24
+ */
25
+ export function warnUnslottedChild(component, child, expected) {
26
+ if (!isDevBuild())
27
+ return;
28
+ if (typeof child === 'string' && child.trim() === '')
29
+ return;
30
+ console.warn(`[${component}] Pominięto dziecko, które nie jest slotem — użyj ${expected}.`, child);
31
+ }
@@ -0,0 +1,12 @@
1
+ /** Miejsca po przecinku w zapisie liczby, także wykładniczym (`0.25` → 2, `1e-7` → 7). */
2
+ export declare function kptDecimals(n: number): number;
3
+ /**
4
+ * Zaokrągla do precyzji, która nie gubi ani kroku, ani wartości bazowej: `0.1 + 0.2` nie może
5
+ * dać `0.30000000000000004`, a `1.25 + 1` musi dać `2.25`, nie `2`.
6
+ */
7
+ export declare function kptRoundTo(n: number, ...sources: number[]): number;
8
+ /**
9
+ * Przyciąga wartość do siatki `min + k·step` i przycina do zakresu. Siatka zaczyna się od `min`,
10
+ * nie od zera — przy `min=1, step=2` dozwolone są 1, 3, 5…, tak samo myszą jak klawiaturą.
11
+ */
12
+ export declare function kptSnapToStep(raw: number, min: number, max: number, step: number): number;
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Arytmetyka kroku kontrolek liczbowych (`kpt-input-number`, `kpt-knob`).
3
+ * Bliźniak w porcie Angulara (`shared/step.ts`) — zmiana w obu.
4
+ */
5
+ /** Miejsca po przecinku w zapisie liczby, także wykładniczym (`0.25` → 2, `1e-7` → 7). */
6
+ export function kptDecimals(n) {
7
+ if (!Number.isFinite(n))
8
+ return 0;
9
+ const [mantissa, exponent] = String(n).toLowerCase().split('e');
10
+ return Math.max(0, (mantissa.split('.')[1] ?? '').length - Number(exponent ?? 0));
11
+ }
12
+ /**
13
+ * Zaokrągla do precyzji, która nie gubi ani kroku, ani wartości bazowej: `0.1 + 0.2` nie może
14
+ * dać `0.30000000000000004`, a `1.25 + 1` musi dać `2.25`, nie `2`.
15
+ */
16
+ export function kptRoundTo(n, ...sources) {
17
+ const decimals = Math.min(20, Math.max(0, ...sources.map(kptDecimals)));
18
+ return Number(n.toFixed(decimals));
19
+ }
20
+ /**
21
+ * Przyciąga wartość do siatki `min + k·step` i przycina do zakresu. Siatka zaczyna się od `min`,
22
+ * nie od zera — przy `min=1, step=2` dozwolone są 1, 3, 5…, tak samo myszą jak klawiaturą.
23
+ */
24
+ export function kptSnapToStep(raw, min, max, step) {
25
+ if (!(step > 0))
26
+ return Math.min(max, Math.max(min, raw));
27
+ const snapped = kptRoundTo(min + Math.round((raw - min) / step) * step, step, min);
28
+ if (snapped > max)
29
+ return kptRoundTo(max - ((max - min) % step), step, min);
30
+ return Math.max(min, snapped);
31
+ }
@@ -5,6 +5,7 @@
5
5
  * Bez animacji, gdy użytkownik prosi o ograniczenie ruchu (`prefers-reduced-motion`) albo gdy nie
6
6
  * ma `requestAnimationFrame` (render po stronie serwera) — wtedy cel ustawia się od razu.
7
7
  */
8
+ import { kptDecimals } from "./step.js";
8
9
  /** Zwolnienie pod koniec (ease-out cubic): ruch rusza od razu i dojeżdża miękko. */
9
10
  const easeOut = (t) => 1 - (1 - t) ** 3;
10
11
  /** Czy animować — reguła wspólna dla wszystkich kontrolek, żeby „mniej ruchu" działało wszędzie. */
@@ -34,7 +35,5 @@ export function kptTween(from, to, duration, onFrame) {
34
35
  }
35
36
  /** Liczba miejsc po przecinku kroku (`0.25` → 2) — licznik w trakcie animacji nie pokazuje szumu. */
36
37
  export function kptStepDecimals(step) {
37
- const text = String(step);
38
- const dot = text.indexOf('.');
39
- return dot === -1 ? 0 : text.length - dot - 1;
38
+ return kptDecimals(step);
40
39
  }
@@ -3,6 +3,11 @@
3
3
  * i niekontrolowanym (`defaultValue` + stan wewnętrzny). Kontrakt jest ten sam
4
4
  * co w natywnych polach formularza, więc komponenty współpracują z natywnym
5
5
  * `<form>`, React 19 Actions i React Hook Form bez adapterów.
6
+ *
7
+ * `setValue` ma stałą tożsamość: rodzic przekazujący `onChange` jako funkcję inline nie restartuje
8
+ * efektów, które mają `setValue` w zależnościach. Przejście między trybami (`value` raz podane,
9
+ * raz `undefined`) działa jak w Reakcie — po cichu przełącza źródło — ale w buildzie deweloperskim
10
+ * ostrzega, tak jak React robi to dla `<input>`.
6
11
  */
7
12
  export declare function useControllableState<T>(options: {
8
13
  value: T | undefined;