ab-nextjs-components 0.1.2 → 0.1.3

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 (112) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +60 -14
  3. package/ab-avatar/index.tsx +90 -0
  4. package/ab-avatar/styles.module.css +62 -0
  5. package/ab-balloon/index.tsx +55 -0
  6. package/ab-balloon/styles.module.css +12 -0
  7. package/ab-brand/index.tsx +88 -0
  8. package/ab-brand/styles.module.css +54 -0
  9. package/ab-button/index.tsx +164 -0
  10. package/ab-button/styles.module.css +63 -0
  11. package/ab-collapsible/index.tsx +188 -0
  12. package/ab-collapsible/styles.module.css +98 -0
  13. package/ab-color/index.tsx +84 -0
  14. package/ab-color/styles.module.css +51 -0
  15. package/ab-demo-box/index.tsx +38 -0
  16. package/ab-demo-box/styles.module.css +13 -0
  17. package/ab-demo-code/index.tsx +62 -0
  18. package/ab-demo-code/styles.module.css +41 -0
  19. package/ab-icon/index.tsx +63 -0
  20. package/ab-icon/styles.module.css +27 -0
  21. package/ab-icon-button/index.tsx +107 -0
  22. package/ab-icon-button/styles.module.css +64 -0
  23. package/ab-image/index.tsx +83 -0
  24. package/ab-image/styles.module.css +27 -0
  25. package/ab-info/index.tsx +89 -0
  26. package/ab-info/styles.module.css +87 -0
  27. package/ab-input/index.tsx +277 -0
  28. package/ab-input/styles.module.css +101 -0
  29. package/ab-key-feature/index.tsx +60 -0
  30. package/ab-key-feature/styles.module.css +47 -0
  31. package/ab-like-button/index.tsx +116 -0
  32. package/ab-like-button/styles.module.css +60 -0
  33. package/ab-logo/index.tsx +94 -0
  34. package/ab-logo/styles.module.css +30 -0
  35. package/ab-main-headline/index.tsx +57 -0
  36. package/ab-main-headline/styles.module.css +24 -0
  37. package/ab-menu/index.tsx +119 -0
  38. package/ab-menu/styles.module.css +99 -0
  39. package/ab-name/index.tsx +49 -0
  40. package/ab-name/styles.module.css +14 -0
  41. package/ab-navbar/index.tsx +68 -0
  42. package/ab-navbar/styles.module.css +35 -0
  43. package/ab-page-paginator/index.tsx +130 -0
  44. package/ab-page-paginator/styles.module.css +47 -0
  45. package/ab-page-switcher/index.tsx +121 -0
  46. package/ab-page-switcher/styles.module.css +37 -0
  47. package/ab-preview/index.tsx +78 -0
  48. package/ab-preview/styles.module.css +26 -0
  49. package/ab-price/index.tsx +109 -0
  50. package/ab-price/styles.module.css +36 -0
  51. package/ab-price-tag/index.tsx +70 -0
  52. package/ab-price-tag/styles.module.css +81 -0
  53. package/ab-product/index.tsx +128 -0
  54. package/ab-product/styles.module.css +44 -0
  55. package/ab-product-item/index.tsx +78 -0
  56. package/ab-product-item/styles.module.css +21 -0
  57. package/ab-progress-chips/index.tsx +105 -0
  58. package/ab-progress-chips/styles.module.css +43 -0
  59. package/ab-search-result/index.tsx +79 -0
  60. package/ab-search-result/styles.module.css +48 -0
  61. package/ab-searchbar/index.tsx +150 -0
  62. package/ab-searchbar/styles.module.css +77 -0
  63. package/ab-sidebar/index.tsx +72 -0
  64. package/ab-sidebar/styles.module.css +41 -0
  65. package/ab-switch-back/index.tsx +72 -0
  66. package/ab-switch-back/styles.module.css +45 -0
  67. package/ab-tab/index.tsx +101 -0
  68. package/ab-tab/styles.module.css +65 -0
  69. package/ab-tabs/index.tsx +45 -0
  70. package/ab-tabs/styles.module.css +17 -0
  71. package/ab-test-hello/index.tsx +37 -0
  72. package/ab-test-hello/styles.module.css +25 -0
  73. package/ab-text/index.tsx +36 -0
  74. package/ab-text/styles.module.css +3 -0
  75. package/index.ts +117 -0
  76. package/package.json +152 -7
  77. package/server/ab-avatar/index.tsx +85 -0
  78. package/server/ab-avatar/styles.module.css +62 -0
  79. package/server/ab-badge/index.tsx +66 -0
  80. package/server/ab-badge/styles.module.css +39 -0
  81. package/server/ab-brand/index.tsx +87 -0
  82. package/server/ab-brand/styles.module.css +54 -0
  83. package/server/ab-button/index.tsx +144 -0
  84. package/server/ab-button/styles.module.css +63 -0
  85. package/server/ab-icon/index.tsx +61 -0
  86. package/server/ab-icon/styles.module.css +27 -0
  87. package/server/ab-key-feature/index.tsx +59 -0
  88. package/server/ab-key-feature/styles.module.css +47 -0
  89. package/server/ab-logo/index.tsx +93 -0
  90. package/server/ab-logo/styles.module.css +30 -0
  91. package/server/ab-name/index.tsx +48 -0
  92. package/server/ab-name/styles.module.css +14 -0
  93. package/server/ab-nav-link/index.tsx +60 -0
  94. package/server/ab-nav-link/styles.module.css +32 -0
  95. package/server/ab-navbar/index.tsx +64 -0
  96. package/server/ab-navbar/styles.module.css +35 -0
  97. package/server/ab-polygon/index.tsx +45 -0
  98. package/server/ab-polygon/styles.module.css +11 -0
  99. package/server/ab-sidebar/index.tsx +68 -0
  100. package/server/ab-sidebar/styles.module.css +41 -0
  101. package/server/ab-switch-back/index.tsx +71 -0
  102. package/server/ab-switch-back/styles.module.css +45 -0
  103. package/server/ab-test-hello/index.tsx +35 -0
  104. package/server/ab-test-hello/styles.module.css +22 -0
  105. package/server/ab-text/index.tsx +33 -0
  106. package/server/ab-text/styles.module.css +3 -0
  107. package/server/index.ts +27 -0
  108. package/types/index.d.ts +54 -0
  109. package/.github/ab-logo-dark.svg +0 -12
  110. package/.github/ab-logo-light.svg +0 -9
  111. package/.github/nextjs-logoname-dark.svg +0 -28
  112. package/.github/nextjs-logoname-light.svg +0 -25
