gbs-add-block 1.2.13 → 1.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +4 -12
  2. package/index.cjs +9 -0
  3. package/package.json +1 -1
  4. package/source/beta-components/breadcrumb/README.md +34 -0
  5. package/source/beta-components/breadcrumb/__tests__/core.test.ts +58 -0
  6. package/source/beta-components/breadcrumb/core/index.ts +4 -0
  7. package/source/beta-components/breadcrumb/core/trail.ts +42 -0
  8. package/source/beta-components/breadcrumb/core/types.ts +20 -0
  9. package/source/beta-components/breadcrumb/index.ts +5 -0
  10. package/source/beta-components/breadcrumb/react/Breadcrumb.tsx +177 -0
  11. package/source/beta-components/breadcrumb/react/locale.ts +6 -0
  12. package/source/beta-components/breadcrumb/react/props.ts +14 -0
  13. package/source/beta-components/breadcrumb/styles.css +139 -0
  14. package/source/beta-components/button/README.md +32 -0
  15. package/source/beta-components/button/__tests__/core.test.ts +31 -0
  16. package/source/beta-components/button/core/index.ts +3 -0
  17. package/source/beta-components/button/core/state.ts +16 -0
  18. package/source/beta-components/button/core/types.ts +28 -0
  19. package/source/beta-components/button/index.ts +5 -0
  20. package/source/beta-components/button/react/Button.tsx +173 -0
  21. package/source/beta-components/button/react/locale.ts +5 -0
  22. package/source/beta-components/button/react/props.ts +23 -0
  23. package/source/beta-components/button/styles.css +233 -0
  24. package/source/beta-components/checkbox/README.md +36 -0
  25. package/source/beta-components/checkbox/__tests__/core.test.ts +40 -0
  26. package/source/beta-components/checkbox/core/group.ts +30 -0
  27. package/source/beta-components/checkbox/core/index.ts +3 -0
  28. package/source/beta-components/checkbox/core/types.ts +17 -0
  29. package/source/beta-components/checkbox/index.ts +9 -0
  30. package/source/beta-components/checkbox/react/Checkbox.tsx +146 -0
  31. package/source/beta-components/checkbox/react/CheckboxGroup.tsx +149 -0
  32. package/source/beta-components/checkbox/react/context.ts +16 -0
  33. package/source/beta-components/checkbox/react/locale.ts +5 -0
  34. package/source/beta-components/checkbox/react/props.ts +6 -0
  35. package/source/beta-components/checkbox/styles.css +212 -0
  36. package/source/beta-components/dialog/README.md +39 -0
  37. package/source/beta-components/dialog/__tests__/core.test.ts +86 -0
  38. package/source/beta-components/dialog/core/api.ts +34 -0
  39. package/source/beta-components/dialog/core/dialog.ts +11 -0
  40. package/source/beta-components/dialog/core/index.ts +7 -0
  41. package/source/beta-components/dialog/core/store.ts +81 -0
  42. package/source/beta-components/dialog/core/types.ts +59 -0
  43. package/source/beta-components/dialog/index.ts +7 -0
  44. package/source/beta-components/dialog/react/Dialog.tsx +279 -0
  45. package/source/beta-components/dialog/react/DialogHost.tsx +45 -0
  46. package/source/beta-components/dialog/react/icons.tsx +51 -0
  47. package/source/beta-components/dialog/react/locale.ts +8 -0
  48. package/source/beta-components/dialog/react/props.ts +13 -0
  49. package/source/beta-components/dialog/styles.css +278 -0
  50. package/source/beta-components/input/README.md +44 -0
  51. package/source/beta-components/input/__tests__/core.test.ts +75 -0
  52. package/source/beta-components/input/core/count.ts +14 -0
  53. package/source/beta-components/input/core/index.ts +11 -0
  54. package/source/beta-components/input/core/otp.ts +64 -0
  55. package/source/beta-components/input/core/types.ts +14 -0
  56. package/source/beta-components/input/index.ts +8 -0
  57. package/source/beta-components/input/react/Input.tsx +219 -0
  58. package/source/beta-components/input/react/OtpInput.tsx +256 -0
  59. package/source/beta-components/input/react/dom.ts +10 -0
  60. package/source/beta-components/input/react/icons.tsx +35 -0
  61. package/source/beta-components/input/react/locale.ts +9 -0
  62. package/source/beta-components/input/react/props.ts +6 -0
  63. package/source/beta-components/input/styles.css +296 -0
  64. package/source/beta-components/modal/README.md +51 -0
  65. package/source/beta-components/modal/__tests__/core.test.ts +55 -0
  66. package/source/beta-components/modal/core/dismiss.ts +29 -0
  67. package/source/beta-components/modal/core/index.ts +3 -0
  68. package/source/beta-components/modal/core/types.ts +27 -0
  69. package/source/beta-components/modal/index.ts +5 -0
  70. package/source/beta-components/modal/react/Modal.tsx +238 -0
  71. package/source/beta-components/modal/react/icons.tsx +19 -0
  72. package/source/beta-components/modal/react/locale.ts +5 -0
  73. package/source/beta-components/modal/react/props.ts +17 -0
  74. package/source/beta-components/modal/styles.css +235 -0
  75. package/source/beta-components/spinner/README.md +29 -0
  76. package/source/beta-components/spinner/__tests__/core.test.ts +48 -0
  77. package/source/beta-components/spinner/core/index.ts +3 -0
  78. package/source/beta-components/spinner/core/types.ts +28 -0
  79. package/source/beta-components/spinner/core/visibility.ts +34 -0
  80. package/source/beta-components/spinner/index.ts +6 -0
  81. package/source/beta-components/spinner/react/Spinner.tsx +96 -0
  82. package/source/beta-components/spinner/react/locale.ts +5 -0
  83. package/source/beta-components/spinner/react/props.ts +4 -0
  84. package/source/beta-components/spinner/react/useDelayedLoading.ts +39 -0
  85. package/source/beta-components/spinner/styles.css +167 -0
  86. package/source/beta-components/tabs/README.md +32 -0
  87. package/source/beta-components/tabs/__tests__/core.test.ts +51 -0
  88. package/source/beta-components/tabs/core/index.ts +3 -0
  89. package/source/beta-components/tabs/core/navigation.ts +54 -0
  90. package/source/beta-components/tabs/core/types.ts +16 -0
  91. package/source/beta-components/tabs/index.ts +5 -0
  92. package/source/beta-components/tabs/react/Tabs.tsx +260 -0
  93. package/source/beta-components/tabs/react/context.ts +21 -0
  94. package/source/beta-components/tabs/styles.css +257 -0
  95. package/source/beta-components/textarea/README.md +39 -0
  96. package/source/beta-components/textarea/__tests__/core.test.ts +38 -0
  97. package/source/beta-components/textarea/core/count.ts +14 -0
  98. package/source/beta-components/textarea/core/index.ts +4 -0
  99. package/source/beta-components/textarea/core/size.ts +23 -0
  100. package/source/beta-components/textarea/core/types.ts +17 -0
  101. package/source/beta-components/textarea/index.ts +5 -0
  102. package/source/beta-components/textarea/react/Textarea.tsx +209 -0
  103. package/source/beta-components/textarea/react/locale.ts +5 -0
  104. package/source/beta-components/textarea/react/props.ts +4 -0
  105. package/source/beta-components/textarea/styles.css +159 -0
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import { useMemo, type CSSProperties, type HTMLAttributes, type ReactNode } from "react";
4
+ import { spinnerPixels } from "../core/visibility";
5
+ import type { SpinnerLocaleText, SpinnerSize, SpinnerVariant } from "../core/types";
6
+ import { defaultSpinnerText } from "./locale";
7
+ import { cx, type SpinnerSlot } from "./props";
8
+ import { useDelayedLoading } from "./useDelayedLoading";
9
+
10
+ export interface SpinnerProps extends Omit<HTMLAttributes<HTMLElement>, "children"> {
11
+ /** Default true. With `children`, the spinner covers them while this is true. */
12
+ loading?: boolean;
13
+ /** Default `md` (24 px); a number is pixels. */
14
+ size?: SpinnerSize | number;
15
+ /** Default `ring`. */
16
+ variant?: SpinnerVariant;
17
+ /** What screen readers announce. Default "Loading". */
18
+ label?: string;
19
+ /** Show the label next to the spinner. */
20
+ showLabel?: boolean;
21
+ /** Milliseconds to wait before showing, so fast work doesn't flash. Default 0. */
22
+ delay?: number;
23
+ /** Once shown, stay at least this many milliseconds. Default 0. */
24
+ minDuration?: number;
25
+ /** Content to cover while loading. It becomes inert, so it can't be clicked or focused. */
26
+ children?: ReactNode;
27
+ classNames?: Partial<Record<SpinnerSlot, string>>;
28
+ localeText?: Partial<SpinnerLocaleText>;
29
+ }
30
+
31
+ /** A loading indicator, on its own or covering content that is busy. */
32
+ export function Spinner(props: SpinnerProps) {
33
+ const {
34
+ loading = true,
35
+ size = "md",
36
+ variant = "ring",
37
+ label,
38
+ showLabel = false,
39
+ delay = 0,
40
+ minDuration = 0,
41
+ children,
42
+ className,
43
+ classNames,
44
+ style,
45
+ localeText,
46
+ ...rest
47
+ } = props;
48
+
49
+ const text = useMemo(() => ({ ...defaultSpinnerText, ...localeText }), [localeText]);
50
+ const visible = useDelayedLoading(loading, { delay, minDuration });
51
+ const sized = { "--sp-size": `${spinnerPixels(size)}px`, ...style } as CSSProperties;
52
+
53
+ const status = (
54
+ <span role="status" className={cx("sp-status", classNames?.status)}>
55
+ <span className={cx("sp-indicator", classNames?.indicator)} data-variant={variant} aria-hidden="true">
56
+ {variant === "dots" ? (
57
+ <>
58
+ <span className="sp-dot" />
59
+ <span className="sp-dot" />
60
+ <span className="sp-dot" />
61
+ </>
62
+ ) : (
63
+ <svg className="sp-ring" viewBox="0 0 24 24" focusable="false">
64
+ <circle className="sp-track" cx="12" cy="12" r="9.5" />
65
+ <circle className="sp-arc" cx="12" cy="12" r="9.5" pathLength={100} />
66
+ </svg>
67
+ )}
68
+ </span>
69
+ <span className={showLabel ? cx("sp-label", classNames?.label) : "sp-sr-only"}>{label ?? text.loading}</span>
70
+ </span>
71
+ );
72
+
73
+ if (children === undefined) {
74
+ if (!visible) return null;
75
+ return (
76
+ <span {...rest} className={cx("sp-root", classNames?.root, className)} style={sized} data-variant={variant}>
77
+ {status}
78
+ </span>
79
+ );
80
+ }
81
+
82
+ return (
83
+ <div
84
+ {...rest}
85
+ className={cx("sp-container", classNames?.root, className)}
86
+ style={sized}
87
+ aria-busy={visible || undefined}
88
+ data-loading={visible || undefined}
89
+ >
90
+ <div className={cx("sp-content", classNames?.content)} inert={visible}>
91
+ {children}
92
+ </div>
93
+ {visible && <div className={cx("sp-overlay", classNames?.overlay)}>{status}</div>}
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,5 @@
1
+ import type { SpinnerLocaleText } from "../core/types";
2
+
3
+ export const defaultSpinnerText: SpinnerLocaleText = {
4
+ loading: "Loading",
5
+ };
@@ -0,0 +1,4 @@
1
+ export type SpinnerSlot = "root" | "status" | "indicator" | "label" | "content" | "overlay";
2
+
3
+ export const cx = (...names: (string | false | null | undefined)[]) =>
4
+ names.filter(Boolean).join(" ");
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { stepVisibility } from "../core/visibility";
5
+ import type { VisibilityMarks, VisibilityOptions } from "../core/types";
6
+
7
+ /**
8
+ * `loading`, smoothed for display: true only after `delay` has passed, and then
9
+ * for at least `minDuration`. Use it for any loading UI, not just the Spinner.
10
+ */
11
+ export function useDelayedLoading(loading: boolean, options: VisibilityOptions = {}): boolean {
12
+ const { delay = 0, minDuration = 0 } = options;
13
+ const immediate = delay <= 0 && minDuration <= 0;
14
+ const [visible, setVisible] = useState(() => loading && delay <= 0);
15
+ const marks = useRef<VisibilityMarks>({ visible: loading && delay <= 0, loadingSince: null, visibleSince: null });
16
+
17
+ useEffect(() => {
18
+ if (immediate) return;
19
+ const current = marks.current;
20
+ const now = Date.now();
21
+ current.loadingSince = loading ? (current.loadingSince ?? now) : null;
22
+ if (current.visible) current.visibleSince ??= now;
23
+
24
+ let timer: ReturnType<typeof setTimeout> | undefined;
25
+ const evaluate = () => {
26
+ const step = stepVisibility(loading, current, Date.now(), { delay, minDuration });
27
+ if (step.visible !== current.visible) {
28
+ current.visible = step.visible;
29
+ current.visibleSince = step.visible ? Date.now() : null;
30
+ setVisible(step.visible);
31
+ }
32
+ if (step.recheckIn !== null) timer = setTimeout(evaluate, step.recheckIn);
33
+ };
34
+ timer = setTimeout(evaluate, 0);
35
+ return () => clearTimeout(timer);
36
+ }, [loading, delay, minDuration, immediate]);
37
+
38
+ return immediate ? loading : visible;
39
+ }
@@ -0,0 +1,167 @@
1
+ /*
2
+ * Spinner styles.
3
+ *
4
+ * The indicator draws in `currentColor`, so it takes the color of its
5
+ * surroundings. Tokens read the DataGrid's --dg-* variables when they are set on
6
+ * an ancestor. Rules live in the `components` layer, so utility classes passed
7
+ * through `className` / `classNames` override them.
8
+ */
9
+ @layer theme, base, components, utilities;
10
+
11
+ @layer components {
12
+ .sp-root,
13
+ .sp-container {
14
+ --sp-size: 24px;
15
+ --sp-color: currentColor;
16
+ --sp-track: color-mix(in oklab, currentColor 18%, transparent);
17
+ --sp-duration: 0.8s;
18
+ --sp-overlay-bg: color-mix(in oklab, var(--dg-bg, light-dark(#ffffff, #0b0b0e)) 72%, transparent);
19
+ --sp-overlay-color: var(--dg-accent, light-dark(#2563eb, #60a5fa));
20
+ --sp-font-size: var(--dg-font-size, 13px);
21
+
22
+ color-scheme: inherit;
23
+ }
24
+
25
+ :where(.sp-root, .sp-container) *,
26
+ :where(.sp-root, .sp-container) *::before,
27
+ :where(.sp-root, .sp-container) *::after {
28
+ box-sizing: border-box;
29
+ }
30
+
31
+ .sp-root {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ vertical-align: middle;
35
+ color: var(--sp-color);
36
+ font-size: var(--sp-font-size);
37
+ }
38
+
39
+ .sp-status {
40
+ display: inline-flex;
41
+ align-items: center;
42
+ gap: 8px;
43
+ }
44
+ .sp-label {
45
+ line-height: 1.4;
46
+ }
47
+
48
+ .sp-indicator {
49
+ display: inline-grid;
50
+ flex: none;
51
+ place-items: center;
52
+ width: var(--sp-size);
53
+ height: var(--sp-size);
54
+ }
55
+
56
+ /* ---------------------------------------------------------------- ring */
57
+
58
+ .sp-ring {
59
+ width: 100%;
60
+ height: 100%;
61
+ animation: sp-rotate var(--sp-duration) linear infinite;
62
+ }
63
+ .sp-track,
64
+ .sp-arc {
65
+ fill: none;
66
+ stroke-width: 2.5;
67
+ }
68
+ .sp-track {
69
+ stroke: var(--sp-track);
70
+ }
71
+ .sp-arc {
72
+ stroke: currentColor;
73
+ stroke-linecap: round;
74
+ stroke-dasharray: 28 100;
75
+ }
76
+ @keyframes sp-rotate {
77
+ to {
78
+ transform: rotate(360deg);
79
+ }
80
+ }
81
+
82
+ /* ---------------------------------------------------------------- dots */
83
+
84
+ .sp-indicator[data-variant="dots"] {
85
+ grid-auto-flow: column;
86
+ gap: calc(var(--sp-size) * 0.1);
87
+ }
88
+ .sp-dot {
89
+ width: calc(var(--sp-size) * 0.22);
90
+ aspect-ratio: 1;
91
+ border-radius: 50%;
92
+ background: currentColor;
93
+ animation: sp-pulse calc(var(--sp-duration) * 1.4) ease-in-out infinite;
94
+ }
95
+ .sp-dot:nth-child(2) {
96
+ animation-delay: calc(var(--sp-duration) * 0.18);
97
+ }
98
+ .sp-dot:nth-child(3) {
99
+ animation-delay: calc(var(--sp-duration) * 0.36);
100
+ }
101
+ @keyframes sp-pulse {
102
+ 0%,
103
+ 80%,
104
+ 100% {
105
+ opacity: 0.25;
106
+ scale: 0.7;
107
+ }
108
+ 40% {
109
+ opacity: 1;
110
+ scale: 1;
111
+ }
112
+ }
113
+
114
+ /* ----------------------------------------------------------- container */
115
+
116
+ .sp-container {
117
+ position: relative;
118
+ }
119
+ .sp-overlay {
120
+ position: absolute;
121
+ inset: 0;
122
+ z-index: 1;
123
+ display: grid;
124
+ place-items: center;
125
+ border-radius: inherit;
126
+ background: var(--sp-overlay-bg);
127
+ color: var(--sp-overlay-color);
128
+ font-size: var(--sp-font-size);
129
+ animation: sp-fade 150ms ease both;
130
+ }
131
+ @keyframes sp-fade {
132
+ from {
133
+ opacity: 0;
134
+ }
135
+ }
136
+
137
+ .sp-sr-only {
138
+ position: absolute;
139
+ width: 1px;
140
+ height: 1px;
141
+ margin: -1px;
142
+ padding: 0;
143
+ overflow: hidden;
144
+ clip-path: inset(50%);
145
+ white-space: nowrap;
146
+ border: 0;
147
+ }
148
+
149
+ /* Slower, not stopped: a frozen spinner reads as a hang. */
150
+ @media (prefers-reduced-motion: reduce) {
151
+ .sp-root,
152
+ .sp-container {
153
+ --sp-duration: 2.4s;
154
+ }
155
+ .sp-overlay {
156
+ animation: none;
157
+ }
158
+ }
159
+
160
+ @media (forced-colors: active) {
161
+ .sp-arc,
162
+ .sp-dot {
163
+ stroke: CanvasText;
164
+ background: CanvasText;
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,32 @@
1
+ # Tabs
2
+
3
+ Accessible tabs for React 19 following the WAI-ARIA tabs pattern, styled to
4
+ match the rest of the library. No runtime dependencies besides React.
5
+
6
+ ```ts
7
+ import { Tab, TabList, TabPanel, Tabs } from "@/components/tabs";
8
+ import "@/components/tabs/styles.css";
9
+ ```
10
+
11
+ ```tsx
12
+ <Tabs defaultValue="overview">
13
+ <TabList aria-label="Project">
14
+ <Tab value="overview">Overview</Tab>
15
+ <Tab value="activity" badge={3}>Activity</Tab>
16
+ <Tab value="billing" disabled>Billing</Tab>
17
+ </TabList>
18
+ <TabPanel value="overview">…</TabPanel>
19
+ <TabPanel value="activity">…</TabPanel>
20
+ </Tabs>
21
+ ```
22
+
23
+ **Tabs:** `value` / `defaultValue` / `onValueChange`, `orientation`
24
+ (`horizontal` `vertical`), `activation` (`automatic` `manual`), `variant` (`line`
25
+ `pills` `enclosed`), `size`, `keepMounted`.
26
+ **Tab:** `value`, `disabled`, `icon`, `badge`.
27
+ **TabPanel:** `value`, `keepMounted`.
28
+
29
+ Arrow keys move between tabs (swapped in right-to-left layouts), Home and End
30
+ jump, and disabled tabs are skipped. `keepMounted` keeps hidden panels' state
31
+ using React's `<Activity>`. `nextTab` and `tabDomId` are exported from the
32
+ framework-free `core`.
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { nextTab, tabDomId } from "../core/navigation";
3
+
4
+ const tabs = [{}, { disabled: true }, {}, {}];
5
+
6
+ describe("keyboard movement", () => {
7
+ it("moves with the arrows, skipping disabled tabs", () => {
8
+ expect(nextTab(tabs, 0, "ArrowRight")).toBe(2);
9
+ expect(nextTab(tabs, 2, "ArrowLeft")).toBe(0);
10
+ });
11
+
12
+ it("wraps around by default, or stops at the ends", () => {
13
+ expect(nextTab(tabs, 3, "ArrowRight")).toBe(0);
14
+ expect(nextTab(tabs, 0, "ArrowLeft")).toBe(3);
15
+ expect(nextTab(tabs, 3, "ArrowRight", { loop: false })).toBeNull();
16
+ expect(nextTab(tabs, 0, "ArrowLeft", { loop: false })).toBeNull();
17
+ });
18
+
19
+ it("jumps with Home and End", () => {
20
+ expect(nextTab(tabs, 2, "Home")).toBe(0);
21
+ expect(nextTab(tabs, 0, "End")).toBe(3);
22
+ expect(nextTab([{ disabled: true }, {}, { disabled: true }], 1, "End")).toBe(1);
23
+ });
24
+
25
+ it("uses up and down when vertical, and ignores the other axis", () => {
26
+ expect(nextTab(tabs, 0, "ArrowDown", { orientation: "vertical" })).toBe(2);
27
+ expect(nextTab(tabs, 2, "ArrowUp", { orientation: "vertical" })).toBe(0);
28
+ expect(nextTab(tabs, 0, "ArrowRight", { orientation: "vertical" })).toBeNull();
29
+ expect(nextTab(tabs, 0, "ArrowDown")).toBeNull();
30
+ });
31
+
32
+ it("swaps left and right in right-to-left layouts", () => {
33
+ expect(nextTab(tabs, 0, "ArrowLeft", { rtl: true })).toBe(2);
34
+ expect(nextTab(tabs, 2, "ArrowRight", { rtl: true })).toBe(0);
35
+ });
36
+
37
+ it("returns null when nothing can take focus", () => {
38
+ expect(nextTab([], 0, "ArrowRight")).toBeNull();
39
+ expect(nextTab([{ disabled: true }], 0, "ArrowRight")).toBeNull();
40
+ expect(nextTab(tabs, 0, "Enter")).toBeNull();
41
+ });
42
+ });
43
+
44
+ describe("ids", () => {
45
+ it("makes safe, distinct ids from any value", () => {
46
+ expect(tabDomId("t1", "tab", "overview")).toBe("t1-tab-overview");
47
+ expect(tabDomId("t1", "panel", "billing & plans")).toBe("t1-panel-billing_20_26_20plans");
48
+ expect(tabDomId("t1", "tab", "a b")).not.toBe(tabDomId("t1", "tab", "a_b"));
49
+ expect(tabDomId("t1", "tab", "v1.2")).toMatch(/^[\w-]+$/);
50
+ });
51
+ });
@@ -0,0 +1,3 @@
1
+ // Framework-free: keyboard movement and ids for the tabs pattern.
2
+ export { nextTab, tabDomId } from "./navigation";
3
+ export type * from "./types";
@@ -0,0 +1,54 @@
1
+ import type { NavigationOptions } from "./types";
2
+
3
+ /**
4
+ * The tab a key moves to, skipping disabled tabs, or null when the key isn't a
5
+ * tab key (or there is nowhere to go). Follows the WAI-ARIA tabs pattern.
6
+ */
7
+ export function nextTab(
8
+ tabs: readonly { disabled?: boolean }[],
9
+ current: number,
10
+ key: string,
11
+ options: NavigationOptions = {},
12
+ ): number | null {
13
+ const { orientation = "horizontal", rtl = false, loop = true } = options;
14
+ const count = tabs.length;
15
+ if (count === 0) return null;
16
+
17
+ const horizontal = orientation === "horizontal";
18
+ const previousKey = horizontal ? (rtl ? "ArrowRight" : "ArrowLeft") : "ArrowUp";
19
+ const nextKey = horizontal ? (rtl ? "ArrowLeft" : "ArrowRight") : "ArrowDown";
20
+
21
+ const scan = (start: number, step: 1 | -1): number | null => {
22
+ let index = start;
23
+ for (let visited = 0; visited < count; visited++) {
24
+ if (index < 0 || index >= count) {
25
+ if (!loop) return null;
26
+ index = (index + count) % count;
27
+ }
28
+ if (!tabs[index].disabled) return index;
29
+ index += step;
30
+ }
31
+ return null;
32
+ };
33
+
34
+ switch (key) {
35
+ case nextKey:
36
+ return scan(current + 1, 1);
37
+ case previousKey:
38
+ return scan(current - 1, -1);
39
+ case "Home":
40
+ return scan(0, 1);
41
+ case "End":
42
+ return scan(count - 1, -1);
43
+ default:
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * A DOM id for a tab or panel. Values may contain spaces or symbols, so they are
50
+ * percent-encoded with `_` in place of `%`, which keeps different values distinct.
51
+ */
52
+ export function tabDomId(base: string, part: "tab" | "panel", value: string): string {
53
+ return `${base}-${part}-${encodeURIComponent(value).replace(/[%.!~*'()]/g, "_")}`;
54
+ }
@@ -0,0 +1,16 @@
1
+ export type TabsOrientation = "horizontal" | "vertical";
2
+
3
+ /** `automatic` selects a tab as soon as it gets focus; `manual` waits for Enter or Space. */
4
+ export type TabsActivation = "automatic" | "manual";
5
+
6
+ export type TabsVariant = "line" | "pills" | "enclosed";
7
+
8
+ export type TabsSize = "sm" | "md" | "lg";
9
+
10
+ export interface NavigationOptions {
11
+ orientation?: TabsOrientation;
12
+ /** Right-to-left: the horizontal arrow keys swap. */
13
+ rtl?: boolean;
14
+ /** Wrap from the last tab to the first. Default true. */
15
+ loop?: boolean;
16
+ }
@@ -0,0 +1,5 @@
1
+ export { Tab, TabList, TabPanel, Tabs } from "./react/Tabs";
2
+ export type { TabListProps, TabPanelProps, TabProps, TabsProps } from "./react/Tabs";
3
+ export { TabsContext, useTabsContext } from "./react/context";
4
+ export type { TabsContextValue } from "./react/context";
5
+ export * from "./core";