sonance-brand-mcp 1.1.4 → 1.1.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.
- package/dist/assets/components/accordion.stories.tsx +310 -0
- package/dist/assets/components/accordion.tsx +56 -30
- package/dist/assets/components/alert.stories.tsx +199 -0
- package/dist/assets/components/autocomplete.stories.tsx +307 -0
- package/dist/assets/components/autocomplete.tsx +28 -4
- package/dist/assets/components/avatar.stories.tsx +175 -0
- package/dist/assets/components/badge.stories.tsx +258 -0
- package/dist/assets/components/breadcrumbs.stories.tsx +175 -0
- package/dist/assets/components/button.stories.tsx +362 -0
- package/dist/assets/components/button.tsx +48 -3
- package/dist/assets/components/calendar.stories.tsx +247 -0
- package/dist/assets/components/card.stories.tsx +275 -0
- package/dist/assets/components/card.tsx +26 -1
- package/dist/assets/components/checkbox-group.stories.tsx +281 -0
- package/dist/assets/components/checkbox.stories.tsx +160 -0
- package/dist/assets/components/checkbox.tsx +32 -4
- package/dist/assets/components/code.stories.tsx +265 -0
- package/dist/assets/components/date-input.stories.tsx +278 -0
- package/dist/assets/components/date-input.tsx +24 -2
- package/dist/assets/components/date-picker.stories.tsx +337 -0
- package/dist/assets/components/date-picker.tsx +28 -4
- package/dist/assets/components/date-range-picker.stories.tsx +340 -0
- package/dist/assets/components/dialog.stories.tsx +285 -0
- package/dist/assets/components/divider.stories.tsx +176 -0
- package/dist/assets/components/drawer.stories.tsx +216 -0
- package/dist/assets/components/dropdown.stories.tsx +342 -0
- package/dist/assets/components/dropdown.tsx +55 -10
- package/dist/assets/components/form.stories.tsx +372 -0
- package/dist/assets/components/image.stories.tsx +348 -0
- package/dist/assets/components/input-otp.stories.tsx +336 -0
- package/dist/assets/components/input-otp.tsx +24 -2
- package/dist/assets/components/input.stories.tsx +223 -0
- package/dist/assets/components/input.tsx +27 -2
- package/dist/assets/components/kbd.stories.tsx +272 -0
- package/dist/assets/components/link.stories.tsx +199 -0
- package/dist/assets/components/link.tsx +50 -1
- package/dist/assets/components/listbox.stories.tsx +287 -0
- package/dist/assets/components/listbox.tsx +30 -7
- package/dist/assets/components/navbar.stories.tsx +218 -0
- package/dist/assets/components/number-input.stories.tsx +295 -0
- package/dist/assets/components/number-input.tsx +30 -8
- package/dist/assets/components/pagination.stories.tsx +280 -0
- package/dist/assets/components/pagination.tsx +45 -21
- package/dist/assets/components/popover.stories.tsx +219 -0
- package/dist/assets/components/progress.stories.tsx +153 -0
- package/dist/assets/components/radio-group.stories.tsx +187 -0
- package/dist/assets/components/radio-group.tsx +30 -6
- package/dist/assets/components/range-calendar.stories.tsx +334 -0
- package/dist/assets/components/scroll-shadow.stories.tsx +335 -0
- package/dist/assets/components/select.stories.tsx +192 -0
- package/dist/assets/components/select.tsx +54 -7
- package/dist/assets/components/skeleton.stories.tsx +166 -0
- package/dist/assets/components/slider.stories.tsx +145 -0
- package/dist/assets/components/slider.tsx +43 -8
- package/dist/assets/components/spacer.stories.tsx +216 -0
- package/dist/assets/components/spinner.stories.tsx +149 -0
- package/dist/assets/components/switch.stories.tsx +170 -0
- package/dist/assets/components/switch.tsx +29 -4
- package/dist/assets/components/table.stories.tsx +322 -0
- package/dist/assets/components/tabs.stories.tsx +306 -0
- package/dist/assets/components/tabs.tsx +25 -4
- package/dist/assets/components/textarea.stories.tsx +103 -0
- package/dist/assets/components/textarea.tsx +27 -3
- package/dist/assets/components/theme-toggle.stories.tsx +248 -0
- package/dist/assets/components/time-input.stories.tsx +365 -0
- package/dist/assets/components/time-input.tsx +25 -3
- package/dist/assets/components/toast.stories.tsx +195 -0
- package/dist/assets/components/tooltip.stories.tsx +226 -0
- package/dist/assets/components/user.stories.tsx +274 -0
- package/dist/index.js +1649 -13
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { Link, Anchor } from './link';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Link> = {
|
|
5
|
+
title: 'Components/Navigation/Link',
|
|
6
|
+
component: Link,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: 'A styled link component with variants for different contexts.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
variant: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['default', 'primary', 'muted', 'subtle', 'nav'],
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
control: 'select',
|
|
22
|
+
options: ['sm', 'md', 'lg'],
|
|
23
|
+
},
|
|
24
|
+
external: { control: 'boolean' },
|
|
25
|
+
showExternalIcon: { control: 'boolean' },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof meta>;
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
href: '#',
|
|
35
|
+
children: 'Default Link',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Primary: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
variant: 'primary',
|
|
42
|
+
href: '#',
|
|
43
|
+
children: 'Primary Link',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Muted: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
variant: 'muted',
|
|
50
|
+
href: '#',
|
|
51
|
+
children: 'Muted Link',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Subtle: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
variant: 'subtle',
|
|
58
|
+
href: '#',
|
|
59
|
+
children: 'Subtle Link',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const Nav: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
variant: 'nav',
|
|
66
|
+
href: '#',
|
|
67
|
+
children: 'Navigation Link',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const ExternalLink: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
href: 'https://sonance.com',
|
|
74
|
+
children: 'Visit Sonance',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const ExternalWithoutIcon: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
href: 'https://sonance.com',
|
|
81
|
+
showExternalIcon: false,
|
|
82
|
+
children: 'Visit Sonance (no icon)',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const Sizes: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<div className="flex items-center gap-4">
|
|
89
|
+
<Link href="#" size="sm">Small</Link>
|
|
90
|
+
<Link href="#" size="md">Medium</Link>
|
|
91
|
+
<Link href="#" size="lg">Large</Link>
|
|
92
|
+
</div>
|
|
93
|
+
),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const AllVariants: Story = {
|
|
97
|
+
render: () => (
|
|
98
|
+
<div className="space-y-4">
|
|
99
|
+
<div>
|
|
100
|
+
<p className="text-xs text-foreground-muted mb-1">Default</p>
|
|
101
|
+
<Link href="#">Click here to learn more</Link>
|
|
102
|
+
</div>
|
|
103
|
+
<div>
|
|
104
|
+
<p className="text-xs text-foreground-muted mb-1">Primary</p>
|
|
105
|
+
<Link href="#" variant="primary">View documentation</Link>
|
|
106
|
+
</div>
|
|
107
|
+
<div>
|
|
108
|
+
<p className="text-xs text-foreground-muted mb-1">Muted</p>
|
|
109
|
+
<Link href="#" variant="muted">Terms of service</Link>
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<p className="text-xs text-foreground-muted mb-1">Subtle</p>
|
|
113
|
+
<Link href="#" variant="subtle">Privacy policy</Link>
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<p className="text-xs text-foreground-muted mb-1">Nav</p>
|
|
117
|
+
<Link href="#" variant="nav">Navigation item</Link>
|
|
118
|
+
</div>
|
|
119
|
+
<div>
|
|
120
|
+
<p className="text-xs text-foreground-muted mb-1">External</p>
|
|
121
|
+
<Link href="https://sonance.com">Visit Sonance</Link>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const InParagraph: Story = {
|
|
128
|
+
render: () => (
|
|
129
|
+
<p className="text-foreground-secondary max-w-md">
|
|
130
|
+
Sonance has been creating premium audio solutions for over 35 years.{' '}
|
|
131
|
+
<Link href="#">Learn more about our history</Link> or{' '}
|
|
132
|
+
<Link href="https://sonance.com" variant="primary">visit our website</Link>{' '}
|
|
133
|
+
for the latest products.
|
|
134
|
+
</p>
|
|
135
|
+
),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const AnchorExample: Story = {
|
|
139
|
+
render: () => (
|
|
140
|
+
<Anchor href="#" className="group">
|
|
141
|
+
<div className="p-4 border border-border hover:border-primary transition-colors">
|
|
142
|
+
<h3 className="font-medium text-foreground group-hover:text-primary transition-colors">
|
|
143
|
+
Clickable Card
|
|
144
|
+
</h3>
|
|
145
|
+
<p className="text-sm text-foreground-muted">
|
|
146
|
+
The Anchor component wraps any element in an unstyled link.
|
|
147
|
+
</p>
|
|
148
|
+
</div>
|
|
149
|
+
</Anchor>
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
154
|
+
export const ResponsiveMatrix: Story = {
|
|
155
|
+
render: () => (
|
|
156
|
+
<div className="space-y-8">
|
|
157
|
+
{/* Mobile */}
|
|
158
|
+
<div>
|
|
159
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
160
|
+
<div className="w-[375px] border border-dashed border-border p-4">
|
|
161
|
+
<p className="text-foreground-secondary text-sm">
|
|
162
|
+
Visit <Link href="#">our products</Link> or <Link href="https://sonance.com">sonance.com</Link>.
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
{/* Tablet */}
|
|
167
|
+
<div>
|
|
168
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
169
|
+
<div className="w-[768px] border border-dashed border-border p-4">
|
|
170
|
+
<div className="flex items-center gap-6">
|
|
171
|
+
<Link href="#" variant="nav">Home</Link>
|
|
172
|
+
<Link href="#" variant="nav">Products</Link>
|
|
173
|
+
<Link href="#" variant="nav">About</Link>
|
|
174
|
+
<Link href="#" variant="nav">Contact</Link>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
{/* Desktop */}
|
|
179
|
+
<div>
|
|
180
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
181
|
+
<div className="w-[1280px] border border-dashed border-border p-4">
|
|
182
|
+
<div className="flex items-center justify-between">
|
|
183
|
+
<div className="flex items-center gap-6">
|
|
184
|
+
<Link href="#" variant="nav">Home</Link>
|
|
185
|
+
<Link href="#" variant="nav">Products</Link>
|
|
186
|
+
<Link href="#" variant="nav">Support</Link>
|
|
187
|
+
<Link href="#" variant="nav">About</Link>
|
|
188
|
+
</div>
|
|
189
|
+
<div className="flex items-center gap-4">
|
|
190
|
+
<Link href="#" variant="muted" size="sm">Terms</Link>
|
|
191
|
+
<Link href="#" variant="muted" size="sm">Privacy</Link>
|
|
192
|
+
<Link href="https://sonance.com" variant="primary">Visit Sonance</Link>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
),
|
|
199
|
+
};
|
|
@@ -3,6 +3,8 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
3
3
|
import { ExternalLink } from "lucide-react";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
|
|
6
|
+
export type LinkState = "default" | "hover" | "focus" | "active" | "visited";
|
|
7
|
+
|
|
6
8
|
const linkVariants = cva(
|
|
7
9
|
"inline-flex items-center gap-1 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2",
|
|
8
10
|
{
|
|
@@ -27,11 +29,53 @@ const linkVariants = cva(
|
|
|
27
29
|
}
|
|
28
30
|
);
|
|
29
31
|
|
|
32
|
+
// State styles for Storybook/Figma visualization
|
|
33
|
+
const getStateStyles = (variant: string | null | undefined, state?: LinkState) => {
|
|
34
|
+
if (!state || state === "default") return "";
|
|
35
|
+
|
|
36
|
+
const stateMap: Record<string, Record<string, string>> = {
|
|
37
|
+
default: {
|
|
38
|
+
hover: "text-foreground-secondary",
|
|
39
|
+
focus: "ring-2 ring-border-focus ring-offset-2",
|
|
40
|
+
active: "text-foreground-muted",
|
|
41
|
+
visited: "text-foreground-muted",
|
|
42
|
+
},
|
|
43
|
+
primary: {
|
|
44
|
+
hover: "text-primary-hover",
|
|
45
|
+
focus: "ring-2 ring-border-focus ring-offset-2",
|
|
46
|
+
active: "text-primary-active",
|
|
47
|
+
visited: "text-primary-hover",
|
|
48
|
+
},
|
|
49
|
+
muted: {
|
|
50
|
+
hover: "text-foreground",
|
|
51
|
+
focus: "ring-2 ring-border-focus ring-offset-2",
|
|
52
|
+
active: "text-foreground",
|
|
53
|
+
visited: "text-foreground-muted",
|
|
54
|
+
},
|
|
55
|
+
subtle: {
|
|
56
|
+
hover: "text-foreground-muted",
|
|
57
|
+
focus: "ring-2 ring-border-focus ring-offset-2",
|
|
58
|
+
active: "text-foreground-muted",
|
|
59
|
+
visited: "text-foreground-subtle",
|
|
60
|
+
},
|
|
61
|
+
nav: {
|
|
62
|
+
hover: "text-foreground",
|
|
63
|
+
focus: "ring-2 ring-border-focus ring-offset-2",
|
|
64
|
+
active: "text-foreground",
|
|
65
|
+
visited: "text-foreground-secondary",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return stateMap[variant || "default"]?.[state] || "";
|
|
70
|
+
};
|
|
71
|
+
|
|
30
72
|
interface LinkProps
|
|
31
73
|
extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
32
74
|
VariantProps<typeof linkVariants> {
|
|
33
75
|
external?: boolean;
|
|
34
76
|
showExternalIcon?: boolean;
|
|
77
|
+
/** Visual state for Storybook/Figma documentation */
|
|
78
|
+
state?: LinkState;
|
|
35
79
|
}
|
|
36
80
|
|
|
37
81
|
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
|
@@ -42,6 +86,7 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
|
|
42
86
|
size,
|
|
43
87
|
external,
|
|
44
88
|
showExternalIcon = true,
|
|
89
|
+
state,
|
|
45
90
|
children,
|
|
46
91
|
href,
|
|
47
92
|
...props
|
|
@@ -54,7 +99,11 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
|
|
|
54
99
|
<a
|
|
55
100
|
ref={ref}
|
|
56
101
|
href={href}
|
|
57
|
-
className={cn(
|
|
102
|
+
className={cn(
|
|
103
|
+
linkVariants({ variant, size }),
|
|
104
|
+
getStateStyles(variant, state),
|
|
105
|
+
className
|
|
106
|
+
)}
|
|
58
107
|
{...(isExternal && {
|
|
59
108
|
target: "_blank",
|
|
60
109
|
rel: "noopener noreferrer",
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { ComponentProps } from 'react';
|
|
3
|
+
import { Speaker, Headphones, Radio, Music } from 'lucide-react';
|
|
4
|
+
import { Listbox, ListboxItem, ListboxSection, type ListboxItemState } from './listbox';
|
|
5
|
+
|
|
6
|
+
type ListboxStoryProps = ComponentProps<typeof Listbox> & {
|
|
7
|
+
state?: ListboxItemState;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const meta: Meta<ListboxStoryProps> = {
|
|
11
|
+
title: 'Components/Forms/Listbox',
|
|
12
|
+
component: Listbox,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component: 'A list selection component supporting single and multiple selection modes.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
argTypes: {
|
|
22
|
+
state: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['default', 'hover', 'focus', 'selected', 'disabled'],
|
|
25
|
+
description: 'Visual state for ListboxItem documentation',
|
|
26
|
+
table: {
|
|
27
|
+
defaultValue: { summary: 'default' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<ListboxStoryProps>;
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<div className="w-64">
|
|
39
|
+
<Listbox defaultValue="speaker1">
|
|
40
|
+
<ListboxItem value="speaker1">VP66 TL</ListboxItem>
|
|
41
|
+
<ListboxItem value="speaker2">VP86 TL</ListboxItem>
|
|
42
|
+
<ListboxItem value="speaker3">Garden Series</ListboxItem>
|
|
43
|
+
<ListboxItem value="speaker4">Soundbar 3.0</ListboxItem>
|
|
44
|
+
</Listbox>
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const WithLabel: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<div className="w-64">
|
|
52
|
+
<Listbox label="Select Speaker" defaultValue="speaker1">
|
|
53
|
+
<ListboxItem value="speaker1">VP66 TL</ListboxItem>
|
|
54
|
+
<ListboxItem value="speaker2">VP86 TL</ListboxItem>
|
|
55
|
+
<ListboxItem value="speaker3">Garden Series</ListboxItem>
|
|
56
|
+
</Listbox>
|
|
57
|
+
</div>
|
|
58
|
+
),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithDescriptions: Story = {
|
|
62
|
+
render: () => (
|
|
63
|
+
<div className="w-80">
|
|
64
|
+
<Listbox label="Choose Product" defaultValue="inwall">
|
|
65
|
+
<ListboxItem value="inwall" description="Invisible sound for any room">
|
|
66
|
+
In-Wall Speakers
|
|
67
|
+
</ListboxItem>
|
|
68
|
+
<ListboxItem value="outdoor" description="Weather-resistant for gardens">
|
|
69
|
+
Outdoor Speakers
|
|
70
|
+
</ListboxItem>
|
|
71
|
+
<ListboxItem value="soundbar" description="Premium home theater audio">
|
|
72
|
+
Soundbar
|
|
73
|
+
</ListboxItem>
|
|
74
|
+
<ListboxItem value="subwoofer" description="Deep bass performance">
|
|
75
|
+
Subwoofer
|
|
76
|
+
</ListboxItem>
|
|
77
|
+
</Listbox>
|
|
78
|
+
</div>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const WithIcons: Story = {
|
|
83
|
+
render: () => (
|
|
84
|
+
<div className="w-72">
|
|
85
|
+
<Listbox label="Audio Source" defaultValue="speakers">
|
|
86
|
+
<ListboxItem
|
|
87
|
+
value="speakers"
|
|
88
|
+
startContent={<Speaker className="h-4 w-4" />}
|
|
89
|
+
description="Main room speakers"
|
|
90
|
+
>
|
|
91
|
+
Speakers
|
|
92
|
+
</ListboxItem>
|
|
93
|
+
<ListboxItem
|
|
94
|
+
value="headphones"
|
|
95
|
+
startContent={<Headphones className="h-4 w-4" />}
|
|
96
|
+
description="Private listening"
|
|
97
|
+
>
|
|
98
|
+
Headphones
|
|
99
|
+
</ListboxItem>
|
|
100
|
+
<ListboxItem
|
|
101
|
+
value="radio"
|
|
102
|
+
startContent={<Radio className="h-4 w-4" />}
|
|
103
|
+
description="FM/AM broadcast"
|
|
104
|
+
>
|
|
105
|
+
Radio
|
|
106
|
+
</ListboxItem>
|
|
107
|
+
<ListboxItem
|
|
108
|
+
value="streaming"
|
|
109
|
+
startContent={<Music className="h-4 w-4" />}
|
|
110
|
+
description="Online music services"
|
|
111
|
+
>
|
|
112
|
+
Streaming
|
|
113
|
+
</ListboxItem>
|
|
114
|
+
</Listbox>
|
|
115
|
+
</div>
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const MultipleSelection: Story = {
|
|
120
|
+
render: () => (
|
|
121
|
+
<div className="w-64">
|
|
122
|
+
<Listbox label="Select Rooms" multiple defaultValue={['living', 'kitchen']}>
|
|
123
|
+
<ListboxItem value="living">Living Room</ListboxItem>
|
|
124
|
+
<ListboxItem value="kitchen">Kitchen</ListboxItem>
|
|
125
|
+
<ListboxItem value="bedroom">Bedroom</ListboxItem>
|
|
126
|
+
<ListboxItem value="office">Office</ListboxItem>
|
|
127
|
+
<ListboxItem value="patio">Patio</ListboxItem>
|
|
128
|
+
</Listbox>
|
|
129
|
+
</div>
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const WithDisabledItems: Story = {
|
|
134
|
+
render: () => (
|
|
135
|
+
<div className="w-64">
|
|
136
|
+
<Listbox label="Select Zone" defaultValue="zone1">
|
|
137
|
+
<ListboxItem value="zone1">Zone 1 - Living</ListboxItem>
|
|
138
|
+
<ListboxItem value="zone2">Zone 2 - Kitchen</ListboxItem>
|
|
139
|
+
<ListboxItem value="zone3" disabled>
|
|
140
|
+
Zone 3 - Offline
|
|
141
|
+
</ListboxItem>
|
|
142
|
+
<ListboxItem value="zone4">Zone 4 - Bedroom</ListboxItem>
|
|
143
|
+
<ListboxItem value="zone5" disabled>
|
|
144
|
+
Zone 5 - Maintenance
|
|
145
|
+
</ListboxItem>
|
|
146
|
+
</Listbox>
|
|
147
|
+
</div>
|
|
148
|
+
),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const WithSections: Story = {
|
|
152
|
+
render: () => (
|
|
153
|
+
<div className="w-72">
|
|
154
|
+
<Listbox label="Select Product" defaultValue="vp66">
|
|
155
|
+
<ListboxSection title="In-Wall">
|
|
156
|
+
<ListboxItem value="vp66">VP66 TL</ListboxItem>
|
|
157
|
+
<ListboxItem value="vp86">VP86 TL</ListboxItem>
|
|
158
|
+
<ListboxItem value="vp88">VP88 R</ListboxItem>
|
|
159
|
+
</ListboxSection>
|
|
160
|
+
<ListboxSection title="Outdoor">
|
|
161
|
+
<ListboxItem value="garden">Garden Series</ListboxItem>
|
|
162
|
+
<ListboxItem value="landscape">Landscape Series</ListboxItem>
|
|
163
|
+
</ListboxSection>
|
|
164
|
+
<ListboxSection title="Soundbars">
|
|
165
|
+
<ListboxItem value="soundbar3">Soundbar 3.0</ListboxItem>
|
|
166
|
+
<ListboxItem value="soundbar5">Soundbar 5.1</ListboxItem>
|
|
167
|
+
</ListboxSection>
|
|
168
|
+
</Listbox>
|
|
169
|
+
</div>
|
|
170
|
+
),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const SettingsList: Story = {
|
|
174
|
+
render: () => (
|
|
175
|
+
<div className="w-80">
|
|
176
|
+
<Listbox label="Audio Quality" defaultValue="high">
|
|
177
|
+
<ListboxItem
|
|
178
|
+
value="low"
|
|
179
|
+
description="128 kbps - Saves data"
|
|
180
|
+
>
|
|
181
|
+
Low Quality
|
|
182
|
+
</ListboxItem>
|
|
183
|
+
<ListboxItem
|
|
184
|
+
value="normal"
|
|
185
|
+
description="256 kbps - Balanced"
|
|
186
|
+
>
|
|
187
|
+
Normal Quality
|
|
188
|
+
</ListboxItem>
|
|
189
|
+
<ListboxItem
|
|
190
|
+
value="high"
|
|
191
|
+
description="320 kbps - Best sound"
|
|
192
|
+
>
|
|
193
|
+
High Quality
|
|
194
|
+
</ListboxItem>
|
|
195
|
+
<ListboxItem
|
|
196
|
+
value="lossless"
|
|
197
|
+
description="FLAC - Studio quality"
|
|
198
|
+
>
|
|
199
|
+
Lossless
|
|
200
|
+
</ListboxItem>
|
|
201
|
+
</Listbox>
|
|
202
|
+
</div>
|
|
203
|
+
),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// State Matrix - Visual documentation of all ListboxItem states
|
|
207
|
+
export const StateMatrix: Story = {
|
|
208
|
+
render: () => {
|
|
209
|
+
const states: ListboxItemState[] = ['default', 'hover', 'focus', 'selected', 'disabled'];
|
|
210
|
+
return (
|
|
211
|
+
<div className="space-y-6 w-64">
|
|
212
|
+
<h3 className="text-sm font-medium text-foreground-muted">ListboxItem States</h3>
|
|
213
|
+
<Listbox>
|
|
214
|
+
{states.map((state) => (
|
|
215
|
+
<ListboxItem key={state} value={state} state={state}>
|
|
216
|
+
{state} state
|
|
217
|
+
</ListboxItem>
|
|
218
|
+
))}
|
|
219
|
+
</Listbox>
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
226
|
+
export const ResponsiveMatrix: Story = {
|
|
227
|
+
render: () => (
|
|
228
|
+
<div className="space-y-8">
|
|
229
|
+
{/* Mobile */}
|
|
230
|
+
<div>
|
|
231
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
232
|
+
<div className="w-[375px] border border-dashed border-border p-4">
|
|
233
|
+
<Listbox label="Select Speaker" defaultValue="speaker1">
|
|
234
|
+
<ListboxItem value="speaker1">VP66 TL</ListboxItem>
|
|
235
|
+
<ListboxItem value="speaker2">VP86 TL</ListboxItem>
|
|
236
|
+
<ListboxItem value="speaker3">Garden Series</ListboxItem>
|
|
237
|
+
</Listbox>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
{/* Tablet */}
|
|
241
|
+
<div>
|
|
242
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
243
|
+
<div className="w-[768px] border border-dashed border-border p-4">
|
|
244
|
+
<div className="grid grid-cols-2 gap-4">
|
|
245
|
+
<Listbox label="Product Category" defaultValue="inwall">
|
|
246
|
+
<ListboxItem value="inwall">In-Wall Speakers</ListboxItem>
|
|
247
|
+
<ListboxItem value="outdoor">Outdoor Speakers</ListboxItem>
|
|
248
|
+
<ListboxItem value="soundbar">Soundbar</ListboxItem>
|
|
249
|
+
</Listbox>
|
|
250
|
+
<Listbox label="Audio Quality" defaultValue="high">
|
|
251
|
+
<ListboxItem value="low">Low Quality</ListboxItem>
|
|
252
|
+
<ListboxItem value="normal">Normal Quality</ListboxItem>
|
|
253
|
+
<ListboxItem value="high">High Quality</ListboxItem>
|
|
254
|
+
</Listbox>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
{/* Desktop */}
|
|
259
|
+
<div>
|
|
260
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
261
|
+
<div className="w-[1280px] border border-dashed border-border p-4">
|
|
262
|
+
<div className="grid grid-cols-3 gap-4">
|
|
263
|
+
<Listbox label="Select Product" defaultValue="vp66">
|
|
264
|
+
<ListboxSection title="In-Wall">
|
|
265
|
+
<ListboxItem value="vp66">VP66 TL</ListboxItem>
|
|
266
|
+
<ListboxItem value="vp86">VP86 TL</ListboxItem>
|
|
267
|
+
</ListboxSection>
|
|
268
|
+
<ListboxSection title="Outdoor">
|
|
269
|
+
<ListboxItem value="garden">Garden Series</ListboxItem>
|
|
270
|
+
</ListboxSection>
|
|
271
|
+
</Listbox>
|
|
272
|
+
<Listbox label="Select Room" multiple defaultValue={['living']}>
|
|
273
|
+
<ListboxItem value="living">Living Room</ListboxItem>
|
|
274
|
+
<ListboxItem value="kitchen">Kitchen</ListboxItem>
|
|
275
|
+
<ListboxItem value="bedroom">Bedroom</ListboxItem>
|
|
276
|
+
</Listbox>
|
|
277
|
+
<Listbox label="Audio Quality" defaultValue="high">
|
|
278
|
+
<ListboxItem value="low" description="128 kbps">Low</ListboxItem>
|
|
279
|
+
<ListboxItem value="high" description="320 kbps">High</ListboxItem>
|
|
280
|
+
<ListboxItem value="lossless" description="FLAC">Lossless</ListboxItem>
|
|
281
|
+
</Listbox>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
),
|
|
287
|
+
};
|
|
@@ -4,6 +4,22 @@ import { forwardRef, createContext, useContext, useState } from "react";
|
|
|
4
4
|
import { Check } from "lucide-react";
|
|
5
5
|
import { cn } from "@/lib/utils";
|
|
6
6
|
|
|
7
|
+
export type ListboxItemState = "default" | "hover" | "focus" | "selected" | "disabled";
|
|
8
|
+
|
|
9
|
+
// State styles for Storybook/Figma visualization
|
|
10
|
+
const getItemStateStyles = (state?: ListboxItemState, isSelected?: boolean) => {
|
|
11
|
+
if (!state || state === "default") return "";
|
|
12
|
+
|
|
13
|
+
const stateMap: Record<string, string> = {
|
|
14
|
+
hover: "bg-secondary-hover",
|
|
15
|
+
focus: "ring-2 ring-border-focus ring-inset",
|
|
16
|
+
selected: "bg-primary text-primary-foreground",
|
|
17
|
+
disabled: "opacity-50 cursor-not-allowed",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return stateMap[state] || "";
|
|
21
|
+
};
|
|
22
|
+
|
|
7
23
|
interface ListboxContextValue {
|
|
8
24
|
value: string | string[];
|
|
9
25
|
onChange: (value: string) => void;
|
|
@@ -83,6 +99,8 @@ interface ListboxItemProps {
|
|
|
83
99
|
endContent?: React.ReactNode;
|
|
84
100
|
className?: string;
|
|
85
101
|
children: React.ReactNode;
|
|
102
|
+
/** Visual state for Storybook/Figma documentation */
|
|
103
|
+
state?: ListboxItemState;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
export const ListboxItem = forwardRef<HTMLLIElement, ListboxItemProps>(
|
|
@@ -95,6 +113,7 @@ export const ListboxItem = forwardRef<HTMLLIElement, ListboxItemProps>(
|
|
|
95
113
|
endContent,
|
|
96
114
|
className,
|
|
97
115
|
children,
|
|
116
|
+
state,
|
|
98
117
|
},
|
|
99
118
|
ref
|
|
100
119
|
) => {
|
|
@@ -105,21 +124,25 @@ export const ListboxItem = forwardRef<HTMLLIElement, ListboxItemProps>(
|
|
|
105
124
|
const isSelected = multiple
|
|
106
125
|
? Array.isArray(selectedValue) && selectedValue.includes(value)
|
|
107
126
|
: selectedValue === value;
|
|
127
|
+
const isSelectedState = state === "selected";
|
|
128
|
+
const isDisabled = disabled || state === "disabled";
|
|
129
|
+
const finalSelected = isSelected || isSelectedState;
|
|
108
130
|
|
|
109
131
|
return (
|
|
110
132
|
<li
|
|
111
133
|
ref={ref}
|
|
112
134
|
role="option"
|
|
113
|
-
aria-selected={
|
|
114
|
-
aria-disabled={
|
|
115
|
-
onClick={() => !
|
|
135
|
+
aria-selected={finalSelected}
|
|
136
|
+
aria-disabled={isDisabled}
|
|
137
|
+
onClick={() => !isDisabled && onChange(value)}
|
|
116
138
|
className={cn(
|
|
117
139
|
"flex items-center gap-3 px-4 py-3 cursor-pointer",
|
|
118
140
|
"transition-colors duration-150",
|
|
119
|
-
|
|
141
|
+
finalSelected
|
|
120
142
|
? "bg-primary text-primary-foreground"
|
|
121
143
|
: "hover:bg-secondary-hover",
|
|
122
|
-
|
|
144
|
+
isDisabled && "cursor-not-allowed opacity-50",
|
|
145
|
+
getItemStateStyles(state, finalSelected),
|
|
123
146
|
className
|
|
124
147
|
)}
|
|
125
148
|
>
|
|
@@ -132,7 +155,7 @@ export const ListboxItem = forwardRef<HTMLLIElement, ListboxItemProps>(
|
|
|
132
155
|
<div
|
|
133
156
|
className={cn(
|
|
134
157
|
"text-xs mt-0.5 truncate",
|
|
135
|
-
|
|
158
|
+
finalSelected ? "text-primary-foreground/70" : "text-foreground-muted"
|
|
136
159
|
)}
|
|
137
160
|
>
|
|
138
161
|
{description}
|
|
@@ -142,7 +165,7 @@ export const ListboxItem = forwardRef<HTMLLIElement, ListboxItemProps>(
|
|
|
142
165
|
{endContent && (
|
|
143
166
|
<span className="shrink-0">{endContent}</span>
|
|
144
167
|
)}
|
|
145
|
-
{
|
|
168
|
+
{finalSelected && !endContent && (
|
|
146
169
|
<Check className="h-4 w-4 shrink-0" />
|
|
147
170
|
)}
|
|
148
171
|
</li>
|