@konce-pt/react 0.11.2 → 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 +14 -1
  24. package/dist/button-group/button-group.js +30 -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 +90 -31
  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,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useEffect, useRef, useState } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -35,16 +36,27 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
35
36
  /** Pozycja, na którą po renderze trzeba przenieść fokus DOM (klawiatura). */
36
37
  const [focusRequest, setFocusRequest] = useState(null);
37
38
  const rootRef = useRef(null);
39
+ // Żądanie nie jest zerowane w efekcie (setState w efekcie = dodatkowy przebieg renderu): każde jest
40
+ // nowym obiektem, więc efekt rusza raz na żądanie, także o ten sam indeks.
38
41
  useEffect(() => {
39
- if (focusRequest === null)
42
+ if (!focusRequest)
40
43
  return;
41
- rootRef.current?.querySelectorAll('[role="option"]')[focusRequest]?.focus();
42
- setFocusRequest(null);
44
+ rootRef.current?.querySelectorAll('[role="option"]')[focusRequest.index]?.focus();
43
45
  }, [focusRequest]);
44
46
  /**
45
47
  * Przesuwa pozycję spod `index` o jedno miejsce i zwraca jej nowy indeks. Jawny indeks zamiast
46
48
  * `selected`: klawiatura zaznacza i przesuwa w jednym zdarzeniu, a stan z domknięcia byłby stary.
47
49
  */
50
+ /** Treść regionu `aria-live` po przestawieniu. */
51
+ const [announcement, setAnnouncement] = useState('');
52
+ /**
53
+ * Ogłasza nową pozycję. Ten sam tekst drugi raz pod rząd nie zostałby przeczytany (region reaguje
54
+ * na zmianę), więc naprzemiennie dokładamy niewidoczną spację.
55
+ */
56
+ const announce = (index, total) => {
57
+ const text = t('orderList.moved', { position: index + 1, total });
58
+ setAnnouncement((current) => (current === text ? text + '\u00a0' : text));
59
+ };
48
60
  const moveFrom = (index, dir) => {
49
61
  const target = index + dir;
50
62
  if (target < 0 || target >= list.length)
@@ -55,6 +67,7 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
55
67
  // Zaznaczenie wędruje za elementem, nie zostaje na pozycji — inaczej kolejne kliknięcie
56
68
  // przesuwałoby sąsiada zamiast tego samego wpisu.
57
69
  setSelected(target);
70
+ announce(target, next.length);
58
71
  return target;
59
72
  };
