@konce-pt/react 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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" role="tablist">
9
- <button class="kpt-tabs__tab is-active" role="tab" id="kpt-tabs-r1-tab-0"
10
- aria-selected="true" aria-controls="kpt-tabs-r1-panel" tabindex="0">General</button>
11
- <button class="kpt-tabs__tab" role="tab" id="kpt-tabs-r1-tab-1"
12
- aria-selected="false" tabindex="-1">Advanced</button>
13
- <span class="kpt-tabs__ink" style="width: 95px; transform: translateX(85px)"></span>
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 header is `role="tablist"` with `role="tab"` buttons carrying `aria-selected`; the body is
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
@@ -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
- buttonsRef.current[index] = el;
107
- }, 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: tab.label }, index))), _jsx("span", { className: "kpt-tabs__ink", style: { width: ink.w, transform: `translateX(${ink.x}px)` } })] }), _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 })] }));
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.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "Open-source React 19 component library on the Koncept UI design system - the same kpt-* CSS layer and --kpt-* tokens as the Angular port. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "konce.pt",
@@ -65,19 +65,19 @@
65
65
  "leaflet": "^1.9.0"
66
66
  },
67
67
  "dependencies": {
68
- "@konce-pt/backdrop": "0.16.1",
69
- "@konce-pt/datetime": "0.16.1",
70
- "@konce-pt/chart": "0.16.1",
71
- "@konce-pt/mention": "0.16.1",
72
- "@konce-pt/icons": "0.16.1",
73
- "@konce-pt/roadmap": "0.16.1",
74
- "@konce-pt/chat": "0.16.1",
75
- "@konce-pt/tokens": "0.16.1",
76
- "@konce-pt/i18n": "0.16.1",
77
- "@konce-pt/speech": "0.16.1",
78
- "@konce-pt/motion": "0.16.1",
79
- "@konce-pt/table": "0.16.1",
80
- "@konce-pt/map": "0.16.1"
68
+ "@konce-pt/backdrop": "0.18.0",
69
+ "@konce-pt/mention": "0.18.0",
70
+ "@konce-pt/chat": "0.18.0",
71
+ "@konce-pt/chart": "0.18.0",
72
+ "@konce-pt/speech": "0.18.0",
73
+ "@konce-pt/i18n": "0.18.0",
74
+ "@konce-pt/icons": "0.18.0",
75
+ "@konce-pt/motion": "0.18.0",
76
+ "@konce-pt/roadmap": "0.18.0",
77
+ "@konce-pt/datetime": "0.18.0",
78
+ "@konce-pt/table": "0.18.0",
79
+ "@konce-pt/tokens": "0.18.0",
80
+ "@konce-pt/map": "0.18.0"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@types/react": "^19.2.0",