aural-ui 2.1.9 → 2.1.11

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.
@@ -0,0 +1,302 @@
1
+ import React from "react"
2
+ import { ArrowRightIcon } from "@icons/arrow-right-icon"
3
+ import { ChevronDoubleRightIcon } from "@icons/chevron-double-right-icon"
4
+ import type { Meta, StoryObj } from "@storybook/react"
5
+
6
+ import { Breadcrumb } from "."
7
+
8
+ const meta: Meta<typeof Breadcrumb> = {
9
+ title: "Components/UI/Breadcrumb",
10
+ component: Breadcrumb,
11
+ parameters: {
12
+ layout: "centered",
13
+ backgrounds: {
14
+ default: "dark",
15
+ values: [
16
+ { name: "dark", value: "#0a0a0a" },
17
+ { name: "light", value: "#ffffff" },
18
+ ],
19
+ },
20
+ docs: {
21
+ description: {
22
+ component: `
23
+ # Breadcrumb Component
24
+
25
+ A navigation component that shows the current page's location within a site hierarchy. Users can navigate back to previous levels by clicking on breadcrumb items.
26
+
27
+ ## Features
28
+
29
+ - **Multiple Separators**: Support for chevron, slash, arrow, and custom separators
30
+ - **Custom Separator**: Use any React component or icon as a separator
31
+ - **Clickable Navigation**: Items can be clickable links or static text
32
+ - **Responsive Design**: Adapts spacing for mobile and desktop
33
+ - **Accessibility**: Proper ARIA labels and semantic HTML structure
34
+ - **Customizable**: Size variants and custom styling options
35
+ - **Truncation**: Optional max items with ellipsis for long breadcrumbs
36
+ - **Home Integration**: Optional home item at the beginning
37
+
38
+ ## Usage Examples
39
+
40
+ ### Basic Breadcrumb
41
+ \`\`\`tsx
42
+ <Breadcrumb
43
+ items={[
44
+ { title: "Home", url: "/" },
45
+ { title: "Products", url: "/products" },
46
+ { title: "Electronics" }
47
+ ]}
48
+ onItemClick={(title, url) => console.log(title, url)}
49
+ />
50
+ \`\`\`
51
+
52
+ ### With Different Separators
53
+ \`\`\`tsx
54
+ <Breadcrumb
55
+ items={items}
56
+ separator="slash"
57
+ onItemClick={handleClick}
58
+ />
59
+ \`\`\`
60
+
61
+ ### With Size Variants
62
+ \`\`\`tsx
63
+ <Breadcrumb
64
+ items={items}
65
+ size="lg"
66
+ onItemClick={handleClick}
67
+ />
68
+ \`\`\`
69
+
70
+ ### With Custom Separator
71
+ \`\`\`tsx
72
+ <Breadcrumb
73
+ items={items}
74
+ customSeparator={<ArrowRightIcon width={16} height={16} />}
75
+ onItemClick={handleClick}
76
+ />
77
+ \`\`\`
78
+ `,
79
+ },
80
+ },
81
+ },
82
+ tags: ["autodocs"],
83
+ argTypes: {
84
+ size: {
85
+ control: { type: "select" },
86
+ options: ["sm", "md", "lg"],
87
+ },
88
+ separator: {
89
+ control: { type: "select" },
90
+ options: ["chevron", "slash", "arrow"],
91
+ },
92
+ showHome: {
93
+ control: { type: "boolean" },
94
+ },
95
+ maxItems: {
96
+ control: { type: "number" },
97
+ },
98
+ onItemClick: {
99
+ action: "item clicked",
100
+ },
101
+ },
102
+ }
103
+
104
+ export default meta
105
+ type Story = StoryObj<typeof Breadcrumb>
106
+
107
+ const sampleItems = [
108
+ { title: "Home", url: "/", isClickable: true },
109
+ { title: "Products", url: "/products", isClickable: true },
110
+ { title: "Electronics", url: "/products/electronics", isClickable: true },
111
+ {
112
+ title: "Smartphones",
113
+ url: "/products/electronics/smartphones",
114
+ isClickable: true,
115
+ },
116
+ { title: "iPhone 15 Pro" },
117
+ ]
118
+
119
+ const longItems = [
120
+ { title: "Home", url: "/", isClickable: true },
121
+ { title: "Products", url: "/products", isClickable: true },
122
+ { title: "Electronics", url: "/products/electronics", isClickable: true },
123
+ {
124
+ title: "Smartphones",
125
+ url: "/products/electronics/smartphones",
126
+ isClickable: true,
127
+ },
128
+ {
129
+ title: "Apple",
130
+ url: "/products/electronics/smartphones/apple",
131
+ isClickable: true,
132
+ },
133
+ {
134
+ title: "iPhone",
135
+ url: "/products/electronics/smartphones/apple/iphone",
136
+ isClickable: true,
137
+ },
138
+ { title: "iPhone 15 Pro" },
139
+ ]
140
+
141
+ export const Default: Story = {
142
+ args: {
143
+ items: sampleItems,
144
+ size: "md",
145
+ separator: "chevron",
146
+ },
147
+ }
148
+
149
+ export const WithSlashSeparator: Story = {
150
+ args: {
151
+ items: sampleItems,
152
+ separator: "slash",
153
+ },
154
+ }
155
+
156
+ export const WithArrowSeparator: Story = {
157
+ args: {
158
+ items: sampleItems,
159
+ separator: "arrow",
160
+ },
161
+ }
162
+
163
+ export const WithCustomSeparator: Story = {
164
+ args: {
165
+ items: sampleItems,
166
+ customSeparator: <ChevronDoubleRightIcon width={16} height={16} />,
167
+ },
168
+ }
169
+
170
+ export const SmallSize: Story = {
171
+ args: {
172
+ items: sampleItems,
173
+ size: "sm",
174
+ },
175
+ }
176
+
177
+ export const LargeSize: Story = {
178
+ args: {
179
+ items: sampleItems,
180
+ size: "lg",
181
+ },
182
+ }
183
+
184
+ export const WithHome: Story = {
185
+ args: {
186
+ items: [
187
+ { title: "Products", url: "/products", isClickable: true },
188
+ { title: "Electronics", url: "/products/electronics", isClickable: true },
189
+ { title: "Smartphones" },
190
+ ],
191
+ showHome: true,
192
+ homeTitle: "Home",
193
+ homeUrl: "/",
194
+ },
195
+ }
196
+
197
+ export const WithMaxItems: Story = {
198
+ args: {
199
+ items: longItems,
200
+ maxItems: 4,
201
+ },
202
+ }
203
+
204
+ export const NonClickable: Story = {
205
+ args: {
206
+ items: [
207
+ { title: "Home", isClickable: false },
208
+ { title: "Products", isClickable: false },
209
+ { title: "Electronics", isClickable: false },
210
+ { title: "Smartphones" },
211
+ ],
212
+ },
213
+ }
214
+
215
+ export const MixedClickable: Story = {
216
+ args: {
217
+ items: [
218
+ { title: "Home", url: "/", isClickable: true },
219
+ { title: "Products", isClickable: false },
220
+ { title: "Electronics", url: "/products/electronics", isClickable: true },
221
+ { title: "Smartphones" },
222
+ ],
223
+ },
224
+ }
225
+
226
+ export const AllVariants: Story = {
227
+ render: () => (
228
+ <div className="flex flex-col gap-8 p-8">
229
+ <div className="space-y-4">
230
+ <h3 className="text-fm-lg font-fm-brand text-neutral-400">
231
+ Size Variants
232
+ </h3>
233
+ <div className="space-y-2">
234
+ <Breadcrumb items={sampleItems} size="sm" />
235
+ <Breadcrumb items={sampleItems} size="md" />
236
+ <Breadcrumb items={sampleItems} size="lg" />
237
+ </div>
238
+ </div>
239
+
240
+ <div className="space-y-4">
241
+ <h3 className="text-fm-lg font-fm-brand text-neutral-400">
242
+ Separator Variants
243
+ </h3>
244
+ <div className="space-y-2">
245
+ <Breadcrumb items={sampleItems} separator="chevron" />
246
+ <Breadcrumb items={sampleItems} separator="slash" />
247
+ <Breadcrumb items={sampleItems} separator="arrow" />
248
+ <Breadcrumb
249
+ items={sampleItems}
250
+ customSeparator={<ArrowRightIcon width={16} height={16} />}
251
+ />
252
+ <Breadcrumb
253
+ items={sampleItems}
254
+ customSeparator={<ChevronDoubleRightIcon width={16} height={16} />}
255
+ />
256
+ <Breadcrumb items={sampleItems} customSeparator="•" />
257
+ </div>
258
+ </div>
259
+
260
+ <div className="space-y-4">
261
+ <h3 className="text-fm-lg font-fm-brand text-neutral-400">With Home</h3>
262
+ <Breadcrumb
263
+ items={[
264
+ { title: "Products", url: "/products", isClickable: true },
265
+ { title: "Electronics" },
266
+ ]}
267
+ showHome={true}
268
+ />
269
+ </div>
270
+
271
+ <div className="space-y-4">
272
+ <h3 className="text-fm-lg font-fm-brand text-neutral-400">
273
+ With Max Items (4)
274
+ </h3>
275
+ <Breadcrumb items={longItems} maxItems={4} />
276
+ </div>
277
+ </div>
278
+ ),
279
+ }
280
+
281
+ export const Interactive: Story = {
282
+ render: () => {
283
+ const [currentPath, setCurrentPath] = React.useState("iPhone 15 Pro")
284
+
285
+ const handleItemClick = (title: string, url?: string) => {
286
+ console.log(`Clicked: ${title}`, url)
287
+ setCurrentPath(title)
288
+ }
289
+
290
+ return (
291
+ <div className="space-y-4 p-8">
292
+ <h3 className="text-fm-lg font-fm-brand text-neutral-400">
293
+ Current Path: {currentPath}
294
+ </h3>
295
+ <Breadcrumb items={sampleItems} onItemClick={handleItemClick} />
296
+ <div className="text-fm-secondary text-fm-sm">
297
+ Click on any breadcrumb item to see the interaction
298
+ </div>
299
+ </div>
300
+ )
301
+ },
302
+ }
@@ -0,0 +1,232 @@
1
+ import React, { forwardRef } from "react"
2
+ import { ChevronRightIcon } from "@icons/chevron-right-icon"
3
+ import { cn } from "@lib/utils"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { Typography } from "../typography"
7
+
8
+ export const breadcrumbVariants = cva("flex items-center gap-1 md:gap-3", {
9
+ variants: {
10
+ size: {
11
+ sm: "text-fm-sm",
12
+ md: "text-fm-md",
13
+ lg: "text-fm-lg",
14
+ },
15
+ separator: {
16
+ chevron: "",
17
+ slash: "",
18
+ arrow: "",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ size: "md",
23
+ separator: "chevron",
24
+ },
25
+ })
26
+
27
+ export interface BreadCrumbsItemProps {
28
+ title: string
29
+ url?: string
30
+ isClickable?: boolean
31
+ className?: string
32
+ }
33
+
34
+ export interface BreadcrumbProps
35
+ extends VariantProps<typeof breadcrumbVariants> {
36
+ items: BreadCrumbsItemProps[]
37
+ onItemClick?: (title: string, url?: string) => void
38
+ className?: string
39
+ maxItems?: number
40
+ showHome?: boolean
41
+ homeTitle?: string
42
+ homeUrl?: string
43
+ customSeparator?: React.ReactNode
44
+ }
45
+
46
+ const BreadCrumbItem = ({
47
+ title,
48
+ url,
49
+ isClickable = true,
50
+ className = "",
51
+ onItemClick,
52
+ isLast = false,
53
+ size = "md",
54
+ separator = "chevron",
55
+ customSeparator,
56
+ }: BreadCrumbsItemProps & {
57
+ onItemClick?: (title: string, url?: string) => void
58
+ isLast: boolean
59
+ size?: "sm" | "md" | "lg"
60
+ separator?: "chevron" | "slash" | "arrow"
61
+ customSeparator?: React.ReactNode
62
+ }) => {
63
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
64
+ e.preventDefault()
65
+ if (isClickable && onItemClick) {
66
+ onItemClick(title, url)
67
+ }
68
+ }
69
+
70
+ const renderSeparator = () => {
71
+ if (isLast) return null
72
+
73
+ if (customSeparator) {
74
+ return (
75
+ <span
76
+ className="text-fm-tertiary mx-1 md:mx-2"
77
+ data-testid="custom-separator"
78
+ >
79
+ {customSeparator}
80
+ </span>
81
+ )
82
+ }
83
+
84
+ switch (separator) {
85
+ case "slash":
86
+ return (
87
+ <span
88
+ className="text-fm-tertiary mx-1 md:mx-2"
89
+ data-testid="slash-separator"
90
+ >
91
+ /
92
+ </span>
93
+ )
94
+ case "arrow":
95
+ return (
96
+ <span
97
+ className="text-fm-tertiary mx-1 md:mx-2"
98
+ data-testid="arrow-separator"
99
+ >
100
+
101
+ </span>
102
+ )
103
+ case "chevron":
104
+ default:
105
+ return (
106
+ <ChevronRightIcon
107
+ className="text-fm-tertiary"
108
+ width={16}
109
+ height={16}
110
+ data-testid="chevron-right"
111
+ />
112
+ )
113
+ }
114
+ }
115
+
116
+ const typographyVariantMap = {
117
+ sm: "caption-medium",
118
+ lg: "body-medium",
119
+ md: "body-small",
120
+ } as const
121
+
122
+ const typographyVariant =
123
+ typographyVariantMap[size] || typographyVariantMap.md
124
+
125
+ return (
126
+ <div className="flex items-center gap-1 md:gap-3">
127
+ {isClickable && url ? (
128
+ <a
129
+ href={url}
130
+ onClick={handleClick}
131
+ className={cn(
132
+ "focus-visible:ring-ring rounded-sm transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-offset-2",
133
+ className
134
+ )}
135
+ data-testid="breadcrumb-link"
136
+ >
137
+ <Typography
138
+ variant={typographyVariant}
139
+ className="text-fm-primary hover:text-fm-primary-600 max-w-[96px] cursor-pointer truncate"
140
+ >
141
+ {title}
142
+ </Typography>
143
+ </a>
144
+ ) : (
145
+ <Typography
146
+ variant={typographyVariant}
147
+ className={cn("text-fm-secondary max-w-[96px] truncate", className)}
148
+ data-testid="breadcrumb-text"
149
+ >
150
+ {title}
151
+ </Typography>
152
+ )}
153
+ {renderSeparator()}
154
+ </div>
155
+ )
156
+ }
157
+
158
+ const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>(
159
+ (
160
+ {
161
+ items,
162
+ onItemClick,
163
+ className = "",
164
+ size = "md",
165
+ separator = "chevron",
166
+ maxItems,
167
+ showHome = false,
168
+ homeTitle = "Home",
169
+ homeUrl = "/",
170
+ customSeparator,
171
+ ...props
172
+ },
173
+ ref
174
+ ) => {
175
+ // Add home item if showHome is true
176
+ const allItems = showHome
177
+ ? [{ title: homeTitle, url: homeUrl, isClickable: true }, ...items]
178
+ : items
179
+
180
+ // Limit items if maxItems is specified
181
+ const displayItems = (() => {
182
+ if (!maxItems || allItems.length <= maxItems) return allItems
183
+
184
+ if (maxItems >= 3) {
185
+ return [
186
+ allItems[0],
187
+ { title: "...", isClickable: false },
188
+ ...allItems.slice(-(maxItems - 2)),
189
+ ]
190
+ }
191
+
192
+ if (maxItems === 2) {
193
+ return [allItems[0], allItems[allItems.length - 1]]
194
+ }
195
+
196
+ return allItems.slice(0, maxItems)
197
+ })()
198
+
199
+ return (
200
+ <nav
201
+ ref={ref}
202
+ className={cn(breadcrumbVariants({ size, separator }), className)}
203
+ aria-label="Breadcrumb"
204
+ {...props}
205
+ >
206
+ <ol
207
+ className="flex items-center gap-1 md:gap-3"
208
+ data-testid="breadcrumb-list"
209
+ >
210
+ {displayItems.map((item, index) => (
211
+ <li key={`${item.title}-${index}`} className="flex items-center">
212
+ <BreadCrumbItem
213
+ {...item}
214
+ onItemClick={onItemClick}
215
+ isLast={index === displayItems.length - 1}
216
+ size={size || "md"}
217
+ separator={separator || "chevron"}
218
+ className={item.className}
219
+ customSeparator={customSeparator}
220
+ />
221
+ </li>
222
+ ))}
223
+ </ol>
224
+ </nav>
225
+ )
226
+ }
227
+ )
228
+
229
+ Breadcrumb.displayName = "Breadcrumb"
230
+
231
+ export { Breadcrumb, BreadCrumbItem }
232
+ export default Breadcrumb
@@ -0,0 +1,14 @@
1
+ export const meta = {
2
+ dependencies: {},
3
+ devDependencies: {},
4
+ internalDependencies: ["typography", "chevron-right-icon"],
5
+ tokens: [
6
+ "--color-fm-primary",
7
+ "--color-fm-primary-600",
8
+ "--color-fm-secondary",
9
+ "--color-fm-tertiary",
10
+ "--text-fm-sm",
11
+ "--text-fm-md",
12
+ "--text-fm-lg",
13
+ ],
14
+ }
@@ -3,6 +3,7 @@ export * from "./aspect-ratio"
3
3
  export * from "./avatar"
