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,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
+