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,81 @@
|
|
|
1
|
+
.abPriceTag {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
width: 16rem;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: 0.5rem;
|
|
7
|
+
padding: 0.5rem 0.25rem 0.5rem 1rem;
|
|
8
|
+
border-radius: 0.5rem;
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
user-select: none;
|
|
12
|
+
background: color-mix(in srgb, var(--ab-primary-container, #eaddff) 35%, transparent);
|
|
13
|
+
backdrop-filter: blur(8px);
|
|
14
|
+
box-shadow: 0 4px 12px color-mix(in srgb, #000 12%, transparent);
|
|
15
|
+
}
|
|
16
|
+
.abPriceTagDetails {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
flex: 1;
|
|
20
|
+
min-width: 0;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
.abPriceTagBrandCategory {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: row;
|
|
26
|
+
gap: 0.5rem;
|
|
27
|
+
align-items: center;
|
|
28
|
+
}
|
|
29
|
+
.abPriceTagBrand {
|
|
30
|
+
border-radius: 0.375rem;
|
|
31
|
+
padding: 0.125rem 0.5rem;
|
|
32
|
+
font-size: 0.75rem;
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
text-transform: uppercase;
|
|
35
|
+
width: fit-content;
|
|
36
|
+
background: color-mix(in srgb, var(--ab-tertiary-container, #ffd8e4) 80%, transparent);
|
|
37
|
+
}
|
|
38
|
+
.abPriceTagCategory {
|
|
39
|
+
font-size: 0.875rem;
|
|
40
|
+
text-transform: lowercase;
|
|
41
|
+
opacity: 0.85;
|
|
42
|
+
truncate: true;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
text-overflow: ellipsis;
|
|
45
|
+
white-space: nowrap;
|
|
46
|
+
}
|
|
47
|
+
.abPriceTagName {
|
|
48
|
+
display: block;
|
|
49
|
+
font-size: 0.875rem;
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
opacity: 0.9;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
}
|
|
56
|
+
.abPriceTagPrice {
|
|
57
|
+
font-size: 2rem;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
line-height: 1.1;
|
|
60
|
+
}
|
|
61
|
+
.abPriceTagIconWrap {
|
|
62
|
+
display: flex;
|
|
63
|
+
width: 4rem;
|
|
64
|
+
height: 4rem;
|
|
65
|
+
align-items: center;
|
|
66
|
+
justify-content: center;
|
|
67
|
+
position: relative;
|
|
68
|
+
opacity: 0.9;
|
|
69
|
+
}
|
|
70
|
+
.abPriceTagIconDivider {
|
|
71
|
+
position: absolute;
|
|
72
|
+
left: 0;
|
|
73
|
+
top: 0.5rem;
|
|
74
|
+
bottom: 0.5rem;
|
|
75
|
+
width: 1px;
|
|
76
|
+
background: color-mix(in srgb, currentColor 25%, transparent);
|
|
77
|
+
}
|
|
78
|
+
.abPriceTagIcon {
|
|
79
|
+
font-size: 1.75rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
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: AbProduct - Client (Adapt of lyd-product; db/hooks stripped)
|
|
10
|
+
* @file: ab-product/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 AbProductImage = {
|
|
21
|
+
id: number | string;
|
|
22
|
+
path: string;
|
|
23
|
+
colorId?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface AbProductProps {
|
|
27
|
+
className?: string;
|
|
28
|
+
hidden?: boolean;
|
|
29
|
+
size?: 'small' | 'normal' | 'medium' | 'large';
|
|
30
|
+
type?: 'contained' | 'outlined' | 'text';
|
|
31
|
+
isLiked?: boolean;
|
|
32
|
+
isCarted?: boolean;
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
id: number | string;
|
|
35
|
+
code?: string;
|
|
36
|
+
brand?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
images?: AbProductImage[];
|
|
40
|
+
posterPath?: string;
|
|
41
|
+
price?: number;
|
|
42
|
+
currency?: string;
|
|
43
|
+
currencySymbol?: string;
|
|
44
|
+
useCurrencySymbol?: boolean;
|
|
45
|
+
onLikeClick?: () => void;
|
|
46
|
+
onCartClick?: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const AbProduct = ({
|
|
50
|
+
className,
|
|
51
|
+
hidden = false,
|
|
52
|
+
size = 'normal',
|
|
53
|
+
type = 'contained',
|
|
54
|
+
isLiked = false,
|
|
55
|
+
isCarted = false,
|
|
56
|
+
disabled = false,
|
|
57
|
+
id,
|
|
58
|
+
brand,
|
|
59
|
+
name,
|
|
60
|
+
description,
|
|
61
|
+
images = [],
|
|
62
|
+
posterPath,
|
|
63
|
+
price,
|
|
64
|
+
currency = 'EUR',
|
|
65
|
+
currencySymbol = '€',
|
|
66
|
+
useCurrencySymbol = true,
|
|
67
|
+
onLikeClick,
|
|
68
|
+
onCartClick,
|
|
69
|
+
}: AbProductProps): ReactElement => {
|
|
70
|
+
const src = posterPath ?? images[0]?.path ?? '';
|
|
71
|
+
const money = useCurrencySymbol ? currencySymbol : currency;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<article
|
|
75
|
+
hidden={hidden}
|
|
76
|
+
data-size={size}
|
|
77
|
+
data-type={type}
|
|
78
|
+
data-id={id}
|
|
79
|
+
className={clsx('AbProduct', styles.abProduct, className)}
|
|
80
|
+
data-disabled={disabled ? "true" : "false"}
|
|
81
|
+
>
|
|
82
|
+
<div className={styles.abProductMedia}>
|
|
83
|
+
{src ? (
|
|
84
|
+
<Image className={styles.abProductImg} src={src} alt={name ?? ''} width={320} height={320} unoptimized />
|
|
85
|
+
) : null}
|
|
86
|
+
</div>
|
|
87
|
+
<div className={styles.abProductBody}>
|
|
88
|
+
{brand ? <span className={styles.abProductBrand}>{brand}</span> : null}
|
|
89
|
+
{name ? <h3 className={styles.abProductName}>{name}</h3> : null}
|
|
90
|
+
{description ? <p className={styles.abProductDesc}>{description}</p> : null}
|
|
91
|
+
{price != null ? (
|
|
92
|
+
<span className={styles.abProductPrice}>
|
|
93
|
+
{money} {price.toFixed(2)}
|
|
94
|
+
</span>
|
|
95
|
+
) : null}
|
|
96
|
+
<div className={styles.abProductActions}>
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
className={styles.abProductBtn}
|
|
100
|
+
data-active={isLiked ? 'true' : 'false'}
|
|
101
|
+
aria-label="Like"
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
onClick={onLikeClick}
|
|
104
|
+
>
|
|
105
|
+
<span className="material-symbols-rounded" aria-hidden>
|
|
106
|
+
{isLiked ? 'favorite' : 'favorite_border'}
|
|
107
|
+
</span>
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
className={styles.abProductBtn}
|
|
112
|
+
data-active={isCarted ? 'true' : 'false'}
|
|
113
|
+
aria-label="Add to cart"
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
onClick={onCartClick}
|
|
116
|
+
>
|
|
117
|
+
<span className="material-symbols-rounded" aria-hidden>
|
|
118
|
+
{isCarted ? 'shopping_cart' : 'add_shopping_cart'}
|
|
119
|
+
</span>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</article>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default AbProduct;
|
|
128
|
+
export { AbProduct };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.abProduct {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 0.75rem;
|
|
5
|
+
padding: 0.75rem;
|
|
6
|
+
border-radius: 1rem;
|
|
7
|
+
background: var(--ab-surface-container, #f3edf7);
|
|
8
|
+
max-width: 20rem;
|
|
9
|
+
}
|
|
10
|
+
.abProduct[data-size='small'] { max-width: 12rem; }
|
|
11
|
+
.abProduct[data-size='large'] { max-width: 28rem; }
|
|
12
|
+
.abProduct[data-type='outlined'] {
|
|
13
|
+
background: transparent;
|
|
14
|
+
border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
|
|
15
|
+
}
|
|
16
|
+
.abProductMedia {
|
|
17
|
+
position: relative;
|
|
18
|
+
aspect-ratio: 1;
|
|
19
|
+
border-radius: 0.75rem;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
background: #ddd;
|
|
22
|
+
}
|
|
23
|
+
.abProductImg { object-fit: cover; width: 100%; height: 100%; }
|
|
24
|
+
.abProductBody { display: flex; flex-direction: column; gap: 0.25rem; }
|
|
25
|
+
.abProductBrand { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; opacity: 0.65; }
|
|
26
|
+
.abProductName { margin: 0; font-size: 1rem; font-weight: 700; }
|
|
27
|
+
.abProductDesc { margin: 0; font-size: 0.8125rem; opacity: 0.75; }
|
|
28
|
+
.abProductPrice { font-size: 1.125rem; font-weight: 700; }
|
|
29
|
+
.abProductActions { display: flex; gap: 0.35rem; margin-top: 0.35rem; }
|
|
30
|
+
.abProductBtn {
|
|
31
|
+
border: none;
|
|
32
|
+
border-radius: 9999px;
|
|
33
|
+
width: 2.25rem;
|
|
34
|
+
height: 2.25rem;
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
background: color-mix(in srgb, var(--ab-primary, #6750a4) 14%, transparent);
|
|
40
|
+
color: inherit;
|
|
41
|
+
}
|
|
42
|
+
.abProductBtn[data-active='true'] { color: var(--ab-primary, #6750a4); }
|
|
43
|
+
|
|
44
|
+
.abProduct[data-disabled="true"] { opacity: 0.55; pointer-events: none; }
|
|
@@ -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: AbProductItem - Client (Adapt of lyd-product-item; providers stripped)
|
|
10
|
+
* @file: ab-product-item/index.tsx
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use client';
|
|
14
|
+
|
|
15
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
16
|
+
import Image from 'next/image';
|
|
17
|
+
import clsx from 'clsx';
|
|
18
|
+
import styles from './styles.module.css';
|
|
19
|
+
|
|
20
|
+
export interface AbProductItemProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
style?: CSSProperties;
|
|
23
|
+
hidden?: boolean;
|
|
24
|
+
width?: number;
|
|
25
|
+
height?: number;
|
|
26
|
+
id: number | string;
|
|
27
|
+
code: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
image: { id: number | string; path: string };
|
|
30
|
+
price?: number;
|
|
31
|
+
currencySymbol?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const AbProductItem = ({
|
|
35
|
+
className,
|
|
36
|
+
style,
|
|
37
|
+
hidden = false,
|
|
38
|
+
width = 72,
|
|
39
|
+
height = 72,
|
|
40
|
+
id,
|
|
41
|
+
code,
|
|
42
|
+
name,
|
|
43
|
+
image,
|
|
44
|
+
price,
|
|
45
|
+
currencySymbol = '€',
|
|
46
|
+
}: AbProductItemProps): ReactElement => {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
hidden={hidden}
|
|
50
|
+
data-id={id}
|
|
51
|
+
style={style}
|
|
52
|
+
className={clsx('AbProductItem', styles.abProductItem, className)}
|
|
53
|
+
>
|
|
54
|
+
<div className={styles.abProductItemMedia} style={{ width, height }}>
|
|
55
|
+
<Image
|
|
56
|
+
className={styles.abProductItemImg}
|
|
57
|
+
src={image.path}
|
|
58
|
+
alt={name ?? code}
|
|
59
|
+
width={width}
|
|
60
|
+
height={height}
|
|
61
|
+
unoptimized
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div className={styles.abProductItemBody}>
|
|
65
|
+
<span className={styles.abProductItemCode}>{code}</span>
|
|
66
|
+
{name ? <h4 className={styles.abProductItemName}>{name}</h4> : null}
|
|
67
|
+
{price != null ? (
|
|
68
|
+
<span className={styles.abProductItemPrice}>
|
|
69
|
+
{currencySymbol} {price.toFixed(2)}
|
|
70
|
+
</span>
|
|
71
|
+
) : null}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default AbProductItem;
|
|
78
|
+
export { AbProductItem };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.abProductItem {
|
|
2
|
+
display: flex;
|
|
3
|
+
gap: 0.75rem;
|
|
4
|
+
align-items: center;
|
|
5
|
+
padding: 0.5rem;
|
|
6
|
+
border-radius: 0.75rem;
|
|
7
|
+
background: var(--ab-surface-container-low, #f7f2fa);
|
|
8
|
+
}
|
|
9
|
+
.abProductItemMedia {
|
|
10
|
+
width: 4.5rem;
|
|
11
|
+
height: 4.5rem;
|
|
12
|
+
border-radius: 0.6rem;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
flex-shrink: 0;
|
|
15
|
+
background: #ddd;
|
|
16
|
+
}
|
|
17
|
+
.abProductItemImg { object-fit: cover; width: 100%; height: 100%; }
|
|
18
|
+
.abProductItemBody { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; flex: 1; }
|
|
19
|
+
.abProductItemCode { font-size: 0.7rem; font-family: ui-monospace, monospace; opacity: 0.6; }
|
|
20
|
+
.abProductItemName { margin: 0; font-size: 0.9375rem; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
21
|
+
.abProductItemPrice { font-weight: 700; }
|
|
@@ -0,0 +1,105 @@
|
|
|
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: AbProgressChips - Client (port of lyd-progress-chips)
|
|
10
|
+
* @file: ab-progress-chips/index.tsx
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use client';
|
|
14
|
+
|
|
15
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import clsx from 'clsx';
|
|
18
|
+
import styles from './styles.module.css';
|
|
19
|
+
|
|
20
|
+
export interface AbProgressChip {
|
|
21
|
+
id: number;
|
|
22
|
+
code: string;
|
|
23
|
+
name: string;
|
|
24
|
+
duration: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AbProgressChipsProps {
|
|
28
|
+
className?: string;
|
|
29
|
+
hidden?: boolean;
|
|
30
|
+
chips?: AbProgressChip[];
|
|
31
|
+
selectedChipId?: number;
|
|
32
|
+
selectedChipCode?: string;
|
|
33
|
+
selectedChipName?: string;
|
|
34
|
+
onChipClick?: (chipId: number, chipCode?: string, chipName?: string, chipDuration?: number) => void;
|
|
35
|
+
isChipNameHidden?: boolean;
|
|
36
|
+
/** When set, fills the selected chip bar to this ratio (0–1). No internal timer. */
|
|
37
|
+
progressRatio?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const DEFAULT_CHIPS: AbProgressChip[] = [
|
|
41
|
+
{ id: 1, code: 'A', name: 'Prep', duration: 20 },
|
|
42
|
+
{ id: 2, code: 'B', name: 'Cook', duration: 40 },
|
|
43
|
+
{ id: 3, code: 'C', name: 'Plate', duration: 15 },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const AbProgressChips = ({
|
|
47
|
+
className,
|
|
48
|
+
hidden = false,
|
|
49
|
+
chips = DEFAULT_CHIPS,
|
|
50
|
+
selectedChipId,
|
|
51
|
+
selectedChipCode,
|
|
52
|
+
selectedChipName,
|
|
53
|
+
onChipClick,
|
|
54
|
+
isChipNameHidden = false,
|
|
55
|
+
progressRatio = 0,
|
|
56
|
+
}: AbProgressChipsProps): ReactElement => {
|
|
57
|
+
const [uncontrolledId, setUncontrolledId] = useState<number | undefined>(selectedChipId);
|
|
58
|
+
|
|
59
|
+
const fromCodeOrName = chips.find(
|
|
60
|
+
(c) =>
|
|
61
|
+
(selectedChipCode != null && c.code === selectedChipCode) ||
|
|
62
|
+
(selectedChipName != null && c.name === selectedChipName),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const selectedId =
|
|
66
|
+
selectedChipId ?? fromCodeOrName?.id ?? uncontrolledId ?? chips[0]?.id;
|
|
67
|
+
|
|
68
|
+
const total = chips.reduce((sum, c) => sum + (c.duration || 0), 0) || 1;
|
|
69
|
+
const ratio = Math.max(0, Math.min(1, progressRatio));
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div hidden={hidden} className={clsx('AbProgressChips', styles.abProgressChips, className)} role="list">
|
|
73
|
+
{chips.map((chip) => {
|
|
74
|
+
const selected = chip.id === selectedId;
|
|
75
|
+
const fallback = Math.min(1, chip.duration / total);
|
|
76
|
+
const style = {
|
|
77
|
+
'--chip-progress': `${(selected ? ratio || fallback : 0) * 100}%`,
|
|
78
|
+
} as CSSProperties;
|
|
79
|
+
return (
|
|
80
|
+
<button
|
|
81
|
+
key={chip.id}
|
|
82
|
+
type="button"
|
|
83
|
+
role="listitem"
|
|
84
|
+
className={styles.abProgressChip}
|
|
85
|
+
data-selected={selected ? 'true' : 'false'}
|
|
86
|
+
style={style}
|
|
87
|
+
onClick={() => {
|
|
88
|
+
if (selectedChipId == null && selectedChipCode == null && selectedChipName == null) {
|
|
89
|
+
setUncontrolledId(chip.id);
|
|
90
|
+
}
|
|
91
|
+
onChipClick?.(chip.id, chip.code, chip.name, chip.duration);
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<span className={styles.abProgressChipBar} aria-hidden />
|
|
95
|
+
<span className={styles.abProgressChipCode}>{chip.code}</span>
|
|
96
|
+
{!isChipNameHidden ? <span className={styles.abProgressChipName}>{chip.name}</span> : null}
|
|
97
|
+
</button>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default AbProgressChips;
|
|
105
|
+
export { AbProgressChips };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.abProgressChips {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
gap: 0.5rem;
|
|
5
|
+
align-items: center;
|
|
6
|
+
}
|
|
7
|
+
.abProgressChip {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 0.4rem;
|
|
12
|
+
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
13
|
+
border-radius: 9999px;
|
|
14
|
+
padding: 0.35rem 0.75rem;
|
|
15
|
+
background: color-mix(in srgb, var(--ab-surface-container, #f3edf7) 80%, transparent);
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
font: inherit;
|
|
18
|
+
color: inherit;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
.abProgressChip[data-selected="true"] {
|
|
22
|
+
border-color: var(--ab-primary, #6750a4);
|
|
23
|
+
background: color-mix(in srgb, var(--ab-primary, #6750a4) 18%, transparent);
|
|
24
|
+
}
|
|
25
|
+
.abProgressChipBar {
|
|
26
|
+
position: absolute;
|
|
27
|
+
left: 0;
|
|
28
|
+
bottom: 0;
|
|
29
|
+
height: 3px;
|
|
30
|
+
background: var(--ab-primary, #6750a4);
|
|
31
|
+
width: var(--chip-progress, 0%);
|
|
32
|
+
transition: width 0.2s linear;
|
|
33
|
+
}
|
|
34
|
+
.abProgressChipName {
|
|
35
|
+
font-size: 0.8125rem;
|
|
36
|
+
font-weight: 600;
|
|
37
|
+
}
|
|
38
|
+
.abProgressChipCode {
|
|
39
|
+
font-size: 0.75rem;
|
|
40
|
+
opacity: 0.7;
|
|
41
|
+
font-family: ui-monospace, monospace;
|
|
42
|
+
}
|
|
43
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
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: AbSearchResult - Client (port of lyd-search-result)
|
|
10
|
+
* @file: ab-search-result/index.tsx
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use client';
|
|
14
|
+
|
|
15
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
16
|
+
import clsx from 'clsx';
|
|
17
|
+
import styles from './styles.module.css';
|
|
18
|
+
|
|
19
|
+
export interface AbSearchResultProps {
|
|
20
|
+
className?: string;
|
|
21
|
+
emptyImageClassName?: string;
|
|
22
|
+
hidden?: boolean;
|
|
23
|
+
isEmpty?: boolean;
|
|
24
|
+
isActionButtonHidden?: boolean;
|
|
25
|
+
emptyImage?: string | null;
|
|
26
|
+
emptyTitle?: string;
|
|
27
|
+
emptyMessage?: string;
|
|
28
|
+
emptyActionLabel?: string;
|
|
29
|
+
onEmptyActionButtonClick?: () => void;
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const AbSearchResult = ({
|
|
34
|
+
className,
|
|
35
|
+
emptyImageClassName,
|
|
36
|
+
hidden = false,
|
|
37
|
+
isEmpty = false,
|
|
38
|
+
isActionButtonHidden = false,
|
|
39
|
+
emptyImage = null,
|
|
40
|
+
emptyTitle = 'No results',
|
|
41
|
+
emptyMessage = 'Try another search.',
|
|
42
|
+
emptyActionLabel = 'Clear search',
|
|
43
|
+
onEmptyActionButtonClick,
|
|
44
|
+
children,
|
|
45
|
+
}: AbSearchResultProps): ReactElement => {
|
|
46
|
+
if (isEmpty) {
|
|
47
|
+
return (
|
|
48
|
+
<div hidden={hidden} className={clsx('AbSearchResult', styles.abSearchResult, className)} role="status">
|
|
49
|
+
<div className={styles.abSearchResultEmpty}>
|
|
50
|
+
{emptyImage ? (
|
|
51
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
52
|
+
<img src={emptyImage} alt="" className={clsx(styles.abSearchResultEmptyImage, emptyImageClassName)} />
|
|
53
|
+
) : (
|
|
54
|
+
<span className="material-symbols-rounded" style={{ fontSize: 64 }} aria-hidden>
|
|
55
|
+
search_off
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
58
|
+
<h3 className={styles.abSearchResultEmptyTitle}>{emptyTitle}</h3>
|
|
59
|
+
<p className={styles.abSearchResultEmptyMessage}>{emptyMessage}</p>
|
|
60
|
+
{!isActionButtonHidden && onEmptyActionButtonClick ? (
|
|
61
|
+
<button type="button" className={styles.abSearchResultEmptyAction} onClick={onEmptyActionButtonClick}>
|
|
62
|
+
{emptyActionLabel}
|
|
63
|
+
</button>
|
|
64
|
+
) : null}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div hidden={hidden} className={clsx('AbSearchResult', styles.abSearchResult, className)}>
|
|
72
|
+
<div className={styles.abSearchResultContent}>{children}</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default AbSearchResult;
|
|
78
|
+
export { AbSearchResult };
|
|
79
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.abSearchResult {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
.abSearchResultEmpty {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
gap: 0.75rem;
|
|
13
|
+
margin: 3rem 0;
|
|
14
|
+
text-align: center;
|
|
15
|
+
}
|
|
16
|
+
.abSearchResultEmptyImage {
|
|
17
|
+
width: 8rem;
|
|
18
|
+
height: 8rem;
|
|
19
|
+
object-fit: contain;
|
|
20
|
+
opacity: 0.85;
|
|
21
|
+
}
|
|
22
|
+
.abSearchResultEmptyTitle {
|
|
23
|
+
margin: 0;
|
|
24
|
+
font-size: 1.125rem;
|
|
25
|
+
font-weight: 700;
|
|
26
|
+
}
|
|
27
|
+
.abSearchResultEmptyMessage {
|
|
28
|
+
margin: 0;
|
|
29
|
+
max-width: 24rem;
|
|
30
|
+
color: color-mix(in srgb, currentColor 65%, transparent);
|
|
31
|
+
font-size: 0.95rem;
|
|
32
|
+
}
|
|
33
|
+
.abSearchResultEmptyAction {
|
|
34
|
+
margin-top: 0.5rem;
|
|
35
|
+
border: none;
|
|
36
|
+
border-radius: 9999px;
|
|
37
|
+
padding: 0.5rem 1rem;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
background: var(--ab-primary, #6750a4);
|
|
40
|
+
color: var(--ab-on-primary, #fff);
|
|
41
|
+
font: inherit;
|
|
42
|
+
}
|
|
43
|
+
.abSearchResultContent {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
width: 100%;
|
|
47
|
+
}
|
|
48
|
+
|