@neoptocom/neopto-ui 0.4.0 → 0.5.0
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/CONSUMER_SETUP.md +55 -36
- package/README.md +25 -9
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +6 -1
- package/scripts/init.mjs +200 -0
- package/src/assets/agent-neopto-dark.svg +9 -0
- package/src/assets/agent-neopto.svg +9 -0
- package/src/components/Autocomplete.tsx +279 -0
- package/src/components/Avatar.tsx +83 -0
- package/src/components/AvatarGroup.tsx +53 -0
- package/src/components/Button.tsx +51 -0
- package/src/components/Chat/AnimatedBgCircle.tsx +51 -0
- package/src/components/Chat/AnimatedBgRectangle.tsx +55 -0
- package/src/components/Chat/ChatButton.tsx +132 -0
- package/src/components/Chat/index.ts +5 -0
- package/src/components/Chip.tsx +38 -0
- package/src/components/Counter.tsx +69 -0
- package/src/components/Icon.tsx +48 -0
- package/src/components/IconButton.tsx +89 -0
- package/src/components/Input.tsx +29 -0
- package/src/components/Modal.tsx +83 -0
- package/src/components/Search.tsx +244 -0
- package/src/components/Skeleton.tsx +29 -0
- package/src/components/Typo.tsx +93 -0
- package/src/index.ts +31 -0
- package/src/stories/Autocomplete.stories.tsx +41 -0
- package/src/stories/Avatar.stories.tsx +38 -0
- package/src/stories/AvatarGroup.stories.tsx +46 -0
- package/src/stories/Button.stories.tsx +103 -0
- package/src/stories/ChatButton.stories.tsx +94 -0
- package/src/stories/Chip.stories.tsx +36 -0
- package/src/stories/Counter.stories.tsx +35 -0
- package/src/stories/Icon.stories.tsx +34 -0
- package/src/stories/IconButton.stories.tsx +116 -0
- package/src/stories/Input.stories.tsx +38 -0
- package/src/stories/Search.stories.tsx +228 -0
- package/src/stories/Skeleton.stories.tsx +43 -0
- package/src/stories/Typo.stories.tsx +66 -0
- package/src/styles/library.css +35 -0
- package/src/styles/tailwind.css +36 -0
- package/src/styles/tokens.css +72 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useId, useMemo, useRef, useState, useCallback } from "react";
|
|
3
|
+
import { IconButton } from "./IconButton";
|
|
4
|
+
import Typo from "./Typo";
|
|
5
|
+
import Avatar from "./Avatar";
|
|
6
|
+
import AvatarGroup from "./AvatarGroup";
|
|
7
|
+
|
|
8
|
+
export type SearchOption = {
|
|
9
|
+
label: string;
|
|
10
|
+
value: any;
|
|
11
|
+
image?: string;
|
|
12
|
+
group?: Array<{ name: string; image?: string }>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface SearchProps
|
|
16
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
17
|
+
/** Array of options to display */
|
|
18
|
+
options: SearchOption[] | string[];
|
|
19
|
+
/** Callback when search is performed (debounced) */
|
|
20
|
+
onSearch: (query: string) => void;
|
|
21
|
+
/** Currently selected option */
|
|
22
|
+
selectedOption: SearchOption | string | null;
|
|
23
|
+
/** Callback when an option is selected */
|
|
24
|
+
onSelect: (option: SearchOption | string | null) => void;
|
|
25
|
+
/** Search delay in milliseconds (default: 300ms) */
|
|
26
|
+
searchDelay?: number;
|
|
27
|
+
/** Whether the component is disabled */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Maximum height of the options dropdown */
|
|
30
|
+
maxHeight?: number;
|
|
31
|
+
/** Optional id base (for accessibility hooks) */
|
|
32
|
+
id?: string;
|
|
33
|
+
/** Optional filter children to render when filters are expanded */
|
|
34
|
+
children?: React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function Search({
|
|
38
|
+
className = "",
|
|
39
|
+
options,
|
|
40
|
+
onSearch,
|
|
41
|
+
selectedOption,
|
|
42
|
+
onSelect,
|
|
43
|
+
searchDelay = 300,
|
|
44
|
+
disabled = false,
|
|
45
|
+
maxHeight = 152,
|
|
46
|
+
id,
|
|
47
|
+
children,
|
|
48
|
+
...props
|
|
49
|
+
}: SearchProps) {
|
|
50
|
+
const inputId = id ?? useId();
|
|
51
|
+
const listboxId = `${inputId}-listbox`;
|
|
52
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
53
|
+
const [open, setOpen] = useState(false);
|
|
54
|
+
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
|
55
|
+
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
|
56
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const listRef = useRef<HTMLUListElement>(null);
|
|
58
|
+
const searchTimeoutRef = useRef<number | null>(null);
|
|
59
|
+
|
|
60
|
+
// Normalize options
|
|
61
|
+
const normalizedOptions: SearchOption[] = useMemo(() => {
|
|
62
|
+
if (Array.isArray(options) && typeof options[0] === "string") {
|
|
63
|
+
return (options as string[]).map((str) => ({ label: str, value: str }));
|
|
64
|
+
}
|
|
65
|
+
return options as SearchOption[];
|
|
66
|
+
}, [options]);
|
|
67
|
+
|
|
68
|
+
const anyOptionHasImage = useMemo(
|
|
69
|
+
() => normalizedOptions.some((o) => !!o.image),
|
|
70
|
+
[normalizedOptions]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const displayValue =
|
|
74
|
+
selectedOption != null
|
|
75
|
+
? typeof selectedOption === "string"
|
|
76
|
+
? selectedOption
|
|
77
|
+
: selectedOption.label
|
|
78
|
+
: searchQuery;
|
|
79
|
+
|
|
80
|
+
// Debounced search function
|
|
81
|
+
const debouncedSearch = useCallback(
|
|
82
|
+
(query: string) => {
|
|
83
|
+
if (searchTimeoutRef.current) {
|
|
84
|
+
clearTimeout(searchTimeoutRef.current);
|
|
85
|
+
}
|
|
86
|
+
searchTimeoutRef.current = window.setTimeout(() => {
|
|
87
|
+
onSearch(query);
|
|
88
|
+
}, searchDelay);
|
|
89
|
+
},
|
|
90
|
+
[onSearch, searchDelay]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
function openList() {
|
|
94
|
+
if (disabled) return;
|
|
95
|
+
setOpen(true);
|
|
96
|
+
}
|
|
97
|
+
function closeList() {
|
|
98
|
+
setOpen(false);
|
|
99
|
+
setActiveIndex(-1);
|
|
100
|
+
}
|
|
101
|
+
function handleSelect(option: SearchOption) {
|
|
102
|
+
onSelect(option);
|
|
103
|
+
setSearchQuery("");
|
|
104
|
+
closeList();
|
|
105
|
+
}
|
|
106
|
+
function handleClear() {
|
|
107
|
+
onSelect(null);
|
|
108
|
+
setSearchQuery("");
|
|
109
|
+
closeList();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function onKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
|
113
|
+
if (!open && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
114
|
+
setOpen(true);
|
|
115
|
+
setActiveIndex(0);
|
|
116
|
+
e.preventDefault();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!open) return;
|
|
120
|
+
if (e.key === "ArrowDown") {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
setActiveIndex((i) => Math.min(i + 1, normalizedOptions.length - 1));
|
|
123
|
+
scrollActiveIntoView();
|
|
124
|
+
} else if (e.key === "ArrowUp") {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
setActiveIndex((i) => Math.max(i - 1, 0));
|
|
127
|
+
scrollActiveIntoView();
|
|
128
|
+
} else if (e.key === "Enter") {
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
const item = normalizedOptions[activeIndex];
|
|
131
|
+
if (item) handleSelect(item);
|
|
132
|
+
} else if (e.key === "Escape") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
closeList();
|
|
135
|
+
} else if (e.key === "Home") {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
setActiveIndex(0);
|
|
138
|
+
scrollActiveIntoView();
|
|
139
|
+
} else if (e.key === "End") {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
setActiveIndex(normalizedOptions.length - 1);
|
|
142
|
+
scrollActiveIntoView();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function scrollActiveIntoView() {
|
|
147
|
+
const list = listRef.current;
|
|
148
|
+
const idx = activeIndex;
|
|
149
|
+
if (!list || idx < 0) return;
|
|
150
|
+
const el = list.children[idx] as HTMLElement | undefined;
|
|
151
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Cleanup timeout on unmount
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
return () => {
|
|
157
|
+
if (searchTimeoutRef.current) {
|
|
158
|
+
clearTimeout(searchTimeoutRef.current);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}, []);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
ref={rootRef}
|
|
166
|
+
className={["relative w-full", className].join(" ")}
|
|
167
|
+
{...props}
|
|
168
|
+
>
|
|
169
|
+
<div
|
|
170
|
+
className={[
|
|
171
|
+
"w-full min-w-0 border rounded-[24px] bg-[var(--surface)] transition-all",
|
|
172
|
+
"border-[var(--border)] focus-within:border-[var(--color-brand)]",
|
|
173
|
+
disabled ? "opacity-60 cursor-not-allowed" : "",
|
|
174
|
+
!filtersExpanded && "h-12"
|
|
175
|
+
].join(" ")}
|
|
176
|
+
>
|
|
177
|
+
<div className="relative flex h-full">
|
|
178
|
+
<div className={[
|
|
179
|
+
"flex flex-col w-full overflow-hidden transition-all",
|
|
180
|
+
filtersExpanded && children ? "h-auto pb-3" : "h-full"
|
|
181
|
+
].join(" ")}>
|
|
182
|
+
<div className="flex w-full items-center h-12 px-2">
|
|
183
|
+
{/* Filter button (if children exist) */}
|
|
184
|
+
{children && (
|
|
185
|
+
<IconButton
|
|
186
|
+
icon="filter_list"
|
|
187
|
+
onClick={() => setFiltersExpanded((prev) => !prev)}
|
|
188
|
+
disabled={disabled}
|
|
189
|
+
aria-label={filtersExpanded ? "Hide filters" : "Show filters"}
|
|
190
|
+
aria-expanded={filtersExpanded}
|
|
191
|
+
className="mr-2"
|
|
192
|
+
/>
|
|
193
|
+
)}
|
|
194
|
+
{/* Input */}
|
|
195
|
+
<input
|
|
196
|
+
id={inputId}
|
|
197
|
+
role="combobox"
|
|
198
|
+
aria-expanded={open}
|
|
199
|
+
aria-controls={listboxId}
|
|
200
|
+
aria-autocomplete="list"
|
|
201
|
+
aria-disabled={disabled || undefined}
|
|
202
|
+
type="text"
|
|
203
|
+
value={displayValue}
|
|
204
|
+
onChange={(e) => {
|
|
205
|
+
const query = e.target.value;
|
|
206
|
+
setSearchQuery(query);
|
|
207
|
+
debouncedSearch(query);
|
|
208
|
+
if (!open) setOpen(true);
|
|
209
|
+
setActiveIndex(0);
|
|
210
|
+
}}
|
|
211
|
+
onFocus={openList}
|
|
212
|
+
onKeyDown={onKeyDown}
|
|
213
|
+
onBlur={() => setTimeout(closeList, 150)}
|
|
214
|
+
disabled={disabled}
|
|
215
|
+
style={{ fontFamily: 'var(--font-display)', fontSize: '16px' }}
|
|
216
|
+
className={[
|
|
217
|
+
"w-full rounded-full border-0 outline-none bg-transparent h-12",
|
|
218
|
+
"leading-tight text-[var(--fg)] placeholder:text-[var(--muted-fg)]",
|
|
219
|
+
"px-2"
|
|
220
|
+
].join(" ")}
|
|
221
|
+
placeholder="Pesquisar"
|
|
222
|
+
onClick={() => !disabled && setOpen(true)}
|
|
223
|
+
/>
|
|
224
|
+
{/* Action button (clear or expand) */}
|
|
225
|
+
<IconButton
|
|
226
|
+
icon="search"
|
|
227
|
+
onClick={
|
|
228
|
+
selectedOption && !open ? handleClear : () => setOpen((s) => !s)
|
|
229
|
+
}
|
|
230
|
+
disabled={disabled}
|
|
231
|
+
aria-label={selectedOption && !open ? "Clear" : open ? "Collapse" : "Expand"}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
{children && (
|
|
235
|
+
<div className="w-full px-4.5 pb-3 pt-2">
|
|
236
|
+
{children}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export type SkeletonProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
rounded?: "none" | "sm" | "md" | "lg" | "xl" | "full";
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const roundMap: Record<NonNullable<SkeletonProps["rounded"]>, string> = {
|
|
8
|
+
none: "rounded-none",
|
|
9
|
+
sm: "rounded-[var(--radius-sm)]",
|
|
10
|
+
md: "rounded-[var(--radius-md)]",
|
|
11
|
+
lg: "rounded-[var(--radius-lg)]",
|
|
12
|
+
xl: "rounded-[var(--radius-xl)]",
|
|
13
|
+
full: "rounded-full"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Skeleton({ className = "", rounded = "md", ...props }: SkeletonProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={[
|
|
20
|
+
"animate-pulse bg-[var(--muted)]",
|
|
21
|
+
roundMap[rounded],
|
|
22
|
+
className
|
|
23
|
+
].join(" ")}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { tv, type VariantProps } from "tailwind-variants";
|
|
3
|
+
|
|
4
|
+
export type TypoVariant =
|
|
5
|
+
| "display-lg" | "display-md" | "display-sm"
|
|
6
|
+
| "headline-lg" | "headline-md" | "headline-sm"
|
|
7
|
+
| "title-lg" | "title-md" | "title-sm"
|
|
8
|
+
| "label-lg" | "label-md" | "label-sm"
|
|
9
|
+
| "body-lg" | "body-md" | "body-sm"
|
|
10
|
+
| "button";
|
|
11
|
+
|
|
12
|
+
export type TypoWeight = "normal" | "medium" | "semibold" | "bold";
|
|
13
|
+
|
|
14
|
+
const styles = tv({
|
|
15
|
+
base: "text-current",
|
|
16
|
+
variants: {
|
|
17
|
+
variant: {
|
|
18
|
+
"display-lg": "text-5xl leading-tight",
|
|
19
|
+
"display-md": "text-4xl leading-tight",
|
|
20
|
+
"display-sm": "text-4xl leading-tight",
|
|
21
|
+
"headline-lg": "text-3xl leading-tight",
|
|
22
|
+
"headline-md": "text-3xl leading-tight",
|
|
23
|
+
"headline-sm": "text-3xl leading-tight",
|
|
24
|
+
"title-lg": "text-xl leading-tight",
|
|
25
|
+
"title-md": "text-lg leading-tight",
|
|
26
|
+
"title-sm": "text-base leading-tight",
|
|
27
|
+
"label-lg": "text-sm leading-tight",
|
|
28
|
+
"label-md": "text-xs leading-tight",
|
|
29
|
+
"label-sm": "text-xs leading-tight",
|
|
30
|
+
"body-lg": "text-base leading-relaxed",
|
|
31
|
+
"body-md": "text-sm leading-relaxed",
|
|
32
|
+
"body-sm": "text-xs leading-relaxed",
|
|
33
|
+
"button": "text-base leading-normal"
|
|
34
|
+
},
|
|
35
|
+
weight: {
|
|
36
|
+
normal: "font-normal",
|
|
37
|
+
medium: "font-medium",
|
|
38
|
+
semibold: "font-semibold",
|
|
39
|
+
bold: "font-bold"
|
|
40
|
+
},
|
|
41
|
+
muted: {
|
|
42
|
+
true: "text-[var(--muted-fg)]"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: {
|
|
46
|
+
weight: "normal"
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export type TypoProps<T extends React.ElementType = "span"> = {
|
|
51
|
+
/**
|
|
52
|
+
* Typography scale name (e.g., "title-md", "body-sm", "display-lg").
|
|
53
|
+
* Uses Tailwind utilities with design tokens for consistent theming.
|
|
54
|
+
*/
|
|
55
|
+
variant: TypoVariant;
|
|
56
|
+
/** Optional font weight override */
|
|
57
|
+
bold?: TypoWeight;
|
|
58
|
+
/** Use muted foreground token */
|
|
59
|
+
muted?: boolean;
|
|
60
|
+
/** Render as a different element (e.g., "p", "h3") */
|
|
61
|
+
as?: T;
|
|
62
|
+
children: React.ReactNode;
|
|
63
|
+
} & Omit<React.ComponentPropsWithoutRef<T>, "as" | "children">;
|
|
64
|
+
|
|
65
|
+
export default function Typo<T extends React.ElementType = "span">({
|
|
66
|
+
variant,
|
|
67
|
+
bold,
|
|
68
|
+
muted,
|
|
69
|
+
as,
|
|
70
|
+
className,
|
|
71
|
+
children,
|
|
72
|
+
...props
|
|
73
|
+
}: TypoProps<T>) {
|
|
74
|
+
const Component = (as ?? "span") as React.ElementType;
|
|
75
|
+
|
|
76
|
+
// Determine font family based on variant
|
|
77
|
+
const getFontFamily = (variant: TypoVariant) => {
|
|
78
|
+
if (variant.startsWith("body")) {
|
|
79
|
+
return "var(--font-body)";
|
|
80
|
+
}
|
|
81
|
+
return "var(--font-display)";
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Component
|
|
86
|
+
className={styles({ variant, weight: bold, muted, className })}
|
|
87
|
+
style={{ fontFamily: getFontFamily(variant) }}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</Component>
|
|
92
|
+
);
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export * from "./components/Input";
|
|
3
|
+
export * from "./components/Modal";
|
|
4
|
+
export { default as Typo } from "./components/Typo";
|
|
5
|
+
export { default as Avatar } from "./components/Avatar";
|
|
6
|
+
export { default as AvatarGroup } from "./components/AvatarGroup";
|
|
7
|
+
export * from "./components/Skeleton";
|
|
8
|
+
export { default as Autocomplete } from "./components/Autocomplete";
|
|
9
|
+
export { default as Search } from "./components/Search";
|
|
10
|
+
export { Button } from "./components/Button";
|
|
11
|
+
export { IconButton } from "./components/IconButton";
|
|
12
|
+
export { default as Icon } from "./components/Icon";
|
|
13
|
+
export { default as Chip } from "./components/Chip";
|
|
14
|
+
export { default as Counter } from "./components/Counter";
|
|
15
|
+
export * from "./components/Chat";
|
|
16
|
+
|
|
17
|
+
// Types
|
|
18
|
+
export type { InputProps } from "./components/Input";
|
|
19
|
+
export type { ModalProps } from "./components/Modal";
|
|
20
|
+
export type { TypoProps, TypoVariant, TypoWeight } from "./components/Typo";
|
|
21
|
+
export type { AvatarProps } from "./components/Avatar";
|
|
22
|
+
export type { AvatarGroupProps } from "./components/AvatarGroup";
|
|
23
|
+
export type { SkeletonProps } from "./components/Skeleton";
|
|
24
|
+
export type { AutocompleteProps, AutocompleteOption } from "./components/Autocomplete";
|
|
25
|
+
export type { SearchProps, SearchOption } from "./components/Search";
|
|
26
|
+
export type { ButtonProps } from "./components/Button";
|
|
27
|
+
export type { IconButtonProps } from "./components/IconButton";
|
|
28
|
+
export type { IconProps } from "./components/Icon";
|
|
29
|
+
export type { ChipProps } from "./components/Chip";
|
|
30
|
+
export type { CounterProps } from "./components/Counter";
|
|
31
|
+
export type { ChatButtonProps } from "./components/Chat";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import Autocomplete, { type AutocompleteOption } from "../components/Autocomplete";
|
|
4
|
+
|
|
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" }
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof Autocomplete> = {
|
|
14
|
+
title: "Components/Autocomplete",
|
|
15
|
+
component: Autocomplete,
|
|
16
|
+
args: {
|
|
17
|
+
title: "Assignee",
|
|
18
|
+
options: OPTIONS,
|
|
19
|
+
selectedOption: null,
|
|
20
|
+
placeholder: "Search people…",
|
|
21
|
+
disabled: false,
|
|
22
|
+
maxHeight: 180
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof Autocomplete>;
|
|
28
|
+
|
|
29
|
+
export const Playground: Story = {
|
|
30
|
+
render: (args) => {
|
|
31
|
+
const [value, setValue] = React.useState<AutocompleteOption | string | null>(args.selectedOption);
|
|
32
|
+
return (
|
|
33
|
+
<div className="max-w-md">
|
|
34
|
+
<Autocomplete {...args} selectedOption={value} onSelect={setValue} />
|
|
35
|
+
<div className="mt-3 text-xs text-[--muted-fg]">
|
|
36
|
+
Selected: {typeof value === "string" ? value : value?.label ?? "none"}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Avatar from "../components/Avatar";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Avatar> = {
|
|
5
|
+
title: "Components/Avatar",
|
|
6
|
+
component: Avatar,
|
|
7
|
+
args: {
|
|
8
|
+
name: "Ada Lovelace",
|
|
9
|
+
size: "sm"
|
|
10
|
+
},
|
|
11
|
+
argTypes: {
|
|
12
|
+
size: { control: "radio", options: ["sm", "md"] }
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof Avatar>;
|
|
17
|
+
|
|
18
|
+
export const WithImage: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
src: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=256&auto=format&fit=facearea&facepad=2"
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const DifferentSizes: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
27
|
+
<Avatar name="Ada Lovelace" size="sm" color="#5ABDCC"/>
|
|
28
|
+
<Avatar name="Grace Hopper" size="md" color="#22A9CB"/>
|
|
29
|
+
</div>)
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export const CustomColor: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
name: "Grace Hopper",
|
|
36
|
+
color: "#5ABDCC"
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import Avatar from "../components/Avatar";
|
|
3
|
+
import AvatarGroup from "../components/AvatarGroup";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof AvatarGroup> = {
|
|
6
|
+
title: "Components/AvatarGroup",
|
|
7
|
+
component: AvatarGroup,
|
|
8
|
+
args: {
|
|
9
|
+
max: 5,
|
|
10
|
+
withRings: true,
|
|
11
|
+
overlapPx: 8
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof AvatarGroup>;
|
|
16
|
+
|
|
17
|
+
const sample = [
|
|
18
|
+
{ name: "Ada Lovelace", src: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=128&auto=format&fit=facearea&facepad=2" },
|
|
19
|
+
{ name: "Grace Hopper", src: "https://images.unsplash.com/photo-1545184180-25d471fe75d8?q=80&w=128&auto=format&fit=facearea&facepad=2" },
|
|
20
|
+
{ name: "Alan Turing" },
|
|
21
|
+
{ name: "Edsger Dijkstra" },
|
|
22
|
+
{ name: "Barbara Liskov" },
|
|
23
|
+
{ name: "Donald Knuth" }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
render: (args) => (
|
|
28
|
+
<AvatarGroup {...args}>
|
|
29
|
+
{sample.map((p, i) => (
|
|
30
|
+
<Avatar key={i} name={p.name} src={p.src} size="sm" />
|
|
31
|
+
))}
|
|
32
|
+
</AvatarGroup>
|
|
33
|
+
)
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const LimitedWithCounter: Story = {
|
|
37
|
+
args: { max: 3 },
|
|
38
|
+
render: (args) => (
|
|
39
|
+
<AvatarGroup {...args}>
|
|
40
|
+
{sample.map((p, i) => (
|
|
41
|
+
<Avatar key={i} name={p.name} src={p.src} size="sm" />
|
|
42
|
+
))}
|
|
43
|
+
</AvatarGroup>
|
|
44
|
+
)
|
|
45
|
+
};
|
|
46
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Button } from "../components/Button";
|
|
3
|
+
import Icon from "../components/Icon";
|
|
4
|
+
import Typo from "../components/Typo";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Button> = {
|
|
7
|
+
title: "Components/Button",
|
|
8
|
+
component: Button,
|
|
9
|
+
args: {
|
|
10
|
+
children: "Button",
|
|
11
|
+
variant: "primary",
|
|
12
|
+
size: "md",
|
|
13
|
+
disabled: false
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
variant: {
|
|
17
|
+
control: "radio",
|
|
18
|
+
options: ["primary", "secondary", "ghost"]
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
control: "radio",
|
|
22
|
+
options: ["sm", "md", "lg"]
|
|
23
|
+
},
|
|
24
|
+
fullWidth: {
|
|
25
|
+
control: "boolean"
|
|
26
|
+
},
|
|
27
|
+
disabled: {
|
|
28
|
+
control: "boolean"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof Button>;
|
|
35
|
+
|
|
36
|
+
export const Playground: Story = {};
|
|
37
|
+
|
|
38
|
+
export const Variants: Story = {
|
|
39
|
+
render: () => (
|
|
40
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
41
|
+
<Button variant="primary">
|
|
42
|
+
<Typo variant="title-sm" bold="semibold">Primary</Typo>
|
|
43
|
+
</Button>
|
|
44
|
+
<Button variant="secondary">
|
|
45
|
+
<Typo variant="title-sm" bold="semibold">Secondary</Typo>
|
|
46
|
+
</Button>
|
|
47
|
+
<Button variant="ghost">
|
|
48
|
+
<Typo variant="title-sm" bold="semibold">Ghost</Typo>
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Sizes: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
57
|
+
<Button size="sm">
|
|
58
|
+
<Typo variant="title-sm" bold="semibold">Small</Typo>
|
|
59
|
+
</Button>
|
|
60
|
+
<Button size="md">
|
|
61
|
+
<Typo variant="title-sm" bold="semibold">Medium</Typo>
|
|
62
|
+
</Button>
|
|
63
|
+
<Button size="lg">
|
|
64
|
+
<Typo variant="title-sm" bold="semibold">Large</Typo>
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const States: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
73
|
+
<Button>
|
|
74
|
+
<Typo variant="title-sm" bold="semibold">Default</Typo>
|
|
75
|
+
</Button>
|
|
76
|
+
<Button disabled>
|
|
77
|
+
<Typo variant="title-sm" bold="semibold">Disabled</Typo>
|
|
78
|
+
</Button>
|
|
79
|
+
<Button fullWidth>
|
|
80
|
+
<Typo variant="title-sm" bold="semibold">Full Width</Typo>
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const WithIcons: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
89
|
+
<Button>
|
|
90
|
+
<Icon name="add" />
|
|
91
|
+
<Typo variant="title-sm" bold="semibold">Add Item</Typo>
|
|
92
|
+
</Button>
|
|
93
|
+
<Button variant="secondary">
|
|
94
|
+
<Icon name="delete" />
|
|
95
|
+
<Typo variant="title-sm" bold="semibold">Delete</Typo>
|
|
96
|
+
</Button>
|
|
97
|
+
<Button variant="ghost">
|
|
98
|
+
<Icon name="settings" />
|
|
99
|
+
<Typo variant="title-sm" bold="semibold">Settings</Typo>
|
|
100
|
+
</Button>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
};
|