@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,6 +1,7 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
- import { aggregate, isEmptyFilterValue, matchesColumnFilter, toText, } from '@konce-pt/table';
3
+ import { useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
+ import { aggregate, createRowComparator, cssLengthToPx, isEmptyFilterValue, matchesColumnFilter, matchesGlobalFilter, toText, } from '@konce-pt/table';
4
5
  import { cn } from "../internal/cn.js";
5
6
  import { useControllableState } from "../internal/use-controllable-state.js";
6
7
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -46,8 +47,14 @@ const EMPTY_FIELDS = [];
46
47
  function getValue(row, field) {
47
48
  return row[field];
48
49
  }
49
- export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, filterRowPosition = 'bottom', pageSize = 0, pageSizeOptions = [5, 10, 20, 50], selectable = 'none', loading = false, emptyMessage, rowKey = null, scrollHeight = null, virtualScroll = false, rowHeight = 41, reorderable = false, exportable = false, exportFilename = 'export.csv', lazy = false, totalRecords = null, expandable = false, multiExpand = true, summaryLabel, summaryValues = null, selection, defaultSelection = EMPTY_ROWS, onSelectionChange, expandedRows, defaultExpandedRows = EMPTY_ROWS, onExpandedRowsChange, hiddenFields, defaultHiddenFields = EMPTY_FIELDS, onHiddenFieldsChange, onRowClick, onLazyLoad, onCellEdit, cells, cellEditors, renderRowDetail, paginatorActions, locale, dictionary, className, style, ...rest }) {
50
- const { t } = useKptMessages({ locale, dictionary });
50
+ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, filterRowPosition = 'bottom', pageSize = 0, pageSizeOptions = [5, 10, 20, 50], selectable = 'none', loading = false, ariaLabel = null, caption = null, clickableRows, emptyMessage, rowKey = null, scrollHeight = null, virtualScroll = false, rowHeight = 41, reorderable = false, exportable = false, exportFilename = 'export.csv', lazy = false, totalRecords = null, expandable = false, multiExpand = true, summaryLabel, summaryValues = null, selection, defaultSelection = EMPTY_ROWS, onSelectionChange, expandedRows, defaultExpandedRows = EMPTY_ROWS, onExpandedRowsChange, hiddenFields, defaultHiddenFields = EMPTY_FIELDS, onHiddenFieldsChange, onRowClick, onLazyLoad, onCellEdit, cells, cellEditors, renderRowDetail, paginatorActions, locale, dictionary, className, style, ...rest }) {
51
+ const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
52
+ const uid = useId();
53
+ const rowsClickable = clickableRows ?? onRowClick !== undefined;
54
+ /** Ogłoszenie po przestawieniu kolumny z klawiatury. */
55
+ const [moveAnnouncement, setMoveAnnouncement] = useState('');
56
+ /** Pole kolumny, której uchwyt ma dostać fokus po przerysowaniu (ustawiane w zdarzeniu, czytane w efekcie). */
57
+ const focusGrip = useRef(null);
51
58
  const [picked, setPicked] = useControllableState({
52
59
  value: selection,
53
60
  defaultValue: defaultSelection,
@@ -58,7 +65,7 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
58
65
  defaultValue: defaultExpandedRows,
59
66
  onChange: onExpandedRowsChange,
60
67
  });
61
- const [hidden, setHidden] = useControllableState({
68
+ const [hidden] = useControllableState({
62
69
  value: hiddenFields,
63
70
  defaultValue: defaultHiddenFields,
64
71
  onChange: onHiddenFieldsChange,
@@ -70,13 +77,16 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
70
77
  const [pageIndex, setPageIndex] = useState(0);
71
78
  const [pageSizeState, setPageSizeState] = useState(pageSize || 10);
72
79
  // Kolejność kolumn żyje w stanie (przestawianie), ale nowe `columns` z zewnątrz ją zastępują.
80
+ // Poprzednie `columns` trzymamy w stanie, nie w refie — wzorzec „zmiana propsa resetuje stan”
81
+ // z react.dev; ref zależałby od tego, czy React nie wyrzucił renderu (StrictMode, concurrent).
73
82
  const [order, setOrder] = useState(columns);
74
- const columnsRef = useRef(columns);
75
- if (columnsRef.current !== columns) {
76
- columnsRef.current = columns;
77
- if (order !== columns)
78
- setOrder(columns);
83
+ const [seenColumns, setSeenColumns] = useState(columns);
84
+ if (seenColumns !== columns) {
85
+ setSeenColumns(columns);
86
+ setOrder(columns);
79
87
  }
88
+ /** Zmierzone szerokości kolumn zamrożonych bez szerokości liczalnej z jednostki (`%`, `ch`). */
89
+ const [measuredWidths, setMeasuredWidths] = useState(() => new Map());
80
90
  const [dragField, setDragField] = useState(null);
81
91
  const [scrollTop, setScrollTop] = useState(0);
82
92
  const [viewportHeight, setViewportHeight] = useState(400);
@@ -93,10 +103,11 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
93
103
  const sameRow = (a, b) => rowKey ? getValue(a, rowKey) === getValue(b, rowKey) : a === b;
94
104
  // ── Potok danych ──
95
105
  const filtered = useMemo(() => {
96
- const query = filterText.trim().toLowerCase();
106
+ const fields = columns.map((col) => col.field);
97
107
  let rows = data;
98
- if (query) {
99
- rows = rows.filter((row) => columns.some((col) => toText(getValue(row, col.field)).toLowerCase().includes(query)));
108
+ if (filterText.trim()) {
109
+ // Bez diakrytyków i wielkości liter — „lodz” znajduje „Łódź” (`@konce-pt/table`).
110
+ rows = rows.filter((row) => matchesGlobalFilter(row, fields, filterText));
100
111
  }
101
112
  if (hasColumnFilters) {
102
113
  rows = rows.filter((row) => columns.every((col) => {
@@ -113,20 +124,10 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
113
124
  const sorted = useMemo(() => {
114
125
  if (!sortField || !sortDir)
115
126
  return filtered;
116
- const sign = sortDir === 'asc' ? 1 : -1;
117
- return [...filtered].sort((a, b) => {
118
- const av = getValue(a, sortField);
119
- const bv = getValue(b, sortField);
120
- // Puste komórki lądują na końcu niezależnie od kierunku — sortowanie ma pokazywać dane.
121
- if (av == null)
122
- return 1;
123
- if (bv == null)
124
- return -1;
125
- if (typeof av === 'number' && typeof bv === 'number')
126
- return (av - bv) * sign;
127
- return toText(av).localeCompare(toText(bv)) * sign;
128
- });
129
- }, [filtered, sortDir, sortField]);
127
+ // Kolator dla aktywnego języka, tworzony raz na sortowanie; puste komórki na końcu.
128
+ const compare = createRowComparator(sortField, sortDir, activeLocale);
129
+ return [...filtered].sort((a, b) => compare(a, b));
130
+ }, [activeLocale, filtered, sortDir, sortField]);
130
131
  const paged = useMemo(() => {
131
132
  if (lazy)
132
133
  return data;
@@ -136,14 +137,14 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
136
137
  return sorted.slice(start, start + pageSizeState);
137
138
  }, [data, lazy, pageIndex, pageSize, pageSizeState, sorted]);
138
139
  const filteredCount = lazy ? (totalRecords ?? data.length) : filtered.length;
139
- // Tryb lazy: każda zmiana stanu to prośba do rodzica o dane.
140
+ // Tryb lazy: każda zmiana stanu to prośba do rodzica o dane. `onLazyLoad` czytamy przez
141
+ // `useEffectEvent`: nowa referencja funkcji przy każdym renderze rodzica nie może restartować
142
+ // efektu, bo wywołałaby pętlę żądań.
143
+ const requestLazyLoad = useEffectEvent((state) => onLazyLoad?.(state));
140
144
  useEffect(() => {
141
145
  if (!lazy)
142
146
  return;
143
- onLazyLoad?.({ pageIndex, pageSize: pageSizeState, sortField, sortDir, filter: filterText, columnFilters });
144
- // `onLazyLoad` celowo poza zależnościami: nowa referencja funkcji przy każdym renderze
145
- // rodzica wywołałaby pętlę żądań.
146
- // eslint-disable-next-line react-hooks/exhaustive-deps
147
+ requestLazyLoad({ pageIndex, pageSize: pageSizeState, sortField, sortDir, filter: filterText, columnFilters });
147
148
  }, [lazy, pageIndex, pageSizeState, sortField, sortDir, filterText, columnFilters]);
148
149
  // Offset sticky drugiego wiersza nagłówka = wysokość pierwszego.
149
150
  useLayoutEffect(() => {
@@ -151,11 +152,46 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
151
152
  if (first)
152
153
  setTheadOffset(first.offsetHeight);
153
154
  }, [filterRowPosition, hasColumnFilters, visibleColumns]);
155
+ // Wysokość viewportu dla wirtualizacji — także po zmianie rozmiaru kontenera bez przewinięcia.
154
156
  useLayoutEffect(() => {
155
157
  const el = scrollRef.current;
156
- if (el)
157
- setViewportHeight(el.clientHeight);
158
+ if (!el)
159
+ return;
160
+ setViewportHeight(el.clientHeight);
161
+ if (typeof ResizeObserver === 'undefined')
162
+ return;
163
+ const observer = new ResizeObserver(() => setViewportHeight(el.clientHeight));
164
+ observer.observe(el);
165
+ return () => observer.disconnect();
158
166
  }, []);
167
+ // Szerokości kolumn zamrożonych, których nie da się policzyć z jednostki — mierzone z nagłówków.
168
+ // Stan zmienia się tylko przy innej wartości, więc pomiar nie zapętla renderu.
169
+ useLayoutEffect(() => {
170
+ const head = theadRef.current;
171
+ if (!head)
172
+ return;
173
+ const next = new Map();
174
+ for (const col of visibleColumns) {
175
+ if (!col.frozen)
176
+ break;
177
+ if (cssLengthToPx(col.width, REM_PX) !== null)
178
+ continue;
179
+ const th = head.querySelector(`th[data-field="${CSS.escape(col.field)}"]`);
180
+ if (th)
181
+ next.set(col.field, th.offsetWidth);
182
+ }
183
+ setMeasuredWidths((prev) => prev.size === next.size && [...next].every(([key, value]) => prev.get(key) === value) ? prev : next);
184
+ }, [visibleColumns]);
185
+ // Po przestawieniu kolumny z klawiatury uchwyt jest przerysowany w nowym miejscu — fokus idzie za nim.
186
+ useLayoutEffect(() => {
187
+ const field = focusGrip.current;
188
+ if (field === null)
189
+ return;
190
+ focusGrip.current = null;
191
+ theadRef.current
192
+ ?.querySelector(`th[data-field="${CSS.escape(field)}"] .kpt-data-table__grip`)
193
+ ?.focus();
194
+ }, [order]);
159
195
  // Wejście w edycję → fokus na kontrolce edytora.
160
196
  useEffect(() => {
161
197
  if (!editingCell)
@@ -176,14 +212,6 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
176
212
  setViewportHeight(el.clientHeight);
177
213
  };
178
214
  // ── Kolumny zamrożone ──
179
- const parseWidthPx = (width) => {
180
- if (!width)
181
- return 8 * REM_PX;
182
- const n = Number.parseFloat(width);
183
- if (Number.isNaN(n))
184
- return 8 * REM_PX;
185
- return width.trim().endsWith('px') ? n : n * REM_PX;
186
- };
187
215
  const leadingCells = (selectable !== 'none' ? 1 : 0) + (hasRowDetail ? 1 : 0);
188
216
  const leadingStickyWidth = (hasRowDetail ? EXPANDER_FROZEN_W * REM_PX : 0) + (selectable !== 'none' ? CHECKBOX_FROZEN_W * REM_PX : 0);
189
217
  const frozenOffsets = useMemo(() => {
@@ -193,10 +221,11 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
193
221
  if (!col.frozen)
194
222
  break; // tylko wiodące, sąsiadujące
195
223
  map.set(col.field, acc);
196
- acc += parseWidthPx(col.width);
224
+ // Z jednostki, potem z pomiaru, a do pierwszego pomiaru 8 rem.
225
+ acc += cssLengthToPx(col.width, REM_PX) ?? measuredWidths.get(col.field) ?? 8 * REM_PX;
197
226
  }
198
227
  return map;
199
- }, [leadingStickyWidth, visibleColumns]);
228
+ }, [leadingStickyWidth, measuredWidths, visibleColumns]);
200
229
  const hasFrozen = frozenOffsets.size > 0;
201
230
  const frozenLeft = (field) => frozenOffsets.get(field);
202
231
  const expanderStickyLeft = hasFrozen ? 0 : undefined;
@@ -230,6 +259,26 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
230
259
  setOrder(cols);
231
260
  setDragField(null);
232
261
  };
262
+ /** Strzałki na uchwycie przestawiają kolumnę o jedno miejsce — alternatywa dla przeciągania. */
263
+ const onGripKeyDown = (field, event) => {
264
+ const delta = event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0;
265
+ if (!delta)
266
+ return;
267
+ event.preventDefault();
268
+ const from = visibleColumns.findIndex((col) => col.field === field);
269
+ const to = from + delta;
270
+ // Kolumny zamrożone zostają wiodące — nie wolno ich minąć ani wyjechać poza tabelę.
271
+ if (from === -1 || to < frozenLeadCount || to >= visibleColumns.length)
272
+ return;
273
+ const cols = [...order];
274
+ const fromIdx = cols.findIndex((col) => col.field === field);
275
+ const toIdx = cols.findIndex((col) => col.field === visibleColumns[to].field);
276
+ const [moved] = cols.splice(fromIdx, 1);
277
+ cols.splice(toIdx, 0, moved);
278
+ setOrder(cols);
279
+ setMoveAnnouncement(t('dataTable.columnMoved', { column: moved.header, position: to + 1, count: visibleColumns.length }));
280
+ focusGrip.current = field;
281
+ };
233
282
  // ── Sortowanie ──
234
283
  const toggleSort = (col) => {
235
284
  if (!col.sortable)
@@ -425,13 +474,26 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
425
474
  return null;
426
475
  }
427
476
  };
428
- const renderRow = (row, index) => (_jsxs("tbody", { style: { display: 'contents' }, children: [_jsxs("tr", { className: cn(isSelected(row) && 'kpt-data-table__row--selected', hasRowDetail && isExpanded(row) && 'kpt-data-table__row--expanded'), style: { height: virtualScroll ? rowHeight : undefined }, onClick: () => onRowClick?.(row), children: [hasRowDetail ? (_jsx("td", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft }, children: _jsx("button", { type: "button", className: cn('kpt-data-table__expander-btn', isExpanded(row) && 'kpt-data-table__expander-btn--open'), "aria-expanded": isExpanded(row), "aria-label": t('dataTable.expandRow'), onClick: (event) => {
477
+ const renderRow = (row, index) => (_jsxs("tbody", { style: { display: 'contents' }, children: [_jsxs("tr", { className: cn(isSelected(row) && 'kpt-data-table__row--selected', hasRowDetail && isExpanded(row) && 'kpt-data-table__row--expanded', rowsClickable && 'kpt-data-table__row--clickable'), style: { height: virtualScroll ? rowHeight : undefined }, tabIndex: rowsClickable ? 0 : undefined, onClick: () => onRowClick?.(row), onKeyDown: rowsClickable
478
+ ? (event) => {
479
+ // Tylko na samym wierszu — Enter w kontrolce komórki należy do niej.
480
+ if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' '))
481
+ return;
482
+ event.preventDefault();
483
+ onRowClick?.(row);
484
+ }
485
+ : undefined, children: [hasRowDetail ? (_jsx("td", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft }, children: _jsx("button", { type: "button", className: cn('kpt-data-table__expander-btn', isExpanded(row) && 'kpt-data-table__expander-btn--open'), "aria-expanded": isExpanded(row), "aria-label": t('dataTable.expandRow'), onClick: (event) => {
429
486
  event.stopPropagation();
430
487
  toggleExpand(row);
431
- }, children: _jsx(KptIcon, { name: "chevron-right" }) }) })) : null, selectable !== 'none' ? (_jsx("td", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft }, children: _jsx("input", { type: "checkbox", checked: isSelected(row), "aria-label": t('dataTable.selectRow'), onClick: (event) => event.stopPropagation(), onChange: () => toggleRow(row) }) })) : null, visibleColumns.map((col) => {
488
+ }, children: _jsx(KptIcon, { name: "chevron-right" }) }) })) : null, selectable !== 'none' ? (_jsx("td", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft }, children: _jsx("input", { type: selectable === 'single' ? 'radio' : 'checkbox', name: selectable === 'single' ? `${uid}-select` : undefined, checked: isSelected(row), "aria-label": t('dataTable.selectRow'), onClick: (event) => {
489
+ event.stopPropagation();
490
+ toggleRow(row);
491
+ }, onChange: () => {
492
+ // Zmianę obsługuje `onClick` (radio nie zgłasza `change` przy odznaczaniu).
493
+ } }) })) : null, visibleColumns.map((col, colIndex) => {
432
494
  const editing = isEditingCell(row, col.field);
433
495
  const editable = isEditable(col) && !editing;
434
- return (_jsx("td", { "data-align": col.align ?? 'start', style: { left: frozenLeft(col.field) }, className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen', editable && 'kpt-data-table__cell--editable'), tabIndex: editable ? 0 : undefined, "aria-label": editable ? t('dataTable.editCell', { column: col.header }) : undefined, onClick: (event) => {
496
+ return (_jsx("td", { "data-align": col.align ?? 'start', style: { left: frozenLeft(col.field) }, className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen', editable && 'kpt-data-table__cell--editable'), tabIndex: editable ? 0 : undefined, "aria-describedby": editable ? `${uid}-edit-${colIndex}` : undefined, onClick: (event) => {
435
497
  if (isEditable(col))
436
498
  startEdit(row, col, event);
437
499
  }, onKeyDown: (event) => {
@@ -439,13 +501,13 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
439
501
  startEdit(row, col, event);
440
502
  }, children: editing ? (_jsx("div", { ref: editorHostRef, className: "kpt-data-table__editor", onClick: (event) => event.stopPropagation(), onBlur: onEditorFocusOut, children: editor(row, col) })) : cells?.[col.field] ? (cells[col.field](row, getValue(row, col.field))) : getValue(row, col.field) }, col.field));
441
503
  })] }), hasRowDetail && isExpanded(row) && !virtualScroll ? (_jsx("tr", { className: "kpt-data-table__detail-row", children: _jsx("td", { className: "kpt-data-table__detail", colSpan: colspan, children: renderRowDetail?.(row) }) })) : null] }, rowKey ? String(getValue(row, rowKey)) : index));
442
- return (_jsxs("div", { className: cn('kpt-data-table', className), style: { ['--kpt-thead-offset']: `${theadOffset}px`, ...style }, ...rest, children: [filterable || exportable ? (_jsxs("div", { className: "kpt-data-table__toolbar", children: [filterable ? (_jsx("input", { className: "kpt-data-table__filter", type: "search", placeholder: t('dataTable.search'), value: filterText, onChange: (event) => {
504
+ return (_jsxs("div", { className: cn('kpt-data-table', className), style: { ['--kpt-thead-offset']: `${theadOffset}px`, ...style }, ...rest, children: [filterable || exportable ? (_jsxs("div", { className: "kpt-data-table__toolbar", children: [filterable ? (_jsx("input", { className: "kpt-data-table__filter", type: "search", placeholder: t('dataTable.search'), "aria-label": t('dataTable.search'), value: filterText, onChange: (event) => {
443
505
  setFilterText(event.target.value);
444
506
  setPageIndex(0);
445
- } })) : null, exportable ? (_jsx(KptButton, { variant: "outline", size: "sm", onClick: () => exportTableCsv(visibleColumns, (lazy ? data : sorted), exportFilename), children: t('dataTable.exportCsv') })) : null] })) : null, _jsx("div", { ref: scrollRef, className: "kpt-data-table__scroll", style: { maxHeight: scrollHeight ?? undefined }, onScroll: onScroll, children: _jsxs("table", { className: cn('kpt-data-table__table', hasFrozen && 'kpt-data-table__table--frozen', virtualScroll && 'kpt-data-table__table--virtual'), children: [_jsxs("thead", { ref: theadRef, children: [hasColumnFilters && filterRowPosition === 'top' ? filterRow : null, _jsxs("tr", { children: [hasRowDetail ? (_jsx("th", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft } })) : null, selectable === 'multiple' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft }, children: _jsx("input", { type: "checkbox", checked: pageAllSelected, ref: (el) => {
507
+ } })) : null, exportable ? (_jsx(KptButton, { variant: "outline", size: "sm", onClick: () => exportTableCsv(visibleColumns, (lazy ? data : sorted), exportFilename), children: t('dataTable.exportCsv') })) : null] })) : null, _jsx("div", { ref: scrollRef, className: "kpt-data-table__scroll", style: { maxHeight: scrollHeight ?? undefined }, onScroll: onScroll, children: _jsxs("table", { className: cn('kpt-data-table__table', hasFrozen && 'kpt-data-table__table--frozen', virtualScroll && 'kpt-data-table__table--virtual'), "aria-label": ariaLabel ?? undefined, "aria-busy": loading || undefined, children: [caption ? _jsx("caption", { className: "kpt-data-table__caption", children: caption }) : null, _jsxs("thead", { ref: theadRef, children: [hasColumnFilters && filterRowPosition === 'top' ? filterRow : null, _jsxs("tr", { children: [hasRowDetail ? (_jsx("th", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft } })) : null, selectable === 'multiple' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft }, children: _jsx("input", { type: "checkbox", checked: pageAllSelected, ref: (el) => {
446
508
  if (el)
447
509
  el.indeterminate = pageSomeSelected;
448
- }, "aria-label": t('dataTable.selectAll'), onChange: toggleAllOnPage }) })) : selectable === 'single' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft } })) : null, visibleColumns.map((col) => (_jsxs("th", { draggable: isColDraggable(col) || undefined, style: { width: col.width, left: frozenLeft(col.field) }, "data-align": col.align ?? 'start', className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen', col.sortable && 'kpt-data-table__th--sortable', isColDraggable(col) && 'kpt-data-table__th--draggable', dragField !== null && dragField !== col.field && !col.frozen && 'kpt-data-table__th--dragover'), "aria-sort": ariaSort(col), onClick: () => toggleSort(col), onDragStart: (event) => {
510
+ }, "aria-label": t('dataTable.selectAll'), onChange: toggleAllOnPage }) })) : selectable === 'single' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft } })) : null, visibleColumns.map((col) => (_jsxs("th", { draggable: isColDraggable(col) || undefined, style: { width: col.width, left: frozenLeft(col.field) }, "data-align": col.align ?? 'start', className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen', col.sortable && 'kpt-data-table__th--sortable', isColDraggable(col) && 'kpt-data-table__th--draggable', dragField !== null && dragField !== col.field && !col.frozen && 'kpt-data-table__th--dragover'), "data-field": col.field, "aria-sort": col.sortable ? ariaSort(col) : undefined, onDragStart: (event) => {
449
511
  if (col.frozen)
450
512
  return;
451
513
  setDragField(col.field);
@@ -457,5 +519,5 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
457
519
  const visIndex = visibleColumns.findIndex((c) => c.field === col.field);
458
520
  if (dragField !== null && visIndex >= frozenLeadCount)
459
521
  event.preventDefault();
460
- }, onDrop: (event) => reorderable && onColDrop(col.field, event), onDragEnd: () => setDragField(null), children: [isColDraggable(col) ? (_jsx(KptIcon, { className: "kpt-data-table__grip", name: "grip-vertical" })) : null, _jsx("span", { children: col.header }), col.sortable ? (_jsx(KptIcon, { className: cn('kpt-data-table__sort', ariaSort(col) !== 'none' && 'kpt-data-table__sort--active'), name: sortField === col.field && sortDir === 'asc' ? 'chevron-up' : 'chevron-down' })) : null] }, col.field)))] }), hasColumnFilters && filterRowPosition === 'bottom' ? filterRow : null] }), loading ? (_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: "kpt-data-table__state", colSpan: colspan, children: t('dataTable.loading') }) }) })) : displayCount === 0 ? (_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: "kpt-data-table__state", colSpan: colspan, children: emptyMessage ?? t('dataTable.empty') }) }) })) : (_jsxs(_Fragment, { children: [virtualScroll && topPad > 0 ? (_jsx("tbody", { children: _jsx("tr", { className: "kpt-data-table__spacer", "aria-hidden": "true", children: _jsx("td", { colSpan: colspan, style: { height: topPad } }) }) })) : null, rowsToRender.map((row, index) => renderRow(row, index)), virtualScroll && bottomPad > 0 ? (_jsx("tbody", { children: _jsx("tr", { className: "kpt-data-table__spacer", "aria-hidden": "true", children: _jsx("td", { colSpan: colspan, style: { height: bottomPad } }) }) })) : null] })), hasSummary && !loading && displayCount > 0 ? (_jsx("tfoot", { children: _jsxs("tr", { className: "kpt-data-table__summary", children: [summaryLabelSpan > 0 ? (_jsx("th", { className: cn('kpt-data-table__summary-label', hasFrozen && 'kpt-data-table__cell--frozen'), colSpan: summaryLabelSpan, style: { left: hasFrozen ? 0 : undefined }, children: summaryLabel ?? t('dataTable.total') })) : null, visibleColumns.map((col, index) => index >= summaryStartIndex ? (_jsx("td", { "data-align": col.align ?? 'end', style: { width: col.width, left: frozenLeft(col.field) }, className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen'), children: summaryText(col) }, col.field)) : null)] }) })) : null] }) }), pageSize > 0 && !virtualScroll ? (_jsxs("div", { className: "kpt-data-table__pager", children: [paginatorActions, _jsx(KptPaginator, { length: filteredCount, pageIndex: pageIndex, onPageIndexChange: setPageIndex, pageSize: pageSizeState, onPageSizeChange: setPageSizeState, pageSizeOptions: pageSizeOptions })] })) : null] }));
522
+ }, onDrop: (event) => reorderable && onColDrop(col.field, event), onDragEnd: () => setDragField(null), children: [isColDraggable(col) ? (_jsx("button", { type: "button", className: "kpt-data-table__grip", "aria-label": t('dataTable.moveColumn', { column: col.header }), "aria-keyshortcuts": "ArrowLeft ArrowRight", onKeyDown: (event) => onGripKeyDown(col.field, event), children: _jsx(KptIcon, { name: "grip-vertical" }) })) : null, col.sortable ? (_jsxs("button", { type: "button", className: "kpt-data-table__sort-btn", onClick: () => toggleSort(col), children: [_jsx("span", { children: col.header }), _jsx(KptIcon, { className: cn('kpt-data-table__sort', ariaSort(col) !== 'none' && 'kpt-data-table__sort--active'), name: sortField !== col.field || !sortDir ? 'selector' : sortDir === 'asc' ? 'chevron-up' : 'chevron-down' })] })) : (_jsx("span", { children: col.header }))] }, col.field)))] }), hasColumnFilters && filterRowPosition === 'bottom' ? filterRow : null] }), loading ? (_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: "kpt-data-table__state", colSpan: colspan, children: t('dataTable.loading') }) }) })) : displayCount === 0 ? (_jsx("tbody", { children: _jsx("tr", { children: _jsx("td", { className: "kpt-data-table__state", colSpan: colspan, children: emptyMessage ?? t('dataTable.empty') }) }) })) : (_jsxs(_Fragment, { children: [virtualScroll && topPad > 0 ? (_jsx("tbody", { children: _jsx("tr", { className: "kpt-data-table__spacer", "aria-hidden": "true", children: _jsx("td", { colSpan: colspan, style: { height: topPad } }) }) })) : null, rowsToRender.map((row, index) => renderRow(row, index)), virtualScroll && bottomPad > 0 ? (_jsx("tbody", { children: _jsx("tr", { className: "kpt-data-table__spacer", "aria-hidden": "true", children: _jsx("td", { colSpan: colspan, style: { height: bottomPad } }) }) })) : null] })), hasSummary && !loading && displayCount > 0 ? (_jsx("tfoot", { children: _jsxs("tr", { className: "kpt-data-table__summary", children: [summaryLabelSpan > 0 ? (_jsx("th", { className: cn('kpt-data-table__summary-label', hasFrozen && 'kpt-data-table__cell--frozen'), colSpan: summaryLabelSpan, style: { left: hasFrozen ? 0 : undefined }, children: summaryLabel ?? t('dataTable.total') })) : null, visibleColumns.map((col, index) => index >= summaryStartIndex ? (_jsx("td", { "data-align": col.align ?? 'end', style: { width: col.width, left: frozenLeft(col.field) }, className: cn(frozenOffsets.has(col.field) && 'kpt-data-table__cell--frozen'), children: summaryText(col) }, col.field)) : null)] }) })) : null] }) }), _jsx("div", { className: "kpt-data-table__live", "aria-live": "polite", children: moveAnnouncement }), _jsx("div", { hidden: true, children: visibleColumns.map((col, colIndex) => isEditable(col) ? (_jsx("span", { id: `${uid}-edit-${colIndex}`, children: t('dataTable.editCell', { column: col.header }) }, col.field)) : null) }), pageSize > 0 && !virtualScroll ? (_jsxs("div", { className: "kpt-data-table__pager", children: [paginatorActions, _jsx(KptPaginator, { length: filteredCount, pageIndex: pageIndex, onPageIndexChange: setPageIndex, pageSize: pageSizeState, onPageSizeChange: setPageSizeState, pageSizeOptions: pageSizeOptions })] })) : null] }));
461
523
  }
