@konce-pt/react 0.16.0 → 0.17.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.
- package/README.md +23 -19
- package/dist/carousel/carousel.d.ts +10 -2
- package/dist/carousel/carousel.js +21 -5
- package/dist/carousel/llms.txt +7 -1
- package/dist/galleria/galleria.d.ts +6 -0
- package/dist/galleria/galleria.js +70 -4
- package/dist/galleria/llms.txt +29 -2
- package/dist/i18n/i18n-context.js +5 -5
- package/dist/image/image.d.ts +4 -0
- package/dist/image/image.js +55 -4
- package/dist/image/llms.txt +15 -0
- package/dist/input/input.d.ts +8 -2
- package/dist/input/input.js +49 -3
- package/dist/input/llms.txt +16 -0
- package/dist/popover/llms.txt +2 -0
- package/dist/popover/popover.d.ts +6 -1
- package/dist/popover/popover.js +2 -2
- package/dist/public-api.d.ts +3 -1
- package/dist/public-api.js +1 -0
- package/dist/tabs/llms.txt +40 -8
- package/dist/tabs/tabs.d.ts +32 -3
- package/dist/tabs/tabs.js +39 -7
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -77,14 +77,14 @@ import '@konce-pt/grid'; // optional - only if you use the layout c
|
|
|
77
77
|
bundle, which is why both ports render identically. The Angular package injects those styles at
|
|
78
78
|
runtime; React has no other source for them, so this import is not optional.
|
|
79
79
|
|
|
80
|
-
Dark theme: set `<html data-theme="dark"
|
|
80
|
+
Dark theme: set `<html data-theme="dark">`. It is not switched automatically - toggle the attribute yourself, e.g. from `matchMedia('(prefers-color-scheme: dark)')`.
|
|
81
81
|
|
|
82
82
|
**Next.js App Router / Server Components:** every interactive component module starts with
|
|
83
83
|
`'use client'`, so you can import components directly into a server component - no wrapper file.
|
|
84
84
|
|
|
85
85
|
> **No CDK here.** Components with a floating panel (`KptSelect`, `KptPopover`, `KptMenu`,
|
|
86
|
-
> `KptAutocomplete`, `KptDatepicker`...) portal into `document.body` through
|
|
87
|
-
>
|
|
86
|
+
> `KptAutocomplete`, `KptDatepicker`...) portal into `document.body` through `KptOverlay`
|
|
87
|
+
> (also exported for your own floating panels), so `@angular/cdk/overlay-prebuilt.css` is **not** needed - positioning lives in the
|
|
88
88
|
> component, the look in `@konce-pt/styles/components`. Inside an open `KptDialog` those panels
|
|
89
89
|
> portal into the `<dialog>` instead, because `showModal()` moves it to the browser top layer.
|
|
90
90
|
|
|
@@ -120,6 +120,7 @@ export function Login() {
|
|
|
120
120
|
onValueChange={setEmail}
|
|
121
121
|
onTouch={() => setTouched((t) => ({ ...t, email: true }))}
|
|
122
122
|
invalid={Boolean(emailError)}
|
|
123
|
+
touched={touched.email}
|
|
123
124
|
/>
|
|
124
125
|
</KptFormField>
|
|
125
126
|
|
|
@@ -131,6 +132,7 @@ export function Login() {
|
|
|
131
132
|
onValueChange={setPassword}
|
|
132
133
|
onTouch={() => setTouched((t) => ({ ...t, password: true }))}
|
|
133
134
|
invalid={Boolean(passwordError)}
|
|
135
|
+
touched={touched.password}
|
|
134
136
|
/>
|
|
135
137
|
</KptFormField>
|
|
136
138
|
|
|
@@ -141,7 +143,8 @@ export function Login() {
|
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
`KptFormField` generates the control id and publishes it through context, so `label[for]` and
|
|
144
|
-
`input[id]` match with no configuration.
|
|
146
|
+
`input[id]` match with no configuration. `touched` gates the invalid state (`aria-invalid`, red border)
|
|
147
|
+
until the field is blurred; leave it out and `invalid` shows right away. Drop `value`/`onValueChange` and the field becomes
|
|
145
148
|
uncontrolled - the value lives in the DOM, and a native `<form action={...}>` or React Hook Form's
|
|
146
149
|
`register()` picks it up as-is.
|
|
147
150
|
|
|
@@ -166,13 +169,13 @@ Data table with sorting, filtering, pagination and a custom cell:
|
|
|
166
169
|
|
|
167
170
|
| Category | Components |
|
|
168
171
|
| --- | --- |
|
|
169
|
-
| **Forms** | `KptInput`, `KptTextarea`, `KptSelect`, `KptAutocomplete`, `KptCheckbox`, `KptSwitch`, `KptSwitchGroup`, `KptRadioGroup`, `KptSlider`, `KptRating`, `KptDatepicker`, `KptDateRange`, `KptClock`, `KptColorPicker`, `KptFileUpload`, `KptInputNumber`, `KptPassword`, `KptInputOtp`, `KptChipsInput`, `KptInputMask`, `KptListbox`, `KptKnob`, `KptRichText`, `KptFormField` |
|
|
172
|
+
| **Forms** | `KptInput`, `KptTextarea`, `KptSelect`, `KptAutocomplete`, `KptCheckbox`, `KptSwitch`, `KptSwitchGroup`, `KptRadioGroup`, `KptSlider`, `KptRating`, `KptDatepicker`, `KptDateRange`, `KptClock`, `KptColorPicker`, `KptFileUpload`, `KptInputNumber`, `KptPassword`, `KptInputOtp`, `KptChipsInput`, `KptInputMask`, `KptListbox`, `KptKnob`, `KptRichText`, `KptFormField`, `KptSpeechButton` + `KptSpeechWave` (dictation) |
|
|
170
173
|
| **Buttons & actions** | `KptButton`, `KptIconButton`, `KptButtonGroup`, `KptFab`, `KptSplitButton`, `KptSpeedDial` |
|
|
171
|
-
| **Layout** | `KptAppShell`, `KptToolbar`, `KptSidenav`, `KptCard` (+ `Header`, `Footer`, `Media`, `Title`...), `KptDivider`, `KptPanel`, `KptFieldset`, `KptSplitter`, `KptScrollTop` |
|
|
174
|
+
| **Layout** | `KptAppShell`, `KptToolbar`, `KptSidenav`, `KptCard` (+ `Header`, `Footer`, `Media`, `Title`...), `KptDivider`, `KptPanel`, `KptFieldset`, `KptSplitter`, `KptScrollTop`, `KptBackdrop` |
|
|
172
175
|
| **Navigation** | `KptTabs`, `KptAccordion`, `KptBreadcrumb`, `KptStepper`, `KptMenu`, `KptMenubar`, `KptMegamenu`, `KptContextMenu` |
|
|
173
176
|
| **Data** | `KptDataTable`, `KptPaginator`, `KptTree`, `KptTimeline`, `KptCarousel`, `KptDataView`, `KptPickList`, `KptOrderList`, `KptGalleria`, `KptMeterGroup` |
|
|
174
|
-
| **Feedback & overlay** | `KptAlert`, `KptDialog`, `kptToast` + `KptToastContainer`, `KptTooltip`, `KptPopover`, `KptDrawer`, `KptBottomSheet`, `kptConfirm`, `KptBadge`, `KptChip`, `KptAvatar`, `KptAvatarGroup`, `KptSpinner`, `KptProgress`, `KptSkeleton`, `KptEmpty`, `KptImage`, `KptAuth` |
|
|
175
|
-
| **Subpaths** | `@konce-pt/react/charts` (`KptChart`), `/chat` (`KptChat`, `KptChatMessage`, `KptChatTyping`, `KptChatComposer`, `KptChatDock`), `/roadmap` (`KptRoadmap`), `/grid` (`KptGrid`, `KptCol`, `KptFlex`), `/map` (`KptMap` +
|
|
177
|
+
| **Feedback & overlay** | `KptAlert`, `KptDialog`, `kptToast` + `KptToastContainer`, `KptTooltip`, `KptPopover`, `KptPeek`, `KptDrawer`, `KptBottomSheet`, `kptConfirm`, `KptBadge`, `KptChip`, `KptAvatar`, `KptAvatarGroup`, `KptIcon`, `KptSpinner`, `KptLoader`, `KptUpdate`, `KptProgress`, `KptSkeleton`, `KptEmpty`, `KptImage`, `KptAuth`, `KptMention`, `KptMentions` |
|
|
178
|
+
| **Subpaths** | `@konce-pt/react/charts` (`KptChart`), `/chat` (`KptChat`, `KptChatMessage`, `KptChatTyping`, `KptChatComposer`, `KptChatDock`), `/roadmap` (`KptRoadmap`), `/grid` (`KptGrid`, `KptCol`, `KptFlex`), `/map` (`KptMap` + `KptMapLegend`, `KptMapPoiCard`, `KptMapSearch`, `KptMapCreateDialog`, `KptMapHoursField`, `KptMapImageField`), `/icons` (full Tabler set) |
|
|
176
179
|
|
|
177
180
|
The heavy pieces (charts, chat, roadmap, maps, the full icon set) live in subpaths on purpose, so the core
|
|
178
181
|
entry point stays small. Every component ships an `llms.txt` API sheet next to its source.
|
|
@@ -199,7 +202,7 @@ classes - that is what the SCSS reads.
|
|
|
199
202
|
|
|
200
203
|
## Icons
|
|
201
204
|
|
|
202
|
-
**
|
|
205
|
+
**98 icons are built in and need no configuration** - what the components themselves draw plus the
|
|
203
206
|
staples of an application shell (`sun`, `moon`, `bell`, `settings`, `users`, `logout`,
|
|
204
207
|
`layout-dashboard`, `home`, `activity`, `chart-bar`, `brand-github` ...). A typical admin panel needs
|
|
205
208
|
nothing else.
|
|
@@ -286,12 +289,12 @@ export function App() {
|
|
|
286
289
|
// <KptMotionProvider enabled={false}> - no motion at all (visual tests, kiosk)
|
|
287
290
|
```
|
|
288
291
|
|
|
289
|
-
| Key | Values | Components (per-component
|
|
292
|
+
| Key | Values | Components (per-component prop) |
|
|
290
293
|
| --- | --- | --- |
|
|
291
294
|
| `enabled` | `true` \| `false` | All. `false` also zeroes CSS durations (`data-kpt-motion="off"`, `--kpt-motion-scale: 0` on `<html>`); loading indicators slow down instead of stopping |
|
|
292
295
|
| `layout` | `spring` \| `arc` \| `morph` \| `none` | DataView, PickList, OrderList (`layoutMotion`) |
|
|
293
296
|
| `collapse` | `spring` \| `fade` \| `none` | Accordion, Fieldset, Panel, Tree (`collapseMotion`) |
|
|
294
|
-
| `enter` | `spring` \| `fade` \| `none` | Chat, ChatMessage, ChatDock, FormField (`enterMotion`); Password meter, ChipsInput and Select chips |
|
|
297
|
+
| `enter` | `spring` \| `fade` \| `none` | Chat, ChatMessage, ChatDock, FormField (`enterMotion`); Password meter, ChipsInput and Select chips, Stepper panels, the Image preview, the Galleria slide |
|
|
295
298
|
| `toggle` | `spring` \| `fade` \| `none` | Checkbox, RadioGroup, Switch (`toggleMotion`); Rating stars, ButtonGroup selection |
|
|
296
299
|
|
|
297
300
|
Precedence: `enabled: false` always wins, then the component's own prop, then the provider value,
|
|
@@ -307,7 +310,7 @@ are available for your own views in [`@konce-pt/motion`](https://www.npmjs.com/p
|
|
|
307
310
|
|
|
308
311
|
## Documentation
|
|
309
312
|
|
|
310
|
-
- **Playground** - live demos with copyable code: [ui.konce.pt/react](https://ui.konce.pt/react).
|
|
313
|
+
- **Playground** - live demos with copyable code: [ui.konce.pt/react](https://ui.konce.pt/react/).
|
|
311
314
|
- **`llms.txt`** - LLM-friendly API sheets per component, shipped inside the package; the
|
|
312
315
|
package-wide sheet lives at [ui.konce.pt/llms/react/llms.txt](https://ui.konce.pt/llms/react/llms.txt)
|
|
313
316
|
(PL mirror: [llms-pl.txt](https://ui.konce.pt/llms/react/llms-pl.txt)).
|
|
@@ -371,20 +374,21 @@ import '@konce-pt/grid'; // opcjonalnie - klasy układu
|
|
|
371
374
|
własnego bundla - stąd identyczny wygląd obu portów. Angular wstrzykuje te style w runtime, React
|
|
372
375
|
nie ma dla nich innego źródła, więc ten import nie jest opcjonalny.
|
|
373
376
|
|
|
374
|
-
Motyw ciemny: `<html data-theme="dark">`
|
|
377
|
+
Motyw ciemny: `<html data-theme="dark">` - nie włącza się sam, atrybut przełączasz samodzielnie
|
|
378
|
+
(np. z `matchMedia('(prefers-color-scheme: dark)')`). Nadpisania `--kpt-color-*`
|
|
375
379
|
w arkuszu importowanym **po** `@konce-pt/styles`.
|
|
376
380
|
|
|
377
381
|
**Next.js App Router / Server Components:** każdy moduł interaktywnego komponentu zaczyna się od
|
|
378
382
|
`'use client'`, więc komponenty importujesz wprost do komponentu serwerowego - bez pliku-opakowania.
|
|
379
383
|
|
|
380
384
|
> **CDK nie jest tu potrzebny.** Komponenty z panelem (`KptSelect`, `KptPopover`, `KptMenu`,
|
|
381
|
-
> `KptAutocomplete`, `KptDatepicker`...) renderują go przez
|
|
382
|
-
> `document.body`, więc `@angular/cdk/overlay-prebuilt.css` nie jest wymagany. W otwartym
|
|
385
|
+
> `KptAutocomplete`, `KptDatepicker`...) renderują go przez `KptOverlay` (eksportowany także dla
|
|
386
|
+
> własnych paneli) do portalu na `document.body`, więc `@angular/cdk/overlay-prebuilt.css` nie jest wymagany. W otwartym
|
|
383
387
|
> `KptDialog` panel trafia do `<dialog>`, bo `showModal()` przenosi go do górnej warstwy przeglądarki.
|
|
384
388
|
|
|
385
389
|
### Ikony
|
|
386
390
|
|
|
387
|
-
**
|
|
391
|
+
**98 ikon jest wbudowanych i nie wymaga konfiguracji** - to, co rysują same komponenty, plus staple
|
|
388
392
|
powłoki aplikacji (`sun`, `moon`, `bell`, `settings`, `users`, `logout`, `layout-dashboard`,
|
|
389
393
|
`home`, `activity`, `chart-bar`, `brand-github`...). Typowy panel administracyjny nie potrzebuje nic
|
|
390
394
|
więcej.
|
|
@@ -441,12 +445,12 @@ nakładki w portalach też go słuchają.
|
|
|
441
445
|
<KptMotionProvider enabled={false}>...</KptMotionProvider> {/* bez ruchu (testy wizualne, kiosk) */}
|
|
442
446
|
```
|
|
443
447
|
|
|
444
|
-
| Klucz | Wartości | Komponenty (
|
|
448
|
+
| Klucz | Wartości | Komponenty (prop per komponent) |
|
|
445
449
|
| --- | --- | --- |
|
|
446
450
|
| `enabled` | `true` \| `false` | Wszystkie. `false` zeruje też czasy w CSS (`data-kpt-motion="off"`, `--kpt-motion-scale: 0` na `<html>`); wskaźniki ładowania zwalniają zamiast stanąć |
|
|
447
451
|
| `layout` | `spring` \| `arc` \| `morph` \| `none` | DataView, PickList, OrderList (`layoutMotion`) |
|
|
448
452
|
| `collapse` | `spring` \| `fade` \| `none` | Accordion, Fieldset, Panel, Tree (`collapseMotion`) |
|
|
449
|
-
| `enter` | `spring` \| `fade` \| `none` | Chat, ChatMessage, ChatDock, FormField (`enterMotion`); miernik hasła, chipy ChipsInput i Select |
|
|
453
|
+
| `enter` | `spring` \| `fade` \| `none` | Chat, ChatMessage, ChatDock, FormField (`enterMotion`); miernik hasła, chipy ChipsInput i Select, panele Steppera, podgląd Image, slajd Galleria |
|
|
450
454
|
| `toggle` | `spring` \| `fade` \| `none` | Checkbox, RadioGroup, Switch (`toggleMotion`); gwiazdki Rating, wybór ButtonGroup |
|
|
451
455
|
|
|
452
456
|
Pierwszeństwo: `enabled: false` wygrywa zawsze, potem prop samego komponentu, potem wartość z providera,
|
|
@@ -463,7 +467,7 @@ Każdy komponent ma obok źródła plik `llms.txt` z opisem API.
|
|
|
463
467
|
|
|
464
468
|
### Dokumentacja
|
|
465
469
|
|
|
466
|
-
Playground z demami: [ui.konce.pt/react](https://ui.konce.pt/react). Opisy API dla modeli:
|
|
470
|
+
Playground z demami: [ui.konce.pt/react](https://ui.konce.pt/react/). Opisy API dla modeli:
|
|
467
471
|
[`llms.txt`](https://ui.konce.pt/llms/react/llms.txt) (EN) i
|
|
468
472
|
[`llms-pl.txt`](https://ui.konce.pt/llms/react/llms-pl.txt) (PL). Repozytorium:
|
|
469
473
|
[gitlab.com/konce-pt/koncept-ui](https://gitlab.com/konce-pt/koncept-ui).
|
|
@@ -9,6 +9,12 @@ export interface KptCarouselProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
9
9
|
renderSlide: (item: T, index: number) => ReactNode;
|
|
10
10
|
/** Nazwa regionu dla czytników ekranu, np. "Nowości"; domyślnie `carousel.label`. */
|
|
11
11
|
label?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Zawijanie strzałek: za ostatnim slajdem "dalej" wraca do pierwszego, przed pierwszym "wstecz" -
|
|
14
|
+
* do ostatniego. `false` - strzałki stają na końcach (np. kroki, które mają początek i koniec).
|
|
15
|
+
* Domyślnie `true`.
|
|
16
|
+
*/
|
|
17
|
+
loop?: boolean;
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
14
20
|
* Karuzela slajdów. Dane przez `items`, wygląd slajdu przez `renderSlide`.
|
|
@@ -19,9 +25,11 @@ export interface KptCarouselProps<T> extends Omit<ComponentPropsWithRef<'div'>,
|
|
|
19
25
|
* Semantyka według wzorca WAI-ARIA "carousel" (bez autoodtwarzania): region z
|
|
20
26
|
* `aria-roledescription="karuzela"`, każdy slajd to grupa "Slajd 2 z 4", a slajdy poza widokiem są
|
|
21
27
|
* `inert` - `Tab` i czytnik ich nie widzą. Zmiana slajdu jest ogłaszana uprzejmie (`aria-live`
|
|
22
|
-
* na torze).
|
|
28
|
+
* na torze). Domyślnie strzałki zawijają: za ostatnim slajdem "dalej" wraca do pierwszego, przed
|
|
29
|
+
* pierwszym "wstecz" - do ostatniego. Z `loop={false}` strzałki stają na końcach i dostają
|
|
30
|
+
* `aria-disabled` zamiast `disabled`, żeby nie zgubić fokusu.
|
|
23
31
|
*
|
|
24
32
|
* @example
|
|
25
33
|
* <KptCarousel items={photos} renderSlide={(photo) => <img src={photo} style={{ width: '100%' }} />} />
|
|
26
34
|
*/
|
|
27
|
-
export declare function KptCarousel<T>({ items, activeIndex, defaultActiveIndex, onActiveIndexChange, renderSlide, label, locale, dictionary, className, ...rest }: Readonly<KptCarouselProps<T>>): import("react").JSX.Element;
|
|
35
|
+
export declare function KptCarousel<T>({ items, activeIndex, defaultActiveIndex, onActiveIndexChange, renderSlide, label, loop, locale, dictionary, className, ...rest }: Readonly<KptCarouselProps<T>>): import("react").JSX.Element;
|
|
@@ -13,12 +13,14 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
13
13
|
* Semantyka według wzorca WAI-ARIA "carousel" (bez autoodtwarzania): region z
|
|
14
14
|
* `aria-roledescription="karuzela"`, każdy slajd to grupa "Slajd 2 z 4", a slajdy poza widokiem są
|
|
15
15
|
* `inert` - `Tab` i czytnik ich nie widzą. Zmiana slajdu jest ogłaszana uprzejmie (`aria-live`
|
|
16
|
-
* na torze).
|
|
16
|
+
* na torze). Domyślnie strzałki zawijają: za ostatnim slajdem "dalej" wraca do pierwszego, przed
|
|
17
|
+
* pierwszym "wstecz" - do ostatniego. Z `loop={false}` strzałki stają na końcach i dostają
|
|
18
|
+
* `aria-disabled` zamiast `disabled`, żeby nie zgubić fokusu.
|
|
17
19
|
*
|
|
18
20
|
* @example
|
|
19
21
|
* <KptCarousel items={photos} renderSlide={(photo) => <img src={photo} style={{ width: '100%' }} />} />
|
|
20
22
|
*/
|
|
21
|
-
export function KptCarousel({ items = [], activeIndex, defaultActiveIndex = 0, onActiveIndexChange, renderSlide, label, locale, dictionary, className, ...rest }) {
|
|
23
|
+
export function KptCarousel({ items = [], activeIndex, defaultActiveIndex = 0, onActiveIndexChange, renderSlide, label, loop = true, locale, dictionary, className, ...rest }) {
|
|
22
24
|
const { t } = useKptMessages({ locale, dictionary });
|
|
23
25
|
const [requested, setActive] = useControllableState({
|
|
24
26
|
value: activeIndex,
|
|
@@ -28,7 +30,21 @@ export function KptCarousel({ items = [], activeIndex, defaultActiveIndex = 0, o
|
|
|
28
30
|
// Indeks przycięty do listy: po skróceniu `items` (filtr) widok nie zostaje pusty.
|
|
29
31
|
const active = Math.min(Math.max(requested, 0), Math.max(items.length - 1, 0));
|
|
30
32
|
const goTo = (index) => setActive(Math.min(Math.max(index, 0), items.length - 1));
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
33
|
+
// Zawijanie ma sens od dwóch slajdów - przy jednym strzałki zostają wyłączone.
|
|
34
|
+
const wraps = loop && items.length > 1;
|
|
35
|
+
// Krok o jeden slajd; poza końcem - zawinięcie albo zatrzymanie, zależnie od `loop`.
|
|
36
|
+
const step = (delta) => {
|
|
37
|
+
const count = items.length;
|
|
38
|
+
if (count === 0)
|
|
39
|
+
return;
|
|
40
|
+
const target = active + delta;
|
|
41
|
+
if (target >= 0 && target < count)
|
|
42
|
+
setActive(target);
|
|
43
|
+
else if (wraps)
|
|
44
|
+
setActive((target + count) % count);
|
|
45
|
+
};
|
|
46
|
+
// Koniec, na którym strzałka staje - tylko bez zawijania.
|
|
47
|
+
const atStart = !wraps && active <= 0;
|
|
48
|
+
const atEnd = !wraps && active >= items.length - 1;
|
|
49
|
+
return (_jsx("div", { className: cn('kpt-carousel-host', className), ...rest, children: _jsxs("div", { className: "kpt-carousel", role: "region", "aria-roledescription": t('carousel.roleCarousel'), "aria-label": label || t('carousel.label'), children: [_jsx("div", { className: "kpt-carousel__viewport", children: _jsx("div", { className: "kpt-carousel__track", "aria-live": "polite", style: { transform: `translateX(calc(var(--_kpt-carousel-dir, 1) * -${active * 100}%))` }, children: items.map((item, index) => (_jsx("div", { className: "kpt-carousel__slide", role: "group", "aria-roledescription": t('carousel.roleSlide'), "aria-label": t('carousel.slideOf', { index: index + 1, total: items.length }), inert: index !== active, "aria-hidden": index === active ? undefined : true, children: renderSlide(item, index) }, index))) }) }), _jsx("button", { type: "button", className: "kpt-carousel__nav kpt-carousel__nav--prev", "aria-disabled": atStart || undefined, "aria-label": t('common.previous'), onClick: () => step(-1), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsx("button", { type: "button", className: "kpt-carousel__nav kpt-carousel__nav--next", "aria-disabled": atEnd || undefined, "aria-label": t('common.next'), onClick: () => step(1), children: _jsx(KptIcon, { name: "chevron-right" }) }), _jsx("div", { className: "kpt-carousel__dots", children: items.map((_, index) => (_jsx("button", { type: "button", className: cn('kpt-carousel__dot', index === active && 'is-active'), "aria-current": index === active ? 'true' : undefined, "aria-label": t('carousel.slide', { index: index + 1 }), onClick: () => goTo(index) }, index))) })] }) }));
|
|
34
50
|
}
|
package/dist/carousel/llms.txt
CHANGED
|
@@ -28,6 +28,7 @@ Import: `import { KptCarousel } from '@konce-pt/react';`
|
|
|
28
28
|
- `activeIndex` / `defaultActiveIndex` / `onActiveIndexChange`
|
|
29
29
|
- `renderSlide(item, index)` - required
|
|
30
30
|
- `label`: string - the region name; empty = `carousel.label`
|
|
31
|
+
- `loop`: boolean (default true) - the arrows wrap from the last slide to the first and back
|
|
31
32
|
- `className`, `ref` and every other `<div>` prop go to the host
|
|
32
33
|
|
|
33
34
|
## The render prop replaces the template
|
|
@@ -49,13 +50,18 @@ The arrows use `common.previous` / `common.next`, the dots `carousel.slide` (`{i
|
|
|
49
50
|
## Tokens
|
|
50
51
|
The active dot `--kpt-color-primary`; the arrows `--kpt-color-surface` (80% via `color-mix`, solid on hover) with `--kpt-elevation-2`; the slide transition `--kpt-motion-duration-base`.
|
|
51
52
|
|
|
53
|
+
## Looping
|
|
54
|
+
By default the arrows wrap: "next" on the last slide goes to the first one and "previous" on the first
|
|
55
|
+
goes to the last; the track rewinds across the slides in between. `loop={false}` stops the arrows at the
|
|
56
|
+
ends instead - for steps that have a beginning and an end (onboarding). There is no autoplay either way.
|
|
57
|
+
|
|
52
58
|
## Accessibility
|
|
53
59
|
The WAI-ARIA carousel pattern without autoplay. The root is `role="region"` with
|
|
54
60
|
`aria-roledescription` from `carousel.roleCarousel` ("carousel") and a name from `label` or
|
|
55
61
|
`carousel.label`. Each slide is `role="group"` with `aria-roledescription` from `carousel.roleSlide`
|
|
56
62
|
and a name from `carousel.slideOf` ("Slide 2 of 4"); slides outside the view are `inert` and
|
|
57
63
|
`aria-hidden`, so Tab never lands on an invisible link. The track is `aria-live="polite"`, so the
|
|
58
|
-
slide that comes in is announced.
|
|
64
|
+
slide that comes in is announced. With `loop` off the arrows use `aria-disabled` at the ends instead of `disabled`,
|
|
59
65
|
so they keep the focus; the active dot carries `aria-current`, and every dot has a 24 px target
|
|
60
66
|
(WCAG 2.5.8) with an 8 px visible dot. The transition is off under `prefers-reduced-motion`; in RTL
|
|
61
67
|
the track and the chevrons follow the reading direction (`:dir(rtl)`).
|
|
@@ -21,6 +21,12 @@ export interface KptGalleriaProps extends Omit<ComponentPropsWithRef<'div'>, 'on
|
|
|
21
21
|
* aktywną. Miniatura ma nazwę z `alt` obrazu i `aria-current` na aktywnej, a zmiana obrazu jest
|
|
22
22
|
* ogłaszana uprzejmie ("Obraz 2 z 4: ...") - dopiero po działaniu użytkownika, nie przy starcie.
|
|
23
23
|
*
|
|
24
|
+
* Ruch: nowe zdjęcie wjeżdża w kierunku zmiany na sprężynie, a poprzednie cofa się wolniej
|
|
25
|
+
* i przygasa (paralaksa); rodzaj z `enter` w `KptMotionProvider`, przy "ogranicz ruch" - przenikanie.
|
|
26
|
+
* Pasek miniatur nie ma paska przewijania: przewija się przeciąganiem (mysz, pióro, palec)
|
|
27
|
+
* albo strzałkami paska, krawędzie gasną tam, gdzie jest coś dalej, a aktywna miniatura
|
|
28
|
+
* zawsze wraca do widoku.
|
|
29
|
+
*
|
|
24
30
|
* @example
|
|
25
31
|
* <KptGalleria items={photos} activeIndex={index} onActiveIndexChange={setIndex} />
|
|
26
32
|
*/
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useRef, useState } from 'react';
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
import { attachDragScroll, playGallerySlide, resolveEnterMotion, revealThumb, scrollStrip, stopGallerySlide, trackStripEdges, } from '@konce-pt/motion';
|
|
5
|
+
import { useKptMotion } from "../motion/motion-context.js";
|
|
4
6
|
import { cn } from "../internal/cn.js";
|
|
5
7
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
6
8
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
@@ -15,6 +17,12 @@ const NAV_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'Home', 'End']);
|
|
|
15
17
|
* aktywną. Miniatura ma nazwę z `alt` obrazu i `aria-current` na aktywnej, a zmiana obrazu jest
|
|
16
18
|
* ogłaszana uprzejmie ("Obraz 2 z 4: ...") - dopiero po działaniu użytkownika, nie przy starcie.
|
|
17
19
|
*
|
|
20
|
+
* Ruch: nowe zdjęcie wjeżdża w kierunku zmiany na sprężynie, a poprzednie cofa się wolniej
|
|
21
|
+
* i przygasa (paralaksa); rodzaj z `enter` w `KptMotionProvider`, przy "ogranicz ruch" - przenikanie.
|
|
22
|
+
* Pasek miniatur nie ma paska przewijania: przewija się przeciąganiem (mysz, pióro, palec)
|
|
23
|
+
* albo strzałkami paska, krawędzie gasną tam, gdzie jest coś dalej, a aktywna miniatura
|
|
24
|
+
* zawsze wraca do widoku.
|
|
25
|
+
*
|
|
18
26
|
* @example
|
|
19
27
|
* <KptGalleria items={photos} activeIndex={index} onActiveIndexChange={setIndex} />
|
|
20
28
|
*/
|
|
@@ -31,8 +39,66 @@ export function KptGalleria({ items = [], activeIndex, defaultActiveIndex = 0, o
|
|
|
31
39
|
const [userMoved, setUserMoved] = useState(false);
|
|
32
40
|
const thumbsRef = useRef([]);
|
|
33
41
|
const rootRef = useRef(null);
|
|
42
|
+
const stageRef = useRef(null);
|
|
43
|
+
const imageRef = useRef(null);
|
|
44
|
+
const stripRef = useRef(null);
|
|
45
|
+
const stripWrapRef = useRef(null);
|
|
46
|
+
const edgesRef = useRef(null);
|
|
47
|
+
/** Co było pokazane przy poprzednim renderze - z tego wynika kierunek i duch poprzedniego zdjęcia. */
|
|
48
|
+
const shownRef = useRef(null);
|
|
49
|
+
const motion = resolveEnterMotion(undefined, useKptMotion());
|
|
34
50
|
const current = items[active] ?? null;
|
|
35
51
|
const last = items.length - 1;
|
|
52
|
+
const currentSrc = current?.src ?? '';
|
|
53
|
+
// Zmiana zdjęcia: po wpisaniu nowego `src` do DOM, przed malowaniem. Pierwszy render i wymiana
|
|
54
|
+
// listy bez zmiany indeksu nie grają ruchu - przesuw ilustruje przejście, nie ładowanie.
|
|
55
|
+
useLayoutEffect(() => {
|
|
56
|
+
const prev = shownRef.current;
|
|
57
|
+
shownRef.current = { index: active, src: currentSrc };
|
|
58
|
+
const strip = stripRef.current;
|
|
59
|
+
const thumb = thumbsRef.current[active];
|
|
60
|
+
// Start na dalekim zdjęciu: pasek od razu pokazuje aktywną miniaturę, bez przewijania na oczach.
|
|
61
|
+
if (!prev) {
|
|
62
|
+
if (strip && thumb)
|
|
63
|
+
revealThumb(strip, thumb, false);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (prev.index === active)
|
|
67
|
+
return;
|
|
68
|
+
const stage = stageRef.current;
|
|
69
|
+
const image = imageRef.current;
|
|
70
|
+
if (stage && image && prev.src && prev.src !== currentSrc) {
|
|
71
|
+
playGallerySlide({ stage, image, previousSrc: prev.src, direction: active > prev.index ? 1 : -1, motion });
|
|
72
|
+
}
|
|
73
|
+
if (strip && thumb)
|
|
74
|
+
revealThumb(strip, thumb);
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- ruch czyta konfigurację z chwili zmiany; sama zmiana konfiguracji nie przesuwa zdjęcia
|
|
76
|
+
}, [active, currentSrc]);
|
|
77
|
+
// Pasek: krawędzie i przeciąganie - podpinane, gdy pasek istnieje, i odpinane razem z nim.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const strip = stripRef.current;
|
|
80
|
+
const wrap = stripWrapRef.current;
|
|
81
|
+
if (!strip || !wrap)
|
|
82
|
+
return;
|
|
83
|
+
const edges = trackStripEdges(strip, wrap);
|
|
84
|
+
edgesRef.current = edges;
|
|
85
|
+
const detach = attachDragScroll(strip);
|
|
86
|
+
return () => {
|
|
87
|
+
edges.dispose();
|
|
88
|
+
detach();
|
|
89
|
+
edgesRef.current = null;
|
|
90
|
+
};
|
|
91
|
+
}, [showThumbnails]);
|
|
92
|
+
// Nowa lista miniatur zmienia szerokość paska bez zmiany jego rozmiaru - krawędzie liczymy na nowo.
|
|
93
|
+
useEffect(() => edgesRef.current?.update(), [items]);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const stage = stageRef.current;
|
|
96
|
+
const image = imageRef;
|
|
97
|
+
return () => {
|
|
98
|
+
if (stage)
|
|
99
|
+
stopGallerySlide(stage, image.current);
|
|
100
|
+
};
|
|
101
|
+
}, []);
|
|
36
102
|
const select = (index) => {
|
|
37
103
|
if (last < 0)
|
|
38
104
|
return active;
|
|
@@ -57,7 +123,7 @@ export function KptGalleria({ items = [], activeIndex, defaultActiveIndex = 0, o
|
|
|
57
123
|
};
|
|
58
124
|
const position = t('galleria.position', { index: active + 1, total: items.length });
|
|
59
125
|
const announcement = userMoved ? (current?.alt ? `${position}: ${current.alt}` : position) : '';
|
|
60
|
-
return (_jsx("div", { className: cn('kpt-galleria-host', className), ...rest, children: _jsxs("div", { ref: rootRef, className: "kpt-galleria", onKeyDown: onKeyDown, children: [_jsxs("div", { className: "kpt-galleria__stage", children: [_jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--prev", "aria-disabled": active <= 0 || undefined, "aria-label": t('common.previous'), onClick: () => select(active - 1), children: _jsx(KptIcon, { name: "chevron-left" }) }), current ? _jsx("img", { className: "kpt-galleria__image", src: current.src, alt: current.alt || '' }) : null, _jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--next", "aria-disabled": active >= last || undefined, "aria-label": t('common.next'), onClick: () => select(active + 1), children: _jsx(KptIcon, { name: "chevron-right" }) })] }), showThumbnails ? (_jsx("div", { className: "kpt-galleria__thumbs", children: items.map((item, index) => (_jsx("button", { ref: (el) => {
|
|
61
|
-
|
|
62
|
-
|
|
126
|
+
return (_jsx("div", { className: cn('kpt-galleria-host', className), ...rest, children: _jsxs("div", { ref: rootRef, className: "kpt-galleria", onKeyDown: onKeyDown, children: [_jsxs("div", { ref: stageRef, className: "kpt-galleria__stage", children: [_jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--prev", "aria-disabled": active <= 0 || undefined, "aria-label": t('common.previous'), onClick: () => select(active - 1), children: _jsx(KptIcon, { name: "chevron-left" }) }), current ? _jsx("img", { ref: imageRef, className: "kpt-galleria__image", src: current.src, alt: current.alt || '' }) : null, _jsx("button", { type: "button", className: "kpt-galleria__nav kpt-galleria__nav--next", "aria-disabled": active >= last || undefined, "aria-label": t('common.next'), onClick: () => select(active + 1), children: _jsx(KptIcon, { name: "chevron-right" }) })] }), showThumbnails ? (_jsxs("div", { ref: stripWrapRef, className: "kpt-galleria__strip", children: [_jsx("button", { type: "button", className: "kpt-galleria__scroll kpt-galleria__scroll--back", tabIndex: -1, "aria-label": t('galleria.scrollBack'), onClick: () => stripRef.current && scrollStrip(stripRef.current, -1), children: _jsx(KptIcon, { name: "chevron-left" }) }), _jsx("div", { ref: stripRef, className: "kpt-galleria__thumbs", children: items.map((item, index) => (_jsx("button", { ref: (el) => {
|
|
127
|
+
thumbsRef.current[index] = el;
|
|
128
|
+
}, type: "button", className: cn('kpt-galleria__thumb', index === active && 'is-active'), "aria-current": index === active ? 'true' : undefined, "aria-label": item.alt || t('galleria.image', { index: index + 1 }), onClick: () => select(index), children: _jsx("img", { src: item.thumbnail || item.src, alt: "", draggable: false }) }, index))) }), _jsx("button", { type: "button", className: "kpt-galleria__scroll kpt-galleria__scroll--forward", tabIndex: -1, "aria-label": t('galleria.scrollForward'), onClick: () => stripRef.current && scrollStrip(stripRef.current, 1), children: _jsx(KptIcon, { name: "chevron-right" }) })] })) : null, _jsx("span", { className: "kpt-galleria__live", "aria-live": "polite", children: announcement })] }) }));
|
|
63
129
|
}
|
package/dist/galleria/llms.txt
CHANGED
|
@@ -11,8 +11,12 @@ Import: `import { KptGalleria, type KptGalleriaItem } from '@konce-pt/react';`
|
|
|
11
11
|
<img class="kpt-galleria__image" src="..." alt="Mountain lake">
|
|
12
12
|
<button class="kpt-galleria__nav kpt-galleria__nav--next" aria-label="Next">...</button>
|
|
13
13
|
</div>
|
|
14
|
-
<div class="kpt-
|
|
15
|
-
<button class="kpt-
|
|
14
|
+
<div class="kpt-galleria__strip" data-scroll-end>
|
|
15
|
+
<button class="kpt-galleria__scroll kpt-galleria__scroll--back" tabindex="-1" aria-label="Scroll thumbnails back">...</button>
|
|
16
|
+
<div class="kpt-galleria__thumbs">
|
|
17
|
+
<button class="kpt-galleria__thumb is-active" aria-current="true" aria-label="Mountain lake"><img src="..." alt="" draggable="false"></button>...
|
|
18
|
+
</div>
|
|
19
|
+
<button class="kpt-galleria__scroll kpt-galleria__scroll--forward" tabindex="-1" aria-label="Scroll thumbnails forward">...</button>
|
|
16
20
|
</div>
|
|
17
21
|
<span class="kpt-galleria__live" aria-live="polite">Image 2 of 4: Pine forest</span>
|
|
18
22
|
</div>
|
|
@@ -43,6 +47,29 @@ The arrows use `common.previous` / `common.next`, the thumbnails without `alt` `
|
|
|
43
47
|
Anywhere inside the gallery, `ArrowLeft`/`ArrowRight` move to the neighbouring image (reversed in
|
|
44
48
|
RTL) and `Home`/`End` to the first/last. When focus is on a thumbnail, it follows the active one.
|
|
45
49
|
|
|
50
|
+
## Motion and the thumbnail strip
|
|
51
|
+
Changing the image slides the new one in from the side of travel on a soft spring, while the
|
|
52
|
+
previous one - a temporary `<img class="kpt-galleria__ghost">` under it - drifts back slower and
|
|
53
|
+
dims (parallax). The kind comes from `enter` in `KptMotionProvider`: `spring` (default), `fade` (a short
|
|
54
|
+
cross-fade) or `none`; with the system "reduce motion" setting it only cross-fades. The first
|
|
55
|
+
render and a new `items` list at the same index do not animate.
|
|
56
|
+
|
|
57
|
+
The strip has no scrollbar. It scrolls by dragging (mouse and pen; a touch screen scrolls natively),
|
|
58
|
+
with the wheel or with two small strip arrows that appear only on the side there is more to see -
|
|
59
|
+
the strip fades out on that edge too (`data-scroll-start` / `data-scroll-end` on
|
|
60
|
+
`.kpt-galleria__strip`). A move under 5 px is still a click on a thumbnail; after a real drag the
|
|
61
|
+
click is swallowed and the strip glides on with inertia. The active thumbnail always returns into
|
|
62
|
+
view, and only as far as needed. The strip arrows are `tabindex="-1"`: from the keyboard the gallery
|
|
63
|
+
arrows drive the strip. All of it lives in `@konce-pt/motion` (`playGallerySlide`, `attachDragScroll`,
|
|
64
|
+
`revealThumb`, `trackStripEdges`), shared by both ports.
|
|
65
|
+
|
|
66
|
+
## Size
|
|
67
|
+
The stage has a fixed aspect ratio and the image fills it, so the gallery takes the full width of
|
|
68
|
+
its container and does not jump in height between images. An image with different proportions is
|
|
69
|
+
cropped (`object-fit: cover`). Two custom properties change it, set on the gallery or any ancestor:
|
|
70
|
+
- `--kpt-galleria-aspect-ratio` (default `16 / 9`), e.g. `4 / 3` or `1`
|
|
71
|
+
- `--kpt-galleria-fit` (default `cover`); `contain` shows the whole image with the stage colour around it
|
|
72
|
+
|
|
46
73
|
## Tokens
|
|
47
74
|
The stage `--kpt-color-muted`; the active thumbnail `--kpt-color-primary`; the arrows `--kpt-color-surface` (85% via `color-mix`, solid on hover) / `--kpt-color-on-surface` with `--kpt-elevation-2`.
|
|
48
75
|
|
|
@@ -15,8 +15,8 @@ const warnedKeys = new Set();
|
|
|
15
15
|
* `translate` z ostrzeżeniem w buildzie deweloperskim, gdy klucza nie ma w żadnym słowniku -
|
|
16
16
|
* inaczej literówka daje po cichu sam klucz zamiast tekstu.
|
|
17
17
|
*/
|
|
18
|
-
function translateChecked(dict, key, params) {
|
|
19
|
-
const text = translate(dict, key, params);
|
|
18
|
+
function translateChecked(dict, key, params, locale) {
|
|
19
|
+
const text = translate(dict, key, params, locale);
|
|
20
20
|
if (text === key && isDevBuild() && !warnedKeys.has(key)) {
|
|
21
21
|
warnedKeys.add(key);
|
|
22
22
|
console.warn(`KptI18n: brak tłumaczenia dla klucza "${key}".`);
|
|
@@ -28,7 +28,7 @@ const FALLBACK = {
|
|
|
28
28
|
setLocale: () => { },
|
|
29
29
|
messages: KPT_MESSAGES_EN,
|
|
30
30
|
messagesFor: () => KPT_MESSAGES_EN,
|
|
31
|
-
t: (key, params) => translateChecked(KPT_MESSAGES_EN, key, params),
|
|
31
|
+
t: (key, params) => translateChecked(KPT_MESSAGES_EN, key, params, 'en'),
|
|
32
32
|
};
|
|
33
33
|
const KptI18nContext = createContext(null);
|
|
34
34
|
/**
|
|
@@ -55,7 +55,7 @@ export function KptI18nProvider({ locale = 'en', messages, children }) {
|
|
|
55
55
|
setLocale: setCurrent,
|
|
56
56
|
messages: active,
|
|
57
57
|
messagesFor,
|
|
58
|
-
t: (key, params) => translateChecked(active, key, params),
|
|
58
|
+
t: (key, params) => translateChecked(active, key, params, current),
|
|
59
59
|
};
|
|
60
60
|
}, [current, messagesFor]);
|
|
61
61
|
return _jsx(KptI18nContext.Provider, { value: value, children: children });
|
|
@@ -81,7 +81,7 @@ export function useKptMessages(overrides) {
|
|
|
81
81
|
...context,
|
|
82
82
|
locale: effectiveLocale,
|
|
83
83
|
messages: merged,
|
|
84
|
-
t: (key, params) => translateChecked(merged, key, params),
|
|
84
|
+
t: (key, params) => translateChecked(merged, key, params, effectiveLocale),
|
|
85
85
|
};
|
|
86
86
|
}, [context, locale, dictionary]);
|
|
87
87
|
}
|
package/dist/image/image.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface KptImageProps extends Omit<ComponentPropsWithRef<'div'>, 'onCli
|
|
|
16
16
|
* stronę i górną warstwę. Na czas podglądu blokujemy przewijanie dokumentu, a po zamknięciu
|
|
17
17
|
* fokus wraca na miniaturę.
|
|
18
18
|
*
|
|
19
|
+
* Ruch: zdjęcie wylatuje z miniatury i ląduje na środku na sprężynie, a przy zamknięciu (`x`,
|
|
20
|
+
* `Escape`, tło) wraca do niej, zanim okno się zamknie. Rodzaj ruchu bierze z `enter`
|
|
21
|
+
* w `KptMotionProvider` (`spring` / `fade` / `none`); przy "ogranicz ruch" podgląd tylko się przenika.
|
|
22
|
+
*
|
|
19
23
|
* @example
|
|
20
24
|
* <KptImage src="/foto.jpg" alt="Zdjęcie" preview width="240px" />
|
|
21
25
|
*/
|
package/dist/image/image.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { playZoomClose, playZoomOpen, resolveEnterMotion, stopZoomMotion } from '@konce-pt/motion';
|
|
5
|
+
import { useKptMotion } from "../motion/motion-context.js";
|
|
4
6
|
import { cn } from "../internal/cn.js";
|
|
5
7
|
import { kptLockScroll } from "../internal/scroll-lock.js";
|
|
6
8
|
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
@@ -13,6 +15,10 @@ import { KptIcon } from "../icon/icon.js";
|
|
|
13
15
|
* stronę i górną warstwę. Na czas podglądu blokujemy przewijanie dokumentu, a po zamknięciu
|
|
14
16
|
* fokus wraca na miniaturę.
|
|
15
17
|
*
|
|
18
|
+
* Ruch: zdjęcie wylatuje z miniatury i ląduje na środku na sprężynie, a przy zamknięciu (`x`,
|
|
19
|
+
* `Escape`, tło) wraca do niej, zanim okno się zamknie. Rodzaj ruchu bierze z `enter`
|
|
20
|
+
* w `KptMotionProvider` (`spring` / `fade` / `none`); przy "ogranicz ruch" podgląd tylko się przenika.
|
|
21
|
+
*
|
|
16
22
|
* @example
|
|
17
23
|
* <KptImage src="/foto.jpg" alt="Zdjęcie" preview width="240px" />
|
|
18
24
|
*/
|
|
@@ -21,32 +27,77 @@ export function KptImage({ src, alt = '', width = null, preview = false, locale,
|
|
|
21
27
|
const [open, setOpen] = useState(false);
|
|
22
28
|
const dialogRef = useRef(null);
|
|
23
29
|
const triggerRef = useRef(null);
|
|
30
|
+
const fullRef = useRef(null);
|
|
31
|
+
const closeRef = useRef(null);
|
|
32
|
+
const closeTimer = useRef(null);
|
|
33
|
+
const motion = resolveEnterMotion(undefined, useKptMotion());
|
|
34
|
+
const zoomElements = () => {
|
|
35
|
+
const thumb = triggerRef.current, image = fullRef.current, dialog = dialogRef.current;
|
|
36
|
+
return thumb && image && dialog ? { thumb, image, dialog, close: closeRef.current } : null;
|
|
37
|
+
};
|
|
24
38
|
useEffect(() => {
|
|
25
39
|
const el = dialogRef.current;
|
|
26
40
|
if (!el || !open)
|
|
27
41
|
return;
|
|
28
|
-
if (!el.open)
|
|
42
|
+
if (!el.open) {
|
|
29
43
|
el.showModal();
|
|
44
|
+
// Tuż po showModal(), przed malowaniem - pierwsza klatka stoi już na miniaturze.
|
|
45
|
+
const els = zoomElements();
|
|
46
|
+
if (els)
|
|
47
|
+
playZoomOpen(els, motion);
|
|
48
|
+
}
|
|
30
49
|
const releaseScroll = kptLockScroll();
|
|
31
50
|
return () => {
|
|
32
51
|
releaseScroll();
|
|
33
52
|
if (el.open)
|
|
34
53
|
el.close();
|
|
35
54
|
};
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- ruch czyta stan z chwili otwarcia; zmiana konfiguracji nie otwiera okna ponownie
|
|
36
56
|
}, [open]);
|
|
57
|
+
// Zegar zamknięcia nie może przeżyć komponentu.
|
|
58
|
+
useEffect(() => () => {
|
|
59
|
+
if (closeTimer.current)
|
|
60
|
+
clearTimeout(closeTimer.current);
|
|
61
|
+
}, []);
|
|
37
62
|
const style = width ? { width } : undefined;
|
|
38
63
|
if (!preview) {
|
|
39
64
|
return (_jsx("div", { className: cn('kpt-image-host', className), ...rest, children: _jsx("div", { className: "kpt-image", style: style, children: _jsx("img", { className: "kpt-image__img", src: src, alt: alt }) }) }));
|
|
40
65
|
}
|
|
41
|
-
//
|
|
66
|
+
// Zamknięcie z `x`, `Escape` i tła: najpierw powrót zdjęcia do miniatury, potem okno.
|
|
67
|
+
const requestClose = () => {
|
|
68
|
+
if (closeTimer.current)
|
|
69
|
+
return;
|
|
70
|
+
const els = zoomElements();
|
|
71
|
+
const ms = els ? playZoomClose(els, motion) : 0;
|
|
72
|
+
if (ms === 0) {
|
|
73
|
+
dialogRef.current?.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
closeTimer.current = setTimeout(() => {
|
|
77
|
+
closeTimer.current = null;
|
|
78
|
+
dialogRef.current?.close();
|
|
79
|
+
}, ms);
|
|
80
|
+
};
|
|
81
|
+
// `Escape` zamknąłby okno od razu - przejmujemy je, żeby zdjęcie zdążyło wrócić.
|
|
82
|
+
const onCancel = (event) => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
requestClose();
|
|
85
|
+
};
|
|
86
|
+
// `close` przychodzi z każdej drogi (także wymuszonej przez przeglądarkę) - tu sprzątamy.
|
|
42
87
|
const onClosed = () => {
|
|
88
|
+
if (closeTimer.current)
|
|
89
|
+
clearTimeout(closeTimer.current);
|
|
90
|
+
closeTimer.current = null;
|
|
91
|
+
const els = zoomElements();
|
|
92
|
+
if (els)
|
|
93
|
+
stopZoomMotion(els);
|
|
43
94
|
setOpen(false);
|
|
44
95
|
triggerRef.current?.focus();
|
|
45
96
|
};
|
|
46
97
|
// Okno wypełnia cały ekran, więc kliknięcie w nie samo (a nie w zdjęcie) to kliknięcie w tło.
|
|
47
98
|
const onDialogClick = (event) => {
|
|
48
99
|
if (event.target === event.currentTarget)
|
|
49
|
-
|
|
100
|
+
requestClose();
|
|
50
101
|
};
|
|
51
|
-
return (_jsxs("div", { className: cn('kpt-image-host', className), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "kpt-image kpt-image--preview", "aria-haspopup": "dialog", "aria-label": alt ? t('image.previewOf', { alt }) : t('image.preview'), style: style, onClick: () => setOpen(true), children: [_jsx("img", { className: "kpt-image__img", src: src, alt: "" }), _jsx("span", { className: "kpt-image__mask", "aria-hidden": "true", children: _jsx(KptIcon, { name: "zoom-in" }) })] }), _jsxs("dialog", { ref: dialogRef, className: "kpt-image__overlay", "aria-label": alt || t('image.preview'), onClose: onClosed, onClick: onDialogClick, children: [_jsx("button", { type: "button", className: "kpt-image__close", onClick:
|
|
102
|
+
return (_jsxs("div", { className: cn('kpt-image-host', className), ...rest, children: [_jsxs("button", { ref: triggerRef, type: "button", className: "kpt-image kpt-image--preview", "aria-haspopup": "dialog", "aria-label": alt ? t('image.previewOf', { alt }) : t('image.preview'), style: style, onClick: () => setOpen(true), children: [_jsx("img", { className: "kpt-image__img", src: src, alt: "" }), _jsx("span", { className: "kpt-image__mask", "aria-hidden": "true", children: _jsx(KptIcon, { name: "zoom-in" }) })] }), _jsxs("dialog", { ref: dialogRef, className: "kpt-image__overlay", "aria-label": alt || t('image.preview'), onClose: onClosed, onCancel: onCancel, onClick: onDialogClick, children: [_jsx("button", { ref: closeRef, type: "button", className: "kpt-image__close", onClick: requestClose, "aria-label": t('common.close'), children: _jsx(KptIcon, { name: "x" }) }), _jsx("img", { ref: fullRef, className: "kpt-image__full", src: src, alt: alt })] })] }));
|
|
52
103
|
}
|
package/dist/image/llms.txt
CHANGED
|
@@ -40,6 +40,21 @@ only add noise for a screen reader.
|
|
|
40
40
|
## i18n
|
|
41
41
|
The thumbnail is named from `image.previewOf` (`{alt}`) or `image.preview`; the close button from `common.close`.
|
|
42
42
|
|
|
43
|
+
## Motion
|
|
44
|
+
The preview flies out of the thumbnail and lands in the middle on a soft spring; the corners keep the
|
|
45
|
+
thumbnail radius in flight and the thumbnail is empty while the photo is out. Closing - the x button,
|
|
46
|
+
`Escape` or a click on the backdrop - sends it back into the thumbnail before the dialog closes
|
|
47
|
+
(`Escape` is taken over through the dialog's `cancel` event). The kind comes from `enter` in the
|
|
48
|
+
motion config: `spring` (default), `fade` (cross-fade) or `none`. With the system "reduce motion"
|
|
49
|
+
setting the preview only cross-fades. The flight lives in `@konce-pt/motion` (`playZoomOpen`,
|
|
50
|
+
`playZoomClose`), shared by both ports.
|
|
51
|
+
|
|
52
|
+
## Preview size
|
|
53
|
+
The full-size photo keeps its proportions and is limited to 80% of the window height and 90% of its
|
|
54
|
+
width, so a portrait photo fits whole with a margin above and below, and the x button does not cover
|
|
55
|
+
it. Change the limits with `--kpt-image-preview-max-height` (default `80vh`) and
|
|
56
|
+
`--kpt-image-preview-max-width` (default `90vw`).
|
|
57
|
+
|
|
43
58
|
## Tokens
|
|
44
59
|
The radius `--kpt-radius-md`; the backdrop and the hover mask `--kpt-color-overlay-scrim`; the x button and the magnifier disc `--kpt-color-surface` / `--kpt-color-on-surface` with `--kpt-elevation-2`.
|
|
45
60
|
|
package/dist/input/input.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef } from 'react';
|
|
2
2
|
import type { KptSpeechMode } from '@konce-pt/speech';
|
|
3
|
+
import { type KptI18nOverrides } from '../i18n/i18n-context.tsx';
|
|
3
4
|
import { type KptSpeechColor } from '../speech/speech-button.tsx';
|
|
4
5
|
import type { KptHostProps } from '../internal/types.ts';
|
|
5
6
|
export type KptInputType = 'text' | 'email' | 'password' | 'number' | 'search' | 'tel' | 'url' | 'time';
|
|
6
|
-
export interface KptInputProps extends Omit<ComponentPropsWithRef<'input'>, 'type' | 'required'
|
|
7
|
+
export interface KptInputProps extends Omit<ComponentPropsWithRef<'input'>, 'type' | 'required'>, KptI18nOverrides {
|
|
7
8
|
/** Typ pola. */
|
|
8
9
|
type?: KptInputType;
|
|
9
10
|
/**
|
|
@@ -36,6 +37,11 @@ export interface KptInputProps extends Omit<ComponentPropsWithRef<'input'>, 'typ
|
|
|
36
37
|
* mikrofonu, a dyktowanie kończy się po jednej wypowiedzi (wzorzec wyszukiwarki).
|
|
37
38
|
*/
|
|
38
39
|
speechMode?: KptSpeechMode;
|
|
40
|
+
/**
|
|
41
|
+
* Przycisk czyszczenia na końcu pola. Bez ustawienia - włączony tylko dla `type="search"`,
|
|
42
|
+
* gdzie zastępuje systemowe "x" (ukryte zawsze, także przy `clearable={false}`).
|
|
43
|
+
*/
|
|
44
|
+
clearable?: boolean;
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
47
|
* Pole tekstowe Koncept UI. Renderuje natywny `<input>` w hoście `.kpt-input-host`.
|
|
@@ -51,4 +57,4 @@ export interface KptInputProps extends Omit<ComponentPropsWithRef<'input'>, 'typ
|
|
|
51
57
|
* <KptInput name="email" type="email" defaultValue="" />
|
|
52
58
|
* <KptInput value={email} onValueChange={setEmail} invalid={!ok} touched={blurred} />
|
|
53
59
|
*/
|
|
54
|
-
export declare function KptInput({ type, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech, speechLang, speechColor, speechMode, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptInputProps>): import("react").JSX.Element;
|
|
60
|
+
export declare function KptInput({ type, required, invalid, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech, speechLang, speechColor, speechMode, clearable, onKeyDown, locale, dictionary, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }: Readonly<KptInputProps>): import("react").JSX.Element;
|
package/dist/input/input.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useRef } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { useKptMessages } from "../i18n/i18n-context.js";
|
|
5
|
+
import { KptIcon } from "../icon/icon.js";
|
|
4
6
|
import { cn } from "../internal/cn.js";
|
|
5
7
|
import { composeRefs } from "../internal/compose-refs.js";
|
|
6
8
|
import { KptSpeechButton } from "../speech/speech-button.js";
|
|
7
9
|
import { useKptSpeechRecognition } from "../speech/use-speech-recognition.js";
|
|
8
10
|
import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
11
|
+
/**
|
|
12
|
+
* Wpisuje wartość setterem z prototypu i wysyła `input`. Samo `el.value = ''` React by połknął:
|
|
13
|
+
* jego tracker wartości uznałby zdarzenie za echo i nie wywołałby `onChange`.
|
|
14
|
+
*/
|
|
15
|
+
function setFieldValue(field, value) {
|
|
16
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(field, value);
|
|
17
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
18
|
+
}
|
|
9
19
|
/**
|
|
10
20
|
* Pole tekstowe Koncept UI. Renderuje natywny `<input>` w hoście `.kpt-input-host`.
|
|
11
21
|
*
|
|
@@ -20,7 +30,7 @@ import { joinIds, useKptFieldA11y } from "../form-field/form-field-context.js";
|
|
|
20
30
|
* <KptInput name="email" type="email" defaultValue="" />
|
|
21
31
|
* <KptInput value={email} onValueChange={setEmail} invalid={!ok} touched={blurred} />
|
|
22
32
|
*/
|
|
23
|
-
export function KptInput({ type = 'text', required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech = false, speechLang, speechColor, speechMode = 'insert', 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }) {
|
|
33
|
+
export function KptInput({ type = 'text', required = false, invalid = false, touched, onValueChange, onTouch, hostProps, className, id, onChange, onBlur, ref, speech = false, speechLang, speechColor, speechMode = 'insert', clearable, onKeyDown, locale, dictionary, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedBy, ...rest }) {
|
|
24
34
|
const { className: hostClassName, ...hostRest } = hostProps ?? {};
|
|
25
35
|
const field = useKptFieldA11y(id, 'kpt-input');
|
|
26
36
|
const innerRef = useRef(null);
|
|
@@ -38,13 +48,49 @@ export function KptInput({ type = 'text', required = false, invalid = false, tou
|
|
|
38
48
|
// Stan nieprawidłowy pokazujemy dopiero po dotknięciu pola - jak natywne `:user-invalid`.
|
|
39
49
|
// Bez `touched` nie ma czego bramkować, więc decyduje samo `invalid`.
|
|
40
50
|
const showInvalid = invalid && (touched ?? true);
|
|
51
|
+
const { t } = useKptMessages({ locale, dictionary });
|
|
52
|
+
// W trybie niekontrolowanym wartość należy do DOM i React jej nie widzi - to, czy jest co
|
|
53
|
+
// czyścić, trzymamy osobno. W kontrolowanym decyduje `value`.
|
|
54
|
+
const [filledUncontrolled, setFilledUncontrolled] = useState(() => String(rest.defaultValue ?? '') !== '');
|
|
55
|
+
const filled = rest.value === undefined ? filledUncontrolled : String(rest.value) !== '';
|
|
56
|
+
const showClear = (clearable ?? type === 'search') && filled && !rest.disabled && !rest.readOnly;
|
|
57
|
+
// Reset formularza (`form.reset()`, a w React 19 także po `<form action>`) przywraca wartość bez
|
|
58
|
+
// zdarzenia `input` - stan "jest co czyścić" odczytujemy wtedy z pola, gdy reset już się wykona.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const form = innerRef.current?.form;
|
|
61
|
+
if (!form)
|
|
62
|
+
return;
|
|
63
|
+
const onReset = () => {
|
|
64
|
+
queueMicrotask(() => setFilledUncontrolled((innerRef.current?.value ?? '') !== ''));
|
|
65
|
+
};
|
|
66
|
+
form.addEventListener('reset', onReset);
|
|
67
|
+
return () => form.removeEventListener('reset', onReset);
|
|
68
|
+
}, []);
|
|
41
69
|
const handleChange = useCallback((event) => {
|
|
70
|
+
setFilledUncontrolled(event.target.value !== '');
|
|
42
71
|
onChange?.(event);
|
|
43
72
|
onValueChange?.(event.target.value);
|
|
44
73
|
}, [onChange, onValueChange]);
|
|
74
|
+
/** Czyści zdarzeniem `input` - przechodzi przez `onChange` jak wpisane, w obu trybach. */
|
|
75
|
+
const clear = () => {
|
|
76
|
+
const field = innerRef.current;
|
|
77
|
+
if (!field)
|
|
78
|
+
return;
|
|
79
|
+
setFieldValue(field, '');
|
|
80
|
+
field.focus();
|
|
81
|
+
};
|
|
82
|
+
/** Escape czyści pełne pole i nie idzie dalej - pierwszy nie zamyka okna, w którym pole stoi. */
|
|
83
|
+
const handleKeyDown = (event) => {
|
|
84
|
+
onKeyDown?.(event);
|
|
85
|
+
if (event.defaultPrevented || event.key !== 'Escape' || !showClear)
|
|
86
|
+
return;
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
clear();
|
|
90
|
+
};
|
|
45
91
|
const handleBlur = useCallback((event) => {
|
|
46
92
|
onBlur?.(event);
|
|
47
93
|
onTouch?.();
|
|
48
94
|
}, [onBlur, onTouch]);
|
|
49
|
-
return (_jsxs("span", { className: cn('kpt-input-host', hostClassName), "data-speech": speechState, ...hostRest, children: [_jsx("input", { className: cn('kpt-input__field', className), id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), type: type, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, ref: composeRefs(innerRef, ref), ...rest }), speech ? (_jsx(KptSpeechButton, { listeningColor: speechColor, speech: mic, size: "sm", unsupported: "hide", disabled: !!rest.disabled || !!rest.readOnly, hostProps: { className: 'kpt-input__speech' } })) : null] }));
|
|
95
|
+
return (_jsxs("span", { className: cn('kpt-input-host', hostClassName), "data-speech": speechState, "data-clear": showClear ? '' : undefined, ...hostRest, children: [_jsx("input", { className: cn('kpt-input__field', className), id: field.id, "aria-describedby": joinIds(ariaDescribedBy, field.describedBy), type: type, "aria-required": required || undefined, "aria-invalid": ariaInvalid ?? (showInvalid || undefined), onChange: handleChange, onBlur: handleBlur, onKeyDown: handleKeyDown, ref: composeRefs(innerRef, ref), ...rest }), showClear ? (_jsx("button", { type: "button", className: "kpt-input__clear", tabIndex: -1, "aria-label": t('input.clear'), onMouseDown: (event) => event.preventDefault(), onClick: clear, children: _jsx(KptIcon, { name: "x" }) })) : null, speech ? (_jsx(KptSpeechButton, { listeningColor: speechColor, speech: mic, size: "sm", unsupported: "hide", disabled: !!rest.disabled || !!rest.readOnly, hostProps: { className: 'kpt-input__speech' } })) : null] }));
|
|
50
96
|
}
|
package/dist/input/llms.txt
CHANGED
|
@@ -15,6 +15,7 @@ Import: `import { KptInput } from '@konce-pt/react';`
|
|
|
15
15
|
- `onTouch()` - fires on blur, the moment the field becomes touched
|
|
16
16
|
- `required`: boolean - see the note below
|
|
17
17
|
- `invalid`: boolean, `touched`: boolean - the error state
|
|
18
|
+
- `clearable`: boolean (default: `true` only for `type="search"`) - a clear button, see "Clear button"
|
|
18
19
|
- `hostProps` - props for the `.kpt-input-host` wrapper
|
|
19
20
|
- `className`, `ref`, `name`, `placeholder`, `autoComplete` and every other `<input>` prop go to
|
|
20
21
|
the inner `<input>`
|
|
@@ -51,6 +52,21 @@ configuration. An explicit `id` prop still wins.
|
|
|
51
52
|
generated. Inside `KptFormField` the wrapper's `label[for]` points at it and the hint or error is
|
|
52
53
|
added to `aria-describedby` (merged with your own). Without a wrapper, name it with `aria-label`.
|
|
53
54
|
|
|
55
|
+
## Clear button
|
|
56
|
+
`type="search"` gets a clear button at the end of the field as soon as it has a value; `clearable`
|
|
57
|
+
turns it on for any other type, `clearable={false}` turns it off. The browser's own "x"
|
|
58
|
+
(`::-webkit-search-cancel-button`) is always hidden - Chromium and Safari draw it differently and
|
|
59
|
+
Firefox not at all. The button is hidden while the field is empty, disabled or read-only.
|
|
60
|
+
|
|
61
|
+
Clearing dispatches a real `input` event, so the value goes through the same path as typed text,
|
|
62
|
+
and the focus stays in the field. The button is `tabindex="-1"` (like the `KptInputNumber`
|
|
63
|
+
steppers); from the keyboard `Escape` clears a filled field and stops the event, so the first
|
|
64
|
+
`Escape` does not close the dialog the field sits in. Its name comes from `input.clear` in the
|
|
65
|
+
dictionary. The host gets `data-clear` while the button is visible.
|
|
66
|
+
|
|
67
|
+
<KptInput type="search" value={query} onValueChange={setQuery} />
|
|
68
|
+
<KptInput clearable value={coupon} onValueChange={setCoupon} />
|
|
69
|
+
|
|
54
70
|
## Tokens
|
|
55
71
|
`--kpt-form-field-{bg,text,placeholder,border,border-hover,border-focus,border-error,radius}`.
|
|
56
72
|
|
package/dist/popover/llms.txt
CHANGED
|
@@ -15,6 +15,8 @@ Import: `import { KptPopover } from '@konce-pt/react';`
|
|
|
15
15
|
- `children`: ReactNode - the panel content
|
|
16
16
|
- `open` / `defaultOpen` / `onOpenChange` - controlled or uncontrolled
|
|
17
17
|
- `placement`: 'bottom' (default) | 'top' - the preferred side; it flips when there is no room
|
|
18
|
+
- `maxWidth`: string - the panel's maximum width, any CSS length (`'24rem'`; default `20rem`). The panel is
|
|
19
|
+
never wider than the window minus the 8 px overlay margin
|
|
18
20
|
- `className`, `ref` and every other `<span>` prop go to the host
|
|
19
21
|
|
|
20
22
|
## The panel is a portal
|
|
@@ -11,6 +11,11 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
|
|
|
11
11
|
placement?: 'bottom' | 'top';
|
|
12
12
|
/** Nazwa panelu (`role="dialog"`) - okno bez nazwy czytnik ogłasza jako samo "okno dialogowe". */
|
|
13
13
|
ariaLabel?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Maksymalna szerokość panelu (dowolna długość CSS, np. `'24rem'`); brak = `20rem`.
|
|
16
|
+
* Panel i tak nie wyjdzie poza okno - ogranicza go szerokość okna minus margines nakładek.
|
|
17
|
+
*/
|
|
18
|
+
maxWidth?: string;
|
|
14
19
|
}
|
|
15
20
|
/**
|
|
16
21
|
* Popover - dowolna treść w dymku przypiętym do wyzwalacza (bogatszy niż tooltip).
|
|
@@ -28,4 +33,4 @@ export interface KptPopoverProps extends Omit<ComponentPropsWithRef<'span'>, 'co
|
|
|
28
33
|
* <div>Bogata treść popovera...</div>
|
|
29
34
|
* </KptPopover>
|
|
30
35
|
*/
|
|
31
|
-
export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, ariaLabel, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
|
|
36
|
+
export declare function KptPopover({ trigger, children, open, defaultOpen, onOpenChange, placement, ariaLabel, maxWidth, className, ...rest }: Readonly<KptPopoverProps>): import("react").JSX.Element;
|
package/dist/popover/popover.js
CHANGED
|
@@ -23,7 +23,7 @@ const FOCUSABLE = 'a[href], button:not(:disabled), input:not(:disabled), select,
|
|
|
23
23
|
* <div>Bogata treść popovera...</div>
|
|
24
24
|
* </KptPopover>
|
|
25
25
|
*/
|
|
26
|
-
export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', ariaLabel = '', className, ...rest }) {
|
|
26
|
+
export function KptPopover({ trigger, children, open, defaultOpen = false, onOpenChange, placement = 'bottom', ariaLabel = '', maxWidth, className, ...rest }) {
|
|
27
27
|
const triggerRef = useRef(null);
|
|
28
28
|
/** Kotwica panelu - w stanie, bo czyta ją render; ref zostaje dla handlerów. */
|
|
29
29
|
const [anchor, setAnchor] = useState(null);
|
|
@@ -65,5 +65,5 @@ export function KptPopover({ trigger, children, open, defaultOpen = false, onOpe
|
|
|
65
65
|
findControl()?.focus();
|
|
66
66
|
setOpen(false);
|
|
67
67
|
}, [setOpen]);
|
|
68
|
-
return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: setTrigger, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", "aria-label": ariaLabel || undefined, children: children }) })] }));
|
|
68
|
+
return (_jsxs("span", { className: cn('kpt-popover-host', className), ...rest, children: [_jsx("span", { className: "kpt-popover__trigger", ref: setTrigger, onClick: () => setOpen(!isOpen), children: trigger }), _jsx(KptOverlay, { open: isOpen, anchor: anchor, onClose: close, offset: 6, preferAbove: placement === 'top', children: _jsx("div", { ref: panelRef, id: panelId, className: "kpt-popover__panel", role: "dialog", "aria-label": ariaLabel || undefined, style: maxWidth ? { '--_max-width': maxWidth } : undefined, children: children }) })] }));
|
|
69
69
|
}
|
package/dist/public-api.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { KptToolbarProps } from './toolbar/toolbar.tsx';
|
|
|
5
5
|
export { KptSidenav, KptSidenavFooter } from './sidenav/sidenav.tsx';
|
|
6
6
|
export type { KptSidenavProps } from './sidenav/sidenav.tsx';
|
|
7
7
|
export { KptTabs, KptTab } from './tabs/tabs.tsx';
|
|
8
|
-
export type { KptTabProps, KptTabsProps } from './tabs/tabs.tsx';
|
|
8
|
+
export type { KptTabIconColor, KptTabProps, KptTabsProps } from './tabs/tabs.tsx';
|
|
9
9
|
export { KptAccordion, KptAccordionPanel } from './accordion/accordion.tsx';
|
|
10
10
|
export type { KptAccordionPanelProps, KptAccordionProps } from './accordion/accordion.tsx';
|
|
11
11
|
export { KptPanel, KptPanelActions } from './panel/panel.tsx';
|
|
@@ -194,6 +194,8 @@ export type { KptCardHeadingLevel, KptCardProps, KptCardVariant } from './card/c
|
|
|
194
194
|
export { KptI18nProvider, useKptI18n, useKptMessages } from './i18n/i18n-context.tsx';
|
|
195
195
|
export type { KptI18nContextValue, KptI18nOverrides, KptI18nProviderProps } from './i18n/i18n-context.tsx';
|
|
196
196
|
export { KPT_MESSAGES_EN, KPT_MESSAGES_PL } from '@konce-pt/i18n';
|
|
197
|
+
export { KptMotionProvider, useKptMotion } from './motion/motion-context.tsx';
|
|
198
|
+
export type { KptMotionProviderProps, KptMotionConfig, KptLayoutMotion } from './motion/motion-context.tsx';
|
|
197
199
|
export type { DeepPartial, KptLocale, KptMessages } from '@konce-pt/i18n';
|
|
198
200
|
export { cn } from './internal/cn.ts';
|
|
199
201
|
export type { ClassValue } from './internal/cn.ts';
|
package/dist/public-api.js
CHANGED
|
@@ -109,6 +109,7 @@ export { KptCard, KptCardActions, KptCardFooter, KptCardHeader, KptCardLead, Kpt
|
|
|
109
109
|
// i18n
|
|
110
110
|
export { KptI18nProvider, useKptI18n, useKptMessages } from "./i18n/i18n-context.js";
|
|
111
111
|
export { KPT_MESSAGES_EN, KPT_MESSAGES_PL } from '@konce-pt/i18n';
|
|
112
|
+
export { KptMotionProvider, useKptMotion } from "./motion/motion-context.js";
|
|
112
113
|
// Prymitywy pomocnicze
|
|
113
114
|
export { cn } from "./internal/cn.js";
|
|
114
115
|
export { composeRefs } from "./internal/compose-refs.js";
|
package/dist/tabs/llms.txt
CHANGED
|
@@ -5,12 +5,15 @@ Import: `import { KptTabs } from '@konce-pt/react';`
|
|
|
5
5
|
|
|
6
6
|
## DOM
|
|
7
7
|
<div class="kpt-tabs-host" style="--kpt-anim-duration: 500ms; --kpt-anim-dir: 1">
|
|
8
|
-
<div class="kpt-tabs__header"
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
<div class="kpt-tabs__header">
|
|
9
|
+
<div class="kpt-tabs__list" role="tablist">
|
|
10
|
+
<button class="kpt-tabs__tab is-active" role="tab" id="kpt-tabs-r1-tab-0"
|
|
11
|
+
aria-selected="true" aria-controls="kpt-tabs-r1-panel" tabindex="0">General</button>
|
|
12
|
+
<button class="kpt-tabs__tab" role="tab" id="kpt-tabs-r1-tab-1"
|
|
13
|
+
aria-selected="false" tabindex="-1">Advanced</button>
|
|
14
|
+
<span class="kpt-tabs__ink" style="width: 95px; transform: translateX(85px)"></span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="kpt-tabs__links"><a class="kpt-tabs__tab kpt-tabs__link" href="...">Docs</a></div>
|
|
14
17
|
</div>
|
|
15
18
|
<div class="kpt-tabs__body" role="tabpanel" tabindex="0" id="kpt-tabs-r1-panel"
|
|
16
19
|
aria-labelledby="kpt-tabs-r1-tab-0">
|
|
@@ -21,13 +24,42 @@ Import: `import { KptTabs } from '@konce-pt/react';`
|
|
|
21
24
|
## Props
|
|
22
25
|
- `activeIndex` / `defaultActiveIndex` / `onActiveIndexChange` - controlled or uncontrolled
|
|
23
26
|
- `animated`: boolean (default true), `animationDuration`: number (ms, default 500)
|
|
27
|
+
- `onNavigate`: (event: KptNavigateEvent<KptTabProps>) => void - a plain click on a link tab
|
|
24
28
|
- `children` - `<KptTabs.Tab>` elements
|
|
25
29
|
|
|
26
30
|
## KptTabs.Tab
|
|
27
|
-
`label` (required), `disabled`, `children`. The tab component never renders itself - `KptTabs`
|
|
31
|
+
`label` (required), `disabled`, `icon`, `iconEnd`, `iconColor`, `iconEndColor`, `href`, `target`, `rel`, `children`. The tab component never renders itself - `KptTabs`
|
|
28
32
|
reads its props and decides what to show, exactly as the Angular version reads the `kpt-tab`
|
|
29
33
|
template. Only the active tab's content is in the DOM.
|
|
30
34
|
|
|
35
|
+
## Icons
|
|
36
|
+
`icon` goes before the label, `iconEnd` after it - either, both or none. `iconColor` /
|
|
37
|
+
`iconEndColor` take a `KptTabIconColor` (`'primary' | 'success' | 'warning' | 'danger' | 'info'`),
|
|
38
|
+
set as `data-color` and mapped to `--kpt-color-<name>`, or any CSS color (`var(--brand-accent)`),
|
|
39
|
+
set inline as `color`. Without a color the icon inherits the tab's text color. Same markup as
|
|
40
|
+
Angular: `<span class="kpt-icon-host kpt-tabs__icon" data-color="success">` inside the button.
|
|
41
|
+
|
|
42
|
+
<KptTabs.Tab label="Checks" icon="circle-check" iconColor="success">...</KptTabs.Tab>
|
|
43
|
+
<KptTabs.Tab label="Issues" icon="bell" iconEnd="alert-triangle" iconEndColor="warning">...</KptTabs.Tab>
|
|
44
|
+
|
|
45
|
+
## Link tabs
|
|
46
|
+
A tab with `href` is a link to another page or an external service, not a tab: it has no panel
|
|
47
|
+
(its content is ignored), it does not count towards `activeIndex`, and it renders **after** the
|
|
48
|
+
tabs, outside `role="tablist"` - a tablist may only contain tabs, and a link announced as a tab would
|
|
49
|
+
promise a panel that does not exist. In the keyboard order the links come after the tab row and
|
|
50
|
+
before the panel. `target="_blank"` gets `rel="noopener noreferrer"` unless you pass `rel`.
|
|
51
|
+
A disabled link renders as `<span aria-disabled="true">` without `href`.
|
|
52
|
+
|
|
53
|
+
A plain click (left button, no modifier keys, no `target`) also emits `onNavigate` with
|
|
54
|
+
`{ item, url, event }`; an app with a router calls `event.preventDefault()` and navigates itself,
|
|
55
|
+
without a page reload.
|
|
56
|
+
|
|
57
|
+
<KptTabs onNavigate={({ event, url }) => { event.preventDefault(); navigate(url); }}>
|
|
58
|
+
<KptTabs.Tab label="Overview">...</KptTabs.Tab>
|
|
59
|
+
<KptTabs.Tab label="Settings" href="/settings" />
|
|
60
|
+
<KptTabs.Tab label="npm" href="https://www.npmjs.com/package/@konce-pt/react" target="_blank" iconEnd="external-link" />
|
|
61
|
+
</KptTabs>
|
|
62
|
+
|
|
31
63
|
## The underline is measured, not computed
|
|
32
64
|
`__ink` takes its width and offset from the active button in the DOM, not from the index: tabs have
|
|
33
65
|
different widths and the labels change with the language. The measurement runs after every render,
|
|
@@ -47,7 +79,7 @@ schedule another render and the component would loop.
|
|
|
47
79
|
`--kpt-color-border`, `--kpt-motion-easing-standard`.
|
|
48
80
|
|
|
49
81
|
## Accessibility
|
|
50
|
-
The
|
|
82
|
+
The tab list (`__list`) is `role="tablist"` with `role="tab"` buttons carrying `aria-selected`; the body is
|
|
51
83
|
`role="tabpanel"`, named by the active tab (`aria-labelledby`), which points back at it with
|
|
52
84
|
`aria-controls`. The tab row is a single `Tab` stop (roving `tabindex`): `ArrowLeft`/`ArrowRight`
|
|
53
85
|
move between tabs, wrapping at the ends, `Home`/`End` jump to the first/last one, and disabled tabs
|
package/dist/tabs/tabs.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef, type ReactNode } from 'react';
|
|
2
|
+
import { type KptNavigateEvent } from '../shared/types.ts';
|
|
3
|
+
/** Barwa semantyczna ikony zakładki - nazwa tokenu `--kpt-color-*`. */
|
|
4
|
+
export type KptTabIconColor = 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
|
2
5
|
export interface KptTabProps {
|
|
3
6
|
label: string;
|
|
4
7
|
disabled?: boolean;
|
|
8
|
+
/** Ikona przed etykietą - nazwa z rejestru ikon. */
|
|
9
|
+
icon?: string;
|
|
10
|
+
/** Ikona za etykietą - nazwa z rejestru ikon. */
|
|
11
|
+
iconEnd?: string;
|
|
12
|
+
/** Kolor ikony przed etykietą: nazwa semantyczna albo dowolna wartość CSS (`var(--brand)`). */
|
|
13
|
+
iconColor?: KptTabIconColor | (string & {});
|
|
14
|
+
/** Kolor ikony za etykietą: nazwa semantyczna albo dowolna wartość CSS (`var(--brand)`). */
|
|
15
|
+
iconEndColor?: KptTabIconColor | (string & {});
|
|
16
|
+
/**
|
|
17
|
+
* Adres - zakładka staje się odnośnikiem do strony albo serwisu zewnętrznego. Nie ma panelu
|
|
18
|
+
* (`children` są pomijane), nie liczy się do `activeIndex` i stoi za zakładkami, poza `tablist`.
|
|
19
|
+
*/
|
|
20
|
+
href?: string;
|
|
21
|
+
/** `target` odnośnika, np. `_blank`. */
|
|
22
|
+
target?: string;
|
|
23
|
+
/** `rel` odnośnika; przy `target="_blank"` domyślnie `noopener noreferrer`. */
|
|
24
|
+
rel?: string;
|
|
5
25
|
children?: ReactNode;
|
|
6
26
|
}
|
|
7
27
|
/**
|
|
@@ -13,11 +33,17 @@ export declare namespace KptTab {
|
|
|
13
33
|
var kptTabsSlot: "tab";
|
|
14
34
|
}
|
|
15
35
|
export interface KptTabsProps extends Omit<ComponentPropsWithRef<'div'>, 'onChange'> {
|
|
36
|
+
/** Indeks wśród zakładek z panelem - odnośniki (`href`) się nie liczą. */
|
|
16
37
|
activeIndex?: number;
|
|
17
38
|
defaultActiveIndex?: number;
|
|
18
39
|
onActiveIndexChange?: (index: number) => void;
|
|
19
40
|
animated?: boolean;
|
|
20
41
|
animationDuration?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Zwykły klik w zakładkę-odnośnik bez `target`. Aplikacja z routerem woła
|
|
44
|
+
* `event.preventDefault()` i nawiguje sama, bez przeładowania strony.
|
|
45
|
+
*/
|
|
46
|
+
onNavigate?: (event: KptNavigateEvent<KptTabProps>) => void;
|
|
21
47
|
children?: ReactNode;
|
|
22
48
|
}
|
|
23
49
|
/**
|
|
@@ -25,7 +51,10 @@ export interface KptTabsProps extends Omit<ComponentPropsWithRef<'div'>, 'onChan
|
|
|
25
51
|
*
|
|
26
52
|
* Klawiatura według wzorca WAI-ARIA "tabs": rząd zakładek to jeden przystanek Tab, strzałki
|
|
27
53
|
* w lewo/prawo oraz `Home`/`End` przechodzą po zakładkach (z pominięciem wyłączonych) i od razu je
|
|
28
|
-
* aktywują, a kolejny `Tab` prowadzi do panelu.
|
|
54
|
+
* aktywują, a kolejny `Tab` prowadzi do odnośników (jeśli są) i dalej do panelu.
|
|
55
|
+
*
|
|
56
|
+
* Zakładka z `href` jest odnośnikiem: stoi za zakładkami, poza `tablist` (ta może zawierać wyłącznie
|
|
57
|
+
* zakładki), nie ma panelu i nie liczy się do `activeIndex`.
|
|
29
58
|
*
|
|
30
59
|
* Podkreślenie aktywnej zakładki (`__ink`) jest mierzone z DOM przycisku, nie liczone z indeksu:
|
|
31
60
|
* zakładki mają różne szerokości, a etykiety zmieniają się z językiem.
|
|
@@ -33,10 +62,10 @@ export interface KptTabsProps extends Omit<ComponentPropsWithRef<'div'>, 'onChan
|
|
|
33
62
|
* @example
|
|
34
63
|
* <KptTabs>
|
|
35
64
|
* <KptTabs.Tab label="Ogólne">...</KptTabs.Tab>
|
|
36
|
-
* <KptTabs.Tab label="Zaawansowane">...</KptTabs.Tab>
|
|
65
|
+
* <KptTabs.Tab label="Zaawansowane" iconEnd="settings">...</KptTabs.Tab>
|
|
37
66
|
* </KptTabs>
|
|
38
67
|
*/
|
|
39
|
-
export declare function KptTabs({ activeIndex, defaultActiveIndex, onActiveIndexChange, animated, animationDuration, className, style, children, ...rest }: Readonly<KptTabsProps>): import("react").JSX.Element;
|
|
68
|
+
export declare function KptTabs({ activeIndex, defaultActiveIndex, onActiveIndexChange, animated, animationDuration, onNavigate, className, style, children, ...rest }: Readonly<KptTabsProps>): import("react").JSX.Element;
|
|
40
69
|
export declare namespace KptTabs {
|
|
41
70
|
var Tab: typeof KptTab;
|
|
42
71
|
}
|
package/dist/tabs/tabs.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { isValidElement, useCallback, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { KptIcon } from "../icon/icon.js";
|
|
4
5
|
import { cn } from "../internal/cn.js";
|
|
5
6
|
import { slotChildren, warnUnslottedChild } from "../internal/slot-children.js";
|
|
6
7
|
import { nextRovingIndex, rovingStop } from "../internal/roving.js";
|
|
7
8
|
import { useControllableState } from "../internal/use-controllable-state.js";
|
|
8
9
|
import { useKptId } from "../internal/use-id.js";
|
|
10
|
+
import { kptIsPlainClick } from "../shared/types.js";
|
|
11
|
+
const ICON_TONES = new Set(['primary', 'success', 'warning', 'danger', 'info']);
|
|
12
|
+
/** Etykieta z ikonami - wspólna dla zakładki i odnośnika. */
|
|
13
|
+
function TabInner({ tab }) {
|
|
14
|
+
return (_jsxs(_Fragment, { children: [tab.icon && _jsx(TabIcon, { name: tab.icon, color: tab.iconColor }), tab.label, tab.iconEnd && _jsx(TabIcon, { name: tab.iconEnd, color: tab.iconEndColor })] }));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Ikona zakładki. Nazwa semantyczna idzie w `data-color` (barwa z tokenu, zmienia się z motywem),
|
|
18
|
+
* a każdy inny napis - np. `var(--brand-accent)` - trafia wprost do `color`. Bez koloru ikona
|
|
19
|
+
* dziedziczy barwę tekstu zakładki.
|
|
20
|
+
*/
|
|
21
|
+
function TabIcon({ name, color }) {
|
|
22
|
+
const tone = color && ICON_TONES.has(color) ? color : undefined;
|
|
23
|
+
return (_jsx(KptIcon, { className: "kpt-tabs__icon", name: name, "data-color": tone, style: color && !tone ? { color } : undefined }));
|
|
24
|
+
}
|
|
9
25
|
/**
|
|
10
26
|
* Pojedyncza zakładka. Umieszczaj w `<KptTabs>`. Treść renderuje się leniwie - dopiero gdy
|
|
11
27
|
* zakładka jest aktywna, tak samo jak szablon `kpt-tab` w porcie Angulara.
|
|
@@ -21,7 +37,10 @@ KptTab.kptTabsSlot = 'tab';
|
|
|
21
37
|
*
|
|
22
38
|
* Klawiatura według wzorca WAI-ARIA "tabs": rząd zakładek to jeden przystanek Tab, strzałki
|
|
23
39
|
* w lewo/prawo oraz `Home`/`End` przechodzą po zakładkach (z pominięciem wyłączonych) i od razu je
|
|
24
|
-
* aktywują, a kolejny `Tab` prowadzi do panelu.
|
|
40
|
+
* aktywują, a kolejny `Tab` prowadzi do odnośników (jeśli są) i dalej do panelu.
|
|
41
|
+
*
|
|
42
|
+
* Zakładka z `href` jest odnośnikiem: stoi za zakładkami, poza `tablist` (ta może zawierać wyłącznie
|
|
43
|
+
* zakładki), nie ma panelu i nie liczy się do `activeIndex`.
|
|
25
44
|
*
|
|
26
45
|
* Podkreślenie aktywnej zakładki (`__ink`) jest mierzone z DOM przycisku, nie liczone z indeksu:
|
|
27
46
|
* zakładki mają różne szerokości, a etykiety zmieniają się z językiem.
|
|
@@ -29,10 +48,10 @@ KptTab.kptTabsSlot = 'tab';
|
|
|
29
48
|
* @example
|
|
30
49
|
* <KptTabs>
|
|
31
50
|
* <KptTabs.Tab label="Ogólne">...</KptTabs.Tab>
|
|
32
|
-
* <KptTabs.Tab label="Zaawansowane">...</KptTabs.Tab>
|
|
51
|
+
* <KptTabs.Tab label="Zaawansowane" iconEnd="settings">...</KptTabs.Tab>
|
|
33
52
|
* </KptTabs>
|
|
34
53
|
*/
|
|
35
|
-
export function KptTabs({ activeIndex, defaultActiveIndex = 0, onActiveIndexChange, animated = true, animationDuration = 500, className, style, children, ...rest }) {
|
|
54
|
+
export function KptTabs({ activeIndex, defaultActiveIndex = 0, onActiveIndexChange, animated = true, animationDuration = 500, onNavigate, className, style, children, ...rest }) {
|
|
36
55
|
const [active, setActive] = useControllableState({
|
|
37
56
|
value: activeIndex,
|
|
38
57
|
defaultValue: defaultActiveIndex,
|
|
@@ -43,6 +62,7 @@ export function KptTabs({ activeIndex, defaultActiveIndex = 0, onActiveIndexChan
|
|
|
43
62
|
const buttonsRef = useRef([]);
|
|
44
63
|
const baseId = useKptId(undefined, 'kpt-tabs');
|
|
45
64
|
const tabs = [];
|
|
65
|
+
const links = [];
|
|
46
66
|
const contents = [];
|
|
47
67
|
for (const child of slotChildren(children)) {
|
|
48
68
|
if (!isValidElement(child) || child.type?.kptTabsSlot !== 'tab') {
|
|
@@ -50,9 +70,18 @@ export function KptTabs({ activeIndex, defaultActiveIndex = 0, onActiveIndexChan
|
|
|
50
70
|
continue;
|
|
51
71
|
}
|
|
52
72
|
const props = child.props;
|
|
73
|
+
if (props.href) {
|
|
74
|
+
links.push(props);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
53
77
|
tabs.push(props);
|
|
54
78
|
contents.push(props.children);
|
|
55
79
|
}
|
|
80
|
+
const onLinkClick = (event, link) => {
|
|
81
|
+
if (link.href && onNavigate && kptIsPlainClick(event, event.currentTarget.target)) {
|
|
82
|
+
onNavigate({ item: link, url: link.href, event: event.nativeEvent });
|
|
83
|
+
}
|
|
84
|
+
};
|
|
56
85
|
const disabled = tabs.map((tab) => !!tab.disabled);
|
|
57
86
|
/**
|
|
58
87
|
* Zakładka pokazywana i jedyna w kolejności Tab: aktywna, a gdy ta jest wyłączona - pierwsza
|
|
@@ -102,8 +131,11 @@ export function KptTabs({ activeIndex, defaultActiveIndex = 0, onActiveIndexChan
|
|
|
102
131
|
['--kpt-anim-duration']: `${animated ? animationDuration : 0}ms`,
|
|
103
132
|
['--kpt-anim-dir']: direction,
|
|
104
133
|
...style,
|
|
105
|
-
}, ...rest, children: [_jsxs("div", { ref: headerRef, className: "kpt-tabs__header", role: "tablist", onKeyDown: onKeyDown, children: [tabs.map((tab, index) => (_jsx("button", { ref: (el) => {
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
}, ...rest, children: [_jsxs("div", { ref: headerRef, className: "kpt-tabs__header", children: [_jsxs("div", { className: "kpt-tabs__list", role: "tablist", onKeyDown: onKeyDown, children: [tabs.map((tab, index) => (_jsx("button", { ref: (el) => {
|
|
135
|
+
buttonsRef.current[index] = el;
|
|
136
|
+
}, type: "button", className: cn('kpt-tabs__tab', index === shown && 'is-active'), role: "tab", id: `${baseId}-tab-${index}`, disabled: tab.disabled, "aria-selected": index === shown, "aria-controls": index === shown ? `${baseId}-panel` : undefined, tabIndex: index === shown ? 0 : -1, onClick: () => select(index), children: _jsx(TabInner, { tab: tab }) }, index))), _jsx("span", { className: "kpt-tabs__ink", style: { width: ink.w, transform: `translateX(${ink.x}px)` } })] }), links.length > 0 && (_jsx("div", { className: "kpt-tabs__links", children: links.map((link, index) =>
|
|
137
|
+
// Wyłączony odnośnik nie jest `<a>` - z `href` dałoby się go otworzyć klawiaturą albo
|
|
138
|
+
// środkowym przyciskiem, a `<a>` bez `href` i tak nie jest już odnośnikiem.
|
|
139
|
+
link.disabled ? (_jsx("span", { className: "kpt-tabs__tab kpt-tabs__link", role: "link", "aria-disabled": "true", children: _jsx(TabInner, { tab: link }) }, index)) : (_jsx("a", { className: "kpt-tabs__tab kpt-tabs__link", href: link.href, target: link.target, rel: link.rel ?? (link.target === '_blank' ? 'noopener noreferrer' : undefined), onClick: (event) => onLinkClick(event, link), children: _jsx(TabInner, { tab: link }) }, index))) }))] }), _jsx("div", { className: "kpt-tabs__body", role: "tabpanel", tabIndex: 0, id: `${baseId}-panel`, "aria-labelledby": `${baseId}-tab-${shown}`, children: contents[shown] !== undefined ? (_jsx("div", { className: "kpt-tabs__panel", children: contents[shown] }, shown)) : null })] }));
|
|
108
140
|
}
|
|
109
141
|
KptTabs.Tab = KptTab;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konce-pt/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Open-source React 19 component library on the Koncept UI design system - the same kpt-* CSS layer and --kpt-* tokens as the Angular port. Zero runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "konce.pt",
|
|
@@ -65,19 +65,19 @@
|
|
|
65
65
|
"leaflet": "^1.9.0"
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@konce-pt/chart": "0.
|
|
69
|
-
"@konce-pt/
|
|
70
|
-
"@konce-pt/
|
|
71
|
-
"@konce-pt/
|
|
72
|
-
"@konce-pt/
|
|
73
|
-
"@konce-pt/
|
|
74
|
-
"@konce-pt/
|
|
75
|
-
"@konce-pt/
|
|
76
|
-
"@konce-pt/
|
|
77
|
-
"@konce-pt/
|
|
78
|
-
"@konce-pt/
|
|
79
|
-
"@konce-pt/
|
|
80
|
-
"@konce-pt/
|
|
68
|
+
"@konce-pt/chart": "0.17.0",
|
|
69
|
+
"@konce-pt/chat": "0.17.0",
|
|
70
|
+
"@konce-pt/backdrop": "0.17.0",
|
|
71
|
+
"@konce-pt/mention": "0.17.0",
|
|
72
|
+
"@konce-pt/motion": "0.17.0",
|
|
73
|
+
"@konce-pt/roadmap": "0.17.0",
|
|
74
|
+
"@konce-pt/icons": "0.17.0",
|
|
75
|
+
"@konce-pt/datetime": "0.17.0",
|
|
76
|
+
"@konce-pt/table": "0.17.0",
|
|
77
|
+
"@konce-pt/speech": "0.17.0",
|
|
78
|
+
"@konce-pt/i18n": "0.17.0",
|
|
79
|
+
"@konce-pt/tokens": "0.17.0",
|
|
80
|
+
"@konce-pt/map": "0.17.0"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@types/react": "^19.2.0",
|