sonance-brand-mcp 1.1.3 → 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.
Files changed (71) hide show
  1. package/dist/assets/components/accordion.stories.tsx +310 -0
  2. package/dist/assets/components/accordion.tsx +56 -30
  3. package/dist/assets/components/alert.stories.tsx +199 -0
  4. package/dist/assets/components/autocomplete.stories.tsx +307 -0
  5. package/dist/assets/components/autocomplete.tsx +28 -4
  6. package/dist/assets/components/avatar.stories.tsx +175 -0
  7. package/dist/assets/components/badge.stories.tsx +258 -0
  8. package/dist/assets/components/breadcrumbs.stories.tsx +175 -0
  9. package/dist/assets/components/button.stories.tsx +362 -0
  10. package/dist/assets/components/button.tsx +48 -3
  11. package/dist/assets/components/calendar.stories.tsx +247 -0
  12. package/dist/assets/components/card.stories.tsx +275 -0
  13. package/dist/assets/components/card.tsx +26 -1
  14. package/dist/assets/components/checkbox-group.stories.tsx +281 -0
  15. package/dist/assets/components/checkbox.stories.tsx +160 -0
  16. package/dist/assets/components/checkbox.tsx +32 -4
  17. package/dist/assets/components/code.stories.tsx +265 -0
  18. package/dist/assets/components/date-input.stories.tsx +278 -0
  19. package/dist/assets/components/date-input.tsx +24 -2
  20. package/dist/assets/components/date-picker.stories.tsx +337 -0
  21. package/dist/assets/components/date-picker.tsx +28 -4
  22. package/dist/assets/components/date-range-picker.stories.tsx +340 -0
  23. package/dist/assets/components/dialog.stories.tsx +285 -0
  24. package/dist/assets/components/divider.stories.tsx +176 -0
  25. package/dist/assets/components/drawer.stories.tsx +216 -0
  26. package/dist/assets/components/dropdown.stories.tsx +342 -0
  27. package/dist/assets/components/dropdown.tsx +55 -10
  28. package/dist/assets/components/form.stories.tsx +372 -0
  29. package/dist/assets/components/image.stories.tsx +348 -0
  30. package/dist/assets/components/input-otp.stories.tsx +336 -0
  31. package/dist/assets/components/input-otp.tsx +24 -2
  32. package/dist/assets/components/input.stories.tsx +223 -0
  33. package/dist/assets/components/input.tsx +27 -2
  34. package/dist/assets/components/kbd.stories.tsx +272 -0
  35. package/dist/assets/components/link.stories.tsx +199 -0
  36. package/dist/assets/components/link.tsx +50 -1
  37. package/dist/assets/components/listbox.stories.tsx +287 -0
  38. package/dist/assets/components/listbox.tsx +30 -7
  39. package/dist/assets/components/navbar.stories.tsx +218 -0
  40. package/dist/assets/components/number-input.stories.tsx +295 -0
  41. package/dist/assets/components/number-input.tsx +30 -8
  42. package/dist/assets/components/pagination.stories.tsx +280 -0
  43. package/dist/assets/components/pagination.tsx +45 -21
  44. package/dist/assets/components/popover.stories.tsx +219 -0
  45. package/dist/assets/components/progress.stories.tsx +153 -0
  46. package/dist/assets/components/radio-group.stories.tsx +187 -0
  47. package/dist/assets/components/radio-group.tsx +30 -6
  48. package/dist/assets/components/range-calendar.stories.tsx +334 -0
  49. package/dist/assets/components/scroll-shadow.stories.tsx +335 -0
  50. package/dist/assets/components/select.stories.tsx +192 -0
  51. package/dist/assets/components/select.tsx +54 -7
  52. package/dist/assets/components/skeleton.stories.tsx +166 -0
  53. package/dist/assets/components/slider.stories.tsx +145 -0
  54. package/dist/assets/components/slider.tsx +43 -8
  55. package/dist/assets/components/spacer.stories.tsx +216 -0
  56. package/dist/assets/components/spinner.stories.tsx +149 -0
  57. package/dist/assets/components/switch.stories.tsx +170 -0
  58. package/dist/assets/components/switch.tsx +29 -4
  59. package/dist/assets/components/table.stories.tsx +322 -0
  60. package/dist/assets/components/tabs.stories.tsx +306 -0
  61. package/dist/assets/components/tabs.tsx +25 -4
  62. package/dist/assets/components/textarea.stories.tsx +103 -0
  63. package/dist/assets/components/textarea.tsx +27 -3
  64. package/dist/assets/components/theme-toggle.stories.tsx +248 -0
  65. package/dist/assets/components/time-input.stories.tsx +365 -0
  66. package/dist/assets/components/time-input.tsx +25 -3
  67. package/dist/assets/components/toast.stories.tsx +195 -0
  68. package/dist/assets/components/tooltip.stories.tsx +226 -0
  69. package/dist/assets/components/user.stories.tsx +274 -0
  70. package/dist/index.js +1732 -13
  71. 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(linkVariants({ variant, size }), className)}
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={isSelected}
114
- aria-disabled={disabled}
115
- onClick={() => !disabled && onChange(value)}
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
- isSelected
141
+ finalSelected
120
142
  ? "bg-primary text-primary-foreground"
121
143
  : "hover:bg-secondary-hover",
122
- disabled && "cursor-not-allowed opacity-50",
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
- isSelected ? "text-primary-foreground/70" : "text-foreground-muted"
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
- {isSelected && !endContent && (
168
+ {finalSelected && !endContent && (
146
169
  <Check className="h-4 w-4 shrink-0" />
147
170
  )}
148
171
  </li>