@@ -0,0 +1,150 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * @project: ab-nextjs-components
9
+ * @name: AbSearchbar - Client (port of lyd-searchbar)
10
+ * @file: ab-searchbar/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { CSSProperties, KeyboardEvent, ReactElement } from 'react';
16
+ import { useId, useRef, useState } from 'react';
17
+ import clsx from 'clsx';
18
+ import styles from './styles.module.css';
19
+
20
+ export interface AbSearchbarProps {
21
+ className?: string;
22
+ style?: CSSProperties;
23
+ hidden?: boolean;
24
+ name?: string;
25
+ value?: string;
26
+ defaultValue?: string;
27
+ placeholder?: string;
28
+ disabled?: boolean;
29
+ isLoading?: boolean;
30
+ isBusy?: boolean;
31
+ dividerHidden?: boolean;
32
+ dropdownButtonHidden?: boolean;
33
+ dropdownButtonLabel?: string;
34
+ clearLabel?: string;
35
+ onChange?: (searchValue: string) => void;
36
+ onEnter?: (searchValue: string, event?: KeyboardEvent<HTMLInputElement>) => void;
37
+ onKeyDown?: (searchValue: string, event?: KeyboardEvent<HTMLInputElement>) => void;
38
+ onKeyUp?: (searchValue: string, event?: KeyboardEvent<HTMLInputElement>) => void;
39
+ onFocus?: () => void;
40
+ onBlur?: () => void;
41
+ onClear?: () => void;
42
+ onDropdownButtonClick?: () => void;
43
+ onLoad?: (searchInputElement?: HTMLInputElement | null) => void;
44
+ }
45
+
46
+ const AbSearchbar = ({
47
+ className,
48
+ style,
49
+ hidden = false,
50
+ name = 'q',
51
+ value,
52
+ defaultValue = '',
53
+ placeholder = 'Search…',
54
+ disabled = false,
55
+ isLoading = false,
56
+ isBusy = false,
57
+ dividerHidden = false,
58
+ dropdownButtonHidden = true,
59
+ dropdownButtonLabel = 'Filters',
60
+ clearLabel = 'Clear',
61
+ onChange,
62
+ onEnter,
63
+ onKeyDown,
64
+ onKeyUp,
65
+ onFocus,
66
+ onBlur,
67
+ onClear,
68
+ onDropdownButtonClick,
69
+ onLoad,
70
+ }: AbSearchbarProps): ReactElement => {
71
+ const id = useId();
72
+ const inputRef = useRef<HTMLInputElement>(null);
73
+ const loadNotified = useRef(false);
74
+ const setInputRef = (node: HTMLInputElement | null) => {
75
+ inputRef.current = node;
76
+ if (node && !loadNotified.current) {
77
+ loadNotified.current = true;
78
+ onLoad?.(node);
79
+ }
80
+ };
81
+ const [internal, setInternal] = useState(defaultValue);
82
+ const controlled = value !== undefined;
83
+ const current = controlled ? value! : internal;
84
+ const busy = isLoading || isBusy;
85
+
86
+
87
+ const setValue = (next: string) => {
88
+ if (!controlled) setInternal(next);
89
+ onChange?.(next);
90
+ };
91
+
92
+ const clear = () => {
93
+ setValue('');
94
+ onClear?.();
95
+ inputRef.current?.focus();
96
+ };
97
+
98
+ return (
99
+ <div
100
+ hidden={hidden}
101
+ style={style}
102
+ className={clsx('AbSearchbar', styles.abSearchbar, disabled && styles.isDisabled, className)}
103
+ data-busy={busy ? 'true' : 'false'}
104
+ >
105
+ <span className={clsx('material-symbols-rounded', styles.abSearchbarIcon)} aria-hidden>
106
+ search
107
+ </span>
108
+ <input
109
+ ref={setInputRef}
110
+ id={id}
111
+ name={name}
112
+ type="search"
113
+ className={styles.abSearchbarInput}
114
+ placeholder={placeholder}
115
+ value={current}
116
+ disabled={disabled}
117
+ aria-busy={busy}
118
+ onChange={(e) => setValue(e.target.value)}
119
+ onFocus={onFocus}
120
+ onBlur={onBlur}
121
+ onKeyDown={(e) => {
122
+ onKeyDown?.(e.currentTarget.value, e);
123
+ if (e.key === 'Enter') onEnter?.(e.currentTarget.value, e);
124
+ }}
125
+ onKeyUp={(e) => onKeyUp?.(e.currentTarget.value, e)}
126
+ />
127
+ {busy ? <span className={styles.abSearchbarSpinner} aria-hidden /> : null}
128
+ {current && !disabled ? (
129
+ <button type="button" className={styles.abSearchbarClear} aria-label={clearLabel} onClick={clear}>
130
+ <span className="material-symbols-rounded" aria-hidden>close</span>
131
+ </button>
132
+ ) : null}
133
+ {!dividerHidden && !dropdownButtonHidden ? <span className={styles.abSearchbarDivider} aria-hidden /> : null}
134
+ {!dropdownButtonHidden ? (
135
+ <button
136
+ type="button"
137
+ className={styles.abSearchbarDropdown}
138
+ disabled={disabled}
139
+ onClick={onDropdownButtonClick}
140
+ >
141
+ {dropdownButtonLabel}
142
+ </button>
143
+ ) : null}
144
+ </div>
145
+ );
146
+ };
147
+
148
+ export default AbSearchbar;
149
+ export { AbSearchbar };
150
+
@@ -0,0 +1,77 @@
1
+ .abSearchbar {
2
+ position: relative;
3
+ display: flex;
4
+ width: 100%;
5
+ height: 2.5rem;
6
+ align-items: center;
7
+ gap: 0.5rem;
8
+ padding: 0 0.625rem;
9
+ box-sizing: border-box;
10
+ border-radius: 9999px;
11
+ overflow: hidden;
12
+ background: color-mix(in srgb, var(--ab-surface-container-high, #e6e0e9) 90%, transparent);
13
+ transition: background 0.3s ease;
14
+ }
15
+ .abSearchbar:focus-within {
16
+ background: var(--ab-surface-container-high, #e6e0e9);
17
+ }
18
+ .abSearchbarIcon {
19
+ font-size: 1.25rem;
20
+ opacity: 0.7;
21
+ pointer-events: none;
22
+ user-select: none;
23
+ }
24
+ .abSearchbarInput {
25
+ flex: 1;
26
+ height: 100%;
27
+ border: none;
28
+ outline: none;
29
+ background: transparent;
30
+ font: inherit;
31
+ min-width: 0;
32
+ }
33
+ .abSearchbarInput::-webkit-search-decoration,
34
+ .abSearchbarInput::-webkit-search-cancel-button {
35
+ appearance: none;
36
+ display: none;
37
+ }
38
+ .abSearchbarDivider {
39
+ width: 1px;
40
+ height: 1.25rem;
41
+ background: color-mix(in srgb, currentColor 20%, transparent);
42
+ flex-shrink: 0;
43
+ }
44
+ .abSearchbarDropdown,
45
+ .abSearchbarClear {
46
+ border: none;
47
+ background: transparent;
48
+ cursor: pointer;
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ padding: 0.25rem 0.5rem;
53
+ border-radius: 9999px;
54
+ font: inherit;
55
+ color: inherit;
56
+ }
57
+ .abSearchbarDropdown:disabled,
58
+ .abSearchbarClear:disabled {
59
+ opacity: 0.4;
60
+ cursor: not-allowed;
61
+ }
62
+ .abSearchbarSpinner {
63
+ width: 1rem;
64
+ height: 1rem;
65
+ border: 2px solid color-mix(in srgb, currentColor 25%, transparent);
66
+ border-top-color: currentColor;
67
+ border-radius: 50%;
68
+ animation: abSearchbarSpin 0.7s linear infinite;
69
+ }
70
+ @keyframes abSearchbarSpin {
71
+ to { transform: rotate(360deg); }
72
+ }
73
+ .isDisabled {
74
+ opacity: 0.55;
75
+ pointer-events: none;
76
+ }
77
+
@@ -0,0 +1,72 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * @project: ab-nextjs-components
9
+ * @name: AbSidebar - Client (port of lyd-sidebar; providers stripped)
10
+ * @file: ab-sidebar/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement } from 'react';
16
+ import Link from 'next/link';
17
+ import { usePathname } from 'next/navigation';
18
+ import clsx from 'clsx';
19
+ import styles from './styles.module.css';
20
+
21
+ export type AbSidebarLink = {
22
+ href: string;
23
+ icon: string;
24
+ value: string;
25
+ label: string;
26
+ };
27
+
28
+ export interface AbSidebarProps {
29
+ className?: string;
30
+ type?: 'customer' | 'admin';
31
+ page?: string;
32
+ links?: AbSidebarLink[];
33
+ brandHref?: string;
34
+ brandLabel?: string;
35
+ }
36
+
37
+ const AbSidebar = ({
38
+ className,
39
+ type = 'customer',
40
+ page,
41
+ links = [],
42
+ brandHref = '/',
43
+ brandLabel = 'abElements',
44
+ }: AbSidebarProps): ReactElement => {
45
+ const pathname = usePathname() ?? '';
46
+ return (
47
+ <aside data-type={type} className={clsx('AbSidebar', styles.abSidebar, className)} aria-label="Sidebar">
48
+ <Link href={brandHref} className={styles.abSidebarBrand}>
49
+ <span className="material-symbols-rounded" aria-hidden>apps</span>
50
+ <span>{brandLabel}</span>
51
+ </Link>
52
+ <ul className={styles.abSidebarList}>
53
+ {links.map((link) => {
54
+ const active = page ? page === link.value : pathname === link.href || pathname.startsWith(`${link.href}/`);
55
+ return (
56
+ <li key={link.value}>
57
+ <Link href={link.href} data-active={active ? 'true' : 'false'} className={styles.abSidebarLink}>
58
+ <span className={clsx('material-symbols-rounded', styles.abSidebarIcon)} aria-hidden>
59
+ {link.icon}
60
+ </span>
61
+ <span>{link.label}</span>
62
+ </Link>
63
+ </li>
64
+ );
65
+ })}
66
+ </ul>
67
+ </aside>
68
+ );
69
+ };
70
+
71
+ export default AbSidebar;
72
+ export { AbSidebar };
@@ -0,0 +1,41 @@
1
+ .abSidebar {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 1rem;
5
+ width: 100%;
6
+ max-width: 16rem;
7
+ padding: 1rem 0.75rem;
8
+ box-sizing: border-box;
9
+ }
10
+ .abSidebarBrand {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 0.5rem;
14
+ font-weight: 700;
15
+ text-decoration: none;
16
+ color: inherit;
17
+ }
18
+ .abSidebarList {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 0.25rem;
22
+ list-style: none;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+ .abSidebarLink {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 0.6rem;
30
+ padding: 0.55rem 0.75rem;
31
+ border-radius: 0.75rem;
32
+ text-decoration: none;
33
+ color: inherit;
34
+ opacity: 0.75;
35
+ font-weight: 600;
36
+ }
37
+ .abSidebarLink[data-active='true'] {
38
+ opacity: 1;
39
+ background: color-mix(in srgb, var(--ab-primary, #6750a4) 14%, transparent);
40
+ }
41
+ .abSidebarIcon { font-size: 1.25rem; }
@@ -0,0 +1,72 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * @project: ab-nextjs-components
9
+ * @name: AbSwitchBack - Client (port of lyd-switch-back)
10
+ * @file: ab-switch-back/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement, ReactNode } from 'react';
16
+ import Link from 'next/link';
17
+ import Image from 'next/image';
18
+ import clsx from 'clsx';
19
+ import styles from './styles.module.css';
20
+
21
+ export interface AbSwitchBackProps {
22
+ className?: string;
23
+ hidden?: boolean;
24
+ switched?: boolean;
25
+ image: string;
26
+ imageAlt: string;
27
+ title?: ReactNode;
28
+ description?: ReactNode;
29
+ cta1Label: string;
30
+ cta1Link: string;
31
+ cta2Label: string;
32
+ cta2Link: string;
33
+ }
34
+
35
+ const AbSwitchBack = ({
36
+ className,
37
+ hidden = false,
38
+ switched = false,
39
+ image,
40
+ imageAlt,
41
+ title,
42
+ description,
43
+ cta1Label,
44
+ cta1Link,
45
+ cta2Label,
46
+ cta2Link,
47
+ }: AbSwitchBackProps): ReactElement => {
48
+ return (
49
+ <section
50
+ hidden={hidden}
51
+ data-switched={switched ? 'true' : 'false'}
52
+ className={clsx('AbSwitchBack', styles.abSwitchBack, className)}
53
+ >
54
+ <div className={styles.abSwitchBackMedia}>
55
+ <Image className={styles.abSwitchBackImage} src={image} alt={imageAlt} width={800} height={600} unoptimized />
56
+ </div>
57
+ <div className={styles.abSwitchBackCopy}>
58
+ {title ? <h2 className={styles.abSwitchBackTitle}>{title}</h2> : null}
59
+ {description ? <div className={styles.abSwitchBackDesc}>{description}</div> : null}
60
+ <div className={styles.abSwitchBackActions}>
61
+ <Link href={cta1Link} className={styles.abSwitchBackCta}>{cta1Label}</Link>
62
+ <Link href={cta2Link} className={clsx(styles.abSwitchBackCta, styles.abSwitchBackCtaSecondary)}>
63
+ {cta2Label}
64
+ </Link>
65
+ </div>
66
+ </div>
67
+ </section>
68
+ );
69
+ };
70
+
71
+ export default AbSwitchBack;
72
+ export { AbSwitchBack };
@@ -0,0 +1,45 @@
1
+ .abSwitchBack {
2
+ display: grid;
3
+ grid-template-columns: 1fr;
4
+ gap: 1.25rem;
5
+ align-items: center;
6
+ }
7
+ @media (min-width: 768px) {
8
+ .abSwitchBack {
9
+ grid-template-columns: 1fr 1fr;
10
+ }
11
+ .abSwitchBack[data-switched='true'] .abSwitchBackMedia { order: 2; }
12
+ .abSwitchBack[data-switched='true'] .abSwitchBackCopy { order: 1; }
13
+ }
14
+ .abSwitchBackMedia {
15
+ position: relative;
16
+ width: 100%;
17
+ aspect-ratio: 4 / 3;
18
+ border-radius: 1rem;
19
+ overflow: hidden;
20
+ background: #ddd;
21
+ }
22
+ .abSwitchBackImage {
23
+ object-fit: cover;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+ .abSwitchBackCopy { display: flex; flex-direction: column; gap: 0.75rem; }
28
+ .abSwitchBackTitle { margin: 0; font-size: 1.5rem; font-weight: 700; }
29
+ .abSwitchBackDesc { margin: 0; opacity: 0.8; }
30
+ .abSwitchBackActions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
31
+ .abSwitchBackCta {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ padding: 0.5rem 1rem;
35
+ border-radius: 9999px;
36
+ text-decoration: none;
37
+ font-weight: 600;
38
+ background: var(--ab-primary, #6750a4);
39
+ color: #fff;
40
+ }
41
+ .abSwitchBackCtaSecondary {
42
+ background: transparent;
43
+ color: var(--ab-primary, #6750a4);
44
+ border: 1px solid color-mix(in srgb, var(--ab-primary, #6750a4) 40%, transparent);
45
+ }
@@ -0,0 +1,101 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * @project: ab-nextjs-components
9
+ * @name: AbTab - Client (port of lyd-tab)
10
+ * @file: ab-tab/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { MouseEvent, ReactElement, ReactNode } from 'react';
16
+ import { forwardRef } from 'react';
17
+ import Link from 'next/link';
18
+ import clsx from 'clsx';
19
+ import styles from './styles.module.css';
20
+
21
+ export interface AbTabProps {
22
+ className?: string;
23
+ hidden?: boolean;
24
+ id: string;
25
+ title: string;
26
+ children?: ReactNode;
27
+ noIndicator?: boolean;
28
+ disabled?: boolean;
29
+ isNaked?: boolean;
30
+ isSelected?: boolean;
31
+ href?: string | null;
32
+ onClick?: (id?: string, element?: HTMLLIElement, event?: MouseEvent<HTMLLIElement>) => void;
33
+ }
34
+
35
+ const AbTab = forwardRef<HTMLLIElement, AbTabProps>(function AbTab(
36
+ {
37
+ className,
38
+ hidden = false,
39
+ id,
40
+ title,
41
+ children,
42
+ noIndicator = false,
43
+ disabled = false,
44
+ isNaked = false,
45
+ isSelected = false,
46
+ href = null,
47
+ onClick,
48
+ },
49
+ ref,
50
+ ): ReactElement {
51
+ const handleClick = (event: MouseEvent<HTMLLIElement>) => {
52
+ if (disabled) return;
53
+ onClick?.(id, event.currentTarget, event);
54
+ };
55
+
56
+ const content = (
57
+ <>
58
+ <span className={styles.abTabText}>{children ?? title}</span>
59
+ {!noIndicator ? (
60
+ <span
61
+ className={clsx(styles.abTabIndicator, isSelected && styles.abTabIndicatorVisible)}
62
+ aria-hidden
63
+ />
64
+ ) : null}
65
+ </>
66
+ );
67
+
68
+ return (
69
+ <li
70
+ role="tab"
71
+ ref={ref}
72
+ id={id}
73
+ hidden={hidden}
74
+ aria-selected={isSelected}
75
+ aria-disabled={disabled || undefined}
76
+ data-selected={isSelected ? 'true' : 'false'}
77
+ data-naked={isNaked ? 'true' : 'false'}
78
+ data-disabled={disabled ? 'true' : 'false'}
79
+ className={clsx(
80
+ 'AbTab',
81
+ styles.abTab,
82
+ isSelected && styles.isSelected,
83
+ disabled && styles.isDisabled,
84
+ isNaked && styles.isNaked,
85
+ className,
86
+ )}
87
+ onClick={handleClick}
88
+ >
89
+ {href && !disabled ? (
90
+ <Link href={href} className={styles.abTabLink} tabIndex={disabled ? -1 : 0}>
91
+ {content}
92
+ </Link>
93
+ ) : (
94
+ content
95
+ )}
96
+ </li>
97
+ );
98
+ });
99
+
100
+ export default AbTab;
101
+ export { AbTab };
@@ -0,0 +1,65 @@
1
+ .abTab {
2
+ position: relative;
3
+ display: flex;
4
+ flex-direction: row;
5
+ align-items: center;
6
+ justify-content: center;
7
+ width: fit-content;
8
+ min-width: fit-content;
9
+ height: auto;
10
+ overflow: hidden;
11
+ font-size: 1rem;
12
+ color: var(--ab-outline, #79747e);
13
+ cursor: pointer;
14
+ list-style: none;
15
+ }
16
+
17
+ .abTab:hover:not(.isDisabled) {
18
+ color: var(--ab-on-surface, #1c1b1f);
19
+ background: color-mix(in srgb, var(--ab-inverse-surface, #313033) 10%, transparent);
20
+ }
21
+
22
+ .abTabText {
23
+ padding: 0.75rem 1rem;
24
+ overflow: hidden;
25
+ text-overflow: ellipsis;
26
+ white-space: nowrap;
27
+ }
28
+
29
+ .abTabLink {
30
+ display: flex;
31
+ align-items: center;
32
+ color: inherit;
33
+ text-decoration: none;
34
+ }
35
+
36
+ .abTabIndicator {
37
+ position: absolute;
38
+ bottom: 0;
39
+ left: 50%;
40
+ width: 50%;
41
+ height: 0.25rem;
42
+ border-radius: 0.5rem 0.5rem 0 0;
43
+ background: var(--ab-outline, #79747e);
44
+ transform: translateX(-50%);
45
+ visibility: hidden;
46
+ }
47
+
48
+ .abTabIndicatorVisible,
49
+ .isSelected .abTabIndicator {
50
+ visibility: visible;
51
+ }
52
+
53
+ .isSelected {
54
+ color: var(--ab-on-surface, #1c1b1f);
55
+ }
56
+
57
+ .isDisabled {
58
+ opacity: 0.5;
59
+ pointer-events: none;
60
+ cursor: not-allowed;
61
+ }
62
+
63
+ .isNaked:hover {
64
+ background: transparent;
65
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * @license MIT
3
+ * ~~~~~~~~~~~~
4
+ * ab-nextjs-components
5
+ * ~~~~~~~~~~~~
6
+ * Copyright (c) 2026 Abraham Ukachi. The abElements Project.
7
+ *
8
+ * @project: ab-nextjs-components
9
+ * @name: AbTabs - Client (port of lyd-tabs)
10
+ * @file: ab-tabs/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement, ReactNode } from 'react';
16
+ import { forwardRef } from 'react';
17
+ import clsx from 'clsx';
18
+ import styles from './styles.module.css';
19
+
20
+ export interface AbTabsProps {
21
+ className?: string;
22
+ hidden?: boolean;
23
+ children?: ReactNode;
24
+ dividerHidden?: boolean;
25
+ }
26
+
27
+ const AbTabs = forwardRef<HTMLUListElement, AbTabsProps>(function AbTabs(
28
+ { className, hidden = false, children, dividerHidden = false },
29
+ ref,
30
+ ): ReactElement {
31
+ return (
32
+ <ul
33
+ ref={ref}
34
+ hidden={hidden}
35
+ role="tablist"
36
+ data-divider-hidden={dividerHidden ? 'true' : 'false'}
37
+ className={clsx('AbTabs', styles.abTabs, dividerHidden && styles.noDivider, className)}
38
+ >
39
+ {children}
40
+ </ul>
41
+ );
42
+ });
43
+
44
+ export default AbTabs;
45
+ export { AbTabs };
@@ -0,0 +1,17 @@
1
+ .abTabs {
2
+ position: relative;
3
+ display: flex;
4
+ flex-direction: row;
5
+ align-items: center;
6
+ width: 100%;
7
+ height: auto;
8
+ margin: 0;
9
+ padding: 0;
10
+ overflow: auto;
11
+ list-style: none;
12
+ border-bottom: 2px solid color-mix(in srgb, var(--ab-outline, #79747e) 30%, transparent);
13
+ }
14
+
15
+ .noDivider {
16
+ border-bottom: none;
17
+ }