aural-ui 3.0.5 → 3.0.7

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,169 @@
1
+ import React from "react"
2
+ import type { Meta, StoryObj } from "@storybook/react-vite"
3
+ import { toast } from "sonner"
4
+
5
+ import { ClampLines } from "."
6
+ import { Toaster } from "../toast"
7
+
8
+ const LONG_TEXT =
9
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
10
+
11
+ const meta: Meta<typeof ClampLines> = {
12
+ title: "Components/UI/ClampLines",
13
+ component: ClampLines,
14
+ parameters: {
15
+ layout: "centered",
16
+ backgrounds: {
17
+ default: "dark",
18
+ values: [
19
+ { name: "dark", value: "#0a0a0a" },
20
+ { name: "light", value: "#ffffff" },
21
+ ],
22
+ },
23
+ },
24
+ tags: ["autodocs"],
25
+ argTypes: {
26
+ wordLimit: {
27
+ control: "number",
28
+ description: "Max number of words to display before truncating",
29
+ },
30
+ responsive: {
31
+ control: "boolean",
32
+ description: "Adjusts word limit based on viewport width",
33
+ },
34
+ minWordLimit: {
35
+ control: "number",
36
+ description:
37
+ "Minimum word limit used on small screens when responsive is true",
38
+ },
39
+ button: {
40
+ control: false,
41
+ description:
42
+ "Render function for a custom toggle button. Receives `open` (boolean) and `toggle` (function) so the button can reflect and control expand/collapse state. Example: `(open, toggle) => <button onClick={toggle}>{open ? 'Less' : 'More'}</button>`",
43
+ },
44
+ classes: {
45
+ control: "object",
46
+ description: "Override classes for root and button elements",
47
+ },
48
+ children: {
49
+ control: "text",
50
+ description: "The text content to render",
51
+ },
52
+ },
53
+ }
54
+
55
+ export default meta
56
+ type Story = StoryObj<typeof ClampLines>
57
+
58
+ export const Default: Story = {
59
+ args: {
60
+ children: LONG_TEXT,
61
+ wordLimit: 20,
62
+ },
63
+ }
64
+
65
+ export const WithCSSClamp: Story = {
66
+ name: "CSS Line Clamp (no wordLimit)",
67
+ args: {
68
+ children: LONG_TEXT,
69
+ },
70
+ decorators: [
71
+ (Story) => (
72
+ <div style={{ width: 400 }}>
73
+ <Story />
74
+ </div>
75
+ ),
76
+ ],
77
+ }
78
+
79
+ export const Responsive: Story = {
80
+ args: {
81
+ children: LONG_TEXT,
82
+ wordLimit: 40,
83
+ responsive: true,
84
+ minWordLimit: 10,
85
+ },
86
+ }
87
+
88
+ export const WithCustomButton: Story = {
89
+ name: "Custom Button",
90
+ args: {
91
+ children: LONG_TEXT,
92
+ wordLimit: 20,
93
+ button: (open, toggle) => (
94
+ <button
95
+ onClick={toggle}
96
+ style={{
97
+ marginLeft: 4,
98
+ padding: "2px 8px",
99
+ border: "1px solid #7c3aed",
100
+ borderRadius: 4,
101
+ color: "#7c3aed",
102
+ background: "transparent",
103
+ cursor: "pointer",
104
+ fontSize: 12,
105
+ }}
106
+ >
107
+ {open ? "Show less ▲" : "Show more ▼"}
108
+ </button>
109
+ ),
110
+ },
111
+ }
112
+
113
+ export const WithOnToggle: Story = {
114
+ name: "With onToggle (Toast)",
115
+ args: {
116
+ children: LONG_TEXT,
117
+ wordLimit: 20,
118
+ onToggle: (open) => {
119
+ toast(open ? "Expanded" : "Collapsed", {
120
+ description: open
121
+ ? "Full text is now visible."
122
+ : "Text has been collapsed.",
123
+ })
124
+ },
125
+ },
126
+ decorators: [
127
+ (Story) => (
128
+ <>
129
+ <Story />
130
+ <Toaster />
131
+ </>
132
+ ),
133
+ ],
134
+ }
135
+
136
+ export const ShortText: Story = {
137
+ name: "Short Text (no truncation)",
138
+ args: {
139
+ children: "This text is short enough that it will never be truncated.",
140
+ wordLimit: 20,
141
+ },
142
+ }
143
+
144
+ export const AllVariants = () => {
145
+ return (
146
+ <div className="flex flex-col gap-6" style={{ width: 400 }}>
147
+ <div>
148
+ <p className="text-fm-tertiary text-fm-xs mb-2 uppercase">
149
+ Word Limit (20 words)
150
+ </p>
151
+ <ClampLines wordLimit={20}>{LONG_TEXT}</ClampLines>
152
+ </div>
153
+ <div>
154
+ <p className="text-fm-tertiary text-fm-xs mb-2 uppercase">
155
+ CSS Line Clamp (3 lines)
156
+ </p>
157
+ <ClampLines>{LONG_TEXT}</ClampLines>
158
+ </div>
159
+ <div>
160
+ <p className="text-fm-tertiary text-fm-xs mb-2 uppercase">
161
+ Responsive (40 words, min 10)
162
+ </p>
163
+ <ClampLines wordLimit={40} responsive minWordLimit={10}>
164
+ {LONG_TEXT}
165
+ </ClampLines>
166
+ </div>
167
+ </div>
168
+ )
169
+ }
@@ -0,0 +1,180 @@
1
+ import React, { useEffect, useRef, useState } from "react"
2
+ import { cn } from "@lib/utils"
3
+
4
+ import { Button } from "../button"
5
+
6
+ export interface IClampLinesProps {
7
+ button?: ((open: boolean, toggle: () => void) => React.ReactNode) | null
8
+ children: string
9
+ classes?: {
10
+ clampLineBtn?: string
11
+ clampLineBtnInnerClass?: string
12
+ root?: string
13
+ content?: string
14
+ }
15
+ wordLimit?: number
16
+ responsive?: boolean
17
+ minWordLimit?: number
18
+ readMoreText?: string
19
+ readLessText?: string
20
+ onToggle?: (open: boolean) => void
21
+ }
22
+
23
+ export const ClampLines = ({
24
+ children,
25
+ classes = {},
26
+ button = null,
27
+ wordLimit,
28
+ responsive = false,
29
+ minWordLimit,
30
+ readMoreText = "READ MORE",
31
+ readLessText = "READ LESS",
32
+ onToggle,
33
+ }: IClampLinesProps) => {
34
+ const [open, setOpen] = useState(false)
35
+ const ref = useRef<HTMLParagraphElement | null>(null)
36
+ const [showLink, setShowLink] = useState(false)
37
+ const [responsiveWordLimit, setResponsiveWordLimit] = useState(wordLimit || 0)
38
+
39
+ const words = children.split(" ")
40
+
41
+ const calculateResponsiveWordLimit = () => {
42
+ if (!responsive || !wordLimit) {
43
+ setResponsiveWordLimit(wordLimit || 0)
44
+ return
45
+ }
46
+
47
+ const width = window.innerWidth
48
+ const baseWordLimit = wordLimit
49
+ const minLimit = minWordLimit || Math.ceil(baseWordLimit * 0.5)
50
+
51
+ const mobileBreakpoint = 640
52
+ const tabletBreakpoint = 1024
53
+
54
+ let calculatedLimit: number
55
+
56
+ if (width >= tabletBreakpoint) {
57
+ calculatedLimit = baseWordLimit
58
+ } else if (width >= mobileBreakpoint) {
59
+ const ratio =
60
+ (width - mobileBreakpoint) / (tabletBreakpoint - mobileBreakpoint)
61
+ const tabletWordLimit = minLimit + (baseWordLimit - minLimit) * 0.7
62
+ calculatedLimit = Math.round(
63
+ tabletWordLimit + (baseWordLimit - tabletWordLimit) * ratio
64
+ )
65
+ } else {
66
+ calculatedLimit = minLimit
67
+ }
68
+
69
+ setResponsiveWordLimit(Math.max(minLimit, calculatedLimit))
70
+ }
71
+
72
+ useEffect(() => {
73
+ let rafId = requestAnimationFrame(() => {
74
+ calculateResponsiveWordLimit()
75
+ })
76
+
77
+ if (responsive && wordLimit) {
78
+ const handleResize = () => {
79
+ cancelAnimationFrame(rafId)
80
+ rafId = requestAnimationFrame(() => {
81
+ calculateResponsiveWordLimit()
82
+ })
83
+ }
84
+
85
+ window.addEventListener("resize", handleResize)
86
+ return () => {
87
+ cancelAnimationFrame(rafId)
88
+ window.removeEventListener("resize", handleResize)
89
+ }
90
+ }
91
+
92
+ return () => cancelAnimationFrame(rafId)
93
+ }, [responsive, wordLimit, minWordLimit])
94
+
95
+ const effectiveWordLimit = responsive ? responsiveWordLimit : wordLimit
96
+
97
+ const shouldTruncate =
98
+ !!effectiveWordLimit && words.length > effectiveWordLimit
99
+
100
+ useEffect(() => {
101
+ const rafId = requestAnimationFrame(() => {
102
+ if (effectiveWordLimit) {
103
+ setShowLink(shouldTruncate)
104
+ } else {
105
+ if (!ref.current) return
106
+ if (ref.current.clientHeight < ref.current.scrollHeight) {
107
+ setShowLink(true)
108
+ } else if (ref.current.scrollHeight > 0 && !open) {
109
+ setShowLink(false)
110
+ }
111
+ }
112
+ })
113
+ return () => cancelAnimationFrame(rafId)
114
+ }, [shouldTruncate, effectiveWordLimit, children, open])
115
+
116
+ const handleClick = () => {
117
+ const next = !open
118
+ setOpen(next)
119
+ onToggle?.(next)
120
+ }
121
+
122
+ const renderButton = !button ? (
123
+ <Button
124
+ data-nosnippet
125
+ variant="text"
126
+ noise="none"
127
+ innerClassName={cn("px-0! h-fit inline", classes.clampLineBtnInnerClass)}
128
+ className={cn(
129
+ "text-fm-tertiary ml-1 cursor-pointer underline",
130
+ classes.clampLineBtn
131
+ )}
132
+ onClick={handleClick}
133
+ >
134
+ {open ? readLessText : readMoreText}
135
+ </Button>
136
+ ) : (
137
+ button(open, handleClick)
138
+ )
139
+
140
+ const displayText =
141
+ effectiveWordLimit && !open && shouldTruncate
142
+ ? words.slice(0, effectiveWordLimit).join(" ")
143
+ : children
144
+ const showEllipsis =
145
+ effectiveWordLimit &&
146
+ !open &&
147
+ shouldTruncate &&
148
+ displayText.length < children.length
149
+
150
+ const renderContent = effectiveWordLimit ? (
151
+ <p className={cn("text-fm-tertiary", classes.content)} data-nosnippet>
152
+ {displayText}
153
+ {showEllipsis && "...."}
154
+ {showLink && renderButton}
155
+ </p>
156
+ ) : (
157
+ <>
158
+ <p
159
+ data-nosnippet
160
+ className={cn(
161
+ open ? "line-clamp-none" : "line-clamp-3",
162
+ "text-fm-tertiary",
163
+ classes.content
164
+ )}
165
+ ref={ref}
166
+ >
167
+ {children}
168
+ </p>
169
+ {showLink && renderButton}
170
+ </>
171
+ )
172
+
173
+ return (
174
+ <section className={classes.root} data-testid="clamp-lines" data-nosnippet>
175
+ {renderContent}
176
+ </section>
177
+ )
178
+ }
179
+
180
+ export default ClampLines
@@ -0,0 +1,6 @@
1
+ export const meta = {
2
+ dependencies: {},
3
+ devDependencies: {},
4
+ internalDependencies: ["button"],
5
+ tokens: [],
6
+ }
@@ -9,6 +9,7 @@ export * from "./card"
9
9
  export * from "./char-count"
10
10
  export * from "./checkbox"
11
11
  export * from "./chip"
12
+ export * from "./clamp-lines"
12
13
  export * from "./collapsible"
13
14
  export * from "./command"
14
15
  export * from "./dialog"
@@ -313,6 +313,8 @@ const InputBase = forwardRef<
313
313
  {
314
314
  "pl-10": startIcon,
315
315
  "pr-10": endIcon,
316
+ "[&::placeholder]:[-webkit-text-fill-color:var(--color-fm-tertiary)]":
317
+ transparentOnAutofill,
316
318
  },
317
319
  className
318
320
  ),