@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.
Files changed (42) hide show
  1. package/CONSUMER_SETUP.md +55 -36
  2. package/README.md +25 -9
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +6 -1
  6. package/scripts/init.mjs +200 -0
  7. package/src/assets/agent-neopto-dark.svg +9 -0
  8. package/src/assets/agent-neopto.svg +9 -0
  9. package/src/components/Autocomplete.tsx +279 -0
  10. package/src/components/Avatar.tsx +83 -0
  11. package/src/components/AvatarGroup.tsx +53 -0
  12. package/src/components/Button.tsx +51 -0
  13. package/src/components/Chat/AnimatedBgCircle.tsx +51 -0
  14. package/src/components/Chat/AnimatedBgRectangle.tsx +55 -0
  15. package/src/components/Chat/ChatButton.tsx +132 -0
  16. package/src/components/Chat/index.ts +5 -0
  17. package/src/components/Chip.tsx +38 -0
  18. package/src/components/Counter.tsx +69 -0
  19. package/src/components/Icon.tsx +48 -0
  20. package/src/components/IconButton.tsx +89 -0
  21. package/src/components/Input.tsx +29 -0
  22. package/src/components/Modal.tsx +83 -0
  23. package/src/components/Search.tsx +244 -0
  24. package/src/components/Skeleton.tsx +29 -0
  25. package/src/components/Typo.tsx +93 -0
  26. package/src/index.ts +31 -0
  27. package/src/stories/Autocomplete.stories.tsx +41 -0
  28. package/src/stories/Avatar.stories.tsx +38 -0
  29. package/src/stories/AvatarGroup.stories.tsx +46 -0
  30. package/src/stories/Button.stories.tsx +103 -0
  31. package/src/stories/ChatButton.stories.tsx +94 -0
  32. package/src/stories/Chip.stories.tsx +36 -0
  33. package/src/stories/Counter.stories.tsx +35 -0
  34. package/src/stories/Icon.stories.tsx +34 -0
  35. package/src/stories/IconButton.stories.tsx +116 -0
  36. package/src/stories/Input.stories.tsx +38 -0
  37. package/src/stories/Search.stories.tsx +228 -0
  38. package/src/stories/Skeleton.stories.tsx +43 -0
  39. package/src/stories/Typo.stories.tsx +66 -0
  40. package/src/styles/library.css +35 -0
  41. package/src/styles/tailwind.css +36 -0
  42. 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
+ };