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,130 @@
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: AbPagePaginator - Client (port of lyd-page-paginator)
10
+ * @file: ab-page-paginator/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement } from 'react';
16
+ import { useMemo } from 'react';
17
+ import clsx from 'clsx';
18
+ import styles from './styles.module.css';
19
+
20
+ export interface AbPagePaginatorProps {
21
+ className?: string;
22
+ hidden?: boolean;
23
+ disabled?: boolean;
24
+ page: number;
25
+ total: number;
26
+ maxLength?: number;
27
+ onChange: (page: number) => void;
28
+ previousLabel?: string;
29
+ nextLabel?: string;
30
+ }
31
+
32
+ function buildPageList(current: number, total: number, maxLength: number): Array<number | 'ellipsis'> {
33
+ if (total <= 0) return [];
34
+ if (total <= maxLength) return Array.from({ length: total }, (_, i) => i + 1);
35
+
36
+ const half = Math.floor(maxLength / 2);
37
+ let start = Math.max(1, current - half);
38
+ const end = Math.min(total, start + maxLength - 1);
39
+ start = Math.max(1, end - maxLength + 1);
40
+
41
+ const pages: Array<number | 'ellipsis'> = [];
42
+ if (start > 1) {
43
+ pages.push(1);
44
+ if (start > 2) pages.push('ellipsis');
45
+ }
46
+ for (let i = start; i <= end; i += 1) pages.push(i);
47
+ if (end < total) {
48
+ if (end < total - 1) pages.push('ellipsis');
49
+ pages.push(total);
50
+ }
51
+ return pages;
52
+ }
53
+
54
+ const AbPagePaginator = ({
55
+ className,
56
+ hidden = false,
57
+ disabled = false,
58
+ page,
59
+ total,
60
+ maxLength = 5,
61
+ onChange,
62
+ previousLabel = 'Previous page',
63
+ nextLabel = 'Next page',
64
+ }: AbPagePaginatorProps): ReactElement => {
65
+ const current = page;
66
+ const pages = useMemo(() => buildPageList(current, total, maxLength), [current, total, maxLength]);
67
+
68
+ const go = (next: number) => {
69
+ if (disabled || next < 1 || next > total || next === current) return;
70
+ onChange(next);
71
+ };
72
+
73
+ return (
74
+ <nav
75
+ hidden={hidden}
76
+ className={clsx('AbPagePaginator', styles.abPagePaginator, className)}
77
+ aria-label="Pagination"
78
+ >
79
+ <button
80
+ type="button"
81
+ className={styles.abPagePaginatorBtn}
82
+ aria-label={previousLabel}
83
+ disabled={disabled || current <= 1}
84
+ onClick={() => go(current - 1)}
85
+ >
86
+ <span className="material-symbols-rounded" aria-hidden>
87
+ chevron_left
88
+ </span>
89
+ </button>
90
+ <ul className={styles.abPagePaginatorList}>
91
+ {pages.map((item, idx) =>
92
+ item === 'ellipsis' ? (
93
+ <li key={`e-${idx}`} className={styles.abPagePaginatorItem}>
94
+ <span className={styles.abPagePaginatorEllipsis} aria-hidden>
95
+ …
96
+ </span>
97
+ </li>
98
+ ) : (
99
+ <li key={item} className={styles.abPagePaginatorItem}>
100
+ <button
101
+ type="button"
102
+ className={styles.abPagePaginatorBtn}
103
+ data-active={item === current ? 'true' : 'false'}
104
+ aria-current={item === current ? 'page' : undefined}
105
+ disabled={disabled}
106
+ onClick={() => go(item)}
107
+ >
108
+ {item}
109
+ </button>
110
+ </li>
111
+ ),
112
+ )}
113
+ </ul>
114
+ <button
115
+ type="button"
116
+ className={styles.abPagePaginatorBtn}
117
+ aria-label={nextLabel}
118
+ disabled={disabled || current >= total}
119
+ onClick={() => go(current + 1)}
120
+ >
121
+ <span className="material-symbols-rounded" aria-hidden>
122
+ chevron_right
123
+ </span>
124
+ </button>
125
+ </nav>
126
+ );
127
+ };
128
+
129
+ export default AbPagePaginator;
130
+ export { AbPagePaginator };
@@ -0,0 +1,47 @@
1
+ .abPagePaginator {
2
+ display: flex;
3
+ width: fit-content;
4
+ align-items: center;
5
+ justify-content: center;
6
+ gap: 0.25rem;
7
+ margin: 0 auto;
8
+ }
9
+ .abPagePaginatorList {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 0.5rem;
13
+ padding: 0 0.5rem;
14
+ list-style: none;
15
+ margin: 0;
16
+ overflow-x: auto;
17
+ }
18
+ .abPagePaginatorItem {
19
+ display: inline-flex;
20
+ }
21
+ .abPagePaginatorBtn {
22
+ min-width: 2rem;
23
+ height: 2rem;
24
+ border-radius: 9999px;
25
+ border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
26
+ background: transparent;
27
+ cursor: pointer;
28
+ font: inherit;
29
+ padding: 0 0.5rem;
30
+ color: inherit;
31
+ }
32
+ .abPagePaginatorBtn:disabled {
33
+ opacity: 0.4;
34
+ cursor: not-allowed;
35
+ }
36
+ .abPagePaginatorBtn[data-active="true"] {
37
+ border-color: var(--ab-primary, #6750a4);
38
+ color: var(--ab-primary, #6750a4);
39
+ font-weight: 700;
40
+ transform: scale(1.08);
41
+ pointer-events: none;
42
+ }
43
+ .abPagePaginatorEllipsis {
44
+ opacity: 0.5;
45
+ padding: 0 0.25rem;
46
+ }
47
+
@@ -0,0 +1,121 @@
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: AbPageSwitcher - Client (port of lyd-page-switcher)
10
+ * @file: ab-page-switcher/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement } from 'react';
16
+ import { useState } from 'react';
17
+ import clsx from 'clsx';
18
+ import styles from './styles.module.css';
19
+
20
+ export interface AbPageSwitcherProps {
21
+ className?: string;
22
+ hidden?: boolean;
23
+ disabled?: boolean;
24
+ page: number;
25
+ total: number;
26
+ min?: number;
27
+ max?: number;
28
+ onChange: (page: number) => void;
29
+ previousLabel?: string;
30
+ nextLabel?: string;
31
+ }
32
+
33
+ const AbPageSwitcher = ({
34
+ className,
35
+ hidden = false,
36
+ disabled = false,
37
+ page,
38
+ total,
39
+ min = 1,
40
+ max,
41
+ onChange,
42
+ previousLabel = 'Previous page',
43
+ nextLabel = 'Next page',
44
+ }: AbPageSwitcherProps): ReactElement => {
45
+ const upper = max ?? total;
46
+ const current = page;
47
+ const [editing, setEditing] = useState(false);
48
+ const [draft, setDraft] = useState(String(page));
49
+ const shown = editing ? draft : String(current);
50
+
51
+ const go = (next: number) => {
52
+ const clamped = Math.min(upper, Math.max(min, next));
53
+ if (disabled || clamped === current) return;
54
+ setDraft(String(clamped));
55
+ setEditing(false);
56
+ onChange(clamped);
57
+ };
58
+
59
+ const commitDraft = () => {
60
+ const parsed = Number.parseInt(draft, 10);
61
+ setEditing(false);
62
+ if (Number.isNaN(parsed)) {
63
+ setDraft(String(current));
64
+ return;
65
+ }
66
+ go(parsed);
67
+ };
68
+
69
+ return (
70
+ <div hidden={hidden} className={clsx('AbPageSwitcher', styles.abPageSwitcher, className)}>
71
+ <button
72
+ type="button"
73
+ className={styles.abPageSwitcherBtn}
74
+ aria-label={previousLabel}
75
+ disabled={disabled || current <= min}
76
+ onClick={() => go(current - 1)}
77
+ >
78
+ <span className="material-symbols-rounded" aria-hidden>
79
+ chevron_left
80
+ </span>
81
+ </button>
82
+ <input
83
+ className={styles.abPageSwitcherInput}
84
+ type="number"
85
+ inputMode="numeric"
86
+ min={min}
87
+ max={upper}
88
+ value={shown}
89
+ disabled={disabled}
90
+ aria-label="Page number"
91
+ onFocus={() => {
92
+ setDraft(String(current));
93
+ setEditing(true);
94
+ }}
95
+ onChange={(e) => setDraft(e.target.value)}
96
+ onBlur={commitDraft}
97
+ onKeyDown={(e) => {
98
+ if (e.key === 'Enter') {
99
+ e.preventDefault();
100
+ commitDraft();
101
+ }
102
+ }}
103
+ />
104
+ <span className={styles.abPageSwitcherMeta}>/ {total}</span>
105
+ <button
106
+ type="button"
107
+ className={styles.abPageSwitcherBtn}
108
+ aria-label={nextLabel}
109
+ disabled={disabled || current >= upper}
110
+ onClick={() => go(current + 1)}
111
+ >
112
+ <span className="material-symbols-rounded" aria-hidden>
113
+ chevron_right
114
+ </span>
115
+ </button>
116
+ </div>
117
+ );
118
+ };
119
+
120
+ export default AbPageSwitcher;
121
+ export { AbPageSwitcher };
@@ -0,0 +1,37 @@
1
+ .abPageSwitcher {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 0.5rem;
5
+ }
6
+ .abPageSwitcherBtn {
7
+ width: 2.25rem;
8
+ height: 2.25rem;
9
+ border-radius: 9999px;
10
+ border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
11
+ background: transparent;
12
+ cursor: pointer;
13
+ display: inline-flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ color: inherit;
17
+ }
18
+ .abPageSwitcherBtn:disabled {
19
+ opacity: 0.4;
20
+ cursor: not-allowed;
21
+ }
22
+ .abPageSwitcherInput {
23
+ width: 3.5rem;
24
+ height: 2.25rem;
25
+ text-align: center;
26
+ border-radius: 0.5rem;
27
+ border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
28
+ background: transparent;
29
+ color: inherit;
30
+ font: inherit;
31
+ }
32
+ .abPageSwitcherMeta {
33
+ font-size: 0.875rem;
34
+ opacity: 0.75;
35
+ white-space: nowrap;
36
+ }
37
+
@@ -0,0 +1,78 @@
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: AbPreview - Client (Adapt of lyd-preview; db/providers stripped)
10
+ * @file: ab-preview/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement } from 'react';
16
+ import Image from 'next/image';
17
+ import clsx from 'clsx';
18
+ import styles from './styles.module.css';
19
+
20
+ export type AbPreviewItem = {
21
+ id: string | number;
22
+ src: string;
23
+ alt?: string;
24
+ href?: string;
25
+ };
26
+
27
+ export interface AbPreviewProps {
28
+ className?: string;
29
+ hidden?: boolean;
30
+ items?: AbPreviewItem[];
31
+ emptyLabel?: string;
32
+ }
33
+
34
+ const AbPreview = ({
35
+ className,
36
+ hidden = false,
37
+ items = [],
38
+ emptyLabel = 'No previews',
39
+ }: AbPreviewProps): ReactElement => {
40
+ if (!items.length) {
41
+ return (
42
+ <div hidden={hidden} className={clsx('AbPreview', styles.abPreview, className)}>
43
+ <div className={styles.abPreviewEmpty}>{emptyLabel}</div>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <div hidden={hidden} className={clsx('AbPreview', styles.abPreview, className)}>
50
+ {items.map((item) => {
51
+ const inner = (
52
+ <Image
53
+ className={styles.abPreviewImg}
54
+ src={item.src}
55
+ alt={item.alt ?? ''}
56
+ width={160}
57
+ height={160}
58
+ unoptimized
59
+ />
60
+ );
61
+ return (
62
+ <div key={item.id} className={styles.abPreviewItem}>
63
+ {item.href ? (
64
+ <a href={item.href} style={{ display: 'contents' }}>
65
+ {inner}
66
+ </a>
67
+ ) : (
68
+ inner
69
+ )}
70
+ </div>
71
+ );
72
+ })}
73
+ </div>
74
+ );
75
+ };
76
+
77
+ export default AbPreview;
78
+ export { AbPreview };
@@ -0,0 +1,26 @@
1
+ .abPreview {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
4
+ gap: 0.75rem;
5
+ }
6
+ .abPreviewItem {
7
+ position: relative;
8
+ aspect-ratio: 1;
9
+ border-radius: 0.85rem;
10
+ overflow: hidden;
11
+ background: color-mix(in srgb, var(--ab-surface-container, #f3edf7) 100%, transparent);
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ }
16
+ .abPreviewImg {
17
+ object-fit: cover;
18
+ width: 100%;
19
+ height: 100%;
20
+ }
21
+ .abPreviewEmpty {
22
+ font-size: 0.75rem;
23
+ opacity: 0.6;
24
+ text-align: center;
25
+ padding: 0.5rem;
26
+ }
@@ -0,0 +1,109 @@
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: AbPrice - Client (port of lyd-price)
10
+ * @file: ab-price/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement } from 'react';
16
+ import { useMemo } from 'react';
17
+ import clsx from 'clsx';
18
+ import styles from './styles.module.css';
19
+
20
+ export interface AbPriceProps {
21
+ className?: string;
22
+ hidden?: boolean;
23
+ weight?: 'bold' | 'normal' | 'light';
24
+ size?: string;
25
+ totalSize?: string;
26
+ noScale?: boolean;
27
+ price: number;
28
+ quantity?: number;
29
+ currency: string;
30
+ symbol: string;
31
+ useSymbol?: boolean;
32
+ isSymbolBefore?: boolean;
33
+ isTaxIncluded?: boolean;
34
+ taxIncludedLabel?: string;
35
+ isTaxIncludedHidden?: boolean;
36
+ locale?: string;
37
+ useLocale?: boolean;
38
+ }
39
+
40
+ const AbPrice = ({
41
+ className,
42
+ hidden = false,
43
+ weight = 'normal',
44
+ size = '12px',
45
+ totalSize = '30px',
46
+ noScale = false,
47
+ price,
48
+ quantity = 1,
49
+ currency,
50
+ symbol,
51
+ useSymbol = false,
52
+ isSymbolBefore = true,
53
+ isTaxIncluded = false,
54
+ taxIncludedLabel = 'Tax included',
55
+ isTaxIncludedHidden = false,
56
+ locale = 'en',
57
+ useLocale = false,
58
+ }: AbPriceProps): ReactElement => {
59
+ const totalPrice = useMemo(() => Number((price * quantity).toFixed(2)), [price, quantity]);
60
+
61
+ const format = (amount: number) =>
62
+ useLocale ? amount.toLocaleString(locale) : amount.toFixed(2);
63
+
64
+ const mark = useSymbol ? symbol : currency;
65
+ const unitStyle = { fontSize: quantity > 1 ? size : totalSize, transform: noScale ? 'none' : undefined };
66
+ const totalStyle = { fontSize: totalSize, transform: noScale ? 'none' : undefined };
67
+
68
+ const amount = (n: number) => (
69
+ <>
70
+ {isSymbolBefore ? <>{mark}{' '}</> : null}
71
+ {format(n)}
72
+ {!isSymbolBefore ? <>{' '}{mark}</> : null}
73
+ </>
74
+ );
75
+
76
+ return (
77
+ <div
78
+ hidden={hidden}
79
+ className={clsx(
80
+ 'AbPrice',
81
+ styles.abPrice,
82
+ weight === 'bold' && styles.isBold,
83
+ weight === 'normal' && styles.isNormal,
84
+ weight === 'light' && styles.isLight,
85
+ className,
86
+ )}
87
+ >
88
+ <span className={styles.abPriceUnit} style={unitStyle} title={format(totalPrice)}>
89
+ {quantity > 1 ? <span className={styles.abPriceMultiplier}>{quantity} × </span> : null}
90
+ <span>{amount(price)}</span>
91
+ </span>
92
+ {quantity > 1 ? (
93
+ <span className={styles.abPriceTotal} style={totalStyle} title={String(totalPrice)}>
94
+ {amount(totalPrice)}
95
+ </span>
96
+ ) : null}
97
+ {isTaxIncluded && !isTaxIncludedHidden ? (
98
+ <span className={styles.abPriceTax}>
99
+ <span className="material-symbols-rounded" aria-hidden>receipt_long</span>
100
+ <b>{taxIncludedLabel}</b>
101
+ </span>
102
+ ) : null}
103
+ </div>
104
+ );
105
+ };
106
+
107
+ export default AbPrice;
108
+ export { AbPrice };
109
+
@@ -0,0 +1,36 @@
1
+ .abPrice {
2
+ display: inline-flex;
3
+ flex-direction: column;
4
+ align-items: flex-start;
5
+ gap: 0.15rem;
6
+ line-height: 1.1;
7
+ }
8
+ .abPrice.isBold { font-weight: 700; }
9
+ .abPrice.isNormal { font-weight: 500; }
10
+ .abPrice.isLight { font-weight: 300; }
11
+ .abPriceUnit {
12
+ display: inline-flex;
13
+ align-items: baseline;
14
+ gap: 0.25rem;
15
+ }
16
+ .abPriceMultiplier {
17
+ opacity: 0.7;
18
+ font-size: 0.85em;
19
+ }
20
+ .abPriceTotal {
21
+ display: inline-flex;
22
+ align-items: baseline;
23
+ gap: 0.25rem;
24
+ }
25
+ .abPriceTax {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ gap: 0.25rem;
29
+ font-size: 0.75rem;
30
+ opacity: 0.8;
31
+ margin-top: 0.15rem;
32
+ }
33
+
34
+ .isBold { font-weight: 700; }
35
+ .isNormal { font-weight: 500; }
36
+ .isLight { font-weight: 300; }
@@ -0,0 +1,70 @@
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: AbPriceTag - Client (port of lyd-price-tag)
10
+ * @file: ab-price-tag/index.tsx
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import type { ReactElement } from 'react';
16
+ import clsx from 'clsx';
17
+ import styles from './styles.module.css';
18
+
19
+ export interface AbPriceTagProps {
20
+ className?: string;
21
+ hidden?: boolean;
22
+ name: string;
23
+ brand: string;
24
+ category: string;
25
+ price: number;
26
+ currency: string;
27
+ currencySymbol: string;
28
+ useCurrencySymbol?: boolean;
29
+ isBrandHidden?: boolean;
30
+ isCategoryHidden?: boolean;
31
+ }
32
+
33
+ const AbPriceTag = ({
34
+ className,
35
+ hidden = false,
36
+ name,
37
+ brand,
38
+ category,
39
+ price,
40
+ currency,
41
+ currencySymbol,
42
+ useCurrencySymbol = false,
43
+ isBrandHidden = false,
44
+ isCategoryHidden = false,
45
+ }: AbPriceTagProps): ReactElement => {
46
+ const money = useCurrencySymbol ? currencySymbol : currency;
47
+
48
+ return (
49
+ <div hidden={hidden} className={clsx('AbPriceTag', styles.abPriceTag, className)}>
50
+ <div className={styles.abPriceTagDetails}>
51
+ <div className={styles.abPriceTagBrandCategory}>
52
+ {!isBrandHidden ? <span className={styles.abPriceTagBrand}>{brand}</span> : null}
53
+ {!isCategoryHidden ? <span className={styles.abPriceTagCategory}>{category}</span> : null}
54
+ </div>
55
+ <span className={styles.abPriceTagName}>{name}</span>
56
+ <span className={styles.abPriceTagPrice}>
57
+ {price} {money}
58
+ </span>
59
+ </div>
60
+ <div className={styles.abPriceTagIconWrap} aria-hidden>
61
+ <span className={styles.abPriceTagIconDivider} />
62
+ <span className={clsx('material-symbols-rounded', styles.abPriceTagIcon)}>shopping_cart</span>
63
+ </div>
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export default AbPriceTag;
69
+ export { AbPriceTag };
70
+