@konce-pt/react 0.11.1 → 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 +15 -2
  24. package/dist/button-group/button-group.js +31 -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 +148 -55
  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
package/README.md CHANGED
@@ -52,7 +52,7 @@ npm i @konce-pt/react @konce-pt/tokens @konce-pt/styles
52
52
  # or: pnpm add / yarn add
53
53
  ```
54
54
 
55
- `react` and `react-dom` (`>=19`) are peers you already have. Optional extras:
55
+ `react` and `react-dom` (`^19.2`) are peers you already have. Optional extras:
56
56
 
57
57
  - [`@konce-pt/grid`](https://www.npmjs.com/package/@konce-pt/grid) — the mobile-first layout system;
58
58
  its React components live in `@konce-pt/react/grid`.
@@ -79,6 +79,9 @@ runtime; React has no other source for them, so this import is not optional.
79
79
 
80
80
  Dark theme: set `<html data-theme="dark">` (or rely on `prefers-color-scheme`).
81
81
 
82
+ **Next.js App Router / Server Components:** every interactive component module starts with
83
+ `'use client'`, so you can import components directly into a server component — no wrapper file.
84
+
82
85
  > **No CDK here.** Components with a floating panel (`KptSelect`, `KptPopover`, `KptMenu`,
83
86
  > `KptAutocomplete`, `KptDatepicker`…) portal into `document.body` through the internal
84
87
  > `KptOverlay`, so `@angular/cdk/overlay-prebuilt.css` is **not** needed — positioning lives in the
@@ -314,7 +317,7 @@ MIT © konce.pt
314
317
  npm i @konce-pt/react @konce-pt/tokens @konce-pt/styles
315
318
  ```
316
319
 
317
- `react` i `react-dom` (`>=19`) to peery, które masz już w aplikacji. Opcjonalnie:
320
+ `react` i `react-dom` (`^19.2`) to peery, które masz już w aplikacji. Opcjonalnie:
318
321
  `@konce-pt/grid` (układ; komponenty w `@konce-pt/react/grid`), `@konce-pt/validators`
319
322
  (walidatory PL: NIP, REGON, PESEL, IBAN, kod pocztowy) oraz `leaflet` (`^1.9`) — **opcjonalny**
320
323
  peer, potrzebny wyłącznie dla `@konce-pt/react/map`.
@@ -338,6 +341,9 @@ nie ma dla nich innego źródła, więc ten import nie jest opcjonalny.
338
341
  Motyw ciemny: `<html data-theme="dark">` (albo `prefers-color-scheme`). Nadpisania `--kpt-color-*`
339
342
  w arkuszu importowanym **po** `@konce-pt/styles`.
340
343
 
344
+ **Next.js App Router / Server Components:** każdy moduł interaktywnego komponentu zaczyna się od
345
+ `'use client'`, więc komponenty importujesz wprost do komponentu serwerowego — bez pliku-opakowania.
346
+
341
347
  > **CDK nie jest tu potrzebny.** Komponenty z panelem (`KptSelect`, `KptPopover`, `KptMenu`,
342
348
  > `KptAutocomplete`, `KptDatepicker`…) renderują go przez wewnętrzny `KptOverlay` do portalu na
343
349
  > `document.body`, więc `@angular/cdk/overlay-prebuilt.css` nie jest wymagany. W otwartym
@@ -1,6 +1,8 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, cloneElement, createContext, isValidElement, useContext, useState, } from 'react';
3
+ import { cloneElement, createContext, isValidElement, useContext, } from 'react';
3
4
  import { cn } from "../internal/cn.js";
5
+ import { slotChildren } from "../internal/slot-children.js";
4
6
  import { useControllableState } from "../internal/use-controllable-state.js";
5
7
  import { useKptId } from "../internal/use-id.js";
6
8
  import { KptIcon } from "../icon/icon.js";
@@ -34,14 +36,13 @@ export function KptAccordion({ multi = false, animated = true, animationDuration
34
36
  },
35
37
  animMs: `${animated ? animationDuration : 0}ms`,
36
38
  };
37
- let index = -1;
38
- const panels = Children.map(children, (child) => {
39
- if (!isValidElement(child) || child.type?.kptAccordionSlot !== 'panel') {
40
- return child;
41
- }
42
- index += 1;
43
- return cloneElement(child, { index });
44
- });
39
+ // Indeks panelu = liczba paneli przed nim; fragmenty rozwinięte, więc panel z warunku też się liczy.
40
+ const items = slotChildren(children);
41
+ const isPanel = (child) => isValidElement(child) && child.type?.kptAccordionSlot === 'panel';
42
+ const panelIndex = items.map((_, at) => items.slice(0, at).filter(isPanel).length);
43
+ const panels = items.map((child, at) => isPanel(child)
44
+ ? cloneElement(child, { key: child.key ?? `panel-${panelIndex[at]}`, index: panelIndex[at] })
45
+ : isValidElement(child) ? cloneElement(child, { key: child.key ?? `child-${at}` }) : child);
45
46
  return (_jsx(AccordionContext.Provider, { value: context, children: _jsx("div", { className: cn('kpt-accordion-host', className), ...rest, children: panels }) }));
46
47
  }
47
48
  /** Panel akordeonu. Umieszczaj w `<KptAccordion>` albo używaj samodzielnie. */
@@ -69,7 +70,7 @@ export function KptAccordionPanel({ title, disabled = false, expanded, defaultEx
69
70
  };
70
71
  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
72
  // 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 }) }) })] }) }));
73
+ role: "region", id: bodyId, "aria-labelledby": headerId, inert: !isOpen, children: _jsx("div", { className: "kpt-accordion-panel__collapse-inner", children: _jsx("div", { className: "kpt-accordion-panel__body", children: children }) }) })] }) }));
73
74
  }
74
75
  KptAccordionPanel.kptAccordionSlot = 'panel';
75
76
  KptAccordion.Panel = KptAccordionPanel;