4
4
  export * from "./badge"
5
5
  export * from "./banner"
6
+ export * from "./breadcrumb"
6
7
  export * from "./button"
7
8
  export * from "./card"
8
9
  export * from "./char-count"
@@ -21,7 +22,9 @@ export * from "./if-else"
21
22
  export * from "./label"
22
23
  export * from "./list"
23
24
  export * from "./marquee"
25
+ export * from "./otp-inputs"
24
26
  export * from "./overlay"
27
+ export * from "./otp-inputs"
25
28
  export * from "./pagination"
26
29
  export * from "./popover"
27
30
  export * from "./radio"
@@ -31,6 +31,8 @@ interface SingleOtpInputType {
31
31
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
32
32
  onFocus: () => void
33
33
  onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
34
+ onPaste?: (event: React.ClipboardEvent<HTMLInputElement>) => void
35
+
34
36
  style?: React.CSSProperties
35
37
  value: string | undefined
36
38
  type?: "text" | "number"
@@ -100,10 +102,6 @@ export default function OtpInputs(props: OTPInputsType) {
100
102
  }
101
103
  }
102
104
 
103
- const onBlur = () => {
104
- setActiveInput(-1)
105
- }
106
-
107
105
  const handleOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
108
106
  switch (e.key) {
109
107
  case "Backspace":
@@ -138,6 +136,42 @@ export default function OtpInputs(props: OTPInputsType) {
138
136
  }
139
137
  }