@@ -1,7 +1,7 @@
1
1
  import type { KptColumn } from './data-table-types.ts';
2
2
  /**
3
- * Zrzuca wiersze do pliku CSV (separator `;`). Tabela woła to samo z przycisku „Eksport CSV";
4
- * funkcja jest wyeksportowana, bo w Reakcie nie ma odpowiednika metody publicznej komponentu
3
+ * Zrzuca wiersze do pliku CSV (separator `;`, końce wierszy CRLF, BOM — patrz `buildCsv`).
4
+ * Tabela woła to samo z przycisku „Eksport CSV"; funkcja jest wyeksportowana, bo w Reakcie nie ma odpowiednika metody publicznej komponentu
5
5
  * — eksport z kodu aplikacji wywołuje się właśnie tak.
6
6
  */
7
7
  export declare function exportTableCsv(columns: readonly KptColumn[], rows: readonly Record<string, unknown>[], filename?: string): void;
@@ -1,19 +1,17 @@
1
- import { escapeCsvCell } from '@konce-pt/table';
1
+ import { buildCsv } from '@konce-pt/table';
2
2
  /**
3
- * Zrzuca wiersze do pliku CSV (separator `;`). Tabela woła to samo z przycisku „Eksport CSV";
4
- * funkcja jest wyeksportowana, bo w Reakcie nie ma odpowiednika metody publicznej komponentu
3
+ * Zrzuca wiersze do pliku CSV (separator `;`, końce wierszy CRLF, BOM — patrz `buildCsv`).
4
+ * Tabela woła to samo z przycisku „Eksport CSV"; funkcja jest wyeksportowana, bo w Reakcie nie ma odpowiednika metody publicznej komponentu
5
5
  * — eksport z kodu aplikacji wywołuje się właśnie tak.
6
6
  */
