@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 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) => ({ label: str, value: 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) => o.label.toLowerCase().includes(q));
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 displayValue = selectedOption != null ? typeof selectedOption === "string" ? selectedOption : selectedOption.label : searchQuery;
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-16",
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 selected = selectedOption != null && (typeof selectedOption === "string" ? selectedOption === option.label : selectedOption.label === option.label);
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(Avatar, { name: option.label, src: option.image || void 0 }),
905
- /* @__PURE__ */ jsxRuntime.jsx(Typo, { variant: "label-lg", className: "font-normal text-[var(--fg)]", children: option.label })
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
- `${option.label}-${index}`
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
- label: string;
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
- label: string;
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) => ({ label: str, value: 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) => o.label.toLowerCase().includes(q));
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 displayValue = selectedOption != null ? typeof selectedOption === "string" ? selectedOption : selectedOption.label : searchQuery;
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-16",
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 selected = selectedOption != null && (typeof selectedOption === "string" ? selectedOption === option.label : selectedOption.label === option.label);
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(Avatar, { name: option.label, src: option.image || void 0 }),
884
- /* @__PURE__ */ jsx(Typo, { variant: "label-lg", className: "font-normal text-[var(--fg)]", children: option.label })
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
- `${option.label}-${index}`
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.1",
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
- label: string;
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) => ({ label: str, value: 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) => o.label.toLowerCase().includes(q));
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-16",
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 === option.label
232
- : selectedOption.label === option.label);
252
+ ? selectedOption === optionName ||
253
+ selectedOption === (option.label ?? "")
254
+ : (selectedOption.name ?? selectedOption.label ?? "") ===
255
+ optionName);
233
256
  return (
234
257
  <li
235
- key={`${option.label}-${index}`}
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 name={option.label} src={option.image || undefined} />
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
- {option.label}
280
+ {optionName}
255
281
  </Typo>
256
282
  </div>
257
283
  {Array.isArray(option.group) && option.group.length > 0 && (
@@ -58,3 +58,7 @@ page load. The interactive example demonstrates an in-app documents flow.
58
58
  - Clickable items expose keyboard handlers, so always keep labels descriptive.
59
59
 
60
60
 
61
+
62
+
63
+
64
+
@@ -76,3 +76,7 @@ export const InteractiveNavigation: Story = {
76
76
  };
77
77
 
78
78
 
79
+
80
+
81
+
82
+
@@ -54,3 +54,7 @@ Use variants to communicate hierarchy:
54
54
  - Disable buttons sparingly—pair with helper text when an action is unavailable.
55
55
 
56
56
 
57
+
58
+
59
+
60
+
@@ -108,3 +108,7 @@ export const FullWidthCallToAction: Story = {
108
108
  };
109
109
 
110
110
 
111
+
112
+
113
+
114
+
@@ -54,3 +54,7 @@ when overlaying text on app backgrounds. For interactive cards, wrap focusable e
54
54
  attaching click handlers to the card root.
55
55
 
56
56
 
57
+
58
+
59
+
60
+
@@ -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
- { label: "Ada Lovelace", value: "ada", image: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=128&auto=format&fit=facearea&facepad=2" },
7
- { label: "Alan Turing", value: "turing" },
8
- { label: "Grace Hopper", value: "hopper", image: "https://images.unsplash.com/photo-1545184180-25d471fe75d8?q=80&w=128&auto=format&fit=facearea&facepad=2" },
9
- { label: "Edsger Dijkstra", value: "dijkstra" },
10
- { label: "Barbara Liskov", value: "liskov" }
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: {typeof value === "string" ? value : value?.label ?? "none"}
48
+ Selected:{" "}
49
+ {typeof value === "string"
50
+ ? value
51
+ : value?.name ?? value?.label ?? "none"}
37
52
  </div>
38
53
  </div>
39
54
  );