@konce-pt/react 0.17.0 → 0.18.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.
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useId, useRef, useState } from 'react';
4
- import { KptChartMotion, areaPath, bandScale, barLayout, chartAccessibleName, chartDataTable, dataScale, deltaKind, domainScale, extent, formatNumber, holdDomain, linePath, linearScale, niceScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, sameChartShape, seriesColor, significantDelta, } from '@konce-pt/chart';
4
+ import { KptChartMotion, areaPath, axisBase, bandPath, bandScale, barLayout, chartScale, chartAccessibleName, chartDataTable, dataScale, deltaKind, domainScale, formatNumber, linePath, linearScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, sameChartShape, scaleRows, seriesColor, significantDelta, stackSeries, } from '@konce-pt/chart';
5
5
  import { cn } from "../internal/cn.js";
6
6
  import { useKptMotion } from "../motion/motion-context.js";
7
7
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -106,8 +106,11 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
106
106
  const to = config.series.map((s) => [...s.data]);
107
107
  const withAxis = config.type === 'line' || config.type === 'bar';
108
108
  const visible = (rows) => rows.filter((_, i) => !hidden.has(`ser:${config.series[i]?.name ?? ''}`));
109
+ const axis = { valueAxis: config.valueAxis, stacked: config.stacked };
110
+ // Skala trzymana na czas ducha obejmuje oba stany - z wierszy już złożonych w stos.
111
+ const held = [...scaleRows(visible(from), config.stacked), ...scaleRows(visible(to), config.stacked)];
109
112
  const scales = withAxis
110
- ? { start: dataScale(visible(from)), hold: dataScale([holdDomain(visible(from), visible(to))]), end: dataScale(visible(to)) }
113
+ ? { start: dataScale(visible(from), axis), hold: chartScale(held, { valueAxis: config.valueAxis }), end: dataScale(visible(to), axis) }
111
114
  : null;
112
115
  motion.transition(from, to, { delta: withAxis && config.animation?.delta !== false, scales });
113
116
  }, [config, hidden, motion, motionConfig]);
@@ -204,16 +207,19 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
204
207
  if (view.type !== 'line')
205
208
  return null;
206
209
  const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
207
- const values = active.flatMap(({ s }) => s.data);
208
- const [mn, mx] = extent(values.length ? values : [0]);
210
+ const rows = active.map(({ s }) => s.data);
209
211
  const scale = frame.domain
210
212
  ? domainScale(frame.domain.target, frame.domain.min, frame.domain.max)
211
- : niceScale(Math.min(0, mn), Math.max(0, mx), 5);
213
+ : chartScale(rows, { valueAxis: view.valueAxis, stacked: view.stacked });
214
+ // Stos: punkty serii na narastającej sumie, pasmo od linii serii pod nią.
215
+ const stack = view.stacked ? stackSeries(rows) : null;
212
216
  const plot = plotArea(width, height, VERTICAL_INSETS);
213
217
  const yScale = linearScale(scale.min, scale.max, plot.y + plot.height, plot.y);
214
218
  const count = view.labels.length;
215
219
  const xAt = (i) => count > 1 ? plot.x + (i / (count - 1)) * plot.width : plot.x + plot.width / 2;
216
- const baseline = yScale(0);
220
+ // Punkt nie wychodzi poza oś - przypięte `valueAxis.min`/`max` mogą uciąć wartość.
221
+ const yAt = (v) => yScale(Math.min(scale.max, Math.max(scale.min, v)));
222
+ const baseline = yScale(axisBase(scale));
217
223
  const interpolation = view.interpolation ?? 'linear';
218
224
  const showPoints = view.points ?? true;
219
225
  const grid = scale.ticks.map((t) => ({
@@ -234,13 +240,21 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
234
240
  text: label,
235
241
  anchor: 'middle',
236
242
  }));
