@konce-pt/react 0.9.1 → 0.10.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 (196) hide show
  1. package/dist/accordion/accordion.js +7 -1
  2. package/dist/accordion/llms.txt +8 -4
  3. package/dist/alert/alert.d.ts +10 -4
  4. package/dist/alert/alert.js +13 -6
  5. package/dist/alert/llms.txt +8 -6
  6. package/dist/app-shell/app-shell.js +28 -2
  7. package/dist/app-shell/llms.txt +3 -0
  8. package/dist/autocomplete/autocomplete.d.ts +7 -2
  9. package/dist/autocomplete/autocomplete.js +46 -9
  10. package/dist/autocomplete/llms.txt +12 -0
  11. package/dist/avatar/avatar.d.ts +4 -2
  12. package/dist/avatar/avatar.js +8 -3
  13. package/dist/avatar/llms.txt +9 -5
  14. package/dist/avatar-group/avatar-group.d.ts +5 -2
  15. package/dist/avatar-group/avatar-group.js +10 -2
  16. package/dist/avatar-group/llms.txt +7 -4
  17. package/dist/badge/badge.d.ts +8 -1
  18. package/dist/badge/badge.js +3 -2
  19. package/dist/badge/llms.txt +11 -5
  20. package/dist/bottom-sheet/bottom-sheet.d.ts +1 -1
  21. package/dist/bottom-sheet/bottom-sheet.js +8 -2
  22. package/dist/bottom-sheet/llms.txt +8 -2
  23. package/dist/breadcrumb/breadcrumb.js +4 -4
  24. package/dist/breadcrumb/llms.txt +12 -5
  25. package/dist/card/card.d.ts +9 -1
  26. package/dist/card/card.js +2 -2
  27. package/dist/card/llms.txt +6 -2
  28. package/dist/carousel/carousel.d.ts +8 -1
  29. package/dist/carousel/carousel.js +9 -2
  30. package/dist/carousel/llms.txt +24 -10
  31. package/dist/checkbox/checkbox.d.ts +6 -1
  32. package/dist/checkbox/checkbox.js +11 -3
  33. package/dist/checkbox/llms.txt +11 -0
  34. package/dist/chip/chip.js +10 -1
  35. package/dist/chip/llms.txt +7 -4
  36. package/dist/chips-input/chips-input.d.ts +7 -1
  37. package/dist/chips-input/chips-input.js +21 -5
  38. package/dist/chips-input/llms.txt +12 -0
  39. package/dist/color-picker/color-picker.d.ts +18 -5
  40. package/dist/color-picker/color-picker.js +48 -14
  41. package/dist/color-picker/llms.txt +14 -0
  42. package/dist/confirm/confirm-container.d.ts +5 -1
  43. package/dist/confirm/confirm-container.js +15 -1
  44. package/dist/confirm/llms.txt +4 -2
  45. package/dist/context-menu/context-menu.d.ts +5 -0
  46. package/dist/context-menu/context-menu.js +48 -4
  47. package/dist/context-menu/llms.txt +11 -4
  48. package/dist/date-range/date-range.d.ts +14 -1
  49. package/dist/date-range/date-range.js +133 -17
  50. package/dist/date-range/llms.txt +26 -3
  51. package/dist/datepicker/datepicker.d.ts +10 -1
  52. package/dist/datepicker/datepicker.js +147 -14
  53. package/dist/datepicker/llms.txt +27 -3
  54. package/dist/dialog/dialog.d.ts +4 -0
  55. package/dist/dialog/dialog.js +7 -1
  56. package/dist/dialog/llms.txt +2 -0
  57. package/dist/drawer/drawer.js +7 -2
  58. package/dist/drawer/llms.txt +10 -5
  59. package/dist/empty/empty.d.ts +6 -1
  60. package/dist/empty/empty.js +2 -2
  61. package/dist/empty/llms.txt +4 -3
  62. package/dist/fab/llms.txt +2 -1
  63. package/dist/file-upload/file-upload.d.ts +5 -0
  64. package/dist/file-upload/file-upload.js +24 -3
  65. package/dist/file-upload/llms.txt +15 -4
  66. package/dist/form-field/form-field-context.d.ts +30 -1
  67. package/dist/form-field/form-field-context.js +27 -0
  68. package/dist/form-field/form-field.js +16 -2
  69. package/dist/form-field/llms.txt +10 -1
  70. package/dist/galleria/galleria.d.ts +6 -1
  71. package/dist/galleria/galleria.js +40 -1
  72. package/dist/galleria/llms.txt +17 -8
  73. package/dist/icon/icon.d.ts +7 -1
  74. package/dist/icon/icon.js +18 -4
  75. package/dist/icon/llms.txt +5 -2
  76. package/dist/image/image.d.ts +5 -0
  77. package/dist/image/image.js +38 -2
  78. package/dist/image/llms.txt +24 -17
  79. package/dist/input/input.d.ts +1 -1
  80. package/dist/input/input.js +4 -6
  81. package/dist/input/llms.txt +5 -0
  82. package/dist/input-mask/input-mask.d.ts +1 -1
  83. package/dist/input-mask/input-mask.js +8 -6
  84. package/dist/input-mask/llms.txt +11 -0
  85. package/dist/input-number/input-number.d.ts +13 -1
  86. package/dist/input-number/input-number.js +49 -9
  87. package/dist/input-number/llms.txt +21 -3
  88. package/dist/input-otp/input-otp.d.ts +7 -2
  89. package/dist/input-otp/input-otp.js +36 -7
  90. package/dist/input-otp/llms.txt +14 -0
  91. package/dist/internal/calendar-focus.d.ts +8 -0
  92. package/dist/internal/calendar-focus.js +38 -0
  93. package/dist/internal/option-nav.d.ts +24 -0
  94. package/dist/internal/option-nav.js +80 -0
  95. package/dist/internal/roving.d.ts +9 -0
  96. package/dist/internal/roving.js +43 -0
  97. package/dist/internal/use-modal-focus.d.ts +10 -0
  98. package/dist/internal/use-modal-focus.js +57 -0
  99. package/dist/knob/knob.d.ts +5 -1
  100. package/dist/knob/knob.js +4 -2
  101. package/dist/knob/llms.txt +8 -0
  102. package/dist/listbox/listbox.d.ts +13 -1
  103. package/dist/listbox/listbox.js +89 -6
  104. package/dist/listbox/llms.txt +21 -5
  105. package/dist/megamenu/llms.txt +28 -10
  106. package/dist/megamenu/megamenu.d.ts +10 -1
  107. package/dist/megamenu/megamenu.js +116 -5
  108. package/dist/menu/llms.txt +12 -1
  109. package/dist/menu/menu-keys.d.ts +15 -0
  110. package/dist/menu/menu-keys.js +53 -0
  111. package/dist/menu/menu-rows.js +1 -1
  112. package/dist/menu/menu.js +52 -4
  113. package/dist/menubar/llms.txt +24 -7
  114. package/dist/menubar/menubar.d.ts +8 -1
  115. package/dist/menubar/menubar.js +92 -6
  116. package/dist/meter-group/llms.txt +10 -3
  117. package/dist/meter-group/meter-group.d.ts +9 -2
  118. package/dist/meter-group/meter-group.js +8 -2
  119. package/dist/order-list/llms.txt +11 -3
  120. package/dist/order-list/order-list.d.ts +5 -1
  121. package/dist/order-list/order-list.js +46 -8
  122. package/dist/panel/llms.txt +9 -6
  123. package/dist/panel/panel.js +3 -1
  124. package/dist/password/llms.txt +8 -0
  125. package/dist/password/password.d.ts +6 -1
  126. package/dist/password/password.js +6 -5
  127. package/dist/pick-list/llms.txt +12 -3
  128. package/dist/pick-list/pick-list.d.ts +5 -1
  129. package/dist/pick-list/pick-list.js +52 -5
  130. package/dist/popover/llms.txt +9 -2
  131. package/dist/popover/popover.d.ts +9 -3
  132. package/dist/popover/popover.js +41 -6
  133. package/dist/public-api.d.ts +6 -6
  134. package/dist/public-api.js +1 -1
  135. package/dist/radio-group/llms.txt +11 -0
  136. package/dist/radio-group/radio-group.d.ts +9 -1
  137. package/dist/radio-group/radio-group.js +5 -2
  138. package/dist/rating/llms.txt +18 -5
  139. package/dist/rating/rating.d.ts +21 -3
  140. package/dist/rating/rating.js +87 -12
  141. package/dist/rich-text/llms.txt +20 -0
  142. package/dist/rich-text/rich-text.d.ts +7 -1
  143. package/dist/rich-text/rich-text.js +155 -28
  144. package/dist/scroll-top/llms.txt +5 -1
  145. package/dist/scroll-top/scroll-top.js +3 -1
  146. package/dist/select/llms.txt +21 -7
  147. package/dist/select/select.d.ts +12 -1
  148. package/dist/select/select.js +110 -38
  149. package/dist/sidenav/llms.txt +6 -0
  150. package/dist/skeleton/llms.txt +3 -2
  151. package/dist/skeleton/skeleton.d.ts +1 -1
  152. package/dist/skeleton/skeleton.js +2 -2
  153. package/dist/slider/llms.txt +14 -1
  154. package/dist/slider/slider.d.ts +11 -2
  155. package/dist/slider/slider.js +15 -2
  156. package/dist/speed-dial/llms.txt +6 -2
  157. package/dist/speed-dial/speed-dial.d.ts +1 -1
  158. package/dist/speed-dial/speed-dial.js +14 -3
  159. package/dist/spinner/llms.txt +12 -8
  160. package/dist/spinner/spinner.d.ts +5 -1
  161. package/dist/spinner/spinner.js +12 -2
  162. package/dist/split-button/llms.txt +11 -3
  163. package/dist/split-button/split-button.d.ts +3 -1
  164. package/dist/split-button/split-button.js +43 -3
  165. package/dist/splitter/llms.txt +14 -4
  166. package/dist/splitter/splitter.d.ts +9 -2
  167. package/dist/splitter/splitter.js +15 -2
  168. package/dist/stepper/llms.txt +8 -4
  169. package/dist/stepper/stepper.d.ts +3 -2
  170. package/dist/stepper/stepper.js +7 -2
  171. package/dist/switch/llms.txt +10 -0
  172. package/dist/switch/switch.d.ts +1 -1
  173. package/dist/switch/switch.js +4 -2
  174. package/dist/switch-group/llms.txt +11 -0
  175. package/dist/switch-group/switch-group.d.ts +5 -1
  176. package/dist/switch-group/switch-group.js +7 -2
  177. package/dist/tabs/llms.txt +10 -4
  178. package/dist/tabs/tabs.d.ts +4 -0
  179. package/dist/tabs/tabs.js +21 -2
  180. package/dist/textarea/llms.txt +10 -0
  181. package/dist/textarea/textarea.d.ts +3 -1
  182. package/dist/textarea/textarea.js +13 -8
  183. package/dist/toast/llms.txt +21 -7
  184. package/dist/toast/toast-container.d.ts +6 -1
  185. package/dist/toast/toast-container.js +52 -1
  186. package/dist/toast/toast-store.d.ts +12 -0
  187. package/dist/toast/toast-store.js +46 -7
  188. package/dist/tooltip/llms.txt +10 -3
  189. package/dist/tooltip/tooltip.d.ts +5 -0
  190. package/dist/tooltip/tooltip.js +80 -7
  191. package/dist/tree/llms.txt +20 -8
  192. package/dist/tree/tree-keys.d.ts +27 -0
  193. package/dist/tree/tree-keys.js +39 -0
  194. package/dist/tree/tree.d.ts +12 -5
  195. package/dist/tree/tree.js +75 -29
  196. package/package.json +12 -12
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Children, cloneElement, createContext, isValidElement, useContext, useState, } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { useControllableState } from "../internal/use-controllable-state.js";
5
+ import { useKptId } from "../internal/use-id.js";
5
6
  import { KptIcon } from "../icon/icon.js";