60
73
  const move = (dir) => {
@@ -67,7 +80,7 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
67
80
  const onItemKeyDown = (index, event) => {
68
81
  if (event.altKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
69
82
  event.preventDefault();
70
- setFocusRequest(moveFrom(index, event.key === 'ArrowUp' ? -1 : 1));
83
+ setFocusRequest({ index: moveFrom(index, event.key === 'ArrowUp' ? -1 : 1) });
71
84
  return;
72
85
  }
73
86
  const next = nextMenuIndex(list.length, index, event.key);
@@ -75,7 +88,7 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
75
88
  return;
76
89
  event.preventDefault();
77
90
  setSelected(next);
78
- setFocusRequest(next);
91
+ setFocusRequest({ index: next });
79
92
  };
80
93
  const clearDrag = () => {
81
94
  setDragIndex(null);
@@ -114,6 +127,7 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
114
127
  next.splice(index, 0, moved);
115
128
  setList(next);
116
129
  setSelected(index);
130
+ announce(index, next.length);
117
131
  };
118
132
  const onListDragOver = (event) => {
119
133
  if (dragIndex === null)
@@ -135,6 +149,7 @@ export function KptOrderList({ items, defaultItems = EMPTY, onItemsChange, heade
135
149
  next.push(moved);
136
150
  setList(next);
137
151
  setSelected(next.length - 1);
152
+ announce(next.length - 1, next.length);
138
153
  };
139
- return (_jsx("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-order-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: cn('kpt-order-list__items', listOver && 'is-dragover'), role: "listbox", "aria-label": header || undefined, onDragOver: onListDragOver, onDragLeave: () => setListOver(false), onDrop: onListDrop, children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected', dragIndex === index && 'is-dragging', dragdrop && 'kpt-order-list__item--draggable', overIndex === index && dragIndex !== index && 'kpt-order-list__item--dragover'), role: "option", "aria-selected": selected === index, tabIndex: index === tabStop ? 0 : -1, onFocus: () => setSelected(index), onKeyDown: (event) => onItemKeyDown(index, event), draggable: dragdrop || undefined, onClick: () => setSelected(index), onDragStart: (event) => onDragStart(index, event), onDragOver: (event) => onDragOver(index, event), onDragLeave: () => setOverIndex(null), onDrop: (event) => onDrop(index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index))) })] })] }) }));
154
+ return (_jsxs("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-order-list-host', className), ...rest, children: [_jsxs("div", { className: "kpt-order-list", children: [_jsxs("div", { className: "kpt-order-list__controls", children: [_jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.up'), onClick: () => move(-1), children: _jsx(KptIcon, { name: "arrow-up" }) }), _jsx("button", { type: "button", disabled: selected === null, "aria-label": t('orderList.down'), onClick: () => move(1), children: _jsx(KptIcon, { name: "arrow-down" }) })] }), _jsxs("div", { className: "kpt-order-list__panel", children: [header ? _jsx("div", { className: "kpt-order-list__header", children: header }) : null, _jsx("ul", { className: cn('kpt-order-list__items', listOver && 'is-dragover'), role: "listbox", "aria-label": header || undefined, onDragOver: onListDragOver, onDragLeave: () => setListOver(false), onDrop: onListDrop, children: list.map((item, index) => (_jsx("li", { className: cn('kpt-order-list__item', selected === index && 'is-selected', dragIndex === index && 'is-dragging', dragdrop && 'kpt-order-list__item--draggable', overIndex === index && dragIndex !== index && 'kpt-order-list__item--dragover'), role: "option", "aria-selected": selected === index, tabIndex: index === tabStop ? 0 : -1, onFocus: () => setSelected(index), onKeyDown: (event) => onItemKeyDown(index, event), draggable: dragdrop || undefined, onClick: () => setSelected(index), onDragStart: (event) => onDragStart(index, event), onDragOver: (event) => onDragOver(index, event), onDragLeave: () => setOverIndex(null), onDrop: (event) => onDrop(index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index))) })] })] }), _jsx("p", { className: "kpt-visually-hidden", "aria-live": "polite", children: announcement })] }));
140
155
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useEffect } from 'react';
3
4
  import { KptButton } from "../button/button.js";
@@ -26,9 +27,11 @@ export function KptPaginator({ length, pageSize, defaultPageSize = 10, onPageSiz
26
27
  defaultValue: defaultPageIndex,
27
28
  onChange: onPageIndexChange,
28
29
  });
29
- const totalPages = Math.max(1, Math.ceil(length / size));
30
- const rangeStart = length === 0 ? 0 : index * size + 1;
31
- const rangeEnd = Math.min(length, (index + 1) * size);
30
+ // Rozmiar strony do rachunków — `0` albo ujemny dawałby `Infinity` stron i wieczne „dalej”.
31
+ const step = Math.max(1, size);
32
+ const totalPages = Math.max(1, Math.ceil(length / step));
33
+ const rangeStart = length === 0 ? 0 : index * step + 1;
34
+ const rangeEnd = Math.min(length, (index + 1) * step);
32
35
  const canPrev = index > 0;
33
36
  const canNext = index < totalPages - 1;
34
37
  // Utrzymuj poprawny indeks, gdy zmieni się długość lub rozmiar strony.
@@ -47,5 +50,5 @@ export function KptPaginator({ length, pageSize, defaultPageSize = 10, onPageSiz
47
50
  const sizeOptions = pageSizeOptions.includes(size)
48
51
  ? pageSizeOptions
49
52
  : [...pageSizeOptions, size].sort((a, b) => a - b);
50
- return (_jsxs("div", { className: cn('kpt-paginator', className), ...rest, children: [_jsx("div", { className: "kpt-paginator__size", children: _jsxs("label", { children: [t('paginator.rowsPerPage'), _jsx("select", { className: "kpt-paginator__select", value: size, onChange: onSizeChange, children: sizeOptions.map((option) => (_jsx("option", { value: option, children: option }, option))) })] }) }), _jsx("span", { className: "kpt-paginator__range", children: t('paginator.range', { start: rangeStart, end: rangeEnd, total: length }) }), _jsxs("div", { className: "kpt-paginator__nav", children: [_jsx(KptButton, { variant: "outline", size: "sm", disabled: !canPrev, onClick: () => setIndex(index - 1), "aria-label": t('paginator.prevPage'), children: prevIcon }), _jsx(KptButton, { variant: "outline", size: "sm", disabled: !canNext, onClick: () => setIndex(index + 1), "aria-label": t('paginator.nextPage'), children: nextIcon })] })] }));
53
+ return (_jsxs("div", { className: cn('kpt-paginator', className), ...rest, children: [_jsx("div", { className: "kpt-paginator__size", children: _jsxs("label", { children: [t('paginator.rowsPerPage'), _jsx("select", { className: "kpt-paginator__select", value: size, onChange: onSizeChange, children: sizeOptions.map((option) => (_jsx("option", { value: option, children: option }, option))) })] }) }), _jsx("span", { className: "kpt-paginator__range", "aria-live": "polite", children: t('paginator.range', { start: rangeStart, end: rangeEnd, total: length }) }), _jsxs("div", { className: "kpt-paginator__nav", children: [_jsx(KptButton, { variant: "outline", size: "sm", disabled: !canPrev, onClick: () => setIndex(index - 1), "aria-label": t('paginator.prevPage'), children: prevIcon }), _jsx(KptButton, { variant: "outline", size: "sm", disabled: !canNext, onClick: () => setIndex(index + 1), "aria-label": t('paginator.nextPage'), children: nextIcon })] })] }));
51
54
  }
@@ -1,6 +1,8 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, isValidElement } from 'react';
3
+ import { isValidElement } from 'react';
3
4
  import { cn } from "../internal/cn.js";
5
+ import { slotChildren } from "../internal/slot-children.js";
4
6
  import { useControllableState } from "../internal/use-controllable-state.js";
5
7
  import { useKptId } from "../internal/use-id.js";
6
8
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -24,10 +26,10 @@ export function KptPanel({ header = '', toggleable = false, collapsed, defaultCo
24
26
  });
25
27
  const actions = [];
26
28
  const body = [];
27
- Children.forEach(children, (child) => {
29
+ for (const child of slotChildren(children)) {
28
30
  const isActions = isValidElement(child) && child.type?.kptPanelSlot === 'actions';
29
31
  (isActions ? actions : body).push(child);
30
- });
32
+ }
31
33
  return (_jsx("div", { className: cn('kpt-panel-host', className), style: { ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`, ...style }, ...rest, children: _jsxs("div", { className: "kpt-panel", children: [_jsxs("div", { className: "kpt-panel__header", children: [_jsx("span", { className: "kpt-panel__title", children: header }), _jsx("span", { className: "kpt-panel__actions", children: actions }), toggleable ? (_jsx("button", { type: "button", className: "kpt-panel__toggle", onClick: () => setCollapsed(!isCollapsed), "aria-expanded": !isCollapsed, "aria-controls": bodyId, "aria-label": header ? t('panel.toggleNamed', { header }) : t('panel.toggle'), children: _jsx(KptIcon, { name: isCollapsed ? 'chevron-down' : 'chevron-up' }) })) : null] }), _jsx("div", { className: cn('kpt-panel__collapse', !isCollapsed && 'is-open'), id: bodyId, inert: isCollapsed, "aria-hidden": isCollapsed, children: _jsx("div", { className: "kpt-panel__collapse-inner", children: _jsx("div", { className: "kpt-panel__body", children: body }) }) })] }) }));
32
34
  }
33
35
  /** Akcje w nagłówku panelu, obok tytułu. */
@@ -28,8 +28,12 @@ fills in the OLD password instead of offering a new one, and the user ends up sa
28
28
  thing.
29
29
 
30
30
  ## How the score is computed
31
- Four independent criteria, one point each: at least 8 characters, both letter cases, a digit, a
32
- non-alphanumeric character. The label comes from `password.strength` — the only array-valued key in
31
+ A 0–4 score. The score follows length first, not composition rules (NIST SP 800-63B advises against them): an
32
+ approximate entropy over the character classes actually used, cut for repeats (`aaaa`), sequences
33
+ (`abcd`, `1234`, `qwerty`) and the "Word + digits + symbol" shape. `Password1!` is weak, thirty
34
+ lowercase letters are strong, and `ą`/`ż` are letters, not special characters. There is no
35
+ dictionary — it is a hint, not a security control.
36
+ The label comes from `password.strength` — the only array-valued key in
33
37
  the dictionary — indexed by the score, so a translation controls the wording without touching code.
34
38
 
35
39
  ## Examples
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useState } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -5,20 +6,8 @@ import { useControllableState } from "../internal/use-controllable-state.js";
5
6
  import { useKptId } from "../internal/use-id.js";
6
7
  import { useKptMessages } from "../i18n/i18n-context.js";
7
8
  import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
9
+ import { kptPasswordScore } from "../internal/password-strength.js";
8
10
  import { KptIcon } from "../icon/icon.js";
9
- /** Cztery niezależne kryteria; każde spełnione podnosi wynik o jeden. */
10
- function scoreOf(password) {
11
- let score = 0;
12
- if (password.length >= 8)
13
- score++;
14
- if (/[A-Z]/.test(password) && /[a-z]/.test(password))
15
- score++;
16
- if (/\d/.test(password))
17
- score++;
18
- if (/[^A-Za-z0-9]/.test(password))
19
- score++;
20
- return Math.min(score, 4);
21
- }
22
11
  /**
23
12
  * Pole hasła z przełącznikiem widoczności i opcjonalnym miernikiem siły.
24
13
  *
@@ -33,6 +22,7 @@ export function KptPassword({ value, defaultValue = '', onValueChange, placehold
33
22
  const [current, setCurrent] = useControllableState({ value, defaultValue, onChange: onValueChange });
34
23
  const [visible, setVisible] = useState(false);
35
24
  const showInvalid = invalid && (touched ?? true);
36
- const score = scoreOf(current);
25
+ // Długość i różnorodność zamiast reguł składu — szczegóły w `internal/password-strength.ts`.
26
+ const score = kptPasswordScore(current);
37
27
  return (_jsxs("div", { className: cn('kpt-password-host', className), ...rest, children: [_jsxs("div", { className: cn('kpt-password', disabled && 'kpt-password--disabled', showInvalid && 'kpt-password--invalid'), children: [_jsx("input", { className: "kpt-password__input", id: inputId, type: visible ? 'text' : 'password', value: current, disabled: disabled, readOnly: readOnly, "aria-invalid": showInvalid || undefined, "aria-required": required || undefined, "aria-label": ariaLabel ?? undefined, "aria-describedby": joinIds(field.describedBy, feedback && current && meterId), placeholder: placeholder, autoComplete: autoComplete ?? undefined, onChange: (event) => setCurrent(event.target.value), onBlur: () => onTouch?.() }), _jsx("button", { type: "button", className: "kpt-password__toggle", disabled: disabled, onClick: () => setVisible(!visible), "aria-label": visible ? t('password.hide') : t('password.show'), children: _jsx(KptIcon, { name: visible ? 'eye-off' : 'eye' }) })] }), feedback && current ? (_jsxs("div", { className: "kpt-password__meter", id: meterId, "data-score": score, children: [_jsx("div", { className: "kpt-password__bars", "aria-hidden": "true", children: [0, 1, 2, 3].map((index) => (_jsx("span", { className: cn('kpt-password__bar', index < score && 'is-on') }, index))) }), _jsx("span", { className: "kpt-password__hint", children: messages.password.strength[score] })] })) : null] }));
38
28
  }
@@ -31,7 +31,7 @@ Hover opens after `openDelay` and leaving closes after `closeDelay` — the poin
31
31
  travel from the trigger to the card, so entering the card cancels the close. Focus opens it with no
32
32
  delay. A click, Enter or a tap **pins** the card: it stays after the pointer leaves and focus moves
33
33
  inside it. That is the only route to the card's content where there is no hover at all. Escape
34
- closes it and hands focus back to the trigger; so does a pointer press outside.
34
+ closes it and hands focus back to the trigger; so does a pointer press outside. Escape on the trigger itself closes a card opened by hover or focus without moving focus (WCAG 1.4.13); with the card closed the key passes through to an enclosing dialog.
35
35
 
36
36
  ## Which of the three
37
37
  `KptTooltip` carries plain text and cannot be clicked. `KptPopover` opens on click and puts a
package/dist/peek/peek.js CHANGED
@@ -1,5 +1,6 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { cloneElement, isValidElement, useCallback, useEffect, useId, useRef, useState, } from 'react';
3
+ import { cloneElement, isValidElement, useCallback, useEffect, useEffectEvent, useId, useRef, useState, } from 'react';
3
4
  import { cn } from "../internal/cn.js";
4
5
  import { KptOverlay } from "../internal/overlay.js";
5
6
  import { mergeSlotProps } from "../internal/slot.js";
@@ -24,6 +25,14 @@ export function KptPeek({ trigger, children, placement = 'bottom', openDelay = 3
24
25
  const panelRef = useRef(null);
25
26
  const timer = useRef(undefined);
26
27
  const [open, setOpen] = useState(false);
28
+ /** Kotwica nakładki w stanie (callback ref), nie `triggerRef.current` czytane w renderze. */
29
+ const [anchor, setAnchor] = useState(null);
30
+ const setTriggerRef = useCallback((el) => {
31
+ triggerRef.current = el;
32
+ setAnchor(el);
33
+ }, []);
34
+ /** Tekst wyzwalacza jako nazwa karty — odczytany przy otwarciu, nie w renderze. */
35
+ const [triggerName, setTriggerName] = useState('');
27
36
  /** Przypięta karta zostaje po zjeździe kursora — przypina ją klik, Enter albo dotyk. */
28
37
  const [pinned, setPinned] = useState(false);
29
38
  const panelId = useId();
@@ -40,21 +49,24 @@ export function KptPeek({ trigger, children, placement = 'bottom', openDelay = 3
40
49
  timer.current = setTimeout(action, delay);
41
50
  }, [stop]);
42
51
  const show = useCallback(() => {
43
- setOpen((was) => {
44
- if (!was)
45
- onOpenChange?.(true);
46
- return true;
47
- });
48
- }, [onOpenChange]);
52
+ setTriggerName(triggerRef.current?.textContent?.trim() ?? '');
53
+ setOpen(true);
54
+ }, []);
55
+ // `onOpenChange` po zmianie stanu, a nie wewnątrz funkcji aktualizującej `setOpen` — ta musi być
56
+ // czysta (StrictMode woła ją dwa razy, a React może ją powtórzyć przy przerwanym renderze).
57
+ const notifyOpen = useEffectEvent((value) => onOpenChange?.(value));
58
+ const reportedOpen = useRef(false);
59
+ useEffect(() => {
60
+ if (reportedOpen.current === open)
61
+ return;
62
+ reportedOpen.current = open;
63
+ notifyOpen(open);
64
+ }, [open]);
49
65
  const close = useCallback(() => {
50
66
  stop();
51
67
  setPinned(false);
52
- setOpen((was) => {
53
- if (was)
54
- onOpenChange?.(false);
55
- return false;
56
- });
57
- }, [onOpenChange, stop]);
68
+ setOpen(false);
69
+ }, [stop]);
58
70
  const hoverIn = useCallback(() => {
59
71
  if (disabled)
60
72
  return;
@@ -106,19 +118,29 @@ export function KptPeek({ trigger, children, placement = 'bottom', openDelay = 3
106
118
  }, [close, open]);
107
119
  const triggerProps = {
108
120
  className: 'kpt-peek__trigger',
109
- ref: triggerRef,
121
+ ref: setTriggerRef,
110
122
  onMouseEnter: hoverIn,
111
123
  onMouseLeave: hoverOut,
112
124
  onFocus: focusIn,
113
125
  onBlur: focusOut,
114
126
  onClick: pin,
127
+ // Escape na wyzwalaczu odprawia kartę bez przenoszenia fokusu (WCAG 1.4.13). Zamknięta karta
128
+ // przepuszcza klawisz dalej — Escape nadal zamyka otaczające okno.
129
+ onKeyDown: (event) => {
130
+ if (event.key !== 'Escape' || !open)
131
+ return;
132
+ event.preventDefault();
133
+ event.stopPropagation();
134
+ close();
135
+ },
115
136
  'aria-expanded': open,
116
137
  'aria-controls': open ? panelId : undefined,
117
138
  };
118
139
  return (_jsxs("span", { ref: hostRef, className: cn('kpt-peek-host', className), ...rest, children: [isValidElement(trigger) ? (
119
140
  // Propsy idą na sam wyzwalacz, nie na opakowanie: `aria-expanded` ma siedzieć na tym
120
141
  // elemencie, po którym jeździ czytnik ekranu, a nie na obojętnym `<span>` obok.
121
- cloneElement(trigger, mergeSlotProps(triggerProps, trigger.props))) : (_jsx("span", { ...triggerProps, children: trigger })), _jsx(KptOverlay, { open: open, anchor: triggerRef.current, onClose: close, offset: 8, withBackdrop: false, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-peek__panel", role: "dialog", tabIndex: -1, "aria-label": label || triggerRef.current?.textContent?.trim() || undefined, "data-pinned": pinned ? '' : undefined, onMouseEnter: hoverIn, onMouseLeave: hoverOut, onFocus: focusIn, onBlur: focusOut, onKeyDown: (event) => {
142
+ // eslint-disable-next-line react-hooks/refs -- mergeSlotProps tylko składa handlery; refy czytają one dopiero w zdarzeniach
143
+ cloneElement(trigger, mergeSlotProps(triggerProps, trigger.props))) : (_jsx("span", { ...triggerProps, children: trigger })), _jsx(KptOverlay, { open: open, anchor: anchor, onClose: close, offset: 8, withBackdrop: false, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-peek__panel", role: "dialog", tabIndex: -1, "aria-label": label || triggerName || undefined, "data-pinned": pinned ? '' : undefined, onMouseEnter: hoverIn, onMouseLeave: hoverOut, onFocus: focusIn, onBlur: focusOut, onKeyDown: (event) => {
122
144
  if (event.key !== 'Escape')
123
145
  return;
124
146
  close();
@@ -73,3 +73,6 @@ Each list is named after its header (`aria-label`). The four move buttons are la
73
73
  dictionary, so "move all right" is distinguishable from "move right" without seeing the double
74
74
  chevron. Native HTML5 drag and drop has no keyboard equivalent — `Enter` and the buttons cover
75
75
  everything it does, and `dragdrop` adds to them, it never replaces them.
76
+
77
+ ## Announcements and buttons
78
+ A transfer between the lists is announced in a polite live region: `pickList.moved` with `{count}` and `{list}` (the target header). The move buttons carry `aria-disabled` when there is nothing to move (no selection, empty list) — they stay focusable, so focus does not drop to `<body>` when the selection clears after a move.
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useEffect, useRef, useState } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -62,13 +63,14 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
62
63
  const setFocusOf = (side) => (side === 'source' ? setFocusLeft : setFocusRight);
63
64
  /** Przystanek Tab listy — ostatnio skupiona pozycja, przycięta do długości listy. */
64
65
  const tabIndexOf = (side) => Math.min(focusOf(side), listOf(side).length - 1);
66
+ // Żądanie nie jest zerowane w efekcie (setState w efekcie = dodatkowy przebieg renderu): każde jest
67
+ // nowym obiektem, więc efekt rusza raz na żądanie, także o ten sam indeks.
65
68
  useEffect(() => {
66
69
  if (!focusRequest)
67
70
  return;
68
71
  const list = rootRef.current?.querySelectorAll('.kpt-pick-list__items')[focusRequest.side === 'source' ? 0 : 1];
69
72
  const options = list?.querySelectorAll('[role="option"]') ?? [];
70
73
  options[Math.min(focusRequest.index, options.length - 1)]?.focus();
71
- setFocusRequest(null);
72
74
  }, [focusRequest]);
73
75
  const focusItem = (side, index) => {
74
76
  setFocusOf(side)(index);
@@ -82,6 +84,17 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
82
84
  next.add(item);
83
85
  setSelectionOf(side)(next);
84
86
  };
87
+ const headerOf = (side) => side === 'source' ? (sourceHeader ?? t('pickList.available')) : (targetHeader ?? t('pickList.selected'));
88
+ /** Treść regionu `aria-live` po przeniesieniu. */
89
+ const [announcement, setAnnouncement] = useState('');
90
+ /**
91
+ * Ogłasza przeniesienie. Ten sam tekst drugi raz pod rząd nie zostałby przeczytany (region
92
+ * reaguje na zmianę), więc naprzemiennie dokładamy niewidoczną spację.
93
+ */
94
+ const announce = (count, to) => {
95
+ const text = t('pickList.moved', { count, list: headerOf(to) });
96
+ setAnnouncement((current) => (current === text ? text + '\u00a0' : text));
97
+ };
85
98
  const move = (from, all) => {
86
99
  const to = other(from);
87
100
  const fromItems = listOf(from);
@@ -92,6 +105,7 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
92
105
  setListOf(from)(fromItems.filter((item) => !moving.includes(item)));
93
106
  setListOf(to)([...listOf(to), ...moving]);
94
107
  setSelectionOf(from)(new Set());
108
+ announce(moving.length, to);
95
109
  };
96
110
  /** Przenosi jeden element między panelami, wstawiając go pod wskazanym indeksem. */
97
111
  const transfer = (fromSide, fromIndex, toSide, toIndex) => {
@@ -103,6 +117,8 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
103
117
  toItems.splice(toIndex, 0, moved);
104
118
  setListOf(fromSide)(fromItems);
105
119
  setListOf(toSide)(toItems);
120
+ if (fromSide !== toSide)
121
+ announce(1, toSide);
106
122
  // Element opuszcza panel — jego zaznaczenie po tamtej stronie przestaje cokolwiek znaczyć.
107
123
  const selection = selectionOf(fromSide);
108
124
  if (selection.has(moved)) {
@@ -208,11 +224,11 @@ export function KptPickList({ source, defaultSource = EMPTY, onSourceChange, tar
208
224
  const column = (side) => {
209
225
  const items = listOf(side);
210
226
  const selection = selectionOf(side);
211
- const header = side === 'source' ? (sourceHeader ?? t('pickList.available')) : (targetHeader ?? t('pickList.selected'));
227
+ const header = headerOf(side);
212
228
  return (_jsxs("div", { className: "kpt-pick-list__col", children: [_jsx("div", { className: "kpt-pick-list__header", children: header }), _jsx("ul", { className: cn('kpt-pick-list__items', listOver === side && 'is-dragover'), role: "listbox", "aria-multiselectable": "true", "aria-label": header, onDragOver: (event) => onListDragOver(side, event), onDragLeave: () => setListOver(null), onDrop: (event) => onListDrop(side, event), children: items.map((item, index) => {
213
229
  const dragging = drag?.side === side && drag.index === index;
214
230
  return (_jsx("li", { className: cn('kpt-pick-list__item', selection.has(item) && 'is-selected', dragging && 'is-dragging', dragdrop && 'kpt-pick-list__item--draggable', over?.side === side && over.index === index && !dragging && 'kpt-pick-list__item--dragover'), role: "option", "aria-selected": selection.has(item), tabIndex: index === tabIndexOf(side) ? 0 : -1, onFocus: () => setFocusOf(side)(index), onKeyDown: (event) => onItemKeyDown(side, index, item, event), draggable: dragdrop || undefined, onClick: () => toggle(side, item), onDoubleClick: () => moveOne(side, item), onDragStart: (event) => onDragStart(side, index, event), onDragOver: (event) => onDragOver(side, index, event), onDragLeave: () => setOver(null), onDrop: (event) => onDrop(side, index, event), onDragEnd: clearDrag, children: renderItem(item, index) }, index));
215
231
  }) })] }));
216
232
  };
217
- return (_jsx("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-pick-list-host', className), ...rest, children: _jsxs("div", { className: "kpt-pick-list", children: [column('source'), _jsxs("div", { className: "kpt-pick-list__controls", children: [_jsxs("button", { type: "button", "aria-label": t('pickList.moveAllRight'), onClick: () => move('source', true), children: [_jsx(KptIcon, { name: "chevron-right" }), _jsx(KptIcon, { name: "chevron-right" })] }), _jsx("button", { type: "button", "aria-label": t('pickList.moveRight'), onClick: () => move('source', false), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("button", { type: "button", "aria-label": t('pickList.moveLeft'), onClick: () => move('target', false), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsxs("button", { type: "button", "aria-label": t('pickList.moveAllLeft'), onClick: () => move('target', true), children: [_jsx(KptIcon, { name: "chevron-left" }), _jsx(KptIcon, { name: "chevron-left" })] })] }), column('target')] }) }));
233
+ return (_jsxs("div", { ref: composeRefs(rootRef, ref), className: cn('kpt-pick-list-host', className), ...rest, children: [_jsxs("div", { className: "kpt-pick-list", children: [column('source'), _jsxs("div", { className: "kpt-pick-list__controls", children: [_jsxs("button", { type: "button", "aria-label": t('pickList.moveAllRight'), "aria-disabled": !left.length || undefined, onClick: () => move('source', true), children: [_jsx(KptIcon, { name: "chevron-right" }), _jsx(KptIcon, { name: "chevron-right" })] }), _jsx("button", { type: "button", "aria-label": t('pickList.moveRight'), "aria-disabled": !selLeft.size || undefined, onClick: () => move('source', false), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("button", { type: "button", "aria-label": t('pickList.moveLeft'), "aria-disabled": !selRight.size || undefined, onClick: () => move('target', false), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsxs("button", { type: "button", "aria-label": t('pickList.moveAllLeft'), "aria-disabled": !right.length || undefined, onClick: () => move('target', true), children: [_jsx(KptIcon, { name: "chevron-left" }), _jsx(KptIcon, { name: "chevron-left" })] })] }), column('target')] }), _jsx("p", { className: "kpt-visually-hidden", "aria-live": "polite", children: announcement })] }));
218
234
  }
@@ -47,3 +47,6 @@ open, `aria-controls` — on the real `<button>`, not on the wrapper. Opening mo
47
47
  interactive element of the bubble; `Escape` or a click outside closes it, and focus returns to the
48
48
  trigger when it was inside the bubble. Focus is not trapped — the popover is supporting content, not
49
49
  a modal.
50
+
51
+ ## Name and Escape
52
+ Give the panel `ariaLabel` — it is a `role="dialog"`. Escape closes only the top-most panel and does not reach an enclosing `<dialog>`, so a popover inside a dialog closes alone.
@@ -9,6 +9,8 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
9
9
  onOpenChange?: (open: boolean) => void;
10
10
  /** Preferowana strona dymka; gdy brakuje miejsca, przechodzi na drugą. */
11
11
  placement?: 'bottom' | 'top';
12
+ /** Nazwa panelu (`role="dialog"`) — okno bez nazwy czytnik ogłasza jako samo „okno dialogowe”. */
13
+ ariaLabel?: string;
12
14
  }
13
15
  /**
14
16
  * Popover — dowolna treść w dymku przypiętym do wyzwalacza (bogatszy niż tooltip).
@@ -26,4 +28,4 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
26
28
  * <div>Bogata treść popovera…</div>
27
29
  * </KptPopover>
28
30
  */
29
- export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
31
+ export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, ariaLabel, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useRef } from 'react';
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
4
  import { cn } from "../internal/cn.js";
4
5
  import { KptOverlay } from "../internal/overlay.js";
5
6
  import { useControllableState } from "../internal/use-controllable-state.js";
@@ -22,8 +23,14 @@ const FOCUSABLE = 'a[href], button:not(:disabled), input:not(:disabled), select,
22
23
  * <div>Bogata treść popovera…</div>
23
24
  * </KptPopover>
24
25
  */
25
- export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', className, ...rest }) {
26
+ export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', ariaLabel = '', className, ...rest }) {
26
27
  const triggerRef = useRef(null);
28
+ /** Kotwica panelu — w stanie, bo czyta ją render; ref zostaje dla handlerów. */
29
+ const [anchor, setAnchor] = useState(null);
30
+ const setTrigger = useCallback((element) => {
31
+ triggerRef.current = element;
32
+ setAnchor(element);
33
+ }, []);
27
34
  const panelRef = useRef(null);
28
35
  const panelId = useKptId(undefined, 'kpt-popover');
29
36
  const [isOpen, setOpen] = useControllableState({
@@ -58,5 +65,5 @@ export function KptPopover({ trigger, children, open, defaultOpen = false, onOpe
58
65
  findControl()?.focus();
59
66
  setOpen(false);
60
67
  }, [setOpen]);
61
- return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: triggerRef, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: triggerRef.current, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", children: children }) })] }));
68
+ return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: setTrigger, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", "aria-label": ariaLabel || undefined, children: children }) })] }));
62
69
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -120,7 +120,7 @@ export { KptButtonGroup } from './button-group/button-group.tsx';
120
120
  export type { KptButtonGroupProps } from './button-group/button-group.tsx';
121
121
  export { KptSelect } from './select/select.tsx';
122
122
  export type { KptSelectProps } from './select/select.tsx';
123
- export type { KptAlertVariant, KptOption } from './shared/types.ts';
123
+ export type { KptAlertVariant, KptNavigateEvent, KptOption } from './shared/types.ts';
124
124
  export { KptFormField } from './form-field/form-field.tsx';
125
125
  export type { KptFormFieldLabelType, KptFormFieldProps } from './form-field/form-field.tsx';
126
126
  export { useKptFieldA11y, useKptFormFieldContext, useKptFormFieldFilled } from './form-field/form-field-context.ts';
@@ -49,3 +49,6 @@ is added to `aria-describedby`. New: `id`, `ariaLabel` and `required` (→ `aria
49
49
  ## Accessibility
50
50
  The container is `role="radiogroup"` with `aria-invalid` when the error shows; every option is a
51
51
  native radio inside a `<label>`, so the label text is clickable and is the accessible name.
52
+
53
+ ## Read-only and required
54
+ `readOnly` blocks the click and the arrow keys (natively they move the selection); the group gets `aria-readonly`.
@@ -16,6 +16,8 @@ export interface KptRadioGroupProps<T> extends Omit<ComponentPropsWithRef<'div'>
16
16
  /** Układ opcji: pod sobą (domyślnie) albo w rzędzie, z zawijaniem. */
17
17
  orientation?: 'vertical' | 'horizontal';
18
18
  disabled?: boolean;
19
+ /** Tylko do odczytu — wybór widać, nie da się go zmienić (`aria-readonly`). */
20
+ readOnly?: boolean;
19
21
  invalid?: boolean;
20
22
  touched?: boolean;
21
23
  onTouch?: () => void;
@@ -29,4 +31,4 @@ export interface KptRadioGroupProps<T> extends Omit<ComponentPropsWithRef<'div'>
29
31
  * @example
30
32
  * <KptRadioGroup options={plans} value={plan} onValueChange={setPlan} />
31
33
  */
32
- export declare function KptRadioGroup<T>({ options, value, defaultValue, onValueChange, name, id, ariaLabel, required, orientation, disabled, invalid, touched, onTouch, className, ...rest }: Readonly<KptRadioGroupProps<T>>): import("react").JSX.Element;
34
+ export declare function KptRadioGroup<T>({ options, value, defaultValue, onValueChange, name, id, ariaLabel, required, orientation, disabled, readOnly, invalid, touched, onTouch, className, ...rest }: Readonly<KptRadioGroupProps<T>>): import("react").JSX.Element;
@@ -1,5 +1,7 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
4
+ import { optionKey } from "../internal/option-key.js";
3
5
  import { useControllableState } from "../internal/use-controllable-state.js";
4
6
  import { useKptId } from "../internal/use-id.js";
5
7
  import { useKptFieldA11y } from "../form-field/form-field-context.js";
@@ -12,7 +14,7 @@ import { useKptFieldA11y } from "../form-field/form-field-context.js";
12
14
  * @example
13
15
  * <KptRadioGroup options={plans} value={plan} onValueChange={setPlan} />
14
16
  */
15
- export function KptRadioGroup({ options = [], value, defaultValue, onValueChange, name, id, ariaLabel = null, required = false, orientation = 'vertical', disabled = false, invalid = false, touched, onTouch, className, ...rest }) {
17
+ export function KptRadioGroup({ options = [], value, defaultValue, onValueChange, name, id, ariaLabel = null, required = false, orientation = 'vertical', disabled = false, readOnly = false, invalid = false, touched, onTouch, className, ...rest }) {
16
18
  /** Grupa nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
17
19
  const field = useKptFieldA11y(id, 'kpt-radio-group');
18
20
  const groupName = useKptId(name, 'kpt-radio');
@@ -22,5 +24,14 @@ export function KptRadioGroup({ options = [], value, defaultValue, onValueChange
22
24
  onChange: onValueChange,
23
25
  });
24
26
  const showInvalid = invalid && (touched ?? true);
25
- return (_jsx("div", { className: cn('kpt-radio-group-host', className), ...rest, children: _jsx("div", { className: cn('kpt-radio-group', orientation === 'horizontal' && 'kpt-radio-group--horizontal'), role: "radiogroup", "aria-orientation": orientation, "aria-invalid": showInvalid || undefined, id: field.id, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-required": required || undefined, children: options.map((option) => (_jsxs("label", { className: cn('kpt-radio', (disabled || option.disabled) && 'kpt-radio--disabled'), children: [_jsx("input", { type: "radio", className: "kpt-radio__input", name: groupName, checked: option.value === current, disabled: disabled || option.disabled, onChange: () => setCurrent(option.value), onBlur: () => onTouch?.() }), _jsx("span", { className: "kpt-radio__dot", "aria-hidden": "true" }), _jsx("span", { className: "kpt-radio__label", children: option.label })] }, String(option.value)))) }) }));
27
+ return (_jsx("div", { className: cn('kpt-radio-group-host', className), ...rest, children: _jsx("div", { className: cn('kpt-radio-group', orientation === 'horizontal' && 'kpt-radio-group--horizontal'), role: "radiogroup", "aria-orientation": orientation, "aria-invalid": showInvalid || undefined, id: field.id, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-required": required || undefined, "aria-readonly": readOnly || undefined,
28
+ // Tylko do odczytu: strzałki natywnie przestawiałyby zaznaczenie.
29
+ onKeyDown: (event) => {
30
+ if (readOnly && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(event.key))
31
+ event.preventDefault();
32
+ }, children: options.map((option, index) => (_jsxs("label", { className: cn('kpt-radio', (disabled || option.disabled) && 'kpt-radio--disabled'), children: [_jsx("input", { type: "radio", className: "kpt-radio__input", name: groupName, checked: option.value === current, disabled: disabled || option.disabled, onChange: () => {
33
+ // Pole kontrolowane: bez zmiany stanu React przywraca poprzednie zaznaczenie.
34
+ if (!readOnly)
35
+ setCurrent(option.value);
36
+ }, onBlur: () => onTouch?.() }), _jsx("span", { className: "kpt-radio__dot", "aria-hidden": "true" }), _jsx("span", { className: "kpt-radio__label", children: option.label })] }, optionKey(option.value, index)))) }) }));
26
37
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useEffect, useRef, useState } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -37,6 +37,11 @@ coming from the application or the HTML source view.
37
37
  Not just paste: a `value` from a database, a CMS or another user is untrusted too. Tags outside the
38
38
  allowlist are unwrapped (the text survives), `on*` handlers and `javascript:` URLs are dropped,
39
39
  `style` is filtered declaration by declaration. `sanitize.ts` knows nothing about React.
40
+ Links keep only relative addresses and `http(s):`, `mailto:`, `tel:` — the same allowlist as the link tool, so
41
+ `data:`, `vbscript:` or `blob:` from a paste are dropped. `class` keeps only the editor's own classes
42
+ (`kpt-rt-*`, `kpt-mention`), a link with `target` always gets `rel="noopener noreferrer"`, and when the
43
+ sanitizer removed something, the cleaned HTML is written back to `value` — the model never holds the
44
+ unsafe original (a pure formatting difference does not touch it, so a clean value does not dirty a form).
40
45
 
41
46
  ## Lists are indented by our own DOM code
42
47
  `execCommand('indent')` produces invalid `ul > ul` in Chrome, so nesting goes through `lists.ts`
@@ -50,6 +55,12 @@ The toolbar is a named `role="toolbar"` ("Formatting", `richText.toolbar`) with
50
55
  `←`/`→` (mirrored in RTL) and `Home`/`End` move between tools, skipping disabled ones; the stop
51
56
  remembers the last tool, so Tab from the toolbar goes straight to the editor.
52
57
 
58
+ Tool panels open in the overlay container at the end of `<body>`, so focus moves into them:
59
+ the paragraph style list is a `listbox` (`↑`/`↓`, `Home`/`End`, `Enter`/`Space` picks, `Escape`/`Tab` close);
60
+ the table grid is one stop (arrows change the size 1–8, the size is announced, `Enter` inserts);
61
+ the colour palette starts on the first swatch (`←`/`→` between swatches, `Tab` cycles inside the
62
+ panel). `Escape` closes any of them and returns focus to the tool.
63
+
53
64
  ## Tables
54
65
  Insert from an 8×8 grid popover. Dragging across cells makes a rectangular selection that grows
55
66
  until it fully covers merged cells on its edges. Right-click opens the menu: insert and delete rows
@@ -89,3 +100,6 @@ generated. A `div` with a role is not named by `label[for]`, so inside `KptFormF
89
100
  `aria-labelledby` pointing at the wrapper's label (unless `ariaLabel` is set), and the hint or error
90
101
  is added to `aria-describedby`. New: `id`, `ariaLabel` and `required` (→ `aria-required`) on the
91
102
  `role="textbox"` area.
103
+
104
+ ## Read-only and required
105
+ `readOnly` keeps the content selectable and copyable but not editable (`aria-readonly`); the toolbar is disabled.
@@ -20,6 +20,8 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
20
20
  /** Oznacza pole jako wymagane atrybutem `aria-required`. */
21
21
  required?: boolean;
22
22
  disabled?: boolean;
23
+ /** Tylko do odczytu — treść da się zaznaczyć i skopiować, nie edytować (`aria-readonly`). */
24
+ readOnly?: boolean;
23
25
  invalid?: boolean;
24
26
  touched?: boolean;
25
27
  onTouch?: () => void;
@@ -33,12 +35,4 @@ export interface KptRichTextProps extends Omit<ComponentPropsWithRef<'div'>, 'on
33
35
  /** Wstawiona wzmianka — moment na powiadomienie osoby albo dopięcie powiązania. */
34
36
  onMention?: (item: KptMentionItem) => void;
35
37
  }
36
- /**
37
- * Lekki edytor tekstu sformatowanego na `contenteditable`, z konfigurowalnym paskiem sekcji
38
- * i edycją tabel. Wartością jest HTML.
39
- *
40
- * @example
41
- * <KptRichText value={html} onValueChange={setHtml} />
42
- * <KptRichText value={html} onValueChange={setHtml} toolbar={[['bold', 'italic'], ['link']]} />
43
- */
44
- export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, id, ariaLabel, required, disabled, invalid, touched, onTouch, mentions, mentionLimit, onMention, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;
38
+ export declare function KptRichText({ value, defaultValue, onValueChange, toolbar, placeholder, maxListLevel, id, ariaLabel, required, disabled: disabledProp, readOnly, invalid, touched, onTouch, mentions, mentionLimit, onMention, locale, dictionary, className, ...rest }: Readonly<KptRichTextProps>): import("react").JSX.Element;