ab-nextjs-components 0.1.1 → 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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +1 -1
- package/README.md +91 -15
- package/ab-avatar/index.tsx +90 -0
- package/ab-avatar/styles.module.css +62 -0
- package/ab-balloon/index.tsx +55 -0
- package/ab-balloon/styles.module.css +12 -0
- package/ab-brand/index.tsx +88 -0
- package/ab-brand/styles.module.css +54 -0
- package/ab-button/index.tsx +164 -0
- package/ab-button/styles.module.css +63 -0
- package/ab-collapsible/index.tsx +188 -0
- package/ab-collapsible/styles.module.css +98 -0
- package/ab-color/index.tsx +84 -0
- package/ab-color/styles.module.css +51 -0
- package/ab-demo-box/index.tsx +38 -0
- package/ab-demo-box/styles.module.css +13 -0
- package/ab-demo-code/index.tsx +62 -0
- package/ab-demo-code/styles.module.css +41 -0
- package/ab-icon/index.tsx +63 -0
- package/ab-icon/styles.module.css +27 -0
- package/ab-icon-button/index.tsx +107 -0
- package/ab-icon-button/styles.module.css +64 -0
- package/ab-image/index.tsx +83 -0
- package/ab-image/styles.module.css +27 -0
- package/ab-info/index.tsx +89 -0
- package/ab-info/styles.module.css +87 -0
- package/ab-input/index.tsx +277 -0
- package/ab-input/styles.module.css +101 -0
- package/ab-key-feature/index.tsx +60 -0
- package/ab-key-feature/styles.module.css +47 -0
- package/ab-like-button/index.tsx +116 -0
- package/ab-like-button/styles.module.css +60 -0
- package/ab-logo/index.tsx +94 -0
- package/ab-logo/styles.module.css +30 -0
- package/ab-main-headline/index.tsx +57 -0
- package/ab-main-headline/styles.module.css +24 -0
- package/ab-menu/index.tsx +119 -0
- package/ab-menu/styles.module.css +99 -0
- package/ab-name/index.tsx +49 -0
- package/ab-name/styles.module.css +14 -0
- package/ab-navbar/index.tsx +68 -0
- package/ab-navbar/styles.module.css +35 -0
- package/ab-page-paginator/index.tsx +130 -0
- package/ab-page-paginator/styles.module.css +47 -0
- package/ab-page-switcher/index.tsx +121 -0
- package/ab-page-switcher/styles.module.css +37 -0
- package/ab-preview/index.tsx +78 -0
- package/ab-preview/styles.module.css +26 -0
- package/ab-price/index.tsx +109 -0
- package/ab-price/styles.module.css +36 -0
- package/ab-price-tag/index.tsx +70 -0
- package/ab-price-tag/styles.module.css +81 -0
- package/ab-product/index.tsx +128 -0
- package/ab-product/styles.module.css +44 -0
- package/ab-product-item/index.tsx +78 -0
- package/ab-product-item/styles.module.css +21 -0
- package/ab-progress-chips/index.tsx +105 -0
- package/ab-progress-chips/styles.module.css +43 -0
- package/ab-search-result/index.tsx +79 -0
- package/ab-search-result/styles.module.css +48 -0
- package/ab-searchbar/index.tsx +150 -0
- package/ab-searchbar/styles.module.css +77 -0
- package/ab-sidebar/index.tsx +72 -0
- package/ab-sidebar/styles.module.css +41 -0
- package/ab-switch-back/index.tsx +72 -0
- package/ab-switch-back/styles.module.css +45 -0
- package/ab-tab/index.tsx +101 -0
- package/ab-tab/styles.module.css +65 -0
- package/ab-tabs/index.tsx +45 -0
- package/ab-tabs/styles.module.css +17 -0
- package/ab-test-hello/index.tsx +37 -0
- package/ab-test-hello/styles.module.css +25 -0
- package/ab-text/index.tsx +36 -0
- package/ab-text/styles.module.css +3 -0
- package/index.ts +117 -0
- package/package.json +152 -7
- package/server/ab-avatar/index.tsx +85 -0
- package/server/ab-avatar/styles.module.css +62 -0
- package/server/ab-badge/index.tsx +66 -0
- package/server/ab-badge/styles.module.css +39 -0
- package/server/ab-brand/index.tsx +87 -0
- package/server/ab-brand/styles.module.css +54 -0
- package/server/ab-button/index.tsx +144 -0
- package/server/ab-button/styles.module.css +63 -0
- package/server/ab-icon/index.tsx +61 -0
- package/server/ab-icon/styles.module.css +27 -0
- package/server/ab-key-feature/index.tsx +59 -0
- package/server/ab-key-feature/styles.module.css +47 -0
- package/server/ab-logo/index.tsx +93 -0
- package/server/ab-logo/styles.module.css +30 -0
- package/server/ab-name/index.tsx +48 -0
- package/server/ab-name/styles.module.css +14 -0
- package/server/ab-nav-link/index.tsx +60 -0
- package/server/ab-nav-link/styles.module.css +32 -0
- package/server/ab-navbar/index.tsx +64 -0
- package/server/ab-navbar/styles.module.css +35 -0
- package/server/ab-polygon/index.tsx +45 -0
- package/server/ab-polygon/styles.module.css +11 -0
- package/server/ab-sidebar/index.tsx +68 -0
- package/server/ab-sidebar/styles.module.css +41 -0
- package/server/ab-switch-back/index.tsx +71 -0
- package/server/ab-switch-back/styles.module.css +45 -0
- package/server/ab-test-hello/index.tsx +35 -0
- package/server/ab-test-hello/styles.module.css +22 -0
- package/server/ab-text/index.tsx +33 -0
- package/server/ab-text/styles.module.css +3 -0
- package/server/index.ts +27 -0
- package/types/index.d.ts +54 -0
|
@@ -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
|
+
|