7
7
  export function exportTableCsv(columns, rows, filename = 'export.csv') {
8
- const header = columns.map((col) => escapeCsvCell(col.header)).join(';');
9
- const body = rows.map((row) => columns.map((col) => escapeCsvCell(row[col.field])).join(';')).join('\n');
10
- // BOM — bez niego Excel czyta UTF-8 jako stronę kodową systemu i psuje polskie znaki.
11
- const csv = `${header}\n${body}`;
12
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
8
+ const blob = new Blob([buildCsv(columns, rows)], { type: 'text/csv;charset=utf-8;' });
13
9
  const url = URL.createObjectURL(blob);
14
10
  const link = document.createElement('a');
15
11
  link.href = url;
16
12
  link.download = filename;
17
13
  link.click();
18
- URL.revokeObjectURL(url);
14
+ // Część przeglądarek zaczyna pobieranie asynchronicznie — natychmiastowe unieważnienie
15
+ // adresu potrafi je przerwać.
16
+ setTimeout(() => URL.revokeObjectURL(url), 0);
19
17
  }
@@ -7,7 +7,9 @@ Import: `import { KptDataTable, type KptColumn } from '@konce-pt/react';`
7
7
 
8
8
  ## The column model (KptColumn)
9
9
  `{ field, header, sortable?, align?, width?, frozen?, filter?, edit?, summary? }`