140
138
 
139
+ const handleOnPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
140
+ e.preventDefault()
141
+ const pastedData = e.clipboardData.getData("text/plain")
142
+
143
+ const validChars = pastedData
144
+ .split("")
145
+ .map((char) => getRightValue(char))
146
+ .filter((char) => char !== "")
147
+ .slice(0, length)
148
+
149
+ if (validChars.length === 0) return
150
+
151
+ const updatedOTPValues = [...otpValues]
152
+ let currentIndex = activeInput
153
+ for (let i = 0; i < validChars.length && currentIndex < length; i++) {
154
+ updatedOTPValues[currentIndex] = validChars[i]
155
+ currentIndex++
156
+ }
157
+
158
+ setOTPValues(updatedOTPValues)
159
+ handleOtpChange(updatedOTPValues)
160
+
161
+ const nextEmptyIndex = updatedOTPValues.findIndex(
162
+ (val, idx) => idx >= activeInput && val === ""
163
+ )
164
+ const focusIndex =
165
+ nextEmptyIndex !== -1
166
+ ? nextEmptyIndex
167
+ : Math.min(length - 1, activeInput + validChars.length)
168
+ focusInput(focusIndex)
169
+ }
170
+
171
+ const onBlur = () => {
172
+ setActiveInput(-1)
173
+ }
174
+
141
175
  const messagesMap = {
142
176
  true: messages?.success ?? "✓ Valid input",
143
177
  false: messages?.error ?? "✗ Invalid input. Try again",
@@ -168,6 +202,7 @@ export default function OtpInputs(props: OTPInputsType) {
168
202
  value={otpValues && otpValues[index]}
169
203
  autoComplete="off"
170
204
  onFocus={handleOnFocus(index)}
205
+ onPaste={handleOnPaste}
171
206
  onChange={handleOnChange}
172
207
  onKeyDown={handleOnKeyDown}
173
208
  onBlur={onBlur}
@@ -162,7 +162,7 @@ const useAutoGrow = (
162
162
  return () => {
163
163
  window.removeEventListener("resize", handleResize)
164
164
  }
165
- }, [adjustHeight, minHeight, autoGrow])
165
+ }, [adjustHeight, minHeight, autoGrow, textareaRef])
166
166
 
167
167
  return adjustHeight
168
168
  }
@@ -10,6 +10,7 @@ export const meta = {
10
10
  "--color-fm-tertiary",
11
11
  "--font-fm-brand",
12
12
  "--font-fm-text",
13
+ "--font-fm-toons",
13
14
  "--leading-fm-2xl",
14
15
  "--leading-fm-3xl",
15
16
  "--leading-fm-4xl",
@@ -71,6 +71,7 @@ export * from "./paper-plane-icon"
71
71
  export * from "./pause-icon"
72
72
  export * from "./pencil-icon"
73
73
  export * from "./plus-icon"
74
+ export * from "./phone-icon"
74
75
  export * from "./search-icon"
75
76
  export * from "./setting-icon"
76
77
  export * from "./share-icon"
@@ -84,6 +85,7 @@ export * from "./spinner-solid-neutral-icon"
84
85
  export * from "./star-icon"
85
86
  export * from "./store-coin-icon"
86
87
  export * from "./suggestion-icon"
88
+ export * from "./shield-icon"
87
89
  export * from "./sun-icon"
88
90
  export * from "./text-color-icon"
89
91
  export * from "./text-indicator-icon"