@@ -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";
@@ -45,3 +45,4 @@ The role follows the weight of the message: `info` and `success` are `role="stat
45
45
  politely, without interrupting), `warning` and `danger` are `role="alert"` (read at once). The icon
46
46
  is decorative; the × button is labelled from `common.close`. After closing, focus falls back to the
47
47
  page — move it yourself in the close handler when the alert was reached with the keyboard.
48
+ A live region announces **changes**: an alert rendered together with the page (or revealed by toggling `hidden`) is not announced reliably. To have it read, add it after the action (`@if` / conditional render) — the inserted element is announced.
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -58,9 +59,15 @@ export function KptAppShell({ toolbar, sidenav, children, sidenavOpen, defaultSi
58
59
  useEffect(() => {
59
60
  if (!isOverlay || !isSidenavOpen)
60
61
  return;
62
+ // Escape zamyka nakładkę nawigacji — ale nie wtedy, gdy zamyka nim coś otwartego nad shellem
63
+ // (okno, menu, popover): te wołają `preventDefault()` albo trzymają fokus w sobie.
61
64
  const onKeyDown = (event) => {
62
- if (event.key === 'Escape')
63
- closeSidenav();
65
+ if (event.key !== 'Escape' || event.defaultPrevented)
66
+ return;
67
+ const target = event.target instanceof Element ? event.target : null;
68
+ if (target?.closest('dialog[open], [popover], [role="menu"], [role="listbox"], [role="dialog"]'))
69
+ return;
70
+ closeSidenav();
64
71
  };
65
72
  document.addEventListener('keydown', onKeyDown);
66
73
  return () => document.removeEventListener('keydown', onKeyDown);
@@ -79,13 +86,13 @@ export function KptAppShell({ toolbar, sidenav, children, sidenavOpen, defaultSi
79
86
  // nie miałby dokąd pójść. Po otwarciu przenosimy go do panelu, po zamknięciu oddajemy tam,
80
87
  // skąd przyszedł — chyba że użytkownik zdążył go przenieść gdzie indziej. Stan `null` przed
81
88
  // pierwszym renderem: nakładka otwarta już przy starcie nie zabiera fokusu.
82
- const asideRef = useRef(null);
89
+ const navRef = useRef(null);
83
90
  const overlayWasOpen = useRef(null);
84
91
  const returnFocusTo = useRef(null);
85
92
  useEffect(() => {
86
93
  const was = overlayWasOpen.current;
87
94
  overlayWasOpen.current = contentInert;
88
- const panel = asideRef.current;
95
+ const panel = navRef.current;
89
96
  if (was === null || was === contentInert || !panel)
90
97
  return;
91
98
  const active = document.activeElement;
@@ -99,7 +106,7 @@ export function KptAppShell({ toolbar, sidenav, children, sidenavOpen, defaultSi
99
106
  returnFocusTo.current.focus();
100
107
  returnFocusTo.current = null;
101
108
  }, [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 })] })] }) }));
109
+ 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("nav", { ref: navRef, className: "kpt-app-shell__sidenav", 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 })] })] }) }));
103
110
  }
104
111
  /** Dostęp do stanu shella. Poza `KptAppShell` rzuca — to zawsze błąd użycia. */
105
112
  export function useKptAppShell() {
@@ -8,7 +8,7 @@ Import: `import { KptAppShell, useKptShellNavToggle } from '@konce-pt/react';`
8
8
  data-sidenav-mode="auto" data-sidenav-breakpoint="lg">
9
9
  …toolbar…
10
10
  <div class="kpt-app-shell__body">
11
- <aside class="kpt-app-shell__sidenav" role="navigation" tabindex="-1" id="…">…sidenav…</aside>
11
+ <nav class="kpt-app-shell__sidenav" tabindex="-1" id="…">…sidenav…</nav>
12
12
  <div class="kpt-app-shell__scrim" aria-hidden="true"></div>
13
13
  <main class="kpt-app-shell__content">…children…</main>
14
14
  </div>
@@ -54,9 +54,10 @@ decides WHICH state the button flips. The scrim is always in the DOM; CSS contro
54
54
  `--kpt-color-overlay-scrim`, `--kpt-motion-duration-base`, `--kpt-motion-easing-standard`.
55
55
 
56
56
  ## Accessibility
57
- - The panel is an `<aside role="navigation">` with a label and an `id` for `aria-controls`.
57
+ - The panel is a `<nav>` 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
- - 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>`
59
+ - While the overlay is open the content gets `inert`, and Escape closes the panel — unless something
60
+ opened above the shell (a dialog, menu or popover) already handled it.
61
+ - Opening the overlay moves focus into the panel (its first focusable element, or the `<nav>`
61
62
  itself); closing it returns focus to where it came from — usually the menu button — unless the user
62
63
  has already moved it elsewhere. An overlay that is open when the page loads does not take focus.
package/dist/auth/auth.js CHANGED
@@ -1,6 +1,8 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Children, Fragment, isValidElement, useId, useRef, useState, } from 'react';
3
+ import { Fragment, isValidElement, useId, useRef, useState, } from 'react';
3
4
  import { cn } from "../internal/cn.js";
5
+ import { slotChildren } from "../internal/slot-children.js";
4
6
  import { useControllableState } from "../internal/use-controllable-state.js";
5
7
  import { useKptMessages } from "../i18n/i18n-context.js";
6
8
  import { KptAlert } from "../alert/alert.js";