10
- `frozen` columns (leading, adjacent) stick during horizontal scrolling — they require `width`.
10
+ `frozen` columns (leading, adjacent) stick during horizontal scrolling. Their offsets come from `width`:
11
+ `px`/`rem`/`em` are computed up front, other units (`%`, `ch`) and a missing width are measured from the
12
+ rendered header.
11
13
 
12
14
  - `filter`: `KptColumnFilter` from `@konce-pt/table` — `text`, `number`, `select` (with `multiple`),
13
15
  `numberRange`, `slider`, `dateRange`. Any column with a `filter` renders the filter row.
@@ -21,9 +23,19 @@ Import: `import { KptDataTable, type KptColumn } from '@konce-pt/react';`
21
23
  `rowKey`, `scrollHeight`, `virtualScroll`, `rowHeight` (41), `reorderable`, `exportable`,
22
24
  `exportFilename`, `lazy`, `totalRecords`, `expandable`, `multiExpand`, `summaryLabel`,
23
25
  `summaryValues`
26
+ - `ariaLabel` (the table's name without a visible caption), `caption` (a visible `<caption>`),
27
+ `clickableRows` (rows focusable and activated with Enter/Space; on by default when `onRowClick` is set)
24
28
  - Controllable state: `selection`, `expandedRows`, `hiddenFields` (each with `default*` and
25
29
  `on*Change`)
26
- - Events: `onRowClick`, `onLazyLoad`, `onCellEdit`
30
+ - Events: `onRowClick`, `onLazyLoad` (also once on start, for the first page), `onCellEdit`
31
+
32
+ ## Keyboard
33
+ - A sortable header is a button: Tab to it, Enter/Space cycles asc → desc → none; `aria-sort` is on the `<th>`.
34
+ - With `reorderable` the grip is a button: `←`/`→` move the column by one place; the new position is
35
+ announced and focus follows the grip.
36
+ - An editable cell is reachable with Tab and opens with Enter; its name stays its value and
37
+ "Edit: {column}" is its description.
38
+ - `selectable="single"` renders radios (one group per table).
27
39
 
28
40
  ## Templates are render props
29
41
  - `cells`: `Record<field, (row, value) => ReactNode>` — the equivalent of `kptCell`
@@ -33,7 +45,9 @@ Import: `import { KptDataTable, type KptColumn } from '@konce-pt/react';`
33
45
  - `paginatorActions` — the slot to the left of the paginator (a column-toggle button, say)
34
46
 
35
47
  ## The logic lives in a package
36
- Filter matching, aggregations, CSV escaping and cell-to-text come from `@konce-pt/table` — pure
48
+ Filter matching (the global filter and the text filter ignore case and diacritics: "lodz" finds
49
+ "Łódź"), sorting (`Intl.Collator` for the active language, natural numbers, empty cells last),
50
+ aggregations, the CSV file (`;`, CRLF, BOM) and cell-to-text come from `@konce-pt/table` — pure
37
51
  functions with `node --test` coverage, shared with the Angular port. What stays here is the state
38
52
  and the markup.
39
53
 
@@ -68,7 +82,8 @@ them while the search box does. With `lazy` the table only holds one page, so it
68
82
  ## i18n
69
83
  The toolbar, the states and the filter labels come from the `dataTable` namespace (`search`,
70
84
  `exportCsv`, `loading`, `empty`, `total`, `selectAll`, `selectRow`, `expandRow`, `editCell`,
71
- `filter` / `filterMin` / `filterMax` with `{column}`, `all`, `min`, `max`, `dateRange`).
85
+ `filter` / `filterMin` / `filterMax` with `{column}`, `all`, `min`, `max`, `dateRange`, `moveColumn`,
86
+ `columnMoved`).
72
87
 
