@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
@@ -8,6 +8,16 @@ export interface KptButtonGroupProps<T> extends Omit<ComponentPropsWithRef<'div'
8
8
  onValueChange?: (value: T) => void;
9
9
  size?: 'sm' | 'md' | 'lg';
10
10
  disabled?: boolean;
11
+ /** Tylko do odczytu — wybór widać, nie da się go zmienić (`aria-readonly`). */
12
+ readOnly?: boolean;
13
+ /** Pole wymagane — `aria-required`. */
14
+ required?: boolean;
15
+ /** Id grupy; bez niego generowane, w `KptFormField` — z wrappera. */
16
+ id?: string;
17
+ /** Nazwa dostępna grupy, gdy nie opisuje jej etykieta (`KptFormField`). */
18
+ ariaLabel?: string | null;
19
+ invalid?: boolean;
20
+ touched?: boolean;
11
21
  /** Wywoływane po wyborze — moment, w którym kontrolka staje się dotknięta. */
12
22
  onTouch?: () => void;
13
23
  /** Chowa etykiety — zostaje sama ikona, a `label` zasila `aria-label` i dymek `KptTooltip`. */
@@ -22,10 +32,13 @@ export interface KptButtonGroupProps<T> extends Omit<ComponentPropsWithRef<'div'
22
32
  * Pozycja może mieć ikonę (`KptOption.icon`); `iconOnly` chowa etykietę, ale zostawia ją
23
33
  * w nazwie dostępnej, więc czytnik ekranu dalej wie, co wybiera.
24
34
  *
35
+ * Wybór jest wyłączny, więc grupa to `role="radiogroup"` z pozycjami `role="radio"` (APG) —
36
+ * czytnik ogłasza „2 z 3”. Jeden przystanek Tab, strzałki przenoszą wybór, `Home`/`End` na skrajne.
37
+ *
25
38
  * @example
26
39
  * <KptButtonGroup value={view} onValueChange={setView}
27
40
  * options={[{ label: 'Lista', value: 'list' }, { label: 'Siatka', value: 'grid' }]} />
28
41
  * <KptButtonGroup value={view} onValueChange={setView} iconOnly
29
42
  * options={[{ icon: 'list', label: 'Lista', value: 'list' }]} />
30
43
  */
31
- export declare function KptButtonGroup<T>({ options, value, defaultValue, onValueChange, size, disabled, onTouch, iconOnly, hostProps, className, ...rest }: Readonly<KptButtonGroupProps<T>>): import("react").JSX.Element;
44
+ export declare function KptButtonGroup<T>({ options, value, defaultValue, onValueChange, size, disabled, readOnly, required, id, ariaLabel, invalid, touched, onTouch, iconOnly, hostProps, className, ...rest }: Readonly<KptButtonGroupProps<T>>): import("react").JSX.Element;
@@ -1,8 +1,11 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useControllableState } from "../internal/use-controllable-state.js";
4
5
  import { KptIcon } from "../icon/icon.js";
5
6
  import { KptTooltip } from "../tooltip/tooltip.js";
7
+ import { useKptFieldA11y } from "../form-field/form-field-context.js";
8
+ import { nextRovingIndex, rovingStop } from "../internal/roving.js";
6
9
  /**
7
10
  * Segmentowana grupa przycisków (toggle) — pojedynczy wybór z opcji.
8
11
  *
@@ -10,27 +13,50 @@ import { KptTooltip } from "../tooltip/tooltip.js";
10
13
  * Pozycja może mieć ikonę (`KptOption.icon`); `iconOnly` chowa etykietę, ale zostawia ją
11
14
  * w nazwie dostępnej, więc czytnik ekranu dalej wie, co wybiera.
12
15
  *
16
+ * Wybór jest wyłączny, więc grupa to `role="radiogroup"` z pozycjami `role="radio"` (APG) —
17
+ * czytnik ogłasza „2 z 3”. Jeden przystanek Tab, strzałki przenoszą wybór, `Home`/`End` na skrajne.
18
+ *
13
19
  * @example
14
20
  * <KptButtonGroup value={view} onValueChange={setView}
15
21
  * options={[{ label: 'Lista', value: 'list' }, { label: 'Siatka', value: 'grid' }]} />
16
22
  * <KptButtonGroup value={view} onValueChange={setView} iconOnly
17
23
  * options={[{ icon: 'list', label: 'Lista', value: 'list' }]} />
18
24
  */