6
7
  const AccordionContext = createContext(null);
7
8
  /**
@@ -46,6 +47,9 @@ export function KptAccordion({ multi = false, animated = true, animationDuration
46
47
  /** Panel akordeonu. Umieszczaj w `<KptAccordion>` albo używaj samodzielnie. */
47
48
  export function KptAccordionPanel({ title, disabled = false, expanded, defaultExpanded = false, onExpandedChange, index, className, style, children, ...rest }) {
48
49
  const accordion = useContext(AccordionContext);
50
+ const uid = useKptId(undefined, 'kpt-accordion');
51
+ const headerId = `${uid}-header`;
52
+ const bodyId = `${uid}-body`;
49
53
  const [standalone, setStandalone] = useControllableState({
50
54
  value: expanded,
51
55
  defaultValue: defaultExpanded,
@@ -63,7 +67,9 @@ export function KptAccordionPanel({ title, disabled = false, expanded, defaultEx
63
67
  setStandalone(willOpen);
64
68
  onExpandedChange?.(willOpen);
65
69
  };
66
- return (_jsx("div", { className: cn('kpt-accordion-panel-host', className), style: { ['--kpt-anim-duration']: accordion?.animMs ?? '500ms', ...style }, ...rest, children: _jsxs("div", { className: cn('kpt-accordion-panel', isOpen && 'is-open'), children: [_jsxs("button", { type: "button", className: "kpt-accordion-panel__header", "aria-expanded": isOpen, disabled: disabled, onClick: toggle, children: [_jsx("span", { className: "kpt-accordion-panel__title", children: title }), _jsx(KptIcon, { name: "chevron-down", className: cn('kpt-accordion-panel__chevron', isOpen && 'is-open') })] }), _jsx("div", { className: cn('kpt-accordion-panel__collapse', isOpen && 'is-open'), inert: !isOpen, "aria-hidden": !isOpen, children: _jsx("div", { className: "kpt-accordion-panel__collapse-inner", children: _jsx("div", { className: "kpt-accordion-panel__body", children: children }) }) })] }) }));
70
+ return (_jsx("div", { className: cn('kpt-accordion-panel-host', className), style: { ['--kpt-anim-duration']: accordion?.animMs ?? '500ms', ...style }, ...rest, children: _jsxs("div", { className: cn('kpt-accordion-panel', isOpen && 'is-open'), children: [_jsxs("button", { type: "button", className: "kpt-accordion-panel__header", id: headerId, "aria-expanded": isOpen, "aria-controls": bodyId, disabled: disabled, onClick: toggle, children: [_jsx("span", { className: "kpt-accordion-panel__title", children: title }), _jsx(KptIcon, { name: "chevron-down", className: cn('kpt-accordion-panel__chevron', isOpen && 'is-open') })] }), _jsx("div", { className: cn('kpt-accordion-panel__collapse', isOpen && 'is-open'),
71
+ // Treść to region nazwany nagłówkiem — czytnik ogłasza nie tylko stan, ale i to, czego dotyczy.
72
+ role: "region", id: bodyId, "aria-labelledby": headerId, inert: !isOpen, "aria-hidden": !isOpen, children: _jsx("div", { className: "kpt-accordion-panel__collapse-inner", children: _jsx("div", { className: "kpt-accordion-panel__body", children: children }) }) })] }) }));
67
73
  }
68
74
  KptAccordionPanel.kptAccordionSlot = 'panel';
69
75
  KptAccordion.Panel = KptAccordionPanel;
@@ -7,11 +7,13 @@ Import: `import { KptAccordion } from '@konce-pt/react';`
7
7
  <div class="kpt-accordion-host">
8
8
  <div class="kpt-accordion-panel-host" style="--kpt-anim-duration: 500ms">
9
9
  <div class="kpt-accordion-panel is-open">
10
- <button class="kpt-accordion-panel__header" aria-expanded="true">
10
+ <button class="kpt-accordion-panel__header" id="kpt-accordion-r1-header"
11
+ aria-expanded="true" aria-controls="kpt-accordion-r1-body">
11
12
  <span class="kpt-accordion-panel__title">Section 1</span>
12
13
  <span class="kpt-icon-host kpt-accordion-panel__chevron is-open">…</span>
13
14
  </button>
14
- <div class="kpt-accordion-panel__collapse is-open">
15
+ <div class="kpt-accordion-panel__collapse is-open" role="region" id="kpt-accordion-r1-body"
16
+ aria-labelledby="kpt-accordion-r1-header">
15
17
  <div class="kpt-accordion-panel__collapse-inner">
16
18
  <div class="kpt-accordion-panel__body">…children…</div>
17
19
  </div>
@@ -52,5 +54,7 @@ nothing inside takes focus or reaches a screen reader.
52
54
  `--kpt-anim-duration` set by the accordion on each panel.
53
55
 
54
56
  ## Accessibility
55
- The header is a native button with `aria-expanded`; the collapsed region is `inert` and
56
- `aria-hidden`, so it is skipped by both the keyboard and the screen reader.
57
+ Each header is a native button with `aria-expanded` and `aria-controls`; the body is a
58
+ `role="region"` named by its header (`aria-labelledby`), so a screen reader announces both the state
59
+ and what it belongs to. A collapsed body is `inert` and `aria-hidden`, skipped by the keyboard and
60
+ the screen reader alike.
@@ -1,4 +1,4 @@
1
- import { type ComponentPropsWithRef, type ReactNode } from 'react';
1
+ import type { ComponentPropsWithRef, ReactNode } from 'react';
2
2
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
3
  import type { KptAlertVariant } from '../shared/types.ts';
4
4
  export type { KptAlertVariant };
@@ -9,17 +9,23 @@ export interface KptAlertProps extends Omit<ComponentPropsWithRef<'div'>, 'title
9
9
  dismissible?: boolean;
10
10
  /** Wywoływane po zamknięciu przez użytkownika. */
11
11
  onClosed?: () => void;
12
+ /** Widoczność (sterowana); bez niej alert sam trzyma stan. */
13
+ visible?: boolean;
14
+ defaultVisible?: boolean;
15
+ onVisibleChange?: (visible: boolean) => void;
12
16
  children?: ReactNode;
13
17
  }
14
18
  /**
15
19
  * Komunikat inline (alert). Warianty info/success/warning/danger, opcjonalny tytuł i zamykanie.
16
20
  *
17
21
  * Zamknięcie chowa komunikat atrybutem `hidden` (tak samo jak port Angulara), a nie odmontowuje
18
- * go — dzięki temu ten sam alert da się pokazać ponownie bez przebudowy drzewa. Gdy wolisz nim
19
- * sterować z zewnątrz, po prostu nie renderuj komponentu.
22
+ * go. `visible`/`onVisibleChange` pozwalają pokazać go ponownie z zewnątrz.
23
+ *
24
+ * Rola idzie za wagą komunikatu: `info` i `success` to `role="status"` (czytnik ogłosi je uprzejmie,
25
+ * nie przerywając), `warning` i `danger` — `role="alert"` (natychmiast).
20
26
  *
21
27
  * @example
22
28
  * <KptAlert variant="success" title="Zapisano">Zmiany zostały zapisane.</KptAlert>
23
29
  * <KptAlert variant="warning" dismissible onClosed={onClosed}>Uwaga…</KptAlert>
24
30
  */
25
- export declare function KptAlert({ variant, title, dismissible, onClosed, locale, dictionary, className, children, ...rest }: Readonly<KptAlertProps>): import("react").JSX.Element;
31
+ export declare function KptAlert({ variant, title, dismissible, onClosed, visible, defaultVisible, onVisibleChange, locale, dictionary, className, children, ...rest }: Readonly<KptAlertProps>): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
2
  import { cn } from "../internal/cn.js";
3
+ import { useControllableState } from "../internal/use-controllable-state.js";
4
4
  import { useKptMessages } from "../i18n/i18n-context.js";
5
5
  import { KptIcon } from "../icon/icon.js";
6
6
  const ICONS = {
@@ -13,19 +13,26 @@ const ICONS = {
13
13
  * Komunikat inline (alert). Warianty info/success/warning/danger, opcjonalny tytuł i zamykanie.
14
14
  *
15
15
  * Zamknięcie chowa komunikat atrybutem `hidden` (tak samo jak port Angulara), a nie odmontowuje
16
- * go — dzięki temu ten sam alert da się pokazać ponownie bez przebudowy drzewa. Gdy wolisz nim
17
- * sterować z zewnątrz, po prostu nie renderuj komponentu.
16
+ * go. `visible`/`onVisibleChange` pozwalają pokazać go ponownie z zewnątrz.
17
+ *
18
+ * Rola idzie za wagą komunikatu: `info` i `success` to `role="status"` (czytnik ogłosi je uprzejmie,
19
+ * nie przerywając), `warning` i `danger` — `role="alert"` (natychmiast).
18
20
  *
19
21
  * @example
20
22
  * <KptAlert variant="success" title="Zapisano">Zmiany zostały zapisane.</KptAlert>
21
23
  * <KptAlert variant="warning" dismissible onClosed={onClosed}>Uwaga…</KptAlert>
22
24
  */
23
- export function KptAlert({ variant = 'info', title = '', dismissible = false, onClosed, locale, dictionary, className, children, ...rest }) {
25
+ export function KptAlert({ variant = 'info', title = '', dismissible = false, onClosed, visible, defaultVisible = true, onVisibleChange, locale, dictionary, className, children, ...rest }) {
24
26
  const { t } = useKptMessages({ locale, dictionary });
25
- const [visible, setVisible] = useState(true);
27
+ const [isVisible, setVisible] = useControllableState({
28
+ value: visible,
29
+ defaultValue: defaultVisible,
30
+ onChange: onVisibleChange,
31
+ });
32
+ const role = variant === 'warning' || variant === 'danger' ? 'alert' : 'status';
26
33
  const dismiss = () => {
27
34
  setVisible(false);
28
35
  onClosed?.();
29
36
  };
30
- return (_jsx("div", { className: cn('kpt-alert-host', className), hidden: !visible, ...rest, children: _jsxs("div", { className: "kpt-alert", "data-variant": variant, role: "alert", children: [_jsx(KptIcon, { className: "kpt-alert__icon", name: ICONS[variant] }), _jsxs("div", { className: "kpt-alert__content", children: [title ? _jsx("div", { className: "kpt-alert__title", children: title }) : null, _jsx("div", { className: "kpt-alert__message", children: children })] }), dismissible ? (_jsx("button", { className: "kpt-alert__close", type: "button", onClick: dismiss, "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })) : null] }) }));
37
+ return (_jsx("div", { className: cn('kpt-alert-host', className), hidden: !isVisible, ...rest, children: _jsxs("div", { className: "kpt-alert", "data-variant": variant, role: role, children: [_jsx(KptIcon, { className: "kpt-alert__icon", name: ICONS[variant] }), _jsxs("div", { className: "kpt-alert__content", children: [title ? _jsx("div", { className: "kpt-alert__title", children: title }) : null, _jsx("div", { className: "kpt-alert__message", children: children })] }), dismissible ? (_jsx("button", { className: "kpt-alert__close", type: "button", onClick: dismiss, "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) })) : null] }) }));
31
38
  }
@@ -5,7 +5,7 @@ Import: `import { KptAlert } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-alert-host">
8
- <div class="kpt-alert" data-variant="success" role="alert">
8
+ <div class="kpt-alert" data-variant="success" role="status">
9
9
  <span class="kpt-icon-host kpt-alert__icon">…</span>
10
10
  <div class="kpt-alert__content">
11
11
  <div class="kpt-alert__title">Saved</div>
@@ -19,14 +19,14 @@ Import: `import { KptAlert } from '@konce-pt/react';`
19
19
  - `variant`: 'info' | 'success' | 'warning' | 'danger' (default 'info')
20
20
  - `title`: string
21
21
  - `dismissible`: boolean — adds the close button
22
+ - `visible` / `defaultVisible` / `onVisibleChange` — controlled or uncontrolled visibility
22
23
  - `onClosed()` — fires after the user dismisses it
23
24
  - `locale`, `dictionary` — i18n overrides for this component only
24
25
  - `className`, `ref` and every other `<div>` prop go to the host
25
26
 
26
27
  ## Dismissal hides, it does not unmount
27
- Closing sets `hidden` on the host, the same as the Angular port, so the same alert can be shown
28
- again without rebuilding the tree. When you would rather drive it from outside, simply do not render
29
- the component — the internal state is a convenience, not the only way.
28
+ Closing sets `hidden` on the host, the same as the Angular port. `visible` / `onVisibleChange`
29
+ make it controllable, so the same alert can be shown again from outside.
30
30
 
31
31
  ## The icon follows the variant
32
32
  Each variant picks its own icon from the built-in set (`info`, `circle-check`, `alert-triangle`,
@@ -41,5 +41,7 @@ The semantic colour pairs of each variant (`--kpt-color-info` / `-success` / `-w
41
41
  `-danger` with their `-subtle` counterparts), `--kpt-radius-md`.
42
42
 
43
43
  ## Accessibility
44
- The message is `role="alert"`, so a screen reader announces it as it appears. Use it for something
45
- that just happened — for a permanent note, style your own container without that role.
44
+ The role follows the weight of the message: `info` and `success` are `role="status"` (read
45
+ politely, without interrupting), `warning` and `danger` are `role="alert"` (read at once). The icon
46
+ is decorative; the × button is labelled from `common.close`. After closing, focus falls back to the
47
+ page — move it yourself in the close handler when the alert was reached with the keyboard.
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useCallback, useContext, useEffect, useMemo, } from 'react';
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { useControllableState } from "../internal/use-controllable-state.js";
5
5
  import { useKptId } from "../internal/use-id.js";
6
6
  import { useKptBreakpointUp } from "../internal/use-breakpoint.js";
7
7
  import { useKptMessages } from "../i18n/i18n-context.js";
8
+ /** Pierwszy element panelu, który przyjmie fokus po otwarciu nakładki. */
9
+ const FOCUSABLE = 'a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
8
10
  const KptAppShellContext = createContext(null);
9
11
  /**
10
12
  * Szkielet aplikacji: pasek u góry, panel boczny i treść.
@@ -73,7 +75,31 @@ export function KptAppShell({ toolbar, sidenav, children, sidenavOpen, defaultSi
73
75
  }), [isSidenavOpen, isOverlay, sidenavId, setSidenavOpen, closeSidenav]);
74
76
  // Scrim blokuje wskaźnik, ale nie klawiaturę — treść pod nakładką nie może łapać fokusu.
75
77
  const contentInert = isOverlay && isSidenavOpen;
76
- return (_jsx(KptAppShellContext.Provider, { value: context, children: _jsxs("div", { className: cn('kpt-app-shell', dockedOpen && 'kpt-app-shell--nav-open', compactOpen && 'kpt-app-shell--nav-compact-open', className), "data-sidenav-mode": sidenavMode, "data-sidenav-breakpoint": sidenavBreakpoint, "data-sidenav-rail": sidenavRailBreakpoint ?? undefined, ...rest, children: [toolbar, _jsxs("div", { className: "kpt-app-shell__body", children: [_jsx("aside", { className: "kpt-app-shell__sidenav", role: "navigation", tabIndex: -1, id: sidenavId, "aria-label": sidenavLabel || t('appShell.navigation'), children: sidenav }), _jsx("div", { className: "kpt-app-shell__scrim", "aria-hidden": "true", onClick: closeSidenav }), _jsx("main", { className: "kpt-app-shell__content", inert: contentInert, children: children })] })] }) }));
78
+ // Otwarta nakładka robi treść `inert`, więc fokus zostawiony w niej (albo na przycisku menu)
79
+ // nie miałby dokąd pójść. Po otwarciu przenosimy go do panelu, po zamknięciu oddajemy tam,
80
+ // skąd przyszedł — chyba że użytkownik zdążył go przenieść gdzie indziej. Stan `null` przed
81
+ // pierwszym renderem: nakładka otwarta już przy starcie nie zabiera fokusu.
82
+ const asideRef = useRef(null);
83
+ const overlayWasOpen = useRef(null);
84
+ const returnFocusTo = useRef(null);
85
+ useEffect(() => {
86
+ const was = overlayWasOpen.current;
87
+ overlayWasOpen.current = contentInert;
88
+ const panel = asideRef.current;
89
+ if (was === null || was === contentInert || !panel)
90
+ return;
91
+ const active = document.activeElement;
92
+ if (contentInert) {
93
+ returnFocusTo.current = active instanceof HTMLElement && !panel.contains(active) ? active : null;
94
+ (panel.querySelector(FOCUSABLE) ?? panel).focus();
95
+ return;
96
+ }
97
+ const focusLost = !active || active === document.body || panel.contains(active);
98
+ if (focusLost && returnFocusTo.current?.isConnected)
99
+ returnFocusTo.current.focus();
100
+ returnFocusTo.current = null;
101
+ }, [contentInert]);
102
+ return (_jsx(KptAppShellContext.Provider, { value: context, children: _jsxs("div", { className: cn('kpt-app-shell', dockedOpen && 'kpt-app-shell--nav-open', compactOpen && 'kpt-app-shell--nav-compact-open', className), "data-sidenav-mode": sidenavMode, "data-sidenav-breakpoint": sidenavBreakpoint, "data-sidenav-rail": sidenavRailBreakpoint ?? undefined, ...rest, children: [toolbar, _jsxs("div", { className: "kpt-app-shell__body", children: [_jsx("aside", { ref: asideRef, className: "kpt-app-shell__sidenav", role: "navigation", tabIndex: -1, id: sidenavId, "aria-label": sidenavLabel || t('appShell.navigation'), children: sidenav }), _jsx("div", { className: "kpt-app-shell__scrim", "aria-hidden": "true", onClick: closeSidenav }), _jsx("main", { className: "kpt-app-shell__content", inert: contentInert, children: children })] })] }) }));
77
103
  }
78
104
  /** Dostęp do stanu shella. Poza `KptAppShell` rzuca — to zawsze błąd użycia. */
79
105
  export function useKptAppShell() {
@@ -57,3 +57,6 @@ decides WHICH state the button flips. The scrim is always in the DOM; CSS contro
57
57
  - The panel is an `<aside role="navigation">` with a label and an `id` for `aria-controls`.
58
58
  - A closed docked panel leaves the tab order through `visibility`, correct on the first frame.
59
59
  - While the overlay is open the content gets `inert`, and Escape closes the panel.
60
+ - Opening the overlay moves focus into the panel (its first focusable element, or the `<aside>`
61
+ itself); closing it returns focus to where it came from — usually the menu button — unless the user
62
+ has already moved it elsewhere. An overlay that is open when the page loads does not take focus.
@@ -1,5 +1,6 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
- export interface KptAutocompleteProps extends Omit<ComponentPropsWithRef<'input'>, 'value' | 'defaultValue' | 'onChange'> {
2
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
+ export interface KptAutocompleteProps extends Omit<ComponentPropsWithRef<'input'>, 'value' | 'defaultValue' | 'onChange'>, KptI18nOverrides {
3
4
  options?: readonly string[];
4
5
  value?: string;
5
6
  defaultValue?: string;
@@ -12,7 +13,11 @@ export interface KptAutocompleteProps extends Omit<ComponentPropsWithRef<'input'
12
13
  * Pole z podpowiedziami. Wartością jest wpisany tekst, nie wybrana opcja — podpowiedzi
13
14
  * skracają pisanie, ale nie ograniczają wpisu, więc pole nadaje się też do wartości spoza listy.
14
15
  *
16
+ * Wzorzec WAI-ARIA „combobox z listą”: fokus zostaje w polu, aktywną podpowiedź ogłasza
17
+ * `aria-activedescendant`, a liczbę podpowiedzi (albo ich brak) — region `polite`. Dopasowanie
18
+ * pomija wielkość liter i diakrytyki („lodz” znajduje „Łódź”).
19
+ *
15
20
  * @example
16
21
  * <KptAutocomplete options={cities} value={city} onValueChange={setCity} placeholder="Miasto…" />
17
22
  */
18
- export declare function KptAutocomplete({ options, value, defaultValue, onValueChange, invalid, touched, onTouch, disabled, className, onBlur, onFocus, onKeyDown, ref, ...rest }: Readonly<KptAutocompleteProps>): import("react").JSX.Element;
23
+ export declare function KptAutocomplete({ options, value, defaultValue, onValueChange, invalid, touched, onTouch, disabled, className, onBlur, onFocus, onKeyDown, ref, id, 'aria-describedby': ariaDescribedBy, locale, dictionary, ...rest }: Readonly<KptAutocompleteProps>): import("react").JSX.Element;
@@ -1,17 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useMemo, useRef, useState, } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { cn } from "../internal/cn.js";
4
4
  import { composeRefs } from "../internal/compose-refs.js";
5
5
  import { KptOverlay } from "../internal/overlay.js";
6
6
  import { useControllableState } from "../internal/use-controllable-state.js";
7
+ import { foldMentionText } from '@konce-pt/mention';
8
+ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
9
+ import { useKptMessages } from "../i18n/i18n-context.js";
7
10
  /**
8
11
  * Pole z podpowiedziami. Wartością jest wpisany tekst, nie wybrana opcja — podpowiedzi
9
12
  * skracają pisanie, ale nie ograniczają wpisu, więc pole nadaje się też do wartości spoza listy.
10
13
  *
14
+ * Wzorzec WAI-ARIA „combobox z listą”: fokus zostaje w polu, aktywną podpowiedź ogłasza
15
+ * `aria-activedescendant`, a liczbę podpowiedzi (albo ich brak) — region `polite`. Dopasowanie
16
+ * pomija wielkość liter i diakrytyki („lodz” znajduje „Łódź”).
17
+ *
11
18
  * @example
12
19
  * <KptAutocomplete options={cities} value={city} onValueChange={setCity} placeholder="Miasto…" />
13
20
  */
14
- export function KptAutocomplete({ options = [], value, defaultValue = '', onValueChange, invalid = false, touched, onTouch, disabled = false, className, onBlur, onFocus, onKeyDown, ref, ...rest }) {
21
+ export function KptAutocomplete({ options = [], value, defaultValue = '', onValueChange, invalid = false, touched, onTouch, disabled = false, className, onBlur, onFocus, onKeyDown, ref, id, 'aria-describedby': ariaDescribedBy, locale, dictionary, ...rest }) {
22
+ const { t } = useKptMessages({ locale, dictionary });
23
+ const field = useKptFieldA11y(id, 'kpt-autocomplete');
15
24
  const inputRef = useRef(null);
16
25
  const [current, setCurrent] = useControllableState({
17
26
  value,
@@ -20,11 +29,26 @@ export function KptAutocomplete({ options = [], value, defaultValue = '', onValu
20
29
  });
21
30
  const [open, setOpen] = useState(false);
22
31
  const [activeIndex, setActiveIndex] = useState(-1);
32
+ /** Wpisano coś od ostatniego otwarcia — dopiero wtedy ogłaszamy liczbę podpowiedzi. */
33
+ const [typed, setTyped] = useState(false);
23
34
  const showInvalid = invalid && (touched ?? true);
24
35
  const filtered = useMemo(() => {
25
- const q = current.trim().toLowerCase();
26
- return q ? options.filter((option) => option.toLowerCase().includes(q)) : options;
36
+ const q = foldMentionText(current.trim());
37
+ return q ? options.filter((option) => foldMentionText(option).includes(q)) : options;
27
38
  }, [options, current]);
39
+ const panelOpen = open && filtered.length > 0;
40
+ const listId = `${field.id}-list`;
41
+ const optionId = (index) => `${field.id}-opt-${index}`;
42
+ const activeOptionId = panelOpen && activeIndex >= 0 && activeIndex < filtered.length ? optionId(activeIndex) : undefined;
43
+ const announcement = open && typed && current.trim()
44
+ ? filtered.length
45
+ ? t('autocomplete.results', { count: filtered.length })
46
+ : t('autocomplete.noResults')
47
+ : '';
48
+ useEffect(() => {
49
+ if (activeOptionId)
50
+ document.getElementById(activeOptionId)?.scrollIntoView({ block: 'nearest' });
51
+ }, [activeOptionId]);
28
52
  const pick = useCallback((option) => {
29
53
  setCurrent(option);
30
54
  setOpen(false);
@@ -38,7 +62,9 @@ export function KptAutocomplete({ options = [], value, defaultValue = '', onValu
38
62
  }
39
63
  else if (event.key === 'ArrowUp') {
40
64
  event.preventDefault();
41
- setActiveIndex((index) => Math.max(index - 1, 0));
65
+ setOpen(true);
66
+ // Z pola (bez aktywnej) `↑` idzie na ostatnią podpowiedź.
67
+ setActiveIndex((index) => (index <= 0 ? filtered.length - 1 : index - 1));
42
68
  }
43
69
  else if (event.key === 'Enter') {
44
70
  const option = filtered[activeIndex];
@@ -48,12 +74,22 @@ export function KptAutocomplete({ options = [], value, defaultValue = '', onValu
48
74
  }
49
75
  }
50
76
  else if (event.key === 'Escape') {
51
- setOpen(false);
77
+ // Pierwszy `Escape` zamyka listę, drugi (przy zamkniętej) czyści pole.
78
+ if (panelOpen) {
79
+ event.preventDefault();
80
+ setOpen(false);
81
+ setActiveIndex(-1);
82
+ }
83
+ else if (current) {
84
+ event.preventDefault();
85
+ setCurrent('');
86
+ }
52
87
  }
53
88
  };
54
- return (_jsxs("span", { className: "kpt-autocomplete-host", children: [_jsx("input", { ref: composeRefs(inputRef, ref), className: cn('kpt-autocomplete__input', className), type: "text", role: "combobox", "aria-expanded": open, "aria-invalid": showInvalid || undefined, disabled: disabled, value: current, onChange: (event) => {
89
+ return (_jsxs("span", { className: "kpt-autocomplete-host", children: [_jsx("input", { ref: composeRefs(inputRef, ref), className: cn('kpt-autocomplete__input', className), type: "text", id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), role: "combobox", "aria-autocomplete": "list", autoComplete: "off", "aria-expanded": panelOpen, "aria-controls": panelOpen ? listId : undefined, "aria-activedescendant": activeOptionId, "aria-invalid": showInvalid || undefined, disabled: disabled, value: current, onChange: (event) => {
55
90
  setCurrent(event.target.value);
56
91
  setOpen(true);
92
+ setTyped(true);
57
93
  setActiveIndex(-1);
58
94
  }, onFocus: (event) => {
59
95
  onFocus?.(event);
@@ -61,12 +97,13 @@ export function KptAutocomplete({ options = [], value, defaultValue = '', onValu
61
97
  }, onBlur: (event) => {
62
98
  onBlur?.(event);
63
99
  setOpen(false);
100
+ setTyped(false);
64
101
  onTouch?.();
65
- }, onKeyDown: handleKeyDown, ...rest }), _jsx(KptOverlay, { open: open && filtered.length > 0, anchor: inputRef.current, onClose: () => setOpen(false), matchAnchorWidth: true, withBackdrop: false, children: _jsx("ul", { className: "kpt-autocomplete__panel", role: "listbox", children: filtered.map((option, index) => (_jsx("li", { className: cn('kpt-autocomplete__option', index === activeIndex && 'is-active'), role: "option", "aria-selected": index === activeIndex,
102
+ }, onKeyDown: handleKeyDown, ...rest }), _jsx(KptOverlay, { open: panelOpen, anchor: inputRef.current, onClose: () => setOpen(false), matchAnchorWidth: true, withBackdrop: false, children: _jsx("ul", { className: "kpt-autocomplete__panel", role: "listbox", id: listId, "aria-label": rest['aria-label'], children: filtered.map((option, index) => (_jsx("li", { id: optionId(index), className: cn('kpt-autocomplete__option', index === activeIndex && 'is-active'), role: "option", "aria-selected": index === activeIndex,
66
103
  // `mousedown` z zablokowanym domyślnym zachowaniem: klik w opcję nie może
67
104
  // zabrać fokusu polu, bo `blur` zamknąłby panel przed wyborem.
68
105
  onMouseDown: (event) => {
69
106
  event.preventDefault();
70
107
  pick(option);
71
- }, onMouseEnter: () => setActiveIndex(index), children: option }, option))) }) })] }));
108
+ }, onMouseEnter: () => setActiveIndex(index), children: option }, option))) }) }), _jsx("span", { className: "kpt-autocomplete__live", "aria-live": "polite", children: announcement })] }));
72
109
  }
@@ -35,6 +35,18 @@ one, Escape closes.
35
35
  ## Examples
36
36
  <KptAutocomplete options={cities} value={city} onValueChange={setCity} placeholder="City…" />
37
37
 
38
+ ## Screen reader and keyboard
39
+ The WAI-ARIA combobox-with-list pattern: focus stays in the field, `aria-autocomplete="list"`,
40
+ `aria-controls` and `aria-activedescendant` point at the active suggestion, and a polite live region
41
+ says how many suggestions there are (`autocomplete.results`) or that there are none
42
+ (`autocomplete.noResults`). Matching ignores case and diacritics ("lodz" finds "Łódź"). `↑` from the
43
+ field goes to the last suggestion; the first `Escape` closes the list, the second clears the field.
44
+
45
+ ## Label and description
46
+ `id` goes to the native field, not the host; without one it comes from `KptFormField` or is
47
+ generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
48
+ added to `aria-describedby` (merged with your own). Without a wrapper, name it with `aria-label`.
49
+
38
50
  ## Tokens
39
51
  `--kpt-form-field-{bg,text,placeholder,border,border-focus,radius}`,
40
52
  `--kpt-color-surface-raised` and `--kpt-elevation-3` for the panel.
@@ -1,4 +1,4 @@
1
- import type { ComponentPropsWithRef } from 'react';
1
+ import { type ComponentPropsWithRef } from 'react';
2
2
  import type { KptHostProps } from '../internal/types.ts';
3
3
  export type KptAvatarSize = 'sm' | 'md' | 'lg' | 'xl';
4
4
  export type KptAvatarShape = 'circle' | 'square';
@@ -11,6 +11,8 @@ export interface KptAvatarProps extends Omit<ComponentPropsWithRef<'span'>, 'chi
11
11
  icon?: string | null;
12
12
  size?: KptAvatarSize;
13
13
  shape?: KptAvatarShape;
14
+ /** Ukrywa awatar przed czytnikiem ekranu, gdy imię jest już napisane obok. */
15
+ decorative?: boolean;
14
16
  /**
15
17
  * Propsy wrappera hosta (`.kpt-avatar-host`). To on jest elementem układu, więc klasy
16
18
  * pozycjonujące awatar w rodzicu — jak `kpt-card__avatar` — idą właśnie tutaj.
@@ -30,4 +32,4 @@ export interface KptAvatarProps extends Omit<ComponentPropsWithRef<'span'>, 'chi
30
32
  * <KptAvatar name="Anna Kowalska" size="lg" />
31
33
  * <KptAvatar icon="user" shape="square" />
32
34
  */
33
- export declare function KptAvatar({ src, name, icon, size, shape, hostProps, className, ...rest }: Readonly<KptAvatarProps>): import("react").JSX.Element;
35
+ export declare function KptAvatar({ src, name, icon, size, shape, decorative, hostProps, className, ...rest }: Readonly<KptAvatarProps>): import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { KptIcon } from "../icon/icon.js";
4
5
  /**
@@ -14,10 +15,14 @@ import { KptIcon } from "../icon/icon.js";
14
15
  * <KptAvatar name="Anna Kowalska" size="lg" />
15
16
  * <KptAvatar icon="user" shape="square" />
16
17
  */
17
- export function KptAvatar({ src = null, name = '', icon = null, size = 'md', shape = 'circle', hostProps, className, ...rest }) {
18
+ export function KptAvatar({ src = null, name = '', icon = null, size = 'md', shape = 'circle', decorative = false, hostProps, className, ...rest }) {
18
19
  const { className: hostClassName, ...hostRest } = hostProps ?? {};
19
- const labelled = !src && !!name;
20
- return (_jsx("span", { className: cn('kpt-avatar-host', hostClassName), ...hostRest, children: _jsx("span", { className: cn('kpt-avatar', className), ...rest, "data-size": size, "data-shape": shape, title: name || undefined, role: labelled ? 'img' : undefined, "aria-label": labelled ? name : undefined, children: src ? (_jsx("img", { className: "kpt-avatar__img", src: src, alt: name })) : name ? (_jsx("span", { className: "kpt-avatar__initials", "aria-hidden": "true", children: initialsOf(name) })) : icon ? (_jsx(KptIcon, { name: icon })) : null }) }));
20
+ // Zdjęcie, które się nie wczytało — awatar wraca wtedy do inicjałów (albo ikony). Pamiętamy,
21
+ // którego `src` dotyczy błąd, więc nowy adres próbuje się wczytać od nowa.
22
+ const [brokenSrc, setBrokenSrc] = useState(null);
23
+ const showImage = !!src && brokenSrc !== src;
24
+ const labelled = !decorative && !showImage && !!name;
25
+ return (_jsx("span", { className: cn('kpt-avatar-host', hostClassName), ...hostRest, children: _jsx("span", { className: cn('kpt-avatar', className), ...rest, "data-size": size, "data-shape": shape, title: decorative ? undefined : name || undefined, role: labelled ? 'img' : undefined, "aria-label": labelled ? name : undefined, "aria-hidden": decorative || undefined, children: showImage ? (_jsx("img", { className: "kpt-avatar__img", src: src, alt: decorative ? '' : name, onError: () => setBrokenSrc(src) })) : name ? (_jsx("span", { className: "kpt-avatar__initials", "aria-hidden": "true", children: initialsOf(name) })) : icon ? (_jsx(KptIcon, { name: icon })) : null }) }));
21
26
  }
22
27
  /** Do dwóch pierwszych liter z kolejnych członów nazwy. */
23
28
  function initialsOf(name) {
@@ -16,12 +16,14 @@ Import: `import { KptAvatar } from '@konce-pt/react';`
16
16
  - `icon`: string | null — a registry icon name, used when there is neither photo nor name
17
17
  - `size`: 'sm' | 'md' | 'lg' | 'xl' (default 'md')
18
18
  - `shape`: 'circle' | 'square' (default 'circle')
19
+ - `decorative`: boolean — hides the avatar from screen readers (the name is written next to it)
19
20
  - `hostProps` — props for the `.kpt-avatar-host` wrapper
20
21
  - `className`, `ref` and every other `<span>` prop go to the inner `.kpt-avatar`
21
22
 
22
23
  ## Fallback order
23
- `src` → `name` (up to two initials, uppercased) → `icon`. With none of them the avatar renders as
24
- an empty shape, which is a valid placeholder.
24
+ `src` → `name` (up to two initials, uppercased) → `icon`. A photo that fails to load drops to the
25
+ next step by itself, and a new `src` is tried again. With none of them the avatar renders as an
26
+ empty shape, which is a valid placeholder.
25
27
 
26
28
  ## Positioning inside a parent
27
29
  The host is the layout element, so classes that place the avatar in its parent go to `hostProps` —
@@ -37,6 +39,8 @@ the overlapping avatar on a card is `hostProps={{ className: 'kpt-card__avatar'
37
39
  `--kpt-color-muted`, `--kpt-color-on-muted`, `--kpt-radius-full`, `--kpt-radius-md` (square).
38
40
 
39
41
  ## Accessibility
40
- `name` becomes the native `title` — initials alone do not say who this is. Without `src` the avatar
41
- also gets `role="img"` and `aria-label`, and the initials are `aria-hidden` so a screen reader does
42
- not spell out "A-K". With `src` the label already lives on the image's `alt`, so it is not doubled.
42
+ `name` goes into the native `title` (initials alone say nothing). With a photo the name is the
43
+ image's `alt`; without one the avatar is `role="img"` with `aria-label`, and the initials are
44
+ `aria-hidden`. A photo that fails to load falls back to the initials (or the icon); a new `src` is
45
+ tried again. `decorative` removes the avatar from the accessibility tree — use it when the name is
46
+ already written next to it.
@@ -1,15 +1,18 @@
1
1
  import type { ComponentPropsWithRef } from 'react';
2
+ import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
2
3
  import { type KptAvatarSize } from '../avatar/avatar.tsx';
3
4
  export interface KptAvatarItem {
4
5
  name?: string;
5
6
  src?: string;
6
7
  icon?: string;
7
8
  }
8
- export interface KptAvatarGroupProps extends ComponentPropsWithRef<'div'> {
9
+ export interface KptAvatarGroupProps extends ComponentPropsWithRef<'div'>, KptI18nOverrides {
9
10
  avatars?: readonly KptAvatarItem[];
10
11
  /** Ile awatarów pokazać, zanim pojawi się „+N". */
11
12
  max?: number;
12
13
  size?: KptAvatarSize;
14
+ /** Nazwa grupy dla czytników ekranu, np. „Członkowie projektu”; bez niej grupa nie ma roli. */
15
+ label?: string;
13
16
  }
14
17
  /**
15
18
  * Grupa nakładających się awatarów z licznikiem „+N". Buduje na `KptAvatar`, więc każdy element
@@ -18,4 +21,4 @@ export interface KptAvatarGroupProps extends ComponentPropsWithRef<'div'> {
18
21
  * @example
19
22
  * <KptAvatarGroup avatars={team} max={4} />
20
23
  */
21
- export declare function KptAvatarGroup({ avatars, max, size, className, ...rest }: Readonly<KptAvatarGroupProps>): import("react").JSX.Element;
24
+ export declare function KptAvatarGroup({ avatars, max, size, label, locale, dictionary, className, ...rest }: Readonly<KptAvatarGroupProps>): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../internal/cn.js";
3
+ import { useKptMessages } from "../i18n/i18n-context.js";
3
4
  import { KptAvatar } from "../avatar/avatar.js";
4
5
  /**
5
6
  * Grupa nakładających się awatarów z licznikiem „+N". Buduje na `KptAvatar`, więc każdy element
@@ -8,8 +9,15 @@ import { KptAvatar } from "../avatar/avatar.js";
8
9
  * @example
9
10
  * <KptAvatarGroup avatars={team} max={4} />
10
11
  */
11
- export function KptAvatarGroup({ avatars = [], max = 4, size = 'md', className, ...rest }) {
12
+ export function KptAvatarGroup({ avatars = [], max = 4, size = 'md', label = '', locale, dictionary, className, ...rest }) {
13
+ const { t } = useKptMessages({ locale, dictionary });
12
14
  const shown = avatars.slice(0, max);
13
15
  const overflow = Math.max(0, avatars.length - max);
14
- return (_jsx("div", { className: cn('kpt-avatar-group-host', className), ...rest, children: _jsxs("div", { className: "kpt-avatar-group", "data-size": size, children: [shown.map((avatar, index) => (_jsx("span", { className: "kpt-avatar-group__item", children: _jsx(KptAvatar, { name: avatar.name || '', src: avatar.src || null, icon: avatar.icon || null, size: size }) }, index))), overflow > 0 ? (_jsxs("span", { className: "kpt-avatar-group__item kpt-avatar-group__more", "data-size": size, children: ["+", overflow] })) : null] }) }));
16
+ // Sam „+2” nic czytnikowi nie mówi — licznik ma nazwę „i 2 więcej” i podpowiedź z imionami.
17
+ const hiddenNames = avatars
18
+ .slice(max)
19
+ .map((avatar) => avatar.name)
20
+ .filter(Boolean)
21
+ .join(', ');
22
+ return (_jsx("div", { className: cn('kpt-avatar-group-host', className), ...rest, children: _jsxs("div", { className: "kpt-avatar-group", "data-size": size, role: label ? 'group' : undefined, "aria-label": label || undefined, children: [shown.map((avatar, index) => (_jsx("span", { className: "kpt-avatar-group__item", children: _jsx(KptAvatar, { name: avatar.name || '', src: avatar.src || null, icon: avatar.icon || null, size: size }) }, index))), overflow > 0 ? (_jsx("span", { className: "kpt-avatar-group__item kpt-avatar-group__more", role: "img", "data-size": size, "aria-label": t('avatarGroup.more', { count: overflow }), title: hiddenNames || undefined, children: _jsxs("span", { "aria-hidden": "true", children: ["+", overflow] }) })) : null] }) }));
15
23
  }
@@ -5,9 +5,10 @@ Import: `import { KptAvatarGroup } from '@konce-pt/react';`
5
5
 
6
6
  ## DOM
7
7
  <div class="kpt-avatar-group-host">
8
- <div class="kpt-avatar-group" data-size="lg">
8
+ <div class="kpt-avatar-group" data-size="lg" role="group" aria-label="Project members">
9
9
  <span class="kpt-avatar-group__item">…KptAvatar…</span>…
10
- <span class="kpt-avatar-group__item kpt-avatar-group__more" data-size="lg">+2</span>
10
+ <span class="kpt-avatar-group__item kpt-avatar-group__more" role="img" data-size="lg"
11
+ aria-label="2 more" title="Marta, Tomasz"><span aria-hidden="true">+2</span></span>
11
12
  </div>
12
13
  </div>
13
14
 
@@ -15,6 +16,7 @@ Import: `import { KptAvatarGroup } from '@konce-pt/react';`
15
16
  - `avatars`: `KptAvatarItem[]` — `{ name?, src?, icon? }`
16
17
  - `max`: number (default 4) — how many to show before "+N"
17
18
  - `size`: `'sm' | 'md' | 'lg' | 'xl'`
19
+ - `label`: string — the group's name; with it the row is `role="group"`
18
20
  - `className`, `ref` and every other `<div>` prop go to the host
19
21
 
20
22
  ## One list, three fallbacks
@@ -34,5 +36,6 @@ Every avatar keeps the `title` from `KptAvatar`, so hovering tells you who it is
34
36
  The overlap ring uses `--kpt-color-surface`, the "+N" chip `--kpt-color-muted`.
35
37
 
36
38
  ## Accessibility
37
- The avatars come from `KptAvatar` with its `role="img"` and `aria-label`; "+N" is plain text, so
38
- the count is read as part of the group.
39
+ Every avatar keeps its own name. With `label` the row is `role="group"` with that name. The “+N”
40
+ counter is `role="img"` named from `avatarGroup.more` (`{count}` — “2 more”), with the hidden names
41
+ in its `title`. The overlap uses `margin-inline-start`, so it mirrors in RTL.
@@ -12,6 +12,13 @@ export interface KptBadgeProps extends ComponentPropsWithRef<'span'> {
12
12
  overlay?: boolean;
13
13
  /** Powyżej tej wartości licznik pokazuje `99+`. */
14
14
  max?: number;
15
+ /**
16
+ * Znaczenie plakietki dla czytnika ekranu, np. „3 nieprzeczytane”. Sama liczba („3”) albo kropka
17
+ * nic nie mówią; z `label` widoczny tekst jest ukryty przed czytnikiem, a odczytywana jest etykieta.
18
+ */
19
+ label?: string;
20
+ /** Chowa plakietkę, gdy `value` to `0` albo `null` (nie dotyczy `dot`). */
21
+ hideZero?: boolean;
15
22
  children?: ReactNode;
16
23
  }
17
24
  /**
@@ -22,4 +29,4 @@ export interface KptBadgeProps extends ComponentPropsWithRef<'span'> {
22
29
  * <KptBadge value={128} overlay><KptIcon name="menu" /></KptBadge>
23
30
  * <KptBadge dot variant="success" overlay><KptAvatar name="AK" /></KptBadge>
24
31
  */
25
- export declare function KptBadge({ value, variant, size, dot, overlay, max, className, children, ...rest }: Readonly<KptBadgeProps>): import("react").JSX.Element;
32
+ export declare function KptBadge({ value, variant, size, dot, overlay, max, label, hideZero, className, children, ...rest }: Readonly<KptBadgeProps>): import("react").JSX.Element;
@@ -8,7 +8,8 @@ import { cn } from "../internal/cn.js";
8
8
  * <KptBadge value={128} overlay><KptIcon name="menu" /></KptBadge>
9
9
  * <KptBadge dot variant="success" overlay><KptAvatar name="AK" /></KptBadge>
10
10
  */
11
- export function KptBadge({ value = null, variant = 'primary', size = 'md', dot = false, overlay = false, max = 99, className, children, ...rest }) {
11
+ export function KptBadge({ value = null, variant = 'primary', size = 'md', dot = false, overlay = false, max = 99, label = '', hideZero = false, className, children, ...rest }) {
12
12
  const display = typeof value === 'number' && value > max ? `${max}+` : (value ?? '');
13
- return (_jsxs("span", { className: cn('kpt-badge-host', className), ...rest, children: [children, _jsx("span", { className: cn('kpt-badge', overlay && 'kpt-badge--overlay', dot && 'kpt-badge--dot'), "data-variant": variant, "data-size": size, children: dot ? '' : display })] }));
13
+ const hiddenZero = hideZero && !dot && (value === 0 || value === null || value === '');
14
+ return (_jsxs("span", { className: cn('kpt-badge-host', className), ...rest, children: [children, hiddenZero ? null : (_jsxs("span", { className: cn('kpt-badge', overlay && 'kpt-badge--overlay', dot && 'kpt-badge--dot'), "data-variant": variant, "data-size": size, children: [_jsx("span", { "aria-hidden": label ? true : undefined, children: dot ? '' : display }), label ? _jsx("span", { className: "kpt-badge__label", children: label }) : null] }))] }));
14
15
  }