73
88
  ## Tokens
74
89
  `--kpt-table-{bg,fg,border,radius,header-bg,header-fg,row-hover,row-selected,cell-padding}`,
@@ -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";
@@ -1,5 +1,5 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
- import { type KptDateRangeMode, type KptDateRangeValue } from '@konce-pt/datetime';
2
+ import { type KptDateDisplayFormat, type KptFirstDayOfWeek, type KptDateRangeMode, type KptDateRangeValue } from '@konce-pt/datetime';
3
3
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
4
4
  export type { KptDateRangeMode, KptDateRangeValue };
5
5
  export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange' | 'defaultValue'>, KptI18nOverrides {
@@ -12,7 +12,13 @@ export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'o
12
12
  hourFormat?: 12 | 24;
13
13
  minuteStep?: number;
14
14
  placeholder?: string;
15
- displayFormat?: 'pl' | 'iso';
15
+ /** Zapis dat: `'pl'` (domyślnie), `'iso'` albo `'locale'` (`Intl` dla aktywnego języka). */
16
+ displayFormat?: KptDateDisplayFormat;
17
+ /** Pierwszy dzień tygodnia (1 = poniedziałek … 7 = niedziela); `null` = wg aktywnego języka. */
18
+ firstDayOfWeek?: KptFirstDayOfWeek | null;
19
+ /** Stan panelu — pozwala otworzyć go z zewnątrz i wiedzieć, kiedy wybór się skończył. */
20
+ open?: boolean;
21
+ onOpenChange?: (open: boolean) => void;
16
22
  /** Najwcześniejsza dozwolona data 'RRRR-MM-DD' (włącznie). */