19
- export function KptButtonGroup({ options = [], value, defaultValue, onValueChange, size = 'md', disabled = false, onTouch, iconOnly = false, hostProps, className, ...rest }) {
25
+ export function KptButtonGroup({ options = [], value, defaultValue, onValueChange, size = 'md', disabled = false, readOnly = false, required = false, id, ariaLabel = null, invalid = false, touched, onTouch, iconOnly = false, hostProps, className, ...rest }) {
20
26
  const { className: hostClassName, ...hostRest } = hostProps ?? {};
21
27
  const [current, setCurrent] = useControllableState({
22
28
  value,
23
29
  defaultValue,
24
30
  onChange: onValueChange,
25
31
  });
32
+ /** Grupa nie dostaje nazwy z `label[for]` — stąd `aria-labelledby` na etykietę wrappera. */
33
+ const field = useKptFieldA11y(id, 'kpt-button-group');
34
+ const showInvalid = invalid && (touched ?? true);
35
+ const disabledMap = options.map((option) => disabled || !!option.disabled);
36
+ /** Jedyny przystanek Tab: wybrana pozycja, a bez wyboru pierwsza dozwolona. */
37
+ const tabStop = rovingStop(disabledMap, options.findIndex((option) => option.value === current));
26
38
  const select = (option) => {
27
- if (option.disabled || disabled)
39
+ if (option.disabled || disabled || readOnly)
28
40
  return;
29
41
  setCurrent(option.value);
30
42
  onTouch?.();
31
43
  };
32
- return (_jsx("span", { className: cn('kpt-button-group-host', hostClassName), ...hostRest, children: _jsx("div", { className: cn('kpt-button-group', className), role: "group", "data-size": size, "data-icon-only": iconOnly ? '' : undefined, ...rest, children: options.map((option) => {
44
+ /** Strzałki (pion traktowany jak poziom), `Home`/`End`: fokus i wybór idą razem — wzorzec radia. */
45
+ const onKeyDown = (event) => {
46
+ const vertical = { ArrowDown: 'ArrowRight', ArrowUp: 'ArrowLeft' };
47
+ const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
48
+ let key = vertical[event.key] ?? event.key;
49
+ if (rtl && (key === 'ArrowLeft' || key === 'ArrowRight'))
50
+ key = key === 'ArrowLeft' ? 'ArrowRight' : 'ArrowLeft';
51
+ const next = nextRovingIndex(disabledMap, tabStop, key);
52
+ if (next === null)
53
+ return;
54
+ event.preventDefault();
55
+ event.currentTarget.querySelectorAll('[role="radio"]')[next]?.focus();
56
+ select(options[next]);
57
+ };
58
+ return (_jsx("span", { className: cn('kpt-button-group-host', hostClassName), ...hostRest, children: _jsx("div", { className: cn('kpt-button-group', showInvalid && 'kpt-button-group--invalid', className), role: "radiogroup", id: field.id, "data-size": size, "data-icon-only": iconOnly ? '' : undefined, "aria-label": ariaLabel ?? undefined, "aria-labelledby": ariaLabel ? undefined : field.labelledBy, "aria-describedby": field.describedBy, "aria-required": required || undefined, "aria-readonly": readOnly || undefined, "aria-invalid": showInvalid || undefined, onKeyDown: onKeyDown, ...rest, children: options.map((option, index) => {
33
59
  const active = option.value === current;
34
- return (_jsx(KptTooltip, { text: iconOnly ? option.label : '', children: _jsxs("button", { type: "button", className: cn('kpt-button-group__item', active && 'is-active'), disabled: disabled || option.disabled, "aria-pressed": active, "aria-label": iconOnly ? option.label : undefined, onClick: () => select(option), children: [option.icon ? _jsx(KptIcon, { className: "kpt-button-group__icon", name: option.icon }) : null, iconOnly ? null : _jsx("span", { className: "kpt-button-group__label", children: option.label })] }) }, String(option.value)));
60
+ return (_jsx(KptTooltip, { text: iconOnly ? option.label : '', children: _jsxs("button", { type: "button", role: "radio", className: cn('kpt-button-group__item', active && 'is-active'), disabled: disabled || option.disabled, tabIndex: index === tabStop ? 0 : -1, "aria-checked": active, "aria-label": iconOnly ? option.label : undefined, onClick: () => select(option), children: [option.icon ? _jsx(KptIcon, { className: "kpt-button-group__icon", name: option.icon }) : null, iconOnly ? null : _jsx("span", { className: "kpt-button-group__label", children: option.label })] }) }, typeof option.value === 'object' && option.value !== null ? `#${index}` : String(option.value)));
35
61
  }) }) }));
36
62
  }
@@ -5,12 +5,12 @@ Import: `import { KptButtonGroup } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <span class="kpt-button-group-host">
8
- <div class="kpt-button-group" role="group" data-size="md">
9
- <button type="button" class="kpt-button-group__item is-active" aria-pressed="true">
8
+ <div class="kpt-button-group" role="radiogroup" data-size="md">
9
+ <button type="button" role="radio" class="kpt-button-group__item is-active" aria-checked="true" tabindex="0">
10
10
  <span class="kpt-button-group__icon kpt-icon-host">…</span>
11
11
  <span class="kpt-button-group__label">List</span>
12
12
  </button>
13
- <button type="button" class="kpt-button-group__item" aria-pressed="false">
13
+ <button type="button" role="radio" class="kpt-button-group__item" aria-checked="false" tabindex="-1">
14
14
  <span class="kpt-button-group__label">Grid</span>
15
15
  </button>
16
16
  </div>
@@ -60,6 +60,9 @@ each one a stable id and map back to the object yourself.
60
60
  `--kpt-color-border-strong`, `--kpt-radius-md`, `--kpt-button-gap`.
61
61
 
62
62
  ## Accessibility
63
- A `role="group"` of native buttons, each with `aria-pressed` — a screen reader announces which
64
- segment is on, and the keyboard walks them like any other buttons. With `iconOnly` the name comes
65
- from `aria-label`, so the announcement does not degrade to a bare "button".
63
+ A `role="radiogroup"` of native buttons with `role="radio"` and `aria-checked` — a screen reader
64
+ announces which segment is on and "2 of 3"; one Tab stop, arrows move the choice. With `iconOnly`
65
+ the name comes from `aria-label`, so the announcement does not degrade to a bare "radio button".
66
+
67
+ ## Read-only and required
68
+ The group is `role="radiogroup"` with `role="radio"` items (`aria-checked`) instead of `aria-pressed` buttons — the choice is exclusive, so a screen reader announces "2 of 3". One Tab stop, arrows move the selection, `Home`/`End` jump to the ends. New: `id`, `ariaLabel`, `readOnly`, `required`, and the error state (`invalid` + `touched`) visible on the group itself.
package/dist/card/card.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, isValidElement } from 'react';
2
+ import { isValidElement } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
+ import { slotChildren } from "../internal/slot-children.js";
4
5
  /*
5
6
  * Karta odtwarza projekcję treści z Angulara: nagłówek i stopka są wyjmowane
6
7
  * z dzieci i renderowane poza `.kpt-card__body`, dokładnie tam, gdzie stawia je
@@ -24,7 +25,7 @@ export function KptCard({ variant = 'elevated', header, headingLevel, flush = fa
24
25
  const headers = [];
25
26
  const footers = [];
26
27
  const body = [];
27
- Children.forEach(children, (child) => {
28
+ for (const child of slotChildren(children)) {
28
29
  const bucket = isValidElement(child) ? slotOf(child) : 'body';
29
30
  if (bucket === 'header')
30
31
  headers.push(child);
@@ -32,7 +33,7 @@ export function KptCard({ variant = 'elevated', header, headingLevel, flush = fa
32
33
  footers.push(child);
33
34
  else
34
35
  body.push(child);
35
- });
36
+ }
36
37
  return (_jsxs("div", { className: cn('kpt-card', className), "data-variant": variant, ...rest, children: [header ? (_jsx("div", { className: "kpt-card__header", role: headingLevel ? 'heading' : undefined, "aria-level": headingLevel, children: header })) : null, headers, _jsx("div", { className: cn('kpt-card__body', flush && 'kpt-card__body--flush'), children: body }), footers] }));
37
38
  }
38
39
  function slotOf(child) {
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useControllableState } from "../internal/use-controllable-state.js";
@@ -19,11 +20,13 @@ import { KptIcon } from "../icon/icon.js";
19
20
  */
20
21
  export function KptCarousel({ items = [], activeIndex, defaultActiveIndex = 0, onActiveIndexChange, renderSlide, label, locale, dictionary, className, ...rest }) {
21
22
  const { t } = useKptMessages({ locale, dictionary });
22
- const [active, setActive] = useControllableState({
23
+ const [requested, setActive] = useControllableState({
23
24
  value: activeIndex,
24
25
  defaultValue: defaultActiveIndex,
25
26
  onChange: onActiveIndexChange,
26
27
  });
28
+ // Indeks przycięty do listy: po skróceniu `items` (filtr) widok nie zostaje pusty.
29
+ const active = Math.min(Math.max(requested, 0), Math.max(items.length - 1, 0));
27
30
  const goTo = (index) => setActive(Math.min(Math.max(index, 0), items.length - 1));
28
31
  const atStart = active <= 0;
29
32
  const atEnd = active >= items.length - 1;
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  /**
@@ -1,6 +1,7 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
2
  import { type KptChartConfig } from '@konce-pt/chart';
3
- export interface KptChartProps extends Omit<ComponentPropsWithRef<'div'>, 'children'> {
3
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
4
+ export interface KptChartProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
4
5
  config: KptChartConfig;
5
6
  /** Szerokość viewBoxa w jednostkach SVG; wykres skaluje się do kontenera. */
6
7
  width?: number;
@@ -15,4 +16,4 @@ export interface KptChartProps extends Omit<ComponentPropsWithRef<'div'>, 'child
15
16
  * @example
16
17
  * <KptChart config={{ type: 'line', labels: ['Sty', 'Lut'], series: [{ name: 'Sprzedaż', data: [12, 19] }] }} />
17
18
  */
18
- export declare function KptChart({ config, width, height, className, style, ...rest }: Readonly<KptChartProps>): import("react").JSX.Element;
19
+ export declare function KptChart({ config, width, height, locale, dictionary, className, style, ...rest }: Readonly<KptChartProps>): import("react").JSX.Element;
@@ -1,7 +1,9 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { areaPath, bandScale, barLayout, extent, formatNumber, linePath, linearScale, niceScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, seriesColor, } from '@konce-pt/chart';
3
+ import { useId, useState } from 'react';
4
+ import { areaPath, bandScale, barLayout, chartAccessibleName, chartDataTable, extent, formatNumber, linePath, linearScale, niceScale, pieSlices, plotArea, polarSlices, polarToCartesian, polygonPath, radarGrid, radarPoints, radialArea, seriesColor, } from '@konce-pt/chart';
4
5
  import { cn } from "../internal/cn.js";
6
+ import { useKptMessages } from "../i18n/i18n-context.js";
5
7
  import { KptChartLegend } from "./chart-legend.js";
6
8
  import { KptChartTooltip } from "./chart-tooltip.js";
7
9
  const VERTICAL_INSETS = { top: 12, right: 16, bottom: 28, left: 46 };
@@ -21,7 +23,13 @@ function styleFor(align) {
21
23
  * @example
22
24
  * <KptChart config={{ type: 'line', labels: ['Sty', 'Lut'], series: [{ name: 'Sprzedaż', data: [12, 19] }] }} />
23
25
  */
24
- export function KptChart({ config, width = 600, height = 360, className, style, ...rest }) {
26
+ export function KptChart({ config, width = 600, height = 360, locale, dictionary, className, style, ...rest }) {
27
+ const { t, locale: activeLocale } = useKptMessages({ locale, dictionary });
28
+ const descId = `${useId()}-desc`;
29
+ // Alternatywa tekstowa (WCAG 1.1.1): nazwa SVG (`ariaLabel` → tytuł → i18n) i dane w ukrytej
30
+ // wizualnie tabeli — dymek na najechanie jest dostępny tylko myszą.
31
+ const accessibleName = chartAccessibleName(config, t('chart.label'));
32
+ const dataTable = chartDataTable(config, activeLocale);
25
33
  const [hidden, setHidden] = useState(new Set());
26
34
  const [highlighted, setHighlighted] = useState(null);
27
35
  const [tooltip, setTooltip] = useState(null);
@@ -227,5 +235,5 @@ export function KptChart({ config, width = 600, height = 360, className, style,
227
235
  fontWeight: config.subtitle.font?.weight,
228
236
  paddingTop: config.subtitle.padding?.top,
229
237
  paddingBottom: config.subtitle.padding?.bottom,
230
- }, children: config.subtitle.text })) : null, _jsxs("div", { className: "kpt-chart__body", children: [_jsxs("div", { className: "kpt-chart__canvas", children: [_jsxs("svg", { className: "kpt-chart__svg", viewBox: `0 0 ${width} ${height}`, role: "img", preserveAspectRatio: "xMidYMid meet", children: [lineVm, barVm, radarVm, pieVm] }), _jsx(KptChartTooltip, { state: tooltip })] }), legendOpts.display ? (_jsx(KptChartLegend, { items: legendItems, position: legendOpts.position, align: legendOpts.align, pointStyle: legendOpts.pointStyle, title: legendOpts.title, onToggle: toggle, onHighlight: setHighlighted })) : null] })] }) }));
238
+ }, children: config.subtitle.text })) : null, _jsxs("div", { className: "kpt-chart__body", children: [_jsxs("div", { className: "kpt-chart__canvas", children: [_jsxs("svg", { className: "kpt-chart__svg", viewBox: `0 0 ${width} ${height}`, role: "img", "aria-label": accessibleName, "aria-describedby": config.description ? descId : undefined, preserveAspectRatio: "xMidYMid meet", children: [lineVm, barVm, radarVm, pieVm] }), config.description ? (_jsx("p", { className: "kpt-chart__sr", id: descId, children: config.description })) : null, _jsxs("table", { className: "kpt-chart__sr", children: [_jsxs("caption", { children: [t('chart.dataTable'), ": ", accessibleName] }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: t('chart.category') }), dataTable.columns.map((col, i) => (_jsx("th", { scope: "col", children: col }, i)))] }) }), _jsx("tbody", { children: dataTable.rows.map((row, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: row.label }), row.values.map((value, j) => (_jsx("td", { children: value }, j)))] }, i))) })] }), _jsx(KptChartTooltip, { state: tooltip })] }), legendOpts.display ? (_jsx(KptChartLegend, { items: legendItems, position: legendOpts.position, align: legendOpts.align, pointStyle: legendOpts.pointStyle, title: legendOpts.title, onToggle: toggle, onHighlight: setHighlighted })) : null] })] }) }));
231
239
  }
@@ -19,7 +19,7 @@ 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? }` — the same shape the Angular port
22
+ 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
25
  ## The geometry lives in a package
@@ -53,5 +53,8 @@ stays a plain object the caller owns, and nothing is written back into it.
53
53
  `KPT_CHART_PALETTE` or `series.color`.
54
54
 
55
55
  ## Accessibility
56
- The `<svg>` carries `role="img"`; the legend is a list of real buttons, so series can be toggled
57
- from the keyboard.
56
+ The `<svg>` is `role="img"` with a name: `config.ariaLabel`, else the title text, else `chart.label`
57
+ from i18n. `config.description` (one sentence with the takeaway) is tied to it through
58
+ `aria-describedby`. The data itself sits in a visually hidden `<table>` (categories in rows, series in
59
+ columns, numbers in the active language), so a screen reader can walk the values — the tooltip is
60
+ pointer-only. The legend is a list of real buttons, so series can be toggled from the keyboard.
package/dist/chat/chat.js CHANGED
@@ -1,5 +1,6 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
+ import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from 'react';
3
4
  import { KPT_CHAT_GROUP_GAP_MS, KPT_CHAT_REACTIONS, buildChatRows, chatDayIndicator, pendingCount } from '@konce-pt/chat';
4
5
  import { cn } from "../internal/cn.js";
5
6
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -77,22 +78,42 @@ export function KptChat({ messages = [], currentUser = '', groupGapMs = KPT_CHAT
77
78
  const reduced = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
78
79
  element.scrollTo({ top: element.scrollHeight, behavior: behavior ?? (reduced ? 'auto' : 'smooth') });
79
80
  };
81
+ const firstIdRef = useRef(null);
82
+ const prevScrollHeight = useRef(0);
83
+ const dayFrame = useRef(0);
84
+ // Wartości czytane w efekcie listy, ale nieuruchamiające go — przez zdarzenia efektu.
85
+ const autoScrollOnNew = useEffectEvent(() => autoScroll);
86
+ const pinnedNow = useEffectEvent(() => pinned);
87
+ const bumpNewCount = useEffectEvent((added) => setNewCount((value) => value + added));
80
88
  // Dosunięcie liczone po ułożeniu, nie po malowaniu — inaczej widać skok listy.
81
89
  useLayoutEffect(() => {
82
90
  if (!viewportRef.current)
83
91
  return;
92
+ const element = viewportRef.current;
84
93
  const count = messages.length;
85
- const grew = count > lastCount.current;
94
+ const added = count - lastCount.current;
86
95
  const first = lastCount.current === 0;
96
+ const firstId = messages[0]?.id ?? null;
97
+ // Starsze wiadomości doklejone na górze (`onLoadMore`) — nie są nowe, a widok ma zostać w miejscu.
98
+ const prepended = !first && added > 0 && firstId !== firstIdRef.current;
87
99
  lastCount.current = count;
88
- if (!grew)
100
+ firstIdRef.current = firstId;
101
+ if (prepended) {
102
+ // Przeglądarki z `overflow-anchor` kotwiczą same; Safari go nie ma i bez tego lista skacze.
103
+ if (!globalThis.CSS?.supports('overflow-anchor', 'auto'))
104
+ element.scrollTop += element.scrollHeight - prevScrollHeight.current;
105
+ prevScrollHeight.current = element.scrollHeight;
106
+ return;
107
+ }
108
+ prevScrollHeight.current = element.scrollHeight;
109
+ if (added <= 0)
89
110
  return;
90
111
  // Pierwsze wejście zawsze ląduje na dole — rozmowę czyta się od końca.
91
- if (first || (autoScroll && pinned))
112
+ if (first || (autoScrollOnNew() && pinnedNow()))
92
113
  scrollToBottom(first ? 'auto' : undefined);
114
+ // Tyle, ile naprawdę przyszło — paczka pięciu wiadomości to „5 nowych”, nie „1”.
93
115
  else
94
- setNewCount((value) => value + 1);
95
- // eslint-disable-next-line react-hooks/exhaustive-deps
116
+ bumpNewCount(added);
96
117
  }, [messages]);
97
118
  // Po każdej zmianie listy wiersze stoją gdzie indziej — pigułkę trzeba przeliczyć, inaczej
98
119
  // pokazywałaby dzień sprzed dojścia nowej wiadomości.
@@ -120,7 +141,13 @@ export function KptChat({ messages = [], currentUser = '', groupGapMs = KPT_CHAT
120
141
  if (atTop && !wasAtTop.current)
121
142
  onLoadMore?.();
122
143
  wasAtTop.current = atTop;
123
- syncDay();
144
+ // Pomiary pigułki raz na klatkę, nie przy każdym zdarzeniu `scroll` (wymuszony układ).
145
+ if (dayFrame.current)
146
+ return;
147
+ dayFrame.current = requestAnimationFrame(() => {
148
+ dayFrame.current = 0;
149
+ syncDay();
150
+ });
124
151
  };
125
152
  const jumpToLatest = () => {
126
153
  setPinned(true);
@@ -147,7 +174,10 @@ export function KptChat({ messages = [], currentUser = '', groupGapMs = KPT_CHAT
147
174
  }).format(label.date);
148
175
  }
149
176
  };
150
- return (_jsxs("div", { className: cn('kpt-chat-host', className), ...rest, children: [offline ? (_jsxs("p", { className: "kpt-chat__offline", role: "status", children: [_jsx(KptIcon, { name: "alert-triangle" }), _jsx("span", { children: t('chat.offline') }), pending ? (_jsx("span", { className: "kpt-chat__offline-count", children: t('chat.pending', { count: pending }) })) : null] })) : null, _jsxs("div", { className: "kpt-chat__body", children: [_jsxs("div", { ref: viewportRef, className: "kpt-chat__viewport", role: "log", "aria-live": "polite", "aria-relevant": "additions", onScroll: (event) => onScroll(event.currentTarget), children: [rows.length === 0 ? (_jsx("p", { className: "kpt-chat__empty", children: emptyLabel || t('chat.empty') })) : null, rows.map((row) => {
177
+ return (_jsxs("div", { className: cn('kpt-chat-host', className), ...rest, children: [offline ? (_jsxs("p", { className: "kpt-chat__offline", role: "status", children: [_jsx(KptIcon, { name: "alert-triangle" }), _jsx("span", { children: t('chat.offline') }), pending ? (_jsx("span", { className: "kpt-chat__offline-count", children: t('chat.pending', { count: pending }) })) : null] })) : null, _jsxs("div", { className: "kpt-chat__body", children: [_jsxs("div", { ref: viewportRef, className: "kpt-chat__viewport", role: "log", "aria-live": "polite", "aria-relevant": "additions",
178
+ // Przewijana historia musi być osiągalna z klawiatury także bez fokusowalnych bąbli.
179
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- przewijany region musi być osiągalny z klawiatury (WCAG 2.1.1) — role="log" nie jest interaktywna, przewijanie tak
180
+ tabIndex: 0, "aria-label": t('chat.history'), onScroll: (event) => onScroll(event.currentTarget), children: [rows.length === 0 ? (_jsx("p", { className: "kpt-chat__empty", children: emptyLabel || t('chat.empty') })) : null, rows.map((row) => {
151
181
  if (row.kind === 'date') {
152
182
  const label = dateText(row.label);
153
183
  return (_jsx("p", { className: "kpt-chat__date", role: "separator", "aria-label": label, children: label }, row.key));
@@ -156,5 +186,5 @@ export function KptChat({ messages = [], currentUser = '', groupGapMs = KPT_CHAT
156
186
  return (_jsx("p", { className: "kpt-chat__unread", role: "separator", "aria-label": t('chat.newMessages', { count: row.count }), children: _jsx("span", { children: t('chat.unread') }) }, row.key));
157
187
  }
158
188
  return (_jsx(KptChatMessage, { message: row.message, text: renderMessage ? '' : null, align: row.own ? 'end' : 'start', position: row.position, showAuthor: row.showAuthor, showAvatar: row.showAvatar, showMeta: row.showMeta, actions: actions, reactionOptions: reactionOptions, locale: locale, dictionary: dictionary, onRetry: () => onRetry?.(row.message), onReply: () => onReply?.(row.message), onReact: (emoji) => onReact?.({ message: row.message, emoji }), onQuoted: (quote) => onQuoted?.(quote), children: renderMessage ? renderMessage(row.message, row) : null }, row.key));
159
- }), typing.length ? _jsx(KptChatTyping, { authors: typing, locale: locale, dictionary: dictionary }) : null] }), _jsx("p", { ref: dayRef, className: "kpt-chat__day", "aria-hidden": "true", "data-hidden": dayVisible ? undefined : '', children: dayLabel }), newCount ? (_jsxs("button", { type: "button", className: "kpt-chat__jump", onClick: jumpToLatest, children: [_jsx(KptIcon, { name: "arrow-down" }), t('chat.newMessages', { count: newCount })] })) : null] })] }));
189
+ }), typing.length ? (_jsx("div", { "aria-live": "off", children: _jsx(KptChatTyping, { authors: typing, locale: locale, dictionary: dictionary }) })) : null] }), _jsx("p", { ref: dayRef, className: "kpt-chat__day", "aria-hidden": "true", "data-hidden": dayVisible ? undefined : '', children: dayLabel }), newCount ? (_jsxs("button", { type: "button", className: "kpt-chat__jump", onClick: jumpToLatest, children: [_jsx(KptIcon, { name: "arrow-down" }), t('chat.newMessages', { count: newCount })] })) : null] })] }));
160
190
  }
@@ -76,3 +76,12 @@ pill `--kpt-color-primary-border` + `--kpt-elevation-2`.
76
76
  `role="log"` with `aria-live="polite"` announces a new message without cutting off what is being
77
77
  read. Separators are `role="separator"` with a label. Auto-scroll uses `behavior: 'auto'` under
78
78
  `prefers-reduced-motion`.
79
+
80
+ ## Keyboard, counters and history
81
+ The scrolling history (`role="log"`) is a Tab stop named "Conversation history" (`chat.history`), so the
82
+ keyboard can scroll it even when no bubble holds a focusable element. The "new messages" button counts
83
+ the messages that actually arrived (a batch of five gives "5"). The typing indicator sits inside the log
84
+ but with `aria-live="off"`, so "X is typing…" is not announced like a message. Older messages loaded at
85
+ the top keep the view on the same message, also in Safari (no `overflow-anchor`). The composer reports
86
+ typing on the first keystroke and then at most every 3 s (`KPT_CHAT_TYPING_THROTTLE_MS`). A custom
87
+ reaction option may carry `label` — its accessible name.
@@ -1,5 +1,5 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
- import type { KptChatReplyRef } from '@konce-pt/chat';
2
+ import { type KptChatReplyRef } from '@konce-pt/chat';
3
3
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
4
4
  /** Ładunek wysyłki. Kompozytor niczego nie wysyła — pakuje dane i oddaje aplikacji. */
5
5
  export interface KptChatComposerSubmit {
@@ -25,7 +25,10 @@ export interface KptChatComposerProps extends Omit<ComponentPropsWithRef<'div'>,
25
25
  defaultReplyTo?: KptChatReplyRef | null;
26
26
  onReplyToChange?: (replyTo: KptChatReplyRef | null) => void;
27
27
  onSend?: (payload: KptChatComposerSubmit) => void;
28
- /** Uderzenie w klawiaturę — sygnał „piszę" dla drugiej strony. */
28
+ /**
29
+ * Sygnał „piszę” dla drugiej strony: przy pierwszym znaku, a w trakcie pisania najwyżej raz na
30
+ * `KPT_CHAT_TYPING_THROTTLE_MS` (3 s) — nie przy każdym znaku.
31
+ */
29
32
  onTyping?: () => void;
30
33
  }
31
34
  /**
@@ -1,5 +1,7 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import { useRef, useState, } from 'react';
4
+ import { KPT_CHAT_TYPING_THROTTLE_MS } from '@konce-pt/chat';
3
5
  import { cn } from "../internal/cn.js";
4
6
  import { useControllableState } from "../internal/use-controllable-state.js";
5
7
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -30,6 +32,8 @@ export function KptChatComposer({ value, defaultValue = '', onValueChange, place
30
32
  const [files, setFiles] = useState([]);
31
33
  const fieldRef = useRef(null);
32
34
  const pickerRef = useRef(null);
35
+ /** Kiedy ostatnio zgłoszono „piszę” — dławienie `onTyping`. */
36
+ const lastTyping = useRef(0);
33
37
  const canSend = !disabled && (text.trim().length > 0 || files.length > 0);
34
38
  const sendLabel = offline ? t('chat.queue') : t('chat.send');
35
39
  /**
@@ -76,6 +80,11 @@ export function KptChatComposer({ value, defaultValue = '', onValueChange, place
76
80
  } }), _jsx(KptIconButton, { size: "sm", disabled: disabled, "aria-label": t('chat.attach'), onClick: () => pickerRef.current?.click(), children: _jsx(KptIcon, { name: "paperclip" }) })] })) : null, _jsx("textarea", { ref: fieldRef, className: "kpt-chat-composer__field", rows: 1, style: { '--_max-rows': maxRows }, value: text, disabled: disabled, placeholder: placeholder || t('chat.placeholder'), "aria-label": placeholder || t('chat.placeholder'), onChange: (event) => {
77
81
  setText(event.currentTarget.value);
78
82
  grow(event.currentTarget);
79
- onTyping?.();
83
+ // „Piszę” przy pierwszym znaku i potem najwyżej raz na KPT_CHAT_TYPING_THROTTLE_MS — nie przy każdym.
84
+ const now = Date.now();
85
+ if (now - lastTyping.current >= KPT_CHAT_TYPING_THROTTLE_MS) {
86
+ lastTyping.current = now;
87
+ onTyping?.();
88
+ }
80
89
  }, onKeyDown: onKeyDown }), _jsx(KptIconButton, { variant: "filled", disabled: !canSend, "aria-label": sendLabel, onClick: submit, children: _jsx(KptIcon, { name: offline ? 'clock' : 'send' }) })] })] }));
81
90
  }
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useEffect, useRef, } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import { useEffect, useRef, useState } from 'react';
3
4
  import { KPT_CHAT_REACTIONS, canRetry, toDate } from '@konce-pt/chat';
@@ -94,7 +95,7 @@ export function KptChatMessage({ message = null, author = null, text = null, tim
94
95
  if (!event.currentTarget.contains(event.relatedTarget))
95
96
  setPickerOpen(false);
96
97
  }, children: [_jsx(KptIconButton, { size: "sm", "aria-label": t('chat.react'), "aria-expanded": pickerOpen, onClick: () => setPickerOpen((open) => !open), children: _jsx(KptIcon, { name: "mood-smile" }) }), pickerOpen ? (_jsx("span", { className: "kpt-chat-message__picker", role: "group", "aria-label": t('chat.react'), children: reactionOptions.map((option) => {
97
- const label = t('chat.react' + option.kind[0].toUpperCase() + option.kind.slice(1));
98
+ const label = option.label ?? t('chat.react' + option.kind[0].toUpperCase() + option.kind.slice(1));
98
99
  const reacted = resolvedReactions.some((item) => item.emoji === option.emoji && item.reacted);
99
100
  return (_jsx("button", { type: "button", className: "kpt-chat-message__pick", "aria-label": label, title: label, "aria-pressed": reacted, onClick: () => {
100
101
  onReact?.(option.emoji);
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -35,4 +35,4 @@ export interface KptCheckboxProps extends Omit<ComponentPropsWithRef<'input'>, '
35
35
  * <KptCheckbox name="terms" label="Akceptuję regulamin" />
36
36
  * <KptCheckbox checked={on} onCheckedChange={setOn} />
37
37
  */
38
- export declare function KptCheckbox({ label, required, invalid, touched, onCheckedChange, onTouch, hostProps, className, disabled, onChange, onBlur, children, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, id, indeterminate, ref, ...rest }: Readonly<KptCheckboxProps>): import("react").JSX.Element;
38
+ export declare function KptCheckbox({ label, required, invalid, touched, onCheckedChange, onTouch, hostProps, className, disabled, readOnly, onClick, onChange, onBlur, children, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, id, indeterminate, ref, ...rest }: Readonly<KptCheckboxProps>): import("react").JSX.Element;
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useCallback, useEffect, useRef } from 'react';
3
4
  import { composeRefs } from "../internal/compose-refs.js";
@@ -16,7 +17,7 @@ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
16
17
  * <KptCheckbox name="terms" label="Akceptuję regulamin" />
17
18
  * <KptCheckbox checked={on} onCheckedChange={setOn} />
18
19
  */
19
- export function KptCheckbox({ label = '', required = false, invalid = false, touched, onCheckedChange, onTouch, hostProps, className, disabled = false, onChange, onBlur, children, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, id, indeterminate = false, ref, ...rest }) {
20
+ export function KptCheckbox({ label = '', required = false, invalid = false, touched, onCheckedChange, onTouch, hostProps, className, disabled = false, readOnly = false, onClick, onChange, onBlur, children, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, id, indeterminate = false, ref, ...rest }) {
20
21
  const inputRef = useRef(null);
21
22
  useEffect(() => {
22
23
  if (inputRef.current)
@@ -33,5 +34,11 @@ export function KptCheckbox({ label = '', required = false, invalid = false, tou
33
34
  onBlur?.(event);
34
35
  onTouch?.();
35
36
  }, [onBlur, onTouch]);
36
- return (_jsx("span", { className: cn('kpt-checkbox-host', hostClassName), ...hostRest, children: _jsxs("label", { className: cn('kpt-checkbox', disabled && 'kpt-checkbox--disabled', className), children: [_jsx("input", { type: "checkbox", className: "kpt-checkbox__input", ref: composeRefs(inputRef, ref), id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), disabled: disabled, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }), _jsxs("span", { className: "kpt-checkbox__box", "aria-hidden": "true", children: [_jsx("svg", { viewBox: "0 0 16 16", className: "kpt-checkbox__check", children: _jsx("path", { d: "M13 4.5 6.5 11 3 7.5" }) }), _jsx("svg", { viewBox: "0 0 16 16", className: "kpt-checkbox__dash", children: _jsx("path", { d: "M4 8h8" }) })] }), label ? _jsx("span", { className: "kpt-checkbox__label", children: label }) : null, children] }) }));
37
+ return (_jsx("span", { className: cn('kpt-checkbox-host', hostClassName), ...hostRest, children: _jsxs("label", { className: cn('kpt-checkbox', disabled && 'kpt-checkbox--disabled', className), children: [_jsx("input", { type: "checkbox", className: "kpt-checkbox__input", ref: composeRefs(inputRef, ref), id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), disabled: disabled, "aria-required": required || undefined, "aria-readonly": readOnly || undefined,
38
+ // Natywne `readonly` nie działa na polach wyboru — klik (i Spacja) jest blokowany ręcznie.
39
+ onClick: (event) => {
40
+ if (readOnly)
41
+ event.preventDefault();
42
+ onClick?.(event);
43
+ }, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ...rest }), _jsxs("span", { className: "kpt-checkbox__box", "aria-hidden": "true", children: [_jsx("svg", { viewBox: "0 0 16 16", className: "kpt-checkbox__check", children: _jsx("path", { d: "M13 4.5 6.5 11 3 7.5" }) }), _jsx("svg", { viewBox: "0 0 16 16", className: "kpt-checkbox__dash", children: _jsx("path", { d: "M4 8h8" }) })] }), label ? _jsx("span", { className: "kpt-checkbox__label", children: label }) : null, children] }) }));
37
44
  }
@@ -48,3 +48,6 @@ Inside `kpt-form-field` the box gets both names: the wrapper label and its own `
48
48
  A native `<input type="checkbox">` inside a `<label>` — the label is the accessible name and the
49
49
  whole row is clickable. The box and the tick are `aria-hidden`, being pure decoration over the real
50
50
  control.
51
+
52
+ ## Read-only and required
53
+ `readOnly` keeps the box focusable but blocks the click and Space (`aria-readonly`) — the native attribute does not work on checkboxes.
package/dist/chip/chip.js CHANGED
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useKptId } from "../internal/use-id.js";
@@ -14,6 +14,8 @@ export interface KptChipsInputProps extends Omit<ComponentPropsWithRef<'div'>, '
14
14
  /** Oznacza pole jako wymagane atrybutem `aria-required`. */
15
15
  required?: boolean;
16
16
  disabled?: boolean;
17
+ /** Tylko do odczytu — tagi widać, nie da się ich dodać ani usunąć. */
18
+ readOnly?: boolean;
17
19
  invalid?: boolean;
18
20
  touched?: boolean;
19
21
  onTouch?: () => void;
@@ -28,4 +30,4 @@ export interface KptChipsInputProps extends Omit<ComponentPropsWithRef<'div'>, '
28
30
  * @example
29
31
  * <KptChipsInput value={tags} onValueChange={setTags} placeholder="Dodaj tag…" />
30
32
  */
31
- export declare function KptChipsInput({ value, defaultValue, onValueChange, placeholder, allowDuplicates, id, ariaLabel, required, disabled, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptChipsInputProps>): import("react").JSX.Element;
33
+ export declare function KptChipsInput({ value, defaultValue, onValueChange, placeholder, allowDuplicates, id, ariaLabel, required, disabled, readOnly, invalid, touched, onTouch, locale, dictionary, className, ...rest }: Readonly<KptChipsInputProps>): import("react").JSX.Element;
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { useRef, useState } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -15,7 +16,7 @@ import { useKptFieldA11y } from "../form-field/form-field-context.js";
15
16
  * @example
16
17
  * <KptChipsInput value={tags} onValueChange={setTags} placeholder="Dodaj tag…" />
17
18
  */
18
- export function KptChipsInput({ value, defaultValue = [], onValueChange, placeholder, allowDuplicates = false, id, ariaLabel = null, required = false, disabled = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
19
+ export function KptChipsInput({ value, defaultValue = [], onValueChange, placeholder, allowDuplicates = false, id, ariaLabel = null, required = false, disabled = false, readOnly = false, invalid = false, touched, onTouch, locale, dictionary, className, ...rest }) {
19
20
  const { t } = useKptMessages({ locale, dictionary });
20
21
  const field = useKptFieldA11y(id, 'kpt-chips-input');
21
22
  const inputRef = useRef(null);
@@ -26,10 +27,15 @@ export function KptChipsInput({ value, defaultValue = [], onValueChange, placeho
26
27
  });
27
28
  const showInvalid = invalid && (touched ?? true);
28
29
  /** Komunikat regionu `polite`: dodanie, usunięcie albo odrzucony duplikat. */
29
- const [announcement, setAnnouncement] = useState('');
30
+ const [announcement, setAnnouncementState] = useState('');
31
+ /**
32
+ * Ten sam komunikat dwa razy z rzędu (drugi odrzucony duplikat) nie zmieniłby treści regionu
33
+ * i czytnik by go nie ogłosił — doklejamy niewidoczną spację, żeby tekst się różnił.
34
+ */
35
+ const setAnnouncement = (message) => setAnnouncementState((current) => (current === message ? `${message}\u00A0` : message));
30
36
  const commit = (el) => {
31
37
  const text = el.value.trim();
32
- if (!text)
38
+ if (!text || readOnly)
33
39
  return;
34
40
  if (!allowDuplicates && chips.includes(text)) {
35
41
  // Duplikat zostaje w polu (do poprawy), a czytnik słyszy, dlaczego nic nie przybyło.
@@ -49,6 +55,9 @@ export function KptChipsInput({ value, defaultValue = [], onValueChange, placeho
49
55
  };
50
56
  const onKeyDown = (event) => {
51
57
  const el = event.currentTarget;
58
+ // W trakcie kompozycji IME (CJK) Enter zatwierdza znak, nie tag.
59
+ if (readOnly || event.nativeEvent.isComposing)
60
+ return;
52
61
  if (event.key === 'Enter' || event.key === ',') {
53
62
  event.preventDefault();
54
63
  commit(el);
@@ -62,9 +71,9 @@ export function KptChipsInput({ value, defaultValue = [], onValueChange, placeho
62
71
  commit(event.currentTarget);
63
72
  onTouch?.();
64
73
  };
65
- return (_jsx("div", { className: cn('kpt-chips-input-host', className), ...rest, children: _jsxs("div", { className: cn('kpt-chips-input', disabled && 'kpt-chips-input--disabled', showInvalid && 'kpt-chips-input--invalid'), children: [_jsx("span", { className: "kpt-chips-input__list", role: "list", children: chips.map((chip, index) => (_jsx("span", { className: "kpt-chips-input__item", role: "listitem", children: _jsx(KptChip, { variant: "primary", removable: !disabled, onRemove: () => {
74
+ return (_jsx("div", { className: cn('kpt-chips-input-host', className), ...rest, children: _jsxs("div", { className: cn('kpt-chips-input', disabled && 'kpt-chips-input--disabled', showInvalid && 'kpt-chips-input--invalid'), children: [_jsx("span", { className: "kpt-chips-input__list", role: "list", children: chips.map((chip, index) => (_jsx("span", { className: "kpt-chips-input__item", role: "listitem", children: _jsx(KptChip, { variant: "primary", removable: !disabled && !readOnly, onRemove: () => {
66
75
  removeAt(index);
67
76
  // Przycisk usuwania znika razem z tagiem — fokus wraca do pola, nie na <body>.
68
77
  inputRef.current?.focus();
69
- }, children: chip }) }, index))) }), _jsx("input", { ref: inputRef, className: "kpt-chips-input__field", id: field.id, "aria-label": ariaLabel ?? undefined, "aria-required": required || undefined, "aria-invalid": showInvalid || undefined, "aria-describedby": field.describedBy, type: "text", placeholder: chips.length ? '' : (placeholder ?? t('chipsInput.placeholder')), disabled: disabled, onKeyDown: onKeyDown, onBlur: onBlur }), _jsx("span", { className: "kpt-chips-input__live", "aria-live": "polite", children: announcement })] }) }));
78
+ }, children: chip }) }, index))) }), _jsx("input", { ref: inputRef, className: "kpt-chips-input__field", id: field.id, "aria-label": ariaLabel ?? undefined, "aria-required": required || undefined, "aria-invalid": showInvalid || undefined, "aria-describedby": field.describedBy, type: "text", placeholder: chips.length ? '' : (placeholder ?? t('chipsInput.placeholder')), disabled: disabled, readOnly: readOnly, onKeyDown: onKeyDown, onBlur: onBlur }), _jsx("span", { className: "kpt-chips-input__live", "aria-live": "polite", children: announcement })] }) }));
70
79
  }
@@ -50,3 +50,6 @@ New: `id`, `ariaLabel`, `required` and `aria-invalid` on the text field.
50
50
  ## Accessibility
51
51
  Each chip is a `KptChip` with a labelled remove button, and the field itself is a native input, so
52
52
  tags are reachable and removable from the keyboard alone.
53
+
54
+ ## Read-only and required
55
+ `readOnly` shows the tags without the remove buttons and ignores Enter, comma and Backspace. Enter during IME composition (CJK) confirms the character, not a tag. A repeated message (a second rejected duplicate) is announced again.