@neoptocom/neopto-ui 1.5.1 → 1.5.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/dist/index.cjs +37 -9
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +37 -9
- package/package.json +1 -1
- package/src/components/Autocomplete.tsx +41 -15
- package/src/components/Breadcrumb.docs.mdx +4 -0
- package/src/components/Breadcrumb.stories.tsx +4 -0
- package/src/components/Button.docs.mdx +4 -0
- package/src/components/Button.stories.tsx +4 -0
- package/src/components/Card.docs.mdx +4 -0
- package/src/stories/Autocomplete.stories.tsx +21 -6
package/dist/index.cjs
CHANGED
|
@@ -728,20 +728,40 @@ function Autocomplete({
|
|
|
728
728
|
const listRef = React3.useRef(null);
|
|
729
729
|
const normalizedOptions = React3.useMemo(() => {
|
|
730
730
|
if (Array.isArray(options) && typeof options[0] === "string") {
|
|
731
|
-
return options.map((str) => ({
|
|
731
|
+
return options.map((str) => ({
|
|
732
|
+
name: str,
|
|
733
|
+
label: str,
|
|
734
|
+
value: str
|
|
735
|
+
}));
|
|
732
736
|
}
|
|
733
|
-
return options
|
|
737
|
+
return options.map((option) => {
|
|
738
|
+
const fallback = option.name ?? option.label ?? "";
|
|
739
|
+
return {
|
|
740
|
+
...option,
|
|
741
|
+
name: option.name ?? fallback,
|
|
742
|
+
label: option.label ?? fallback
|
|
743
|
+
};
|
|
744
|
+
});
|
|
734
745
|
}, [options]);
|
|
735
746
|
const filtered = React3.useMemo(() => {
|
|
736
747
|
const q = searchQuery.trim().toLowerCase();
|
|
737
748
|
if (!q) return normalizedOptions;
|
|
738
|
-
return normalizedOptions.filter((o) =>
|
|
749
|
+
return normalizedOptions.filter((o) => {
|
|
750
|
+
const name = o.name?.toLowerCase() ?? "";
|
|
751
|
+
const label = o.label?.toLowerCase() ?? "";
|
|
752
|
+
return name.includes(q) || label.includes(q);
|
|
753
|
+
});
|
|
739
754
|
}, [normalizedOptions, searchQuery]);
|
|
740
755
|
const anyOptionHasImage = React3.useMemo(
|
|
741
756
|
() => normalizedOptions.some((o) => !!o.image),
|
|
742
757
|
[normalizedOptions]
|
|
743
758
|
);
|
|
744
|
-
const
|
|
759
|
+
const optionDisplay = (option) => {
|
|
760
|
+
if (!option) return "";
|
|
761
|
+
if (typeof option === "string") return option;
|
|
762
|
+
return option.name ?? option.label ?? "";
|
|
763
|
+
};
|
|
764
|
+
const displayValue = selectedOption != null ? optionDisplay(selectedOption) : searchQuery;
|
|
745
765
|
function openList() {
|
|
746
766
|
if (disabled) return;
|
|
747
767
|
setOpen(true);
|
|
@@ -811,7 +831,7 @@ function Autocomplete({
|
|
|
811
831
|
"fieldset",
|
|
812
832
|
{
|
|
813
833
|
className: [
|
|
814
|
-
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-
|
|
834
|
+
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-13",
|
|
815
835
|
"border-[var(--border)] focus-within:border-[var(--color-brand)]",
|
|
816
836
|
disabled ? "opacity-60 cursor-not-allowed" : ""
|
|
817
837
|
].join(" "),
|
|
@@ -865,6 +885,7 @@ function Autocomplete({
|
|
|
865
885
|
onClick: selectedOption && !open ? handleClear : () => setOpen((s) => !s),
|
|
866
886
|
disabled,
|
|
867
887
|
"aria-label": selectedOption && !open ? "Clear" : open ? "Collapse" : "Expand",
|
|
888
|
+
className: "absolute right-0 top-[-30%]",
|
|
868
889
|
iconClassName: [
|
|
869
890
|
"transition-transform duration-300 text-[var(--muted-fg)]",
|
|
870
891
|
open ? "rotate-180 text-[var(--color-brand)]" : ""
|
|
@@ -885,7 +906,8 @@ function Autocomplete({
|
|
|
885
906
|
style: { maxHeight },
|
|
886
907
|
children: filtered.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("ul", { id: listboxId, role: "listbox", ref: listRef, children: filtered.map((option, index) => {
|
|
887
908
|
const active = index === activeIndex;
|
|
888
|
-
const
|
|
909
|
+
const optionName = option.name ?? option.label ?? "";
|
|
910
|
+
const selected = selectedOption != null && (typeof selectedOption === "string" ? selectedOption === optionName || selectedOption === (option.label ?? "") : (selectedOption.name ?? selectedOption.label ?? "") === optionName);
|
|
889
911
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
890
912
|
"li",
|
|
891
913
|
{
|
|
@@ -901,13 +923,19 @@ function Autocomplete({
|
|
|
901
923
|
onClick: () => handleSelect(option),
|
|
902
924
|
children: [
|
|
903
925
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
904
|
-
anyOptionHasImage && /* @__PURE__ */ jsxRuntime.jsx(
|
|
905
|
-
|
|
926
|
+
anyOptionHasImage && /* @__PURE__ */ jsxRuntime.jsx(
|
|
927
|
+
Avatar,
|
|
928
|
+
{
|
|
929
|
+
name: optionName,
|
|
930
|
+
src: option.image || void 0
|
|
931
|
+
}
|
|
932
|
+
),
|
|
933
|
+
/* @__PURE__ */ jsxRuntime.jsx(Typo, { variant: "label-lg", className: "font-normal text-[var(--fg)]", children: optionName })
|
|
906
934
|
] }),
|
|
907
935
|
Array.isArray(option.group) && option.group.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(AvatarGroup, { children: option.group.map((member, i) => /* @__PURE__ */ jsxRuntime.jsx(Avatar, { name: member.name, src: member.image || void 0 }, i)) })
|
|
908
936
|
]
|
|
909
937
|
},
|
|
910
|
-
`${
|
|
938
|
+
`${optionName || index}-${index}`
|
|
911
939
|
);
|
|
912
940
|
}) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(Typo, { variant: "body-sm", className: "text-[var(--muted-fg)]", children: "No results found" }) })
|
|
913
941
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -161,7 +161,9 @@ type SkeletonProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
161
161
|
declare function Skeleton({ className, rounded, ...props }: SkeletonProps): react_jsx_runtime.JSX.Element;
|
|
162
162
|
|
|
163
163
|
type AutocompleteOption = {
|
|
164
|
-
|
|
164
|
+
name?: string;
|
|
165
|
+
/** @deprecated Prefer using `name`. */
|
|
166
|
+
label?: string;
|
|
165
167
|
value: any;
|
|
166
168
|
image?: string;
|
|
167
169
|
group?: Array<{
|
package/dist/index.d.ts
CHANGED
|
@@ -161,7 +161,9 @@ type SkeletonProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
161
161
|
declare function Skeleton({ className, rounded, ...props }: SkeletonProps): react_jsx_runtime.JSX.Element;
|
|
162
162
|
|
|
163
163
|
type AutocompleteOption = {
|
|
164
|
-
|
|
164
|
+
name?: string;
|
|
165
|
+
/** @deprecated Prefer using `name`. */
|
|
166
|
+
label?: string;
|
|
165
167
|
value: any;
|
|
166
168
|
image?: string;
|
|
167
169
|
group?: Array<{
|
package/dist/index.js
CHANGED
|
@@ -707,20 +707,40 @@ function Autocomplete({
|
|
|
707
707
|
const listRef = useRef(null);
|
|
708
708
|
const normalizedOptions = useMemo(() => {
|
|
709
709
|
if (Array.isArray(options) && typeof options[0] === "string") {
|
|
710
|
-
return options.map((str) => ({
|
|
710
|
+
return options.map((str) => ({
|
|
711
|
+
name: str,
|
|
712
|
+
label: str,
|
|
713
|
+
value: str
|
|
714
|
+
}));
|
|
711
715
|
}
|
|
712
|
-
return options
|
|
716
|
+
return options.map((option) => {
|
|
717
|
+
const fallback = option.name ?? option.label ?? "";
|
|
718
|
+
return {
|
|
719
|
+
...option,
|
|
720
|
+
name: option.name ?? fallback,
|
|
721
|
+
label: option.label ?? fallback
|
|
722
|
+
};
|
|
723
|
+
});
|
|
713
724
|
}, [options]);
|
|
714
725
|
const filtered = useMemo(() => {
|
|
715
726
|
const q = searchQuery.trim().toLowerCase();
|
|
716
727
|
if (!q) return normalizedOptions;
|
|
717
|
-
return normalizedOptions.filter((o) =>
|
|
728
|
+
return normalizedOptions.filter((o) => {
|
|
729
|
+
const name = o.name?.toLowerCase() ?? "";
|
|
730
|
+
const label = o.label?.toLowerCase() ?? "";
|
|
731
|
+
return name.includes(q) || label.includes(q);
|
|
732
|
+
});
|
|
718
733
|
}, [normalizedOptions, searchQuery]);
|
|
719
734
|
const anyOptionHasImage = useMemo(
|
|
720
735
|
() => normalizedOptions.some((o) => !!o.image),
|
|
721
736
|
[normalizedOptions]
|
|
722
737
|
);
|
|
723
|
-
const
|
|
738
|
+
const optionDisplay = (option) => {
|
|
739
|
+
if (!option) return "";
|
|
740
|
+
if (typeof option === "string") return option;
|
|
741
|
+
return option.name ?? option.label ?? "";
|
|
742
|
+
};
|
|
743
|
+
const displayValue = selectedOption != null ? optionDisplay(selectedOption) : searchQuery;
|
|
724
744
|
function openList() {
|
|
725
745
|
if (disabled) return;
|
|
726
746
|
setOpen(true);
|
|
@@ -790,7 +810,7 @@ function Autocomplete({
|
|
|
790
810
|
"fieldset",
|
|
791
811
|
{
|
|
792
812
|
className: [
|
|
793
|
-
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-
|
|
813
|
+
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-13",
|
|
794
814
|
"border-[var(--border)] focus-within:border-[var(--color-brand)]",
|
|
795
815
|
disabled ? "opacity-60 cursor-not-allowed" : ""
|
|
796
816
|
].join(" "),
|
|
@@ -844,6 +864,7 @@ function Autocomplete({
|
|
|
844
864
|
onClick: selectedOption && !open ? handleClear : () => setOpen((s) => !s),
|
|
845
865
|
disabled,
|
|
846
866
|
"aria-label": selectedOption && !open ? "Clear" : open ? "Collapse" : "Expand",
|
|
867
|
+
className: "absolute right-0 top-[-30%]",
|
|
847
868
|
iconClassName: [
|
|
848
869
|
"transition-transform duration-300 text-[var(--muted-fg)]",
|
|
849
870
|
open ? "rotate-180 text-[var(--color-brand)]" : ""
|
|
@@ -864,7 +885,8 @@ function Autocomplete({
|
|
|
864
885
|
style: { maxHeight },
|
|
865
886
|
children: filtered.length > 0 ? /* @__PURE__ */ jsx("ul", { id: listboxId, role: "listbox", ref: listRef, children: filtered.map((option, index) => {
|
|
866
887
|
const active = index === activeIndex;
|
|
867
|
-
const
|
|
888
|
+
const optionName = option.name ?? option.label ?? "";
|
|
889
|
+
const selected = selectedOption != null && (typeof selectedOption === "string" ? selectedOption === optionName || selectedOption === (option.label ?? "") : (selectedOption.name ?? selectedOption.label ?? "") === optionName);
|
|
868
890
|
return /* @__PURE__ */ jsxs(
|
|
869
891
|
"li",
|
|
870
892
|
{
|
|
@@ -880,13 +902,19 @@ function Autocomplete({
|
|
|
880
902
|
onClick: () => handleSelect(option),
|
|
881
903
|
children: [
|
|
882
904
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
883
|
-
anyOptionHasImage && /* @__PURE__ */ jsx(
|
|
884
|
-
|
|
905
|
+
anyOptionHasImage && /* @__PURE__ */ jsx(
|
|
906
|
+
Avatar,
|
|
907
|
+
{
|
|
908
|
+
name: optionName,
|
|
909
|
+
src: option.image || void 0
|
|
910
|
+
}
|
|
911
|
+
),
|
|
912
|
+
/* @__PURE__ */ jsx(Typo, { variant: "label-lg", className: "font-normal text-[var(--fg)]", children: optionName })
|
|
885
913
|
] }),
|
|
886
914
|
Array.isArray(option.group) && option.group.length > 0 && /* @__PURE__ */ jsx(AvatarGroup, { children: option.group.map((member, i) => /* @__PURE__ */ jsx(Avatar, { name: member.name, src: member.image || void 0 }, i)) })
|
|
887
915
|
]
|
|
888
916
|
},
|
|
889
|
-
`${
|
|
917
|
+
`${optionName || index}-${index}`
|
|
890
918
|
);
|
|
891
919
|
}) }) : /* @__PURE__ */ jsx("div", { className: "px-4 py-3", children: /* @__PURE__ */ jsx(Typo, { variant: "body-sm", className: "text-[var(--muted-fg)]", children: "No results found" }) })
|
|
892
920
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neoptocom/neopto-ui",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A modern React component library built with Tailwind CSS v4 and TypeScript. Features dark mode, design tokens, and comprehensive Storybook documentation. Requires Tailwind v4+.",
|
|
6
6
|
"keywords": [
|
|
@@ -6,7 +6,9 @@ import Avatar from "./Avatar";
|
|
|
6
6
|
import AvatarGroup from "./AvatarGroup";
|
|
7
7
|
|
|
8
8
|
export type AutocompleteOption = {
|
|
9
|
-
|
|
9
|
+
name?: string;
|
|
10
|
+
/** @deprecated Prefer using `name`. */
|
|
11
|
+
label?: string;
|
|
10
12
|
value: any;
|
|
11
13
|
image?: string;
|
|
12
14
|
group?: Array<{ name: string; image?: string }>;
|
|
@@ -55,16 +57,31 @@ export default function Autocomplete({
|
|
|
55
57
|
// Normalize options
|
|
56
58
|
const normalizedOptions: AutocompleteOption[] = useMemo(() => {
|
|
57
59
|
if (Array.isArray(options) && typeof options[0] === "string") {
|
|
58
|
-
return (options as string[]).map((str) => ({
|
|
60
|
+
return (options as string[]).map((str) => ({
|
|
61
|
+
name: str,
|
|
62
|
+
label: str,
|
|
63
|
+
value: str
|
|
64
|
+
}));
|
|
59
65
|
}
|
|
60
|
-
return options as AutocompleteOption[]
|
|
66
|
+
return (options as AutocompleteOption[]).map((option) => {
|
|
67
|
+
const fallback = option.name ?? option.label ?? "";
|
|
68
|
+
return {
|
|
69
|
+
...option,
|
|
70
|
+
name: option.name ?? fallback,
|
|
71
|
+
label: option.label ?? fallback
|
|
72
|
+
};
|
|
73
|
+
});
|
|
61
74
|
}, [options]);
|
|
62
75
|
|
|
63
76
|
// Filter options
|
|
64
77
|
const filtered = useMemo(() => {
|
|
65
78
|
const q = searchQuery.trim().toLowerCase();
|
|
66
79
|
if (!q) return normalizedOptions;
|
|
67
|
-
return normalizedOptions.filter((o) =>
|
|
80
|
+
return normalizedOptions.filter((o) => {
|
|
81
|
+
const name = o.name?.toLowerCase() ?? "";
|
|
82
|
+
const label = o.label?.toLowerCase() ?? "";
|
|
83
|
+
return name.includes(q) || label.includes(q);
|
|
84
|
+
});
|
|
68
85
|
}, [normalizedOptions, searchQuery]);
|
|
69
86
|
|
|
70
87
|
const anyOptionHasImage = useMemo(
|
|
@@ -72,12 +89,14 @@ export default function Autocomplete({
|
|
|
72
89
|
[normalizedOptions]
|
|
73
90
|
);
|
|
74
91
|
|
|
92
|
+
const optionDisplay = (option?: AutocompleteOption | string | null) => {
|
|
93
|
+
if (!option) return "";
|
|
94
|
+
if (typeof option === "string") return option;
|
|
95
|
+
return option.name ?? option.label ?? "";
|
|
96
|
+
};
|
|
97
|
+
|
|
75
98
|
const displayValue =
|
|
76
|
-
selectedOption != null
|
|
77
|
-
? typeof selectedOption === "string"
|
|
78
|
-
? selectedOption
|
|
79
|
-
: selectedOption.label
|
|
80
|
-
: searchQuery;
|
|
99
|
+
selectedOption != null ? optionDisplay(selectedOption) : searchQuery;
|
|
81
100
|
|
|
82
101
|
function openList() {
|
|
83
102
|
if (disabled) return;
|
|
@@ -148,7 +167,7 @@ export default function Autocomplete({
|
|
|
148
167
|
>
|
|
149
168
|
<fieldset
|
|
150
169
|
className={[
|
|
151
|
-
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-
|
|
170
|
+
"w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-13",
|
|
152
171
|
"border-[var(--border)] focus-within:border-[var(--color-brand)]",
|
|
153
172
|
disabled ? "opacity-60 cursor-not-allowed" : ""
|
|
154
173
|
].join(" ")}
|
|
@@ -203,6 +222,7 @@ export default function Autocomplete({
|
|
|
203
222
|
}
|
|
204
223
|
disabled={disabled}
|
|
205
224
|
aria-label={selectedOption && !open ? "Clear" : open ? "Collapse" : "Expand"}
|
|
225
|
+
className="absolute right-0 top-[-30%]"
|
|
206
226
|
iconClassName={[
|
|
207
227
|
"transition-transform duration-300 text-[var(--muted-fg)]",
|
|
208
228
|
open ? "rotate-180 text-[var(--color-brand)]" : ""
|
|
@@ -225,14 +245,17 @@ export default function Autocomplete({
|
|
|
225
245
|
<ul id={listboxId} role="listbox" ref={listRef}>
|
|
226
246
|
{filtered.map((option, index) => {
|
|
227
247
|
const active = index === activeIndex;
|
|
248
|
+
const optionName = option.name ?? option.label ?? "";
|
|
228
249
|
const selected =
|
|
229
250
|
selectedOption != null &&
|
|
230
251
|
(typeof selectedOption === "string"
|
|
231
|
-
? selectedOption ===
|
|
232
|
-
|
|
252
|
+
? selectedOption === optionName ||
|
|
253
|
+
selectedOption === (option.label ?? "")
|
|
254
|
+
: (selectedOption.name ?? selectedOption.label ?? "") ===
|
|
255
|
+
optionName);
|
|
233
256
|
return (
|
|
234
257
|
<li
|
|
235
|
-
key={`${
|
|
258
|
+
key={`${optionName || index}-${index}`}
|
|
236
259
|
role="option"
|
|
237
260
|
aria-selected={selected}
|
|
238
261
|
className={[
|
|
@@ -248,10 +271,13 @@ export default function Autocomplete({
|
|
|
248
271
|
>
|
|
249
272
|
<div className="flex items-center gap-2">
|
|
250
273
|
{anyOptionHasImage && (
|
|
251
|
-
<Avatar
|
|
274
|
+
<Avatar
|
|
275
|
+
name={optionName}
|
|
276
|
+
src={option.image || undefined}
|
|
277
|
+
/>
|
|
252
278
|
)}
|
|
253
279
|
<Typo variant="label-lg" className="font-normal text-[var(--fg)]">
|
|
254
|
-
{
|
|
280
|
+
{optionName}
|
|
255
281
|
</Typo>
|
|
256
282
|
</div>
|
|
257
283
|
{Array.isArray(option.group) && option.group.length > 0 && (
|
|
@@ -3,11 +3,23 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|
|
3
3
|
import Autocomplete, { type AutocompleteOption } from "../components/Autocomplete";
|
|
4
4
|
|
|
5
5
|
const OPTIONS: AutocompleteOption[] = [
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
{
|
|
7
|
+
name: "Ada Lovelace",
|
|
8
|
+
label: "Ada Lovelace",
|
|
9
|
+
value: "ada",
|
|
10
|
+
image:
|
|
11
|
+
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=128&auto=format&fit=facearea&facepad=2"
|
|
12
|
+
},
|
|
13
|
+
{ name: "Alan Turing", value: "turing" },
|
|
14
|
+
{
|
|
15
|
+
name: "Grace Hopper",
|
|
16
|
+
label: "Grace Hopper",
|
|
17
|
+
value: "hopper",
|
|
18
|
+
image:
|
|
19
|
+
"https://images.unsplash.com/photo-1545184180-25d471fe75d8?q=80&w=128&auto=format&fit=facearea&facepad=2"
|
|
20
|
+
},
|
|
21
|
+
{ name: "Edsger Dijkstra", value: "dijkstra" },
|
|
22
|
+
{ name: "Barbara Liskov", value: "liskov" }
|
|
11
23
|
];
|
|
12
24
|
|
|
13
25
|
const meta: Meta<typeof Autocomplete> = {
|
|
@@ -33,7 +45,10 @@ export const Playground: Story = {
|
|
|
33
45
|
<div className="max-w-md">
|
|
34
46
|
<Autocomplete {...args} selectedOption={value} onSelect={setValue} />
|
|
35
47
|
<div className="mt-3 text-xs text-[--muted-fg]">
|
|
36
|
-
Selected:
|
|
48
|
+
Selected:{" "}
|
|
49
|
+
{typeof value === "string"
|
|
50
|
+
? value
|
|
51
|
+
: value?.name ?? value?.label ?? "none"}
|
|
37
52
|
</div>
|
|
38
53
|
</div>
|
|
39
54
|
);
|