17
23
  minDate?: string | null;
18
24
  /** Najpóźniejsza dozwolona data 'RRRR-MM-DD' (włącznie). */
@@ -22,6 +28,10 @@ export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'o
22
28
  /** Id pola (nie hosta); bez niego generowane, w `KptFormField` — z wrappera. */
23
29
  id?: string;
24
30
  disabled?: boolean;
31
+ /** Tylko do odczytu — wartość widać, panel się nie otwiera (`aria-readonly`). */
32
+ readOnly?: boolean;
33
+ /** Pole wymagane — `aria-required`. */
34
+ required?: boolean;
25
35
  invalid?: boolean;
26
36
  touched?: boolean;
27
37
  onTouch?: () => void;
@@ -34,10 +44,12 @@ export interface KptDateRangeProps extends Omit<ComponentPropsWithRef<'div'>, 'o
34
44
  * Klawiatura jak w `KptDatepicker`: `Enter`/`Spacja`/`↓` otwierają panel z fokusem na początku
35
45
  * zakresu (albo dziś), strzałki, `Home`/`End` i `PageUp`/`PageDown` (z `Shift` — rok) chodzą po
36
46
  * dniach przez wszystkie panele, `Tab` krąży w panelu, `Escape` i wybór końca oddają fokus polu.
