@neoptocom/neopto-ui 1.4.4 → 1.5.1

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.
@@ -1,24 +1,25 @@
1
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";
2
+ import Icon from "./Icon";
3
+ import Typo from "./Typo";
4
+ import { Button } from "./Button";
5
5
 
6
6
  const meta: Meta<typeof Button> = {
7
7
  title: "Components/Button",
8
8
  component: Button,
9
+ tags: ["autodocs"],
9
10
  args: {
10
- children: "Button",
11
+ children: "Primary action",
11
12
  variant: "primary",
12
13
  size: "md",
13
14
  disabled: false
14
15
  },
15
16
  argTypes: {
16
17
  variant: {
17
- control: "radio",
18
+ control: "inline-radio",
18
19
  options: ["primary", "secondary", "ghost"]
19
20
  },
20
21
  size: {
21
- control: "radio",
22
+ control: "inline-radio",
22
23
  options: ["sm", "md", "lg"]
23
24
  },
24
25
  fullWidth: {
@@ -39,13 +40,19 @@ export const Variants: Story = {
39
40
  render: () => (
40
41
  <div className="flex flex-wrap items-center gap-4">
41
42
  <Button variant="primary">
42
- <Typo variant="title-sm" bold="semibold">Primary</Typo>
43
+ <Typo variant="title-sm" bold="semibold">
44
+ Primary
45
+ </Typo>
43
46
  </Button>
44
47
  <Button variant="secondary">
45
- <Typo variant="title-sm" bold="semibold">Secondary</Typo>
48
+ <Typo variant="title-sm" bold="semibold">
49
+ Secondary
50
+ </Typo>
46
51
  </Button>
47
52
  <Button variant="ghost">
48
- <Typo variant="title-sm" bold="semibold">Ghost</Typo>
53
+ <Typo variant="title-sm" bold="semibold">
54
+ Ghost
55
+ </Typo>
49
56
  </Button>
50
57
  </div>
51
58
  )
@@ -55,29 +62,19 @@ export const Sizes: Story = {
55
62
  render: () => (
56
63
  <div className="flex flex-wrap items-center gap-4">
57
64
  <Button size="sm">
58
- <Typo variant="title-sm" bold="semibold">Small</Typo>
65
+ <Typo variant="title-sm" bold="semibold">
66
+ Small
67
+ </Typo>
59
68
  </Button>
60
69
  <Button size="md">
61
- <Typo variant="title-sm" bold="semibold">Medium</Typo>
70
+ <Typo variant="title-sm" bold="semibold">
71
+ Medium
72
+ </Typo>
62
73
  </Button>
63
74
  <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>
75
+ <Typo variant="title-sm" bold="semibold">
76
+ Large
77
+ </Typo>
81
78
  </Button>
82
79
  </div>
83
80
  )
@@ -88,16 +85,26 @@ export const WithIcons: Story = {
88
85
  <div className="flex flex-wrap items-center gap-4">
89
86
  <Button>
90
87
  <Icon name="add" />
91
- <Typo variant="title-sm" bold="semibold">Add Item</Typo>
88
+ <span>Add Item</span>
92
89
  </Button>
93
90
  <Button variant="secondary">
94
- <Icon name="delete" />
95
- <Typo variant="title-sm" bold="semibold">Delete</Typo>
91
+ <Icon name="download" />
92
+ <span>Download</span>
96
93
  </Button>
97
94
  <Button variant="ghost">
98
95
  <Icon name="settings" />
99
- <Typo variant="title-sm" bold="semibold">Settings</Typo>
96
+ <span>Settings</span>
100
97
  </Button>
101
98
  </div>
102
99
  )
103
100
  };
101
+
102
+ export const FullWidthCallToAction: Story = {
103
+ args: {
104
+ fullWidth: true,
105
+ size: "lg",
106
+ children: "Start free trial"
107
+ }
108
+ };
109
+
110
+
@@ -0,0 +1,56 @@
1
+ import { Meta, Canvas, Story, ArgsTable } from "@storybook/blocks";
2
+ import { Card } from "./Card";
3
+ import * as CardStories from "./Card.stories";
4
+
5
+ <Meta of={CardStories} />
6
+
7
+ # Card
8
+
9
+ Cards provide glassmorphic containers for grouping content. They offer decorative borders, elevated
10
+ shadows, and an app-background variant that mirrors hero surfaces.
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { Card } from "@neoptocom/neopto-ui";
16
+
17
+ <Card>
18
+ <h3>Weekly summary</h3>
19
+ <p>Use cards to separate content into digestible surfaces.</p>
20
+ </Card>;
21
+ ```
22
+
23
+ <Canvas>
24
+ <Story of={CardStories.Playground} />
25
+ </Canvas>
26
+
27
+ <ArgsTable of={CardStories.Playground} />
28
+
29
+ ## Variants
30
+
31
+ - `showDecorations` adds gradient strokes suited for marketing hero cards.
32
+ - `variant="app-background"` injects the NeoPTO hero artwork and auto-switches with theme.
33
+ - `elevated` applies a high-emphasis drop shadow—combine with default or app backgrounds.
34
+
35
+ <Canvas>
36
+ <Story of={CardStories.AppBackground} />
37
+ </Canvas>
38
+
39
+ ## Layout tips
40
+
41
+ - Allow cards to breathe: default padding is `p-6`, override via the `className` prop for custom
42
+ spacing.
43
+ - Stack cards in responsive grids to create dashboards or settings panels.
44
+ - Avoid nesting too many cards; use muted surfaces inside when grouping secondary information.
45
+
46
+ <Canvas>
47
+ <Story of={CardStories.DashboardLayout} />
48
+ </Canvas>
49
+
50
+ ## Accessibility
51
+
52
+ Cards render as semantic `<div>` elements. Provide meaningful headings and maintain contrast ratios
53
+ when overlaying text on app backgrounds. For interactive cards, wrap focusable elements instead of
54
+ attaching click handlers to the card root.
55
+
56
+
@@ -0,0 +1,129 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Button } from "./Button";
3
+ import Typo from "./Typo";
4
+ import { Card } from "./Card";
5
+
6
+ const meta: Meta<typeof Card> = {
7
+ title: "Components/Card",
8
+ component: Card,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ layout: "padded"
12
+ }
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof Card>;
17
+
18
+ export const Playground: Story = {
19
+ render: (args) => (
20
+ <div className="max-w-md">
21
+ <Card {...args}>
22
+ <Typo variant="headline-sm" bold="semibold">
23
+ Glassmorphic card
24
+ </Typo>
25
+ <Typo variant="body-sm" className="mt-2 text-[var(--muted-fg)]">
26
+ Cards wrap related content with elevated styling and optional backgrounds.
27
+ </Typo>
28
+ </Card>
29
+ </div>
30
+ ),
31
+ args: {
32
+ children: undefined
33
+ }
34
+ };
35
+
36
+ export const WithDecorations: Story = {
37
+ render: () => (
38
+ <div className="max-w-md">
39
+ <Card showDecorations>
40
+ <Typo variant="headline-sm" bold="semibold">
41
+ Decorative frame
42
+ </Typo>
43
+ <Typo variant="body-sm" className="mt-2 text-[var(--muted-fg)]">
44
+ Enable gradient borders for hero cards or marketing content.
45
+ </Typo>
46
+ </Card>
47
+ </div>
48
+ )
49
+ };
50
+
51
+ export const AppBackground: Story = {
52
+ render: () => (
53
+ <div className="max-w-lg">
54
+ <Card variant="app-background" className="p-8">
55
+ <Typo variant="headline-sm" bold="semibold">
56
+ App background variant
57
+ </Typo>
58
+ <Typo variant="body-sm" className="mt-3 text-[var(--muted-fg)]">
59
+ Uses the same artwork as the `AppBackground` component and adapts to theme changes.
60
+ </Typo>
61
+ <div className="mt-6 flex gap-3">
62
+ <Button variant="primary">Get started</Button>
63
+ <Button variant="secondary">Learn more</Button>
64
+ </div>
65
+ </Card>
66
+ </div>
67
+ )
68
+ };
69
+
70
+ export const ElevatedComparison: Story = {
71
+ render: () => (
72
+ <div className="grid max-w-3xl grid-cols-1 gap-6 md:grid-cols-2">
73
+ <Card>
74
+ <Typo variant="title-md" bold="semibold">
75
+ Default
76
+ </Typo>
77
+ <Typo variant="body-sm" className="mt-2 text-[var(--muted-fg)]">
78
+ Soft glassmorphism without drop shadow.
79
+ </Typo>
80
+ </Card>
81
+ <Card elevated>
82
+ <Typo variant="title-md" bold="semibold">
83
+ Elevated
84
+ </Typo>
85
+ <Typo variant="body-sm" className="mt-2 text-[var(--muted-fg)]">
86
+ Adds `var(--shadow-elevated)` for emphasis and layering.
87
+ </Typo>
88
+ </Card>
89
+ </div>
90
+ )
91
+ };
92
+
93
+ export const DashboardLayout: Story = {
94
+ render: () => (
95
+ <div className="grid max-w-4xl gap-6 md:grid-cols-3">
96
+ <Card>
97
+ <Typo variant="title-md" bold="semibold">
98
+ Daily active users
99
+ </Typo>
100
+ <Typo variant="display-sm" bold="bold" className="mt-4">
101
+ 1,248
102
+ </Typo>
103
+ <Typo variant="body-sm" className="mt-2 text-[var(--success)]">
104
+ +12% vs last week
105
+ </Typo>
106
+ </Card>
107
+ <Card elevated>
108
+ <Typo variant="title-md" bold="semibold">
109
+ New signups
110
+ </Typo>
111
+ <Typo variant="display-sm" bold="bold" className="mt-4">
112
+ 327
113
+ </Typo>
114
+ <Typo variant="body-sm" className="mt-2 text-[var(--muted-fg)]">
115
+ Compared to rolling average
116
+ </Typo>
117
+ </Card>
118
+ <Card showDecorations>
119
+ <Typo variant="title-md" bold="semibold">
120
+ Retention cohort
121
+ </Typo>
122
+ <Typo variant="body-sm" className="mt-3 text-[var(--muted-fg)]">
123
+ Combine props to create data-heavy layouts with visual hierarchy.
124
+ </Typo>
125
+ </Card>
126
+ </div>
127
+ )
128
+ };
129
+
@@ -9,6 +9,8 @@ export type ChipProps = React.HTMLAttributes<HTMLDivElement> & {
9
9
  backgroundColor?: string;
10
10
  /** Custom text color (overrides variant) */
11
11
  textColor?: string;
12
+ /** Optional handler to render a delete affordance */
13
+ onDelete?: React.MouseEventHandler<HTMLButtonElement>;
12
14
  };
13
15
 
14
16
  export default function Chip({
@@ -19,6 +21,7 @@ export default function Chip({
19
21
  backgroundColor,
20
22
  textColor,
21
23
  style,
24
+ onDelete,
22
25
  ...props
23
26
  }: ChipProps) {
24
27
  const base =
@@ -42,17 +45,27 @@ export default function Chip({
42
45
  const mergedStyle: React.CSSProperties = {
43
46
  ...style,
44
47
  ...(backgroundColor && { backgroundColor }),
45
- ...(textColor && { color: textColor }),
48
+ ...(textColor && { color: textColor })
46
49
  };
47
50
 
48
51
  return (
49
- <div
50
- className={[base, colorClasses, className].join(" ")}
52
+ <div
53
+ className={[base, colorClasses, className].join(" ")}
51
54
  style={mergedStyle}
52
55
  {...props}
53
56
  >
54
57
  {icon ? <Icon name={icon} size="sm" className="mr-0.5" /> : null}
55
58
  <span>{label}</span>
59
+ {onDelete ? (
60
+ <button
61
+ type="button"
62
+ onClick={onDelete}
63
+ className="ml-1 flex h-4 w-4 items-center justify-center rounded-full transition-colors hover:bg-black/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-black/30"
64
+ aria-label="Remove"
65
+ >
66
+ <Icon name="close" size="sm" />
67
+ </button>
68
+ ) : null}
56
69
  </div>
57
70
  );
58
71
  }
@@ -1,36 +1,121 @@
1
1
  import * as React from "react";
2
2
 
3
- export type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> & {
3
+ export type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> & {
4
4
  /** Input visual variant */
5
5
  variant?: "default" | "inline";
6
+ /** Optional floating label (renders a fieldset wrapper when provided) */
7
+ label?: string;
8
+ /** Additional props for the surrounding fieldset when label is set */
9
+ fieldsetProps?: React.FieldsetHTMLAttributes<HTMLFieldSetElement>;
10
+ /** Additional props for the legend when label is set */
11
+ legendProps?: React.HTMLAttributes<HTMLLegendElement>;
12
+ /** Flag to visually mark the input as errored */
13
+ error?: boolean;
6
14
  };
7
15
 
8
16
  export const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
- ({ className, disabled, variant = "default", ...props }, ref) => {
10
- const isInline = variant === "inline";
11
-
17
+ (
18
+ {
19
+ className,
20
+ disabled,
21
+ variant = "default",
22
+ label,
23
+ fieldsetProps,
24
+ legendProps,
25
+ error = false,
26
+ ...props
27
+ },
28
+ ref
29
+ ) => {
30
+ const isInlineVariant = variant === "inline";
31
+ const shouldUseInlineStyles = isInlineVariant || Boolean(label);
32
+ const isError = error && !disabled;
33
+
34
+ const inputClasses: string[] = [
35
+ "w-full bg-transparent outline-none transition-colors",
36
+ shouldUseInlineStyles ? "h-9" : "h-12 px-4 rounded-full",
37
+ "font-['Poppins'] text-sm placeholder:text-[var(--muted-fg)]"
38
+ ];
39
+
40
+ if (!shouldUseInlineStyles) {
41
+ inputClasses.push("border");
42
+ }
43
+
44
+ if (disabled) {
45
+ inputClasses.push("text-[#3F424F]", "cursor-not-allowed");
46
+ if (!shouldUseInlineStyles) {
47
+ inputClasses.push("border-[#3F424F]");
48
+ }
49
+ } else {
50
+ inputClasses.push("text-[var(--muted-fg)]", "focus:text-[var(--fg)]");
51
+ if (!shouldUseInlineStyles) {
52
+ inputClasses.push(
53
+ isError ? "border-[var(--destructive)]" : "border-[var(--muted-fg)]",
54
+ isError ? "hover:border-[var(--destructive)]" : "hover:border-[var(--border)]",
55
+ isError ? "focus:border-[var(--destructive)]" : "focus:border-[var(--color-brand)]"
56
+ );
57
+ }
58
+ }
59
+
60
+ if (className) {
61
+ inputClasses.push(className);
62
+ }
63
+
64
+ const inputClassName = inputClasses.join(" ");
65
+
66
+ const inputElement = (
67
+ <input ref={ref} disabled={disabled} className={inputClassName} {...props} />
68
+ );
69
+
70
+ if (!label) {
71
+ return inputElement;
72
+ }
73
+
74
+ const { className: fieldsetClassNameProp = "", ...restFieldsetProps } = fieldsetProps ?? {};
75
+
76
+ const { className: legendClassNameProp = "", ...restLegendProps } = legendProps ?? {};
77
+
78
+ const fieldsetClassName = [
79
+ "w-full min-w-0 rounded-full border bg-[var(--surface)] transition-colors h-14",
80
+ isError ? "border-[var(--destructive)]" : "border-[var(--border)]",
81
+ isError
82
+ ? "focus-within:border-[var(--destructive)]"
83
+ : "focus-within:border-[var(--color-brand)]",
84
+ disabled ? "opacity-60 cursor-not-allowed" : "",
85
+ fieldsetClassNameProp
86
+ ]
87
+ .filter(Boolean)
88
+ .join(" ");
89
+
90
+ const legendColorClass = disabled
91
+ ? "text-[var(--muted-fg)]"
92
+ : isError
93
+ ? "text-[var(--destructive)]"
94
+ : "text-[var(--muted-fg)]";
95
+
96
+ const legendClassNameCombined = [
97
+ "ml-4 px-1 text-sm leading-none relative font-normal select-none",
98
+ legendColorClass,
99
+ legendClassNameProp
100
+ ]
101
+ .filter(Boolean)
102
+ .join(" ");
103
+
12
104
  return (
13
- <input
14
- ref={ref}
15
- disabled={disabled}
16
- className={[
17
- "w-full bg-transparent outline-none transition-colors",
18
- isInline ? "" : "h-12 px-4 rounded-full",
19
- "font-['Poppins'] text-sm placeholder:text-[var(--muted-fg)]",
20
- !isInline && "border",
21
- disabled
22
- ? "text-[#3F424F] cursor-not-allowed" + (isInline ? "" : " border-[#3F424F]")
23
- : [
24
- "text-[var(--muted-fg)]",
25
- isInline ? "" : "border-[var(--muted-fg)]",
26
- isInline ? "" : "hover:border-[var(--border)]",
27
- "focus:text-[var(--fg)]",
28
- isInline ? "" : "focus:border-[var(--color-brand)]"
29
- ].join(" "),
30
- className
31
- ].join(" ")}
32
- {...props}
33
- />
105
+ <fieldset
106
+ {...restFieldsetProps}
107
+ className={fieldsetClassName}
108
+ >
109
+ <legend
110
+ {...restLegendProps}
111
+ className={legendClassNameCombined}
112
+ >
113
+ {label}
114
+ </legend>
115
+ <div className="relative flex pl-5 pr-3 pb-1 h-full">
116
+ <div className="flex w-full">{inputElement}</div>
117
+ </div>
118
+ </fieldset>
34
119
  );
35
120
  }
36
121
  );
@@ -33,4 +33,19 @@ export const Variants: Story = {
33
33
  )
34
34
  };
35
35
 
36
+ export const Deletable: Story = {
37
+ args: {
38
+ label: "Filter: Active",
39
+ variant: "light",
40
+ onDelete: () => console.log("delete")
41
+ },
42
+ render: (args) => (
43
+ <div className="flex flex-wrap items-center gap-3">
44
+ <Chip {...args} onDelete={args.onDelete} />
45
+ <Chip label="Team: Core" onDelete={args.onDelete} variant="dark" />
46
+ <Chip label="Status: Pending" onDelete={args.onDelete} variant="warning" />
47
+ </div>
48
+ )
49
+ };
50
+
36
51
 
@@ -47,3 +47,29 @@ export const Inline: Story = {
47
47
  </div>
48
48
  )
49
49
  };
50
+
51
+ export const WithLabel: Story = {
52
+ render: () => (
53
+ <div className="flex flex-col gap-4 w-96">
54
+ <Input label="Project name" placeholder="Neo PTO" />
55
+ <Input label="Email" type="email" placeholder="you@example.com" />
56
+ <Input label="Password" type="password" placeholder="••••••••" />
57
+ <Input label="Disabled field" placeholder="Not editable" disabled />
58
+ </div>
59
+ )
60
+ };
61
+
62
+ export const Error: Story = {
63
+ render: () => (
64
+ <div className="flex flex-col gap-4 w-96">
65
+ <Input error placeholder="Unlabeled error" />
66
+ <Input label="Email" type="email" placeholder="you@example.com" error />
67
+ <Input
68
+ label="Password"
69
+ type="password"
70
+ placeholder="This is required"
71
+ error
72
+ />
73
+ </div>
74
+ )
75
+ };