@@ -59,13 +61,13 @@ export function KptAuth({ mode, defaultMode = 'signIn', onModeChange, layout = '
59
61
  const headingRef = useRef(null);
60
62
  // ── Sloty ──
61
63
  const slots = {};
62
- Children.forEach(children, (child) => {
64
+ for (const child of slotChildren(children)) {
63
65
  if (!isValidElement(child))
64
- return;
66
+ continue;
65
67
  const name = child.type?.kptAuthSlot;
66
68
  if (name)
67
69
  slots[name] = child.props.children;
68
- });
70
+ }
69
71
  // ── Modele formularzy ──
70
72
  const [signInModel, setSignInModel] = useState({
71
73
  email: '',
@@ -125,7 +127,8 @@ export function KptAuth({ mode, defaultMode = 'signIn', onModeChange, layout = '
125
127
  const anyError = (kinds) => Object.values(kinds).some((kind) => kind !== null);
126
128
  const submitAll = (form, fields) => {
127
129
  // Submit dotyka całego formularza — inaczej pola, których użytkownik nie ruszył, nie
128
- // pokazałyby błędu i przycisk wyglądałby na zepsuty.
130
+ // pokazałyby błędu. Dlatego przycisk wysyłki nie jest wyłączany przy błędach: pokazuje,
131
+ // czego brakuje, zamiast stać martwy (WCAG 3.3.1).
129
132
  setTouched((prev) => {
130
133
  const next = new Set(prev);
131
134
  for (const field of fields)
@@ -152,22 +155,28 @@ export function KptAuth({ mode, defaultMode = 'signIn', onModeChange, layout = '
152
155
  const promoBody = promoText === null ? t(`auth.promo.${promoMode}Text`) : promoText;
153
156
  return (_jsx("div", { className: cn('kpt-auth-host', className), "data-layout": layout, "data-mode": activeMode, style: { ['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`, ...style }, ...rest, children: _jsx("div", { className: "kpt-auth", children: _jsxs("div", { className: "kpt-auth__card", children: [_jsx("div", { className: "kpt-auth__form-side", children: _jsxs("div", { className: "kpt-auth__inner", "data-mode": activeMode, children: [_jsx("div", { className: "kpt-auth__brand", children: slots.brand }), _jsx("h2", { ref: headingRef, className: "kpt-auth__title", id: ids[activeMode], "data-mode": activeMode, tabIndex: -1, children: t(`auth.${activeMode}Title`) }), _jsx("p", { className: "kpt-auth__subtitle", children: t(`auth.${activeMode}Subtitle`) }), error ? _jsx(KptAlert, { variant: "danger", children: error }) : null, notice ? _jsx(KptAlert, { variant: "success", children: notice }) : null, activeMode === 'signIn' ? (_jsxs("form", { className: "kpt-auth__form", "aria-labelledby": ids.signIn, onSubmit: (event) => {
154
157
  event.preventDefault();
158
+ if (busy)
159
+ return;
155
160
  submitAll('signIn', ['email', 'password']);
156
161
  if (!anyError(signInErrors))
157
162
  onSignIn?.({ ...signInModel });
158
- }, children: [_jsx(KptFormField, { label: t('auth.email'), error: errorFor('signIn', 'email', signInErrors), required: true, controlId: ids.email, children: _jsx(KptInput, { type: "email", autoComplete: "email", id: ids.email, placeholder: t('auth.emailPlaceholder'), value: signInModel.email, onValueChange: (email) => setSignInModel({ ...signInModel, email }), onTouch: () => touch('signIn.email') }) }), _jsx(KptFormField, { label: t('auth.password'), error: errorFor('signIn', 'password', signInErrors), required: true, controlId: ids.password, children: _jsx(KptPassword, { autoComplete: "current-password", id: ids.password, value: signInModel.password, onValueChange: (password) => setSignInModel({ ...signInModel, password }), onTouch: () => touch('signIn.password') }) }), slots.fields, _jsxs("div", { className: "kpt-auth__row", children: [showRemember ? (_jsx(KptCheckbox, { label: t('auth.remember'), checked: signInModel.remember, onCheckedChange: (remember) => setSignInModel({ ...signInModel, remember }) })) : (_jsx("span", {})), _jsx("button", { type: "button", className: "kpt-auth__link", onClick: () => changeMode('forgot'), children: t('auth.forgotLink') })] }), _jsx(KptButton, { type: "submit", fullWidth: true, loading: busy, disabled: anyError(signInErrors), children: t('auth.signInSubmit') })] })) : null, activeMode === 'signUp' ? (_jsxs("form", { className: "kpt-auth__form", "aria-labelledby": ids.signUp, onSubmit: (event) => {
163
+ }, children: [_jsx(KptFormField, { label: t('auth.email'), error: errorFor('signIn', 'email', signInErrors), required: true, controlId: ids.email, children: _jsx(KptInput, { type: "email", autoComplete: "email", id: ids.email, placeholder: t('auth.emailPlaceholder'), value: signInModel.email, onValueChange: (email) => setSignInModel({ ...signInModel, email }), onTouch: () => touch('signIn.email') }) }), _jsx(KptFormField, { label: t('auth.password'), error: errorFor('signIn', 'password', signInErrors), required: true, controlId: ids.password, children: _jsx(KptPassword, { autoComplete: "current-password", id: ids.password, value: signInModel.password, onValueChange: (password) => setSignInModel({ ...signInModel, password }), onTouch: () => touch('signIn.password') }) }), slots.fields, _jsxs("div", { className: "kpt-auth__row", children: [showRemember ? (_jsx(KptCheckbox, { label: t('auth.remember'), checked: signInModel.remember, onCheckedChange: (remember) => setSignInModel({ ...signInModel, remember }) })) : (_jsx("span", {})), _jsx("button", { type: "button", className: "kpt-auth__link", onClick: () => changeMode('forgot'), children: t('auth.forgotLink') })] }), _jsx(KptButton, { type: "submit", fullWidth: true, loading: busy, children: t('auth.signInSubmit') })] })) : null, activeMode === 'signUp' ? (_jsxs("form", { className: "kpt-auth__form", "aria-labelledby": ids.signUp, onSubmit: (event) => {
159
164
  event.preventDefault();
165
+ if (busy)
166
+ return;
160
167
  submitAll('signUp', ['name', 'email', 'password', 'confirm', 'terms']);
161
168
  if (!anyError(signUpErrors)) {
162
169
  const { name, email, password, terms } = signUpModel;
163
170
  onSignUp?.({ name, email, password, terms });
164
171
  }
165
- }, children: [showName ? (_jsx(KptFormField, { label: t('auth.name'), error: errorFor('signUp', 'name', signUpErrors), required: true, controlId: ids.name, children: _jsx(KptInput, { autoComplete: "name", id: ids.name, value: signUpModel.name, onValueChange: (name) => setSignUpModel({ ...signUpModel, name }), onTouch: () => touch('signUp.name') }) })) : null, _jsx(KptFormField, { label: t('auth.email'), error: errorFor('signUp', 'email', signUpErrors), required: true, controlId: ids.regEmail, children: _jsx(KptInput, { type: "email", autoComplete: "email", id: ids.regEmail, placeholder: t('auth.emailPlaceholder'), value: signUpModel.email, onValueChange: (email) => setSignUpModel({ ...signUpModel, email }), onTouch: () => touch('signUp.email') }) }), _jsx(KptFormField, { label: t('auth.password'), error: errorFor('signUp', 'password', signUpErrors), hint: t('auth.passwordHint', { min: KPT_AUTH_MIN_PASSWORD }), required: true, controlId: ids.regPassword, children: _jsx(KptPassword, { autoComplete: "new-password", feedback: true, id: ids.regPassword, value: signUpModel.password, onValueChange: (password) => setSignUpModel({ ...signUpModel, password }), onTouch: () => touch('signUp.password') }) }), _jsx(KptFormField, { label: t('auth.confirmPassword'), error: errorFor('signUp', 'confirm', signUpErrors), required: true, controlId: ids.regConfirm, children: _jsx(KptPassword, { autoComplete: "new-password", id: ids.regConfirm, value: signUpModel.confirm, onValueChange: (confirm) => setSignUpModel({ ...signUpModel, confirm }), onTouch: () => touch('signUp.confirm') }) }), slots.fields, requireTerms ? (_jsx(KptFormField, { error: errorFor('signUp', 'terms', signUpErrors), children: _jsx(KptCheckbox, { label: t('auth.terms'), checked: signUpModel.terms, onCheckedChange: (terms) => setSignUpModel({ ...signUpModel, terms }), onTouch: () => touch('signUp.terms') }) })) : null, _jsx(KptButton, { type: "submit", fullWidth: true, loading: busy, disabled: anyError(signUpErrors), children: t('auth.signUpSubmit') })] })) : null, activeMode === 'forgot' ? (_jsxs("form", { className: "kpt-auth__form", "aria-labelledby": ids.forgot, onSubmit: (event) => {
172
+ }, children: [showName ? (_jsx(KptFormField, { label: t('auth.name'), error: errorFor('signUp', 'name', signUpErrors), required: true, controlId: ids.name, children: _jsx(KptInput, { autoComplete: "name", id: ids.name, value: signUpModel.name, onValueChange: (name) => setSignUpModel({ ...signUpModel, name }), onTouch: () => touch('signUp.name') }) })) : null, _jsx(KptFormField, { label: t('auth.email'), error: errorFor('signUp', 'email', signUpErrors), required: true, controlId: ids.regEmail, children: _jsx(KptInput, { type: "email", autoComplete: "email", id: ids.regEmail, placeholder: t('auth.emailPlaceholder'), value: signUpModel.email, onValueChange: (email) => setSignUpModel({ ...signUpModel, email }), onTouch: () => touch('signUp.email') }) }), _jsx(KptFormField, { label: t('auth.password'), error: errorFor('signUp', 'password', signUpErrors), hint: t('auth.passwordHint', { min: KPT_AUTH_MIN_PASSWORD }), required: true, controlId: ids.regPassword, children: _jsx(KptPassword, { autoComplete: "new-password", feedback: true, id: ids.regPassword, value: signUpModel.password, onValueChange: (password) => setSignUpModel({ ...signUpModel, password }), onTouch: () => touch('signUp.password') }) }), _jsx(KptFormField, { label: t('auth.confirmPassword'), error: errorFor('signUp', 'confirm', signUpErrors), required: true, controlId: ids.regConfirm, children: _jsx(KptPassword, { autoComplete: "new-password", id: ids.regConfirm, value: signUpModel.confirm, onValueChange: (confirm) => setSignUpModel({ ...signUpModel, confirm }), onTouch: () => touch('signUp.confirm') }) }), slots.fields, requireTerms ? (_jsx(KptFormField, { error: errorFor('signUp', 'terms', signUpErrors), children: _jsx(KptCheckbox, { label: t('auth.terms'), checked: signUpModel.terms, onCheckedChange: (terms) => setSignUpModel({ ...signUpModel, terms }), onTouch: () => touch('signUp.terms') }) })) : null, _jsx(KptButton, { type: "submit", fullWidth: true, loading: busy, children: t('auth.signUpSubmit') })] })) : null, activeMode === 'forgot' ? (_jsxs("form", { className: "kpt-auth__form", "aria-labelledby": ids.forgot, onSubmit: (event) => {
166
173
  event.preventDefault();
174
+ if (busy)
175
+ return;
167
176
  submitAll('forgot', ['email']);
168
177
  if (!anyError(resetErrors))
169
178
  onResetPassword?.({ ...resetModel });
170
- }, children: [_jsx(KptFormField, { label: t('auth.email'), error: errorFor('forgot', 'email', resetErrors), required: true, controlId: ids.resetEmail, children: _jsx(KptInput, { type: "email", autoComplete: "email", id: ids.resetEmail, placeholder: t('auth.emailPlaceholder'), value: resetModel.email, onValueChange: (email) => setResetModel({ email }), onTouch: () => touch('forgot.email') }) }), _jsx(KptButton, { type: "submit", fullWidth: true, loading: busy, disabled: anyError(resetErrors), children: t('auth.resetSubmit') }), _jsx("button", { type: "button", className: "kpt-auth__link kpt-auth__link--center", onClick: () => changeMode('signIn'), children: t('auth.backToSignIn') })] })) : null, activeMode !== 'forgot' ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-auth__sso", children: slots.sso }), providers.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-auth__separator", role: "separator", "aria-label": t('auth.orContinueWith'), children: _jsx("span", { className: "kpt-auth__separator-text", children: t('auth.orContinueWith') }) }), _jsx("div", { className: "kpt-auth__providers", "data-layout": providerLayout, children: providers.map((provider) => {
179
+ }, children: [_jsx(KptFormField, { label: t('auth.email'), error: errorFor('forgot', 'email', resetErrors), required: true, controlId: ids.resetEmail, children: _jsx(KptInput, { type: "email", autoComplete: "email", id: ids.resetEmail, placeholder: t('auth.emailPlaceholder'), value: resetModel.email, onValueChange: (email) => setResetModel({ email }), onTouch: () => touch('forgot.email') }) }), _jsx(KptButton, { type: "submit", fullWidth: true, loading: busy, children: t('auth.resetSubmit') }), _jsx("button", { type: "button", className: "kpt-auth__link kpt-auth__link--center", onClick: () => changeMode('signIn'), children: t('auth.backToSignIn') })] })) : null, activeMode !== 'forgot' ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-auth__sso", children: slots.sso }), providers.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "kpt-auth__separator", role: "separator", "aria-label": t('auth.orContinueWith'), children: _jsx("span", { className: "kpt-auth__separator-text", children: t('auth.orContinueWith') }) }), _jsx("div", { className: "kpt-auth__providers", "data-layout": providerLayout, children: providers.map((provider) => {
171
180
  const custom = renderProvider?.(provider, () => selectProvider(provider));
172
181
  // Fragment, nie `<span>`: lista dostawców to grid, więc dodatkowy
173
182
  // element stałby się jej dzieckiem i własny przycisk przestałby
@@ -31,8 +31,9 @@ Three independent forms, one per mode, validated on every render: e-mail require
31
31
  password required (sign up: at least 8 characters), the confirmation matching, consent required
32
32
  when `requireTerms`. The validator returns a `kind`, and the message is composed at render time,
33
33
  so **changing the language repaints the errors**. The "do not shout at an untouched field" gate
34
- lives in the presentation, so the submit button's disabled state tells the truth from the start;
35
- submitting marks the whole form as touched.
34
+ lives in the presentation. The submit button is **not** disabled while the form is invalid:
35
+ submitting marks the whole form as touched and shows what is missing (WCAG 3.3.1), and `onSignIn` /
36
+ `onSignUp` / `onResetPassword` fire only for a valid form.
36
37
 
37
38
  ## The heading takes focus after a mode change
38
39
  Without it a screen reader never announces the change, and focus stays on a button that has just
@@ -20,4 +20,4 @@ export interface KptAutocompleteProps extends Omit<ComponentPropsWithRef<'input'
20
20
  * @example
21
21
  * <KptAutocomplete options={cities} value={city} onValueChange={setCity} placeholder="Miasto…" />
22
22
  */
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;
23
+ export declare function KptAutocomplete({ options, value, defaultValue, onValueChange, invalid, touched, onTouch, disabled, readOnly, className, onBlur, onFocus, onKeyDown, ref, id, 'aria-describedby': ariaDescribedBy, locale, dictionary, ...rest }: Readonly<KptAutocompleteProps>): 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, useMemo, useRef, useState, } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -18,10 +19,13 @@ import { useKptMessages } from "../i18n/i18n-context.js";
18
19
  * @example
19
20
  * <KptAutocomplete options={cities} value={city} onValueChange={setCity} placeholder="Miasto…" />
20
21
  */
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
+ export function KptAutocomplete({ options = [], value, defaultValue = '', onValueChange, invalid = false, touched, onTouch, disabled = false, readOnly = false, className, onBlur, onFocus, onKeyDown, ref, id, 'aria-describedby': ariaDescribedBy, locale, dictionary, ...rest }) {
22
23
  const { t } = useKptMessages({ locale, dictionary });
23
24
  const field = useKptFieldA11y(id, 'kpt-autocomplete');
24
25
  const inputRef = useRef(null);
26
+ /** Kotwica nakładki w stanie (callback ref), nie `inputRef.current` czytane w renderze. */
27
+ const [anchor, setAnchor] = useState(null);
28
+ const setRefs = useMemo(() => composeRefs(inputRef, ref, setAnchor), [ref]);
25
29
  const [current, setCurrent] = useControllableState({
26
30
  value,
27
31
  defaultValue,
@@ -55,6 +59,9 @@ export function KptAutocomplete({ options = [], value, defaultValue = '', onValu
55
59
  }, [setCurrent]);
56
60
  const handleKeyDown = (event) => {
57
61
  onKeyDown?.(event);
62
+ // Tylko do odczytu: podpowiedzi się nie otwierają, a Escape nie czyści wartości.
63
+ if (readOnly)
64
+ return;
58
65
  if (event.key === 'ArrowDown') {
59
66
  event.preventDefault();
60
67
  setOpen(true);
@@ -86,20 +93,21 @@ export function KptAutocomplete({ options = [], value, defaultValue = '', onValu
86
93
  }
87
94
  }
88
95
  };
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) => {
96
+ return (_jsxs("span", { className: "kpt-autocomplete-host", children: [_jsx("input", { ref: setRefs, 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, "aria-readonly": readOnly || undefined, disabled: disabled, readOnly: readOnly, value: current, onChange: (event) => {
90
97
  setCurrent(event.target.value);
91
98
  setOpen(true);
92
99
  setTyped(true);
93
100
  setActiveIndex(-1);
94
101
  }, onFocus: (event) => {
95
102
  onFocus?.(event);
96
- setOpen(true);
103
+ if (!readOnly)
104
+ setOpen(true);
97
105
  }, onBlur: (event) => {
98
106
  onBlur?.(event);
99
107
  setOpen(false);
100
108
  setTyped(false);
101
109
  onTouch?.();
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,
110
+ }, onKeyDown: handleKeyDown, ...rest }), _jsx(KptOverlay, { open: panelOpen, anchor: anchor, 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,
103
111
  // `mousedown` z zablokowanym domyślnym zachowaniem: klik w opcję nie może
104
112
  // zabrać fokusu polu, bo `blur` zamknąłby panel przed wyborem.
105
113
  onMouseDown: (event) => {
@@ -54,3 +54,6 @@ added to `aria-describedby` (merged with your own). Without a wrapper, name it w
54
54
  ## Accessibility
55
55
  The input is `role="combobox"` with `aria-expanded`; the panel is `role="listbox"` with
56
56
  `role="option"` rows carrying `aria-selected`.
57
+
58
+ ## Read-only and required
59
+ `readOnly` is the native read-only field with `aria-readonly`; the suggestions do not open and Escape does not clear the value.
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import { useState } 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 } from "react/jsx-runtime";
2
3
  import { cn } from "../internal/cn.js";
3
4
  import { useKptMessages } from "../i18n/i18n-context.js";
@@ -57,23 +57,4 @@ export interface KptBackdropProps extends ComponentPropsWithRef<'div'> {
57
57
  /** Propsy warstwy z treścią (`.kpt-backdrop__content`). */
58
58
  contentProps?: ComponentPropsWithRef<'div'>;
59
59
  }
60
- /**
61
- * Dekoracyjna warstwa tła dla sekcji: komponent obejmuje treść i maluje pod nią animowany canvas.
62
- * Sięga się po niego tam, gdzie płaska powierzchnia wygląda na niedokończoną, a wideo albo duży
63
- * obraz kosztowałyby transfer i nie poszłyby za motywem.
64
- *
65
- * Komponent jest opakowaniem: canvas leży pod spodem, a dzieci idą warstwę wyżej i zachowują się
66
- * zwyczajnie. Rysowanie stoi, gdy nie ma czego oglądać — sekcja poza ekranem, karta w tle,
67
- * `paused`, `speed` równe zeru albo ograniczony ruch w systemie; wtedy zostaje jedna statyczna
68
- * klatka zamiast pustki, bo efekt jest częścią kompozycji, nie ozdobą.
69
- *
70
- * Rysowanie i cała matematyka siedzą w `@konce-pt/backdrop`, wspólnym z portem Angulara —
71
- * ten komponent to sam cykl życia.
72
- *
73
- * @example
74
- * <KptBackdrop effect="wave" rotation={18} colors={['#5b8af2', '#a86df2', '#f25b9e']}>
75
- * <h1>Build your app from the first prompt</h1>
76
- * <KptButton>Get started</KptButton>
77
- * </KptBackdrop>
78
- */
79
60
  export declare function KptBackdrop({ effect, background, speed, seed, paused, adaptive, color, anchor, cell, gap, density, falloff, colors, rotation, band, amplitude, contentProps, className, style, children, ...rest }: Readonly<KptBackdropProps>): import("react").JSX.Element;
@@ -1,5 +1,6 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
4
  import { KPT_BACKDROP_MAX_DPR, KPT_BACKDROP_MAX_STEP, KPT_SIGNAL_GRID_CELL, KPT_SIGNAL_GRID_DENSITY, KPT_SIGNAL_GRID_FALLOFF, KPT_SIGNAL_GRID_GAP, KPT_WAVE_AMPLITUDE, KPT_WAVE_BAND, KPT_WAVE_ROTATION, adaptBackdropValue, createBackdropRenderer, observeBackdropTheme, readBackdropInk, readBackdropLead, readBackdropSurface, resolveBackdropColors, } from '@konce-pt/backdrop';
4
5
  import { cn } from "../internal/cn.js";
5
6
  import { isDevBuild } from "../internal/dev.js";
@@ -23,6 +24,18 @@ const EMPTY = [];
23
24
  * <KptButton>Get started</KptButton>
24
25
  * </KptBackdrop>
25
26
  */
27
+ const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
28
+ /** `prefers-reduced-motion` jako zewnętrzny magazyn — bez `setState` w efekcie i bez mignięcia ruchu. */
29
+ function subscribeReducedMotion(onChange) {
30
+ if (typeof matchMedia === 'undefined')
31
+ return () => undefined;
32
+ const query = matchMedia(REDUCED_MOTION);
33
+ query.addEventListener('change', onChange);
34
+ return () => query.removeEventListener('change', onChange);
35
+ }
36
+ function readReducedMotion() {
37
+ return typeof matchMedia !== 'undefined' && matchMedia(REDUCED_MOTION).matches;
38
+ }
26
39
  export function KptBackdrop({ effect = 'signal-grid', background = '', speed = 1, seed = 0, paused = false, adaptive = false, color = '', anchor = 'right', cell = KPT_SIGNAL_GRID_CELL, gap = KPT_SIGNAL_GRID_GAP, density = KPT_SIGNAL_GRID_DENSITY, falloff = KPT_SIGNAL_GRID_FALLOFF, colors = EMPTY, rotation = KPT_WAVE_ROTATION, band = KPT_WAVE_BAND, amplitude = KPT_WAVE_AMPLITUDE, contentProps, className, style, children, ...rest }) {
27
40
  const host = useRef(null);
28
41
  const canvas = useRef(null);
@@ -42,10 +55,8 @@ export function KptBackdrop({ effect = 'signal-grid', background = '', speed = 1
42
55
  * i wtedy treść zostaje przy `--kpt-color-on-surface` ze stylu.
43
56
  */
44
57
  const [ink, setInk] = useState({ tone: '', color: '' });
45
- const rate = useRef(speed);
46
- rate.current = speed;
47
58
  const [onScreen, setOnScreen] = useState(true);
48
- const [reduced, setReduced] = useState(false);
59
+ const reduced = useSyncExternalStore(subscribeReducedMotion, readReducedMotion, () => false);
49
60
  /** Licznik zmian motywu; podbicie każe przeliczyć kolory z tokenów na nowo. */
50
61
  const [theme, setTheme] = useState(0);
51
62
  const paint = useCallback(() => {
@@ -146,13 +157,6 @@ export function KptBackdrop({ effect = 'signal-grid', background = '', speed = 1
146
157
  document.removeEventListener('visibilitychange', onTabSwitch);
147
158
  };
148
159
  }, []);
149
- useEffect(() => {
150
- const query = matchMedia('(prefers-reduced-motion: reduce)');
151
- setReduced(query.matches);
152
- const onChange = (event) => setReduced(event.matches);
153
- query.addEventListener('change', onChange);
154
- return () => query.removeEventListener('change', onChange);
155
- }, []);
156
160
  // Zmiana motywu przestawia tokeny, a kolory renderera są już wyliczone — trzeba je odczytać
157
161
  // na nowo. Nasłuch trafia też w zmiany niezwiązane z barwą, więc licznik podbija się dopiero
158
162
  // wtedy, gdy odczyt naprawdę dał inny kolor.
@@ -180,8 +184,9 @@ export function KptBackdrop({ effect = 'signal-grid', background = '', speed = 1
180
184
  const tick = (now) => {
181
185
  const step = last === 0 ? 0 : Math.min(now - last, KPT_BACKDROP_MAX_STEP);
182
186
  last = now;
183
- // Prędkość wchodzi w zegar, nie w fazę — suwak przyspiesza obraz zamiast go przeskakiwać.
184
- clock.current += (step / 1000) * rate.current;
187
+ // Prędkość wchodzi w zegar, nie w fazę — suwak przyspiesza obraz zamiast go przeskakiwać. Pętla
188
+ // restartuje się przy zmianie `speed` (zależność efektu), a zegar żyje w refie, więc nic nie skacze.
189
+ clock.current += (step / 1000) * speed;
185
190
  paint();
186
191
  frame = requestAnimationFrame(tick);
187
192
  };
@@ -4,6 +4,11 @@ export interface KptBottomSheetProps extends ComponentPropsWithRef<'div'>, KptI1
4
4
  open: boolean;
5
5
  onOpenChange: (open: boolean) => void;
6
6
  header?: string;
7
+ /**
8
+ * Nazwa panelu bez `header` — okno dialogowe bez nazwy czytnik ogłasza jako samo „okno
9
+ * dialogowe” (WCAG 4.1.2). Z nagłówkiem nazwą jest nagłówek (`aria-labelledby`).
10
+ */
11
+ ariaLabel?: string;
7
12
  closeOnBackdrop?: boolean;
8
13
  children?: ReactNode;
9
14
  }
@@ -18,4 +23,4 @@ export interface KptBottomSheetProps extends ComponentPropsWithRef<'div'>, KptI1
18
23
  * @example
19
24
  * <KptBottomSheet open={open} onOpenChange={setOpen} header="Udostępnij">…</KptBottomSheet>
20
25
  */
21
- export declare function KptBottomSheet({ open, onOpenChange, header, closeOnBackdrop, locale, dictionary, className, children, ...rest }: Readonly<KptBottomSheetProps>): import("react").JSX.Element;
26
+ export declare function KptBottomSheet({ open, onOpenChange, header, ariaLabel, closeOnBackdrop, locale, dictionary, className, children, ...rest }: Readonly<KptBottomSheetProps>): 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 } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -15,13 +16,13 @@ import { useKptMessages } from "../i18n/i18n-context.js";
15
16
  * @example
16
17
  * <KptBottomSheet open={open} onOpenChange={setOpen} header="Udostępnij">…</KptBottomSheet>
17
18
  */
18
- export function KptBottomSheet({ open, onOpenChange, header = '', closeOnBackdrop = true, locale, dictionary, className, children, ...rest }) {
19
+ export function KptBottomSheet({ open, onOpenChange, header = '', ariaLabel = '', closeOnBackdrop = true, locale, dictionary, className, children, ...rest }) {
19
20
  const { t } = useKptMessages({ locale, dictionary });
20
21
  const close = () => onOpenChange(false);
21
22
  const panelRef = useRef(null);
22
23
  const titleId = useKptId(undefined, 'kpt-bottom-sheet-title');
23
24
  useModalFocus(open, panelRef);
24
- return (_jsxs("div", { className: cn('kpt-bottom-sheet-host', className), ...rest, children: [_jsx("div", { className: cn('kpt-bottom-sheet__backdrop', open && 'is-open'), onClick: () => closeOnBackdrop && close() }), _jsxs("div", { ref: panelRef, className: cn('kpt-bottom-sheet__panel', open && 'is-open'), role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, tabIndex: -1, onKeyDown: (event) => {
25
+ return (_jsxs("div", { className: cn('kpt-bottom-sheet-host', className), ...rest, children: [_jsx("div", { className: cn('kpt-bottom-sheet__backdrop', open && 'is-open'), onClick: () => closeOnBackdrop && close() }), _jsxs("div", { ref: panelRef, className: cn('kpt-bottom-sheet__panel', open && 'is-open'), role: "dialog", "aria-modal": "true", "aria-labelledby": header ? titleId : undefined, "aria-label": header ? undefined : ariaLabel || undefined, tabIndex: -1, onKeyDown: (event) => {
25
26
  if (event.key === 'Escape')
26
27
  close();
27
28
  }, children: [_jsx("button", { type: "button", className: "kpt-bottom-sheet__handle", onClick: close, "aria-label": t('common.close') }), header ? (_jsx("div", { className: "kpt-bottom-sheet__header", id: titleId, children: header })) : null, _jsx("div", { className: "kpt-bottom-sheet__body", children: children })] })] }));
@@ -47,3 +47,6 @@ The panel `--kpt-color-surface-raised`, the top radius `--kpt-radius-lg`, `--kpt
47
47
  ## Accessibility
48
48
  `role="dialog"`, `aria-modal` and `aria-labelledby` pointing at the header; the handle is a real button
49
49
  labelled from `common.close`. Focus handling — see "Focus and keyboard" above.
50
+
51
+ ## Name and scrolling
52
+ Without `header` give the sheet `ariaLabel` — a `role="dialog"` without a name is announced as a bare "dialog". An open sheet locks page scrolling underneath.
@@ -1,4 +1,5 @@
1
1
  import type { ComponentPropsWithRef } from 'react';
2
+ import { type KptNavigateEvent } from '../shared/types.ts';
2
3
  import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
3
4
  export interface KptBreadcrumbItem {
4
5
  label: string;
@@ -6,6 +7,11 @@ export interface KptBreadcrumbItem {
6
7
  }
7
8
  export interface KptBreadcrumbProps extends Omit<ComponentPropsWithRef<'div'>, 'children'>, KptI18nOverrides {
8
9
  items?: readonly KptBreadcrumbItem[];
10
+ /**
11
+ * Zwykły klik w odnośnik. Aplikacja z routerem woła `event.preventDefault()` i nawiguje sama —
12
+ * bez tego `<a href>` przeładowuje stronę.
13
+ */
14
+ onNavigate?: (event: KptNavigateEvent<KptBreadcrumbItem>) => void;
9
15
  }
10
16
  /**
11
17
  * Ścieżka nawigacji (breadcrumbs).
@@ -16,4 +22,4 @@ export interface KptBreadcrumbProps extends Omit<ComponentPropsWithRef<'div'>, '
16
22
  * @example
17
23
  * <KptBreadcrumb items={[{ label: 'Start', url: '/' }, { label: 'Ustawienia' }]} />
18
24
  */
19
- export declare function KptBreadcrumb({ items, locale, dictionary, className, ...rest }: Readonly<KptBreadcrumbProps>): import("react").JSX.Element;
25
+ export declare function KptBreadcrumb({ items, onNavigate, locale, dictionary, className, ...rest }: Readonly<KptBreadcrumbProps>): import("react").JSX.Element;
@@ -1,4 +1,6 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { kptIsPlainClick } from "../shared/types.js";
2
4
  import { cn } from "../internal/cn.js";
3
5
  import { useKptMessages } from "../i18n/i18n-context.js";
4
6
  import { KptIcon } from "../icon/icon.js";
@@ -11,10 +13,16 @@ import { KptIcon } from "../icon/icon.js";
11
13
  * @example
12
14
  * <KptBreadcrumb items={[{ label: 'Start', url: '/' }, { label: 'Ustawienia' }]} />
13
15
  */
14
- export function KptBreadcrumb({ items = [], locale, dictionary, className, ...rest }) {
16
+ export function KptBreadcrumb({ items = [], onNavigate, locale, dictionary, className, ...rest }) {
15
17
  const { t } = useKptMessages({ locale, dictionary });
18
+ /** Zwykły klik w odnośnik → `onNavigate` (Ctrl/⌘/Shift, środkowy przycisk i `_blank` zostają natywne). */
19
+ const linkClick = (event, item) => {
20
+ if (item.url && onNavigate && kptIsPlainClick(event, event.currentTarget.target)) {
21
+ onNavigate({ item, url: item.url, event: event.nativeEvent });
22
+ }
23
+ };
16
24
  return (_jsx("div", { className: cn('kpt-breadcrumb-host', className), ...rest, children: _jsx("nav", { className: "kpt-breadcrumb", "aria-label": t('breadcrumb.label'), children: _jsx("ol", { className: "kpt-breadcrumb__list", children: items.map((item, index) => {
17
25
  const last = index === items.length - 1;
18
- return (_jsxs("li", { className: "kpt-breadcrumb__item", children: [item.url && !last ? (_jsx("a", { className: "kpt-breadcrumb__link", href: item.url, children: item.label })) : (_jsx("span", { className: "kpt-breadcrumb__current", "aria-current": last ? 'page' : undefined, children: item.label })), last ? null : _jsx(KptIcon, { name: "chevron-right", className: "kpt-breadcrumb__sep" })] }, index));
26
+ return (_jsxs("li", { className: "kpt-breadcrumb__item", children: [item.url && !last ? (_jsx("a", { className: "kpt-breadcrumb__link", href: item.url, onClick: (event) => linkClick(event, item), children: item.label })) : (_jsx("span", { className: "kpt-breadcrumb__current", "aria-current": last ? 'page' : undefined, children: item.label })), last ? null : _jsx(KptIcon, { name: "chevron-right", className: "kpt-breadcrumb__sep" })] }, item.url ?? `${index}-${item.label}`));
19
27
  }) }) }) }));
20
28
  }
@@ -40,3 +40,11 @@ The nav label comes from `breadcrumb.label` in the dictionary.
40
40
  A `<nav>` landmark labelled from `breadcrumb.label`, holding an ordered list (`<ol>`/`<li>`), so a
41
41
  screen reader announces how many levels the trail has. The current page carries
42
42
  `aria-current="page"`; separators are icons hidden from assistive technology.
43
+
44
+ ## Router
45
+ Links are plain `<a href>`, so without a router a click reloads the page. `onNavigate` fires on a plain
46
+ click (left button, no Ctrl/⌘/Shift, no `target="_blank"`) with `{ item, url, event }`: call
47
+ `event.preventDefault()` and navigate with your router. Modified clicks stay native (a new tab, a new
48
+ window). The library depends on no router.
49
+
50
+ <KptBreadcrumb items={items} onNavigate={({ event, url }) => { event.preventDefault(); navigate(url); }} />
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { isValidElement } from 'react';
3
4
  import { cn } from "../internal/cn.js";
@@ -50,7 +51,7 @@ function renderInner(args) {
50
51
  // kliknąć i wejść na niego z klawiatury.
51
52
  href: isDisabled ? undefined : href, target: target,
52
53
  // `target="_blank"` bez `rel` to wektor na `window.opener` — dokładamy domyślne.
53
- rel: rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined), "aria-disabled": isDisabled || undefined, tabIndex: isDisabled ? -1 : undefined, ...buttonProps, children: content }));
54
+ rel: rel ?? (target === '_blank' ? 'noopener noreferrer' : undefined), "aria-disabled": isDisabled || undefined, tabIndex: isDisabled ? -1 : undefined, ...(buttonProps), children: content }));
54
55
  }
55
56
  return (_jsx("button", { type: type, disabled: isDisabled, ...buttonProps, children: content }));
56
57
  }