37
- * Krańce i środek zakresu są w nazwie dnia („…, początek zakresu”), a wybór ogłasza region `polite`.
47
+ * Krańce i środek zakresu są w nazwie dnia („…, początek zakresu”), a wybór i przewinięcie miesiąca
48
+ * ogłasza region `polite`. Pole to `role="combobox"` z `aria-haspopup="dialog"`, więc niesie
49
+ * `aria-required`, `aria-readonly` i `aria-invalid`. Tydzień zaczyna się wg aktywnego języka.
38
50
  *
39
51
  * @example
40
52
  * <KptDateRange value={period} onValueChange={setPeriod} visibleMonths={2} />
41
53
  * <KptDateRange mode="datetime" hourFormat={12} value={span} onValueChange={setSpan} />
42
54
  */
43
- export declare function KptDateRange({ value, defaultValue, onValueChange, mode, visibleMonths, hourFormat, minuteStep, placeholder, displayFormat, minDate, maxDate, ariaLabel, id, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDateRangeProps>): import("react").JSX.Element;
55
+ export declare function KptDateRange({ value, defaultValue, onValueChange, mode, visibleMonths, hourFormat, minuteStep, placeholder, displayFormat, firstDayOfWeek, open, onOpenChange, minDate, maxDate, ariaLabel, id, disabled, readOnly, required, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptDateRangeProps>): import("react").JSX.Element;