237
- const series = active.map(({ s, i }) => ({
243
+ const series = active.map(({ s, i }, k) => ({
238
244
  key: `ser:${s.name}`,
239
245
  name: s.name,
240
246
  color: seriesColor(s.color, i),
241
247
  i,
242
- points: s.data.map((v, idx) => ({ x: xAt(idx), y: yScale(v), value: v, label: view.labels[idx] ?? '' })),
248
+ points: s.data.map((v, idx) => ({ x: xAt(idx), y: yAt(stack?.top[k]?.[idx] ?? v), value: v, label: view.labels[idx] ?? '' })),
249
+ floor: stack ? s.data.map((_, idx) => ({ x: xAt(idx), y: yAt(stack.base[k]?.[idx] ?? 0) })) : null,
243
250
  }));
251
+ // Duch w stosie stoi na sumach poprzedniego stanu, tak jak bieżące punkty.
252
+ const ghostRows = ghostState
253
+ ? (() => {
254
+ const prev = active.map(({ i }) => ghostState.prev[i] ?? []);
255
+ return view.stacked ? stackSeries(prev).top : prev;
256
+ })()
257
+ : [];
244
258
  const crosshairCat = config.crosshair && activeIndex !== null ? catLabels[activeIndex] : undefined;
245
259
  // Prowadnica i pigułka jadą razem z dymkiem.
246
260
  const crossX = crosshairCat ? (frame.tip?.x ?? crosshairCat.x) : 0;
@@ -268,10 +282,10 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
268
282
  ? series.map((s) => {
269
283
  const prev = ghostState.prev[s.i] ?? [];
270
284
  const next = ghostState.next[s.i] ?? [];
271
- const gpts = prev.map((v, idx) => ({ x: xAt(idx), y: yScale(v) }));
285
+ const gpts = (ghostRows[series.indexOf(s)] ?? prev).map((v, idx) => ({ x: xAt(idx), y: yAt(v) }));
272
286
  return (_jsxs("g", { className: "kpt-chart__ghost", "aria-hidden": "true", children: [_jsx("path", { className: "kpt-chart__ghost-line", d: linePath(gpts, interpolation) }), gpts.map((p, idx) => (_jsx("line", { className: "kpt-chart__delta", "data-kind": deltaKind(prev[idx] ?? 0, next[idx] ?? 0), x1: p.x, x2: p.x, y1: p.y, y2: s.points[idx]?.y ?? p.y }, idx)))] }, `${ghostState.id}:${s.key}`));
273
287
  })
274
- : null, series.map((s) => (_jsxs("g", { className: cn('kpt-chart__series', dim(s.key) && 'kpt-chart__series--dim'), children: [view.area ? (_jsx("path", { className: "kpt-chart__area", d: areaPath(s.points, baseline, interpolation), style: { fill: s.color } })) : null, _jsx("path", { className: "kpt-chart__line", d: linePath(s.points, interpolation), style: { stroke: s.color }, pathLength: 1 }), showPoints
288
+ : null, series.map((s) => (_jsxs("g", { className: cn('kpt-chart__series', dim(s.key) && 'kpt-chart__series--dim'), children: [view.area ? (_jsx("path", { className: "kpt-chart__area", d: s.floor ? bandPath(s.points, s.floor, interpolation) : areaPath(s.points, baseline, interpolation), style: { fill: s.color } })) : null, _jsx("path", { className: "kpt-chart__line", d: linePath(s.points, interpolation), style: { stroke: s.color }, pathLength: 1 }), showPoints
275
289
  ? s.points.map((p, idx) => (_jsx("circle", { className: cn('kpt-chart__point', activeIndex === idx && 'kpt-chart__point--active'), cx: p.x, cy: p.y, r: 4, style: { fill: s.color, ['--kpt-chart-i']: idx } }, idx)))
276
290
  : null] }, s.key))), sheen
277
291
  ? series.map((s) => (_jsx("path", { className: "kpt-chart__sheen-line", d: linePath(s.points, interpolation), stroke: `url(#${sheenId})`, style: { opacity: sheen.opacity }, "aria-hidden": "true" }, s.key)))
@@ -301,7 +315,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
301
315
  const active = view.series.map((s, i) => ({ s, i })).filter(({ s }) => !hidden.has(`ser:${s.name}`));
302
316
  const plot = plotArea(width, height, vertical ? VERTICAL_INSETS : HORIZONTAL_INSETS);
303
317
  const radius = view.borderRadius ?? 4;
304
- const layout = barLayout(active.map(({ s }) => s.data), view.labels.length, plot, orientation, radius, frame.domain ? domainScale(frame.domain.target, frame.domain.min, frame.domain.max) : undefined);
318
+ const layout = barLayout(active.map(({ s }) => s.data), view.labels.length, plot, orientation, radius, frame.domain ? domainScale(frame.domain.target, frame.domain.min, frame.domain.max) : undefined, { stacked: view.stacked, valueAxis: view.valueAxis });
305
319
  const colors = active.map(({ s, i }) => seriesColor(s.color, i));
306
320
  const names = active.map(({ s }) => s.name);
307
321
  const cat = vertical
@@ -335,7 +349,7 @@ export function KptChart({ config, width = 600, height = 360, locale, dictionary
335
349
  ? (() => {
336
350
  const prevRows = active.map(({ i }) => ghostState.prev[i] ?? []);
337
351
  const nextRows = active.map(({ i }) => ghostState.next[i] ?? []);
338
- return barLayout(prevRows, view.labels.length, plot, orientation, radius, layout.scale).bars.map((g, k) => {
352
+ return barLayout(prevRows, view.labels.length, plot, orientation, radius, layout.scale, { stacked: view.stacked }).bars.map((g, k) => {
339
353
  const cur = layout.bars[k] ?? g;
340
354
  const a = barEnd(g, vertical);
341
355
  const z = barEnd(cur, vertical);
@@ -19,9 +19,19 @@ proportionally (`width={300} height={180}`) instead of letting a 600-unit chart
19
19
 
20
20
  ## The config is the API
21
21
  `{ type, labels, series, points?, area?, interpolation?, orientation?, borderRadius?, innerRadius?,
22
- crosshair?, tooltip?, legend?, title?, subtitle?, animation?, ariaLabel?, description? }` - the same shape the Angular port
22
+ valueAxis?, stacked?, crosshair?, tooltip?, legend?, title?, subtitle?, animation?, ariaLabel?, description? }` - the same shape the Angular port
23
23
  takes, because both read it from `@konce-pt/chart`.
24
24
 
25
+ ## Value axis and stacking (line, bar)
26
+ - `valueAxis?: { zero?, min?, max? }` - the value axis starts at zero by default, so a bar and the area under a line
27
+ show the size directly. `zero: false` fits the axis to the data (a weight of 79-90 kg is no longer a flat line under
28
+ the ceiling); `min` / `max` pin an end of the axis exactly and win over `zero` - a value beyond a pinned end is
29
+ cut at the edge. Bars and areas grow from zero, or from the end of the axis nearer to it
30
+ - `stacked?: boolean` - stacked series: bars on top of each other in one band per category (positive values up,
31
+ negative down; only the outer segment is rounded), lines as running totals with a band under each one (with
32
+ `area`). The axis covers the totals; the tooltip shows the values of the series, not the totals
33
+ - The rules live in `@konce-pt/chart`: `axisScale`, `chartScale`, `stackSeries`, `bandPath` (tested)
34
+
25
35
  ## The geometry lives in a package
26
36
  Scales, paths, bar layout and arcs come from `@konce-pt/chart` - pure functions with `node --test`
27
37
  coverage, shared with the Angular port. The component only turns them into SVG elements; there is
@@ -1,4 +1,5 @@
1
1
  import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type KptColumnGroup } from '@konce-pt/table';
2
3
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
4
  import { type KptSpeechColor } from '../speech/speech-button.tsx';
4
5
  import type { KptCellEditEvent, KptCellEditRenderers, KptCellRenderers, KptColumn, KptTableLazyState } from './data-table-types.ts';
@@ -45,6 +46,12 @@ export interface KptDataTableProps<T> extends Omit<ComponentPropsWithRef<'div'>,
45
46
  /** Szerokość kolumny bez liczalnego `width` w trybie `virtualScroll` (`px`/`rem`/`em`). */
46
47
  columnWidth?: string;
47
48
  rowHeight?: number;
49
+ /**
50
+ * Nagłówki grupowane: wiersz nad etykietami, w którym sąsiednie kolumny grupy dzielą komórkę
51
+ * (`[{ header: 'Makroskładniki', fields: ['carbs', 'protein', 'fat'] }]`). Liczony z kolumn
52
+ * widocznych, w bieżącej kolejności - ukryta albo przestawiona kolumna wypada z grupy sama.
53
+ */
54
+ columnGroups?: readonly KptColumnGroup[];
48
55
  reorderable?: boolean;
49
56
  /**
50
57
  * Przykleja kolumny systemowe (rozwijanie, zaznaczanie) do lewej krawędzi także bez kolumn `frozen`.
@@ -83,4 +90,4 @@ export interface KptDataTableProps<T> extends Omit<ComponentPropsWithRef<'div'>,
83
90
  /** Treść po lewej stronie paginatora (odpowiednik slotu `[kptPaginatorActions]`). */
84
91
  paginatorActions?: ReactNode;
85
92
  }
86
- export declare function KptDataTable<T = Record<string, unknown>>({ columns, data, filterable, filterSpeech, filterSpeechLang, filterSpeechColor, filterRowPosition, pageSize, pageSizeOptions, selectable, loading, ariaLabel, caption, clickableRows, emptyMessage, rowKey, scrollHeight, virtualScroll, columnWidth, rowHeight, reorderable, frozenSystemColumns, exportable, exportFilename, lazy, totalRecords, expandable, multiExpand, summaryLabel, summaryValues, selection, defaultSelection, onSelectionChange, expandedRows, defaultExpandedRows, onExpandedRowsChange, hiddenFields, defaultHiddenFields, onHiddenFieldsChange, onRowClick, onLazyLoad, onCellEdit, cells, cellEditors, renderRowDetail, paginatorActions, locale, dictionary, className, style, ...rest }: Readonly<KptDataTableProps<T>>): import("react").JSX.Element;
93
+ export declare function KptDataTable<T = Record<string, unknown>>({ columns, data, filterable, filterSpeech, filterSpeechLang, filterSpeechColor, filterRowPosition, pageSize, pageSizeOptions, selectable, loading, ariaLabel, caption, clickableRows, emptyMessage, rowKey, scrollHeight, virtualScroll, columnWidth, rowHeight, reorderable, columnGroups, frozenSystemColumns, exportable, exportFilename, lazy, totalRecords, expandable, multiExpand, summaryLabel, summaryValues, selection, defaultSelection, onSelectionChange, expandedRows, defaultExpandedRows, onExpandedRowsChange, hiddenFields, defaultHiddenFields, onHiddenFieldsChange, onRowClick, onLazyLoad, onCellEdit, cells, cellEditors, renderRowDetail, paginatorActions, locale, dictionary, className, style, ...rest }: Readonly<KptDataTableProps<T>>): import("react").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, } from 'react';
4
- import { aggregate, columnOffsets, createGlobalFilter, cssLengthToPx, isEmptyFilterValue, matchesColumnFilter, sortRows, toText, virtualColumnRange, virtualRange, } from '@konce-pt/table';
4
+ import { aggregate, columnOffsets, createGlobalFilter, cssLengthToPx, groupHeaderCells, isEmptyFilterValue, matchesColumnFilter, sortRows, toText, virtualColumnRange, virtualRange, } from '@konce-pt/table';
5
5
  import { cn } from "../internal/cn.js";
6
6
  import { useControllableState } from "../internal/use-controllable-state.js";
7
7
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -82,7 +82,7 @@ const EMPTY_FIELDS = [];
82
82
  function getValue(row, field) {
83
83
  return row[field];
84
84
  }
85
- export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, filterSpeech = false, filterSpeechLang, filterSpeechColor, 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, columnWidth = '10rem', rowHeight = 41, reorderable = false, frozenSystemColumns = 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 }) {
85
+ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, filterSpeech = false, filterSpeechLang, filterSpeechColor, 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, columnWidth = '10rem', rowHeight = 41, reorderable = false, columnGroups, frozenSystemColumns = 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 }) {
86
86
  const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
87
87
  const uid = useId();
88
88
  const rowsClickable = clickableRows ?? onRowClick !== undefined;
@@ -149,6 +149,8 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
149
149
  const [viewport, setViewport] = useState({ height: 400, width: 1200 });
150
150
  const scrollFrame = useRef(0);
151
151
  const [theadOffset, setTheadOffset] = useState(0);
152
+ /** Offset sticky trzeciego wiersza nagłówka (grupy + etykiety + filtry). */
153
+ const [theadOffset2, setTheadOffset2] = useState(0);
152
154
  const scrollRef = useRef(null);
153
155
  const theadRef = useRef(null);
154
156
  const editorHostRef = useRef(null);
@@ -234,14 +236,17 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
234
236
  return;
235
237
  requestLazyLoad({ pageIndex, pageSize: pageSizeState, sortField, sortDir, filter: filterText, columnFilters });
236
238
  }, [lazy, pageIndex, pageSizeState, sortField, sortDir, filterText, columnFilters]);
237
- // Offset sticky drugiego wiersza nagłówka = wysokość pierwszego.
239
+ // Offsety sticky drugiego i trzeciego wiersza nagłówka = wysokość jednego i dwóch pierwszych.
238
240
  useLayoutEffect(() => {
239
- const first = theadRef.current?.rows?.[0];
241
+ const rows = theadRef.current?.rows;
240
242
  // Dokładna wysokość, nie `offsetHeight`: ten zaokrągla (46,5 -> 47) i przy skali ekranu innej
241
243
  // niż 100% między wierszami nagłówka zostawała szpara, przez którą prześwitywały wiersze.
242
- if (first)
243
- setTheadOffset(first.getBoundingClientRect().height);
244
- }, [filterRowPosition, hasColumnFilters, visibleColumns]);
244
+ const h1 = rows?.[0]?.getBoundingClientRect().height ?? 0;
245
+ const h2 = rows?.[1]?.getBoundingClientRect().height ?? 0;
246
+ if (rows?.[0])
247
+ setTheadOffset(h1);
248
+ setTheadOffset2(h1 + h2);
249
+ }, [filterRowPosition, hasColumnFilters, visibleColumns, columnGroups]);
245
250
  // Rozmiar viewportu dla wirtualizacji - także po zmianie rozmiaru kontenera bez przewinięcia.
246
251
  useLayoutEffect(() => {
247
252
  const el = scrollRef.current;
@@ -400,8 +405,22 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
400
405
  const tableWidth = virtualScroll
401
406
  ? scrollable.pinnedWidth + scrollable.offsets[scrollable.offsets.length - 1]
402
407
  : undefined;
403
- /** Liczba wierszy nagłówka (etykiety + ewentualnie filtry) - przesunięcie `aria-rowindex`. */
404
- const headerRowCount = hasColumnFilters ? 2 : 1;
408
+ /**
409
+ * Wiersz grup nad etykietami: komórki nad pozycjami `slots`. Komórka przykleja się do lewej, gdy
410
+ * wszystkie jej kolumny są zamrożone - inaczej przewija się z nimi. Pusta lista = bez wiersza.
411
+ */
412
+ const groupCells = columnGroups?.length
413
+ ? groupHeaderCells(slots.map((s) => s.meta?.col.field ?? null), columnGroups).map((cell) => {
414
+ const metas = slots.slice(cell.start, cell.start + cell.span).map((s) => s.meta);
415
+ const spacer = metas.some((m) => m === null);
416
+ const frozen = !spacer && metas.every((m) => m?.frozen);
417
+ return { ...cell, key: slots[cell.start]?.key ?? String(cell.start), spacer, frozen, left: frozen ? (metas[0]?.left ?? undefined) : undefined };
418
+ })
419
+ : [];
420
+ /** Wiersz grup jest pierwszym wierszem nagłówka - przesuwa `aria-rowindex` pozostałych. */
421
+ const groupRows = groupCells.length ? 1 : 0;
422
+ /** Liczba wierszy nagłówka (grupy, etykiety, ewentualnie filtry) - przesunięcie `aria-rowindex`. */
423
+ const headerRowCount = groupRows + (hasColumnFilters ? 2 : 1);
405
424
  /** `aria-colindex` komórki kolumny (od 1, z kolumnami systemowymi) - tylko w trybie wirtualnym. */
406
425
  const colIndex = (meta) => virtualScroll ? leadingCells + meta.index + 1 : undefined;
407
426
  // -- Przestawianie kolumn --
@@ -671,7 +690,7 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
671
690
  return null;
672
691
  }
673
692
  };
674
- const filterRowIndex = filterRowPosition === 'top' ? 1 : 2;
693
+ const filterRowIndex = groupRows + (filterRowPosition === 'top' ? 1 : 2);
675
694
  const filterRow = (_jsxs("tr", { className: "kpt-data-table__filter-row", "aria-rowindex": virtualScroll ? filterRowIndex : undefined, children: [hasRowDetail ? (_jsx("th", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft } })) : null, selectable !== 'none' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft } })) : null, slots.map((slot) => slot.meta ? (_jsx("th", { style: { width: virtualScroll ? undefined : slot.meta.col.width, left: slot.meta.left }, "data-align": slot.meta.align, "aria-colindex": colIndex(slot.meta), className: cn(slot.meta.frozen && 'kpt-data-table__cell--frozen'), children: filterCell(slot.meta.col) }, slot.key)) : (hspacer(slot)))] }));
676
695
  const editor = (row, col) => {
677
696
  const custom = cellEditors?.[col.field];
@@ -743,7 +762,7 @@ export function KptDataTable({ columns, data = EMPTY_ROWS, filterable = false, f
743
762
  startEdit(row, col, event);
744
763
  }, 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) }, slot.key));
745
764
  })] }), 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));
746
- 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 ? (_jsxs("div", { className: "kpt-data-table__search", "data-speech": filterSpeechState, children: [_jsx("input", { ref: filterRef, className: "kpt-data-table__filter", type: "search", placeholder: t('dataTable.search'), "aria-label": t('dataTable.search'), value: filterInput, onChange: (event) => onFilterInput(event.target.value) }), filterSpeech ? (_jsx(KptSpeechButton, { listeningColor: filterSpeechColor, speech: filterMic, size: "sm", unsupported: "hide", hostProps: { className: 'kpt-data-table__speech' } })) : null] })) : 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'), style: { width: tableWidth }, "aria-label": ariaLabel ?? undefined, "aria-busy": loading || undefined, "aria-rowcount": virtualScroll ? headerRowCount + sorted.length + (hasSummary ? 1 : 0) : undefined, "aria-colcount": virtualScroll ? leadingCells + visibleColumns.length : undefined, children: [caption ? _jsx("caption", { className: "kpt-data-table__caption", children: _jsx("span", { className: "kpt-data-table__caption-text", children: caption }) }) : null, virtualScroll ? (_jsxs("colgroup", { children: [hasRowDetail ? _jsx("col", { className: "kpt-data-table__col-expander" }) : null, selectable !== 'none' ? _jsx("col", { className: "kpt-data-table__col-checkbox" }) : null, slots.map((slot) => (_jsx("col", { style: { width: slot.width } }, slot.key)))] })) : null, _jsxs("thead", { ref: theadRef, children: [hasColumnFilters && filterRowPosition === 'top' ? filterRow : null, _jsxs("tr", { "aria-rowindex": virtualScroll ? (hasColumnFilters && filterRowPosition === 'top' ? 2 : 1) : undefined, 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) => {
765
+ return (_jsxs("div", { className: cn('kpt-data-table', className), style: { ['--kpt-thead-offset']: `${theadOffset}px`, ['--kpt-thead-offset-2']: `${theadOffset2}px`, ...style }, ...rest, children: [filterable || exportable ? (_jsxs("div", { className: "kpt-data-table__toolbar", children: [filterable ? (_jsxs("div", { className: "kpt-data-table__search", "data-speech": filterSpeechState, children: [_jsx("input", { ref: filterRef, className: "kpt-data-table__filter", type: "search", placeholder: t('dataTable.search'), "aria-label": t('dataTable.search'), value: filterInput, onChange: (event) => onFilterInput(event.target.value) }), filterSpeech ? (_jsx(KptSpeechButton, { listeningColor: filterSpeechColor, speech: filterMic, size: "sm", unsupported: "hide", hostProps: { className: 'kpt-data-table__speech' } })) : null] })) : 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'), style: { width: tableWidth }, "aria-label": ariaLabel ?? undefined, "aria-busy": loading || undefined, "aria-rowcount": virtualScroll ? headerRowCount + sorted.length + (hasSummary ? 1 : 0) : undefined, "aria-colcount": virtualScroll ? leadingCells + visibleColumns.length : undefined, children: [caption ? _jsx("caption", { className: "kpt-data-table__caption", children: _jsx("span", { className: "kpt-data-table__caption-text", children: caption }) }) : null, virtualScroll ? (_jsxs("colgroup", { children: [hasRowDetail ? _jsx("col", { className: "kpt-data-table__col-expander" }) : null, selectable !== 'none' ? _jsx("col", { className: "kpt-data-table__col-checkbox" }) : null, slots.map((slot) => (_jsx("col", { style: { width: slot.width } }, slot.key)))] })) : null, _jsxs("thead", { ref: theadRef, children: [groupCells.length ? (_jsxs("tr", { className: "kpt-data-table__group-row", "aria-rowindex": virtualScroll ? 1 : undefined, children: [hasRowDetail ? (_jsx("th", { className: cn('kpt-data-table__expander', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: expanderStickyLeft } })) : null, selectable !== 'none' ? (_jsx("th", { className: cn('kpt-data-table__checkbox', hasFrozen && 'kpt-data-table__cell--frozen'), style: { left: checkboxStickyLeft } })) : null, groupCells.map((cell) => cell.spacer ? (_jsx("td", { className: "kpt-data-table__hspacer", colSpan: cell.span, "aria-hidden": "true" }, cell.key)) : (_jsx("th", { className: cn('kpt-data-table__group', !cell.header && 'kpt-data-table__group--empty', cell.frozen && 'kpt-data-table__cell--frozen'), style: { left: cell.left }, colSpan: cell.span > 1 ? cell.span : undefined, scope: cell.header ? 'colgroup' : undefined, children: cell.header }, cell.key)))] })) : null, hasColumnFilters && filterRowPosition === 'top' ? filterRow : null, _jsxs("tr", { "aria-rowindex": virtualScroll ? groupRows + (hasColumnFilters && filterRowPosition === 'top' ? 2 : 1) : undefined, 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) => {
747
766
  if (el)
748
767
  el.indeterminate = pageSomeSelected;
749
768
  }, "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, slots.map((slot) => {
@@ -18,6 +18,11 @@ rendered header.
18
18
 
19
19
  ## Props
20
20
  - `columns` (required), `data`
21
+ - `columnGroups`: KptColumnGroup[] - grouped headers: a row above the column labels where adjacent columns of a
22
+ group share one cell (`[{ header: 'Macros', fields: ['carbs', 'protein', 'fat'] }]`, `scope="colgroup"`). It is
23
+ computed from the visible columns in their current order: a hidden column drops out, a group split by reordering
24
+ gets two cells with the same text, a column without a group gets an empty cell. A cell sticks to the left only
25
+ when all its columns are `frozen`. With column filters the header has three rows and all of them stay sticky
21
26
  - `filterable`, `filterRowPosition` (`'bottom'` default), `pageSize` (0 = no pagination),
22
27
  `pageSizeOptions`, `selectable` (`'none' | 'single' | 'multiple'`), `loading`, `emptyMessage`,
23
28
  `rowKey`, `scrollHeight`, `virtualScroll` (rows **and** columns, see below), `columnWidth`
@@ -113,7 +118,7 @@ The toolbar, the states and the filter labels come from the `dataTable` namespac
113
118
  the expanded row `--kpt-table-detail-bg`, the summary row `--kpt-table-summary-bg`.
114
119
 
115
120
  ## Not ported yet
116
- Grouped columns (absent in the Angular port too). A ready column-toggle UI is not part of either
121
+ A ready column-toggle UI is not part of either
117
122
  port - `hiddenFields` is the hook to build one on.
118
123
 
119
124
  ## Filter by voice
@@ -50,9 +50,16 @@ export function KptDialog({ open, onOpenChange, header = '', size = 'md', dismis
50
50
  * `close` przychodzi z każdej drogi. Przy `dismissible=false` zamknąć wolno tylko z kodu:
51
51
  * Chromium z CloseWatcherem pozwala anulować `cancel` jedynie po aktywacji użytkownika, więc drugi
52
52
  * Escape z rzędu zamyka okno mimo `preventDefault` - wtedy otwieramy je z powrotem.
53
+ *
54
+ * Zdarzenie `close` jest asynchroniczne, więc może dojść, gdy okno jest już znowu otwarte: pod
55
+ * `StrictMode` efekt montuje się dwa razy (`showModal` -> sprzątanie `close` -> `showModal`)
56
+ * i dialog zamontowany od razu z `open` dostawał `onOpenChange(false)` od własnego sprzątania.
57
+ * Otwarty element znaczy, że to zamknięcie jest nieaktualne.
53
58
  */
54
59
  const onClose = () => {
55
60
  const el = dialogRef.current;
61
+ if (el?.open)
62
+ return;
56
63
  if (!dismissible && open && el && !el.open) {
57
64
  el.showModal();
58
65
  return;
@@ -42,6 +42,11 @@ export interface KptInputProps extends Omit<ComponentPropsWithRef<'input'>, 'typ
42
42
  * gdzie zastępuje systemowe "x" (ukryte zawsze, także przy `clearable={false}`).
43
43
  */
44
44
  clearable?: boolean;
45
+ /**
46
+ * Ikona z rejestru na początku pola (`search`, `mail`...). Dekoracyjna - nazwę pola daje etykieta
47
+ * albo `aria-label`, więc ikona nie trafia do drzewa dostępności.
48
+ */
49
+ icon?: string;
45
50
  }
46
51
  /**
47
52
  * Pole tekstowe Koncept UI. Renderuje natywny `<input>` w hoście `.kpt-input-host`.
@@ -57,4 +62,4 @@ export interface KptInputProps extends Omit<ComponentPropsWithRef<'input'>, 'typ
57
62
  * <KptInput name="email" type="email" defaultValue="" />
58
63
  * <KptInput value={email} onValueChange={setEmail} invalid={!ok} touched={blurred} />
59
64
  */
60
- export declare function KptInput({ type, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech, speechLang, speechColor, speechMode, clearable, onKeyDown, locale, dictionary, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptInputProps>): import("react").JSX.Element;
65
+ export declare function KptInput({ type, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech, speechLang, speechColor, speechMode, clearable, icon, onKeyDown, locale, dictionary, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptInputProps>): import("react").JSX.Element;
@@ -30,7 +30,7 @@ function setFieldValue(field, value) {
30
30
  * <KptInput name="email" type="email" defaultValue="" />
31
31
  * <KptInput value={email} onValueChange={setEmail} invalid={!ok} touched={blurred} />
32
32
  */
33
- export function KptInput({ type = 'text', required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech = false, speechLang, speechColor, speechMode = 'insert', clearable, onKeyDown, locale, dictionary, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }) {
33
+ export function KptInput({ type = 'text', required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech = false, speechLang, speechColor, speechMode = 'insert', clearable, icon, onKeyDown, locale, dictionary, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }) {
34
34
  const { className: hostClassName, ...hostRest } = hostProps ?? {};
35
35
  const field = useKptFieldA11y(id, 'kpt-input');
36
36
  const innerRef = useRef(null);
@@ -92,5 +92,5 @@ export function KptInput({ type = 'text', required = false, invalid = false, tou
92
92
  onBlur?.(event);
93
93
  onTouch?.();
94
94
  }, [onBlur, onTouch]);
95
- return (_jsxs("span", { className: cn('kpt-input-host', hostClassName), "data-speech": speechState, "data-clear": showClear ? '' : undefined, ...hostRest, children: [_jsx("input", { className: cn('kpt-input__field', className), id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), type: type, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, onKeyDown: handleKeyDown, ref: composeRefs(innerRef, ref), ...rest }), showClear ? (_jsx("button", { type: "button", className: "kpt-input__clear", tabIndex: -1, "aria-label": t('input.clear'), onMouseDown: (event) => event.preventDefault(), onClick: clear, children: _jsx(KptIcon, { name: "x" }) })) : null, speech ? (_jsx(KptSpeechButton, { listeningColor: speechColor, speech: mic, size: "sm", unsupported: "hide", disabled: !!rest.disabled || !!rest.readOnly, hostProps: { className: 'kpt-input__speech' } })) : null] }));
95
+ return (_jsxs("span", { className: cn('kpt-input-host', hostClassName), "data-speech": speechState, "data-clear": showClear ? '' : undefined, "data-icon": icon ? '' : undefined, ...hostRest, children: [icon ? _jsx(KptIcon, { className: "kpt-input__icon", name: icon }) : null, _jsx("input", { className: cn('kpt-input__field', className), id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), type: type, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, onKeyDown: handleKeyDown, ref: composeRefs(innerRef, ref), ...rest }), showClear ? (_jsx("button", { type: "button", className: "kpt-input__clear", tabIndex: -1, "aria-label": t('input.clear'), onMouseDown: (event) => event.preventDefault(), onClick: clear, children: _jsx(KptIcon, { name: "x" }) })) : null, speech ? (_jsx(KptSpeechButton, { listeningColor: speechColor, speech: mic, size: "sm", unsupported: "hide", disabled: !!rest.disabled || !!rest.readOnly, hostProps: { className: 'kpt-input__speech' } })) : null] }));
96
96
  }
@@ -16,6 +16,9 @@ Import: `import { KptInput } from '@konce-pt/react';`
16
16
  - `required`: boolean - see the note below
17
17
  - `invalid`: boolean, `touched`: boolean - the error state
18
18
  - `clearable`: boolean (default: `true` only for `type="search"`) - a clear button, see "Clear button"
19
+ - `icon`: string - a registry icon at the start of the field (`search`, `mail`...). Decorative: the field's
20
+ name comes from the label or the aria label, so the icon stays out of the accessibility tree; a click on it
21
+ lands in the field
19
22
  - `hostProps` - props for the `.kpt-input-host` wrapper
20
23
  - `className`, `ref`, `name`, `placeholder`, `autoComplete` and every other `<input>` prop go to
21
24
  the inner `<input>`
@@ -66,6 +69,7 @@ dictionary. The host gets `data-clear` while the button is visible.
66
69
 
67
70
  <KptInput type="search" value={query} onValueChange={setQuery} />
68
71
  <KptInput clearable value={coupon} onValueChange={setCoupon} />
72
+ <KptInput type="search" icon="search" value={query} onValueChange={setQuery} />
69
73
 
70
74
  ## Tokens
71
75
  `--kpt-form-field-{bg,text,placeholder,border,border-hover,border-focus,border-error,radius}`.
@@ -17,6 +17,9 @@ Import: `import { KptPopover } from '@konce-pt/react';`
17
17
  - `placement`: 'bottom' (default) | 'top' - the preferred side; it flips when there is no room
18
18
  - `maxWidth`: string - the panel's maximum width, any CSS length (`'24rem'`; default `20rem`). The panel is
19
19
  never wider than the window minus the 8 px overlay margin
20
+ - `ariaLabel`: string - the accessible name of the panel (`role="dialog"`); give one whenever the panel has
21
+ no visible heading. It names the panel, not the host - `aria-label` in the rest props would land on the
22
+ host `<span>`
20
23
  - `className`, `ref` and every other `<span>` prop go to the host
21
24
 
22
25
  ## The panel is a portal
@@ -74,7 +74,7 @@ export { KptDataTable } from './data-table/data-table.tsx';
74
74
  export type { KptDataTableProps } from './data-table/data-table.tsx';
75
75
  export { exportTableCsv } from './data-table/export-csv.ts';
76
76
  export type { KptCellEditContext, KptCellEditEvent, KptCellEditRenderers, KptCellRenderers, KptColumn, KptColumnEdit, KptColumnEditType, KptSortDir, KptTableLazyState, } from './data-table/data-table-types.ts';
77
- export type { KptColumnFilter, KptColumnFilterType, KptColumnSummary, KptSummaryFn } from '@konce-pt/table';
77
+ export type { KptColumnFilter, KptColumnFilterType, KptColumnGroup, KptColumnSummary, KptSummaryFn } from '@konce-pt/table';
78
78
  export { KptDataView } from './data-view/data-view.tsx';
79
79
  export type { KptDataViewLayout, KptDataViewProps } from './data-view/data-view.tsx';
80
80
  export { KptTree } from './tree/tree.tsx';
@@ -123,7 +123,7 @@ export type { KptOverlayProps } from './internal/overlay.tsx';
123
123
  export { KptButtonGroup } from './button-group/button-group.tsx';
124
124
  export type { KptButtonGroupProps } from './button-group/button-group.tsx';
125
125
  export { KptSelect } from './select/select.tsx';
126
- export type { KptSelectProps } from './select/select.tsx';
126
+ export type { KptSelectMultipleProps, KptSelectProps, KptSelectSingleProps } from './select/select.tsx';
127
127
  export type { KptAlertVariant, KptNavigateEvent, KptOption } from './shared/types.ts';
128
128
  export { KptFormField } from './form-field/form-field.tsx';
129
129
  export type { KptFormFieldLabelType, KptFormFieldProps } from './form-field/form-field.tsx';
@@ -15,7 +15,11 @@ A select on its own panel. Variants: single, with a search box (`filter`) and mu
15
15
 
16
16
  ## Props
17
17
  - `options`: `KptOption<T>[]` - `{ label, value, disabled? }`
18
- - `value` / `defaultValue` / `onValueChange` - a single `T`, or `T[]` in `multiple` mode
18
+ - `value` / `defaultValue` / `onValueChange` - a single `T`, or `T[]` in `multiple` mode. The handler type
19
+ follows `multiple` (overloads): without it `(value: T) => void` - a single select cannot clear its
20
+ choice, so it never reports an array or `null`; with `multiple` `(value: T[]) => void`. A flag computed at
21
+ run time (`multiple={flag}`) keeps the wide `T | T[] | null`. Types: `KptSelectSingleProps`,
22
+ `KptSelectMultipleProps`, `KptSelectProps` (the wide one)
19
23
  - `open` / `defaultOpen` / `onOpenChange` - the panel state, so the list can be opened from outside
20
24
  - `placeholder`, `multiple`, `filter`, `disabled`, `invalid`, `touched`
21
25
  - `onTouch()` - fires when the panel closes or focus leaves the closed trigger
@@ -1,4 +1,4 @@
1
- import { type ComponentPropsWithRef } from 'react';
1
+ import { type ComponentPropsWithRef, type JSX } from 'react';
2
2
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  import type { KptOption } from '../shared/types.ts';
4
4
  export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'children' | 'onChange' | 'defaultValue'>, KptI18nOverrides {
@@ -28,6 +28,23 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
28
28
  /** Wywoływane, gdy panel się zamyka albo trigger traci fokus. */
29
29
  onTouch?: () => void;
30
30
  }
31
+ /**
32
+ * Pojedynczy wybór: `onValueChange` dostaje zawsze wybraną opcję - nigdy tablicę ani `null`, bo
33
+ * lista bez `multiple` nie ma jak wyczyścić wyboru. Rodzic trzyma więc zwykłe `T`.
34
+ */
35
+ export interface KptSelectSingleProps<T> extends Omit<KptSelectProps<T>, 'multiple' | 'value' | 'defaultValue' | 'onValueChange'> {
36
+ multiple?: false;
37
+ value?: T | null;
38
+ defaultValue?: T | null;
39
+ onValueChange?: (value: T) => void;
40
+ }
41
+ /** Wybór wielokrotny: wartością jest zawsze tablica (pusta, gdy nic nie wybrano). */
42
+ export interface KptSelectMultipleProps<T> extends Omit<KptSelectProps<T>, 'multiple' | 'value' | 'defaultValue' | 'onValueChange'> {
43
+ multiple: true;
44
+ value?: T[] | null;
45
+ defaultValue?: T[] | null;
46
+ onValueChange?: (value: T[]) => void;
47
+ }
31
48
  /**
32
49
  * Lista wyboru (select) na własnym panelu. Warianty: pojedynczy, z wyszukiwarką (`filter`)
33
50
  * i wielokrotny (`multiple`, chipy).
@@ -46,5 +63,10 @@ export interface KptSelectProps<T> extends Omit<ComponentPropsWithRef<'div'>, 'c
46
63
  * @example
47
64
  * <KptSelect options={roles} value={role} onValueChange={setRole} placeholder="Wybierz..." />
48
65
  * <KptSelect options={tags} multiple filter value={tags} onValueChange={setTags} />
66
+ *
67
+ * Typ `onValueChange` idzie za `multiple`: bez niego `(value: T)`, z `multiple` - `(value: T[])`.
68
+ * `multiple` liczone w locie (`multiple={flag}`) zostawia szeroki typ `T | T[] | null`.
49
69
  */
50
- export declare function KptSelect<T>({ options, placeholder, multiple, filter, disabled, readOnly, invalid, touched, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptSelectProps<T>>): import("react").JSX.Element;
70
+ export declare function KptSelect<T>(props: Readonly<KptSelectSingleProps<T>>): JSX.Element;
71
+ export declare function KptSelect<T>(props: Readonly<KptSelectMultipleProps<T>>): JSX.Element;
72
+ export declare function KptSelect<T>(props: Readonly<KptSelectProps<T>>): JSX.Element;
@@ -18,25 +18,6 @@ function filterSelectOptions(options, text) {
18
18
  const q = foldMentionText(text.trim());
19
19
  return q ? options.filter((option) => foldMentionText(option.label).includes(q)) : options;
20
20
  }
21
- /**
22
- * Lista wyboru (select) na własnym panelu. Warianty: pojedynczy, z wyszukiwarką (`filter`)
23
- * i wielokrotny (`multiple`, chipy).
24
- *
25
- * Klawiatura według wzorca WAI-ARIA "select-only combobox": fokus zostaje na polu, a aktywna opcja
26
- * idzie przez `aria-activedescendant`. `ArrowDown`/`ArrowUp`/`Enter`/`Spacja` otwierają listę na wybranej opcji,
27
- * strzałki, `Home`/`End`, `PageUp`/`PageDown` chodzą po opcjach (z pominięciem wyłączonych),
28
- * wpisywane litery skaczą do pasującej opcji (bez polskich znaków), `Enter` wybiera, `Escape`
29
- * zamyka. Z `filter` fokus przechodzi do pola wyszukiwania, które obsługuje te same klawisze.
30
- * W trybie `multiple` `Backspace` na zamkniętym polu usuwa ostatni wybór.
31
- *
32
- * Wewnątrz `KptFormField` zgłasza wypełnienie hookiem `useKptFormFieldFilled`, więc etykieta
33
- * Float Label unosi się nad wybraną wartością - natywnego pola, z którego wrapper mógłby to
34
- * odczytać, tu nie ma.
35
- *
36
- * @example
37
- * <KptSelect options={roles} value={role} onValueChange={setRole} placeholder="Wybierz..." />
38
- * <KptSelect options={tags} multiple filter value={tags} onValueChange={setTags} />
39
- */
40
21
  export function KptSelect({ options = [], placeholder = '', multiple = false, filter = false, disabled = false, readOnly = false, invalid = false, touched, value, defaultValue = null, onValueChange, open, defaultOpen = false, onOpenChange, onTouch, locale, dictionary, className, onBlur, id, required = false, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, ...rest }) {
41
22
  /** `div` z rolą nie dostaje nazwy z `label[for]` - stąd `aria-labelledby` na etykietę wrappera. */
42
23
  const field = useKptFieldA11y(id, 'kpt-select-trigger');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konce-pt/react",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Open-source React 19 component library on the Koncept UI design system - the same kpt-* CSS layer and --kpt-* tokens as the Angular port. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "konce.pt",
@@ -65,19 +65,19 @@
65
65
  "leaflet": "^1.9.0"
66
66
  },
67
67
  "dependencies": {
68
- "@konce-pt/chart": "0.17.0",
69
- "@konce-pt/chat": "0.17.0",
70
- "@konce-pt/backdrop": "0.17.0",
71
- "@konce-pt/mention": "0.17.0",
72
- "@konce-pt/motion": "0.17.0",
73
- "@konce-pt/roadmap": "0.17.0",
74
- "@konce-pt/icons": "0.17.0",
75
- "@konce-pt/datetime": "0.17.0",
76
- "@konce-pt/table": "0.17.0",
77
- "@konce-pt/speech": "0.17.0",
78
- "@konce-pt/i18n": "0.17.0",
79
- "@konce-pt/tokens": "0.17.0",
80
- "@konce-pt/map": "0.17.0"
68
+ "@konce-pt/backdrop": "0.18.0",
69
+ "@konce-pt/mention": "0.18.0",
70
+ "@konce-pt/chat": "0.18.0",
71
+ "@konce-pt/chart": "0.18.0",
72
+ "@konce-pt/speech": "0.18.0",
73
+ "@konce-pt/i18n": "0.18.0",
74
+ "@konce-pt/icons": "0.18.0",
75
+ "@konce-pt/motion": "0.18.0",
76
+ "@konce-pt/roadmap": "0.18.0",
77
+ "@konce-pt/datetime": "0.18.0",
78
+ "@konce-pt/table": "0.18.0",
79
+ "@konce-pt/tokens": "0.18.0",
80
+ "@konce-pt/map": "0.18.0"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@types/react": "^19.2.0",