sonance-brand-mcp 1.2.5 → 1.3.2

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 (189) hide show
  1. package/dist/assets/api/sonance-analyze/route.ts +1116 -0
  2. package/dist/assets/api/sonance-assets/route.ts +113 -0
  3. package/dist/assets/api/sonance-components/route.ts +41 -0
  4. package/dist/assets/api/sonance-inject-id/route.ts +363 -0
  5. package/dist/assets/api/sonance-save-logo/route.ts +426 -0
  6. package/dist/assets/api/sonance-theme/route.ts +106 -0
  7. package/dist/assets/brand-system.ts +1265 -0
  8. package/dist/assets/components/accordion.stories.tsx +26 -26
  9. package/dist/assets/components/accordion.tsx +3 -3
  10. package/dist/assets/components/alert-dialog.stories.tsx +142 -0
  11. package/dist/assets/components/alert-dialog.tsx +143 -0
  12. package/dist/assets/components/alert.stories.tsx +3 -3
  13. package/dist/assets/components/alert.tsx +4 -3
  14. package/dist/assets/components/aspect-ratio.stories.tsx +70 -0
  15. package/dist/assets/components/aspect-ratio.tsx +8 -0
  16. package/dist/assets/components/autocomplete.stories.tsx +9 -9
  17. package/dist/assets/components/autocomplete.tsx +3 -3
  18. package/dist/assets/components/avatar.stories.tsx +5 -5
  19. package/dist/assets/components/avatar.tsx +67 -23
  20. package/dist/assets/components/badge.stories.tsx +10 -10
  21. package/dist/assets/components/badge.tsx +3 -3
  22. package/dist/assets/components/breadcrumbs.stories.tsx +7 -7
  23. package/dist/assets/components/breadcrumbs.tsx +13 -8
  24. package/dist/assets/components/button.stories.tsx +74 -74
  25. package/dist/assets/components/button.tsx +2 -0
  26. package/dist/assets/components/calendar.stories.tsx +11 -11
  27. package/dist/assets/components/calendar.tsx +4 -4
  28. package/dist/assets/components/card.stories.tsx +22 -22
  29. package/dist/assets/components/card.tsx +7 -3
  30. package/dist/assets/components/carousel.stories.tsx +158 -0
  31. package/dist/assets/components/carousel.tsx +264 -0
  32. package/dist/assets/components/chart.stories.tsx +376 -0
  33. package/dist/assets/components/chart.tsx +384 -0
  34. package/dist/assets/components/checkbox-group.stories.tsx +6 -6
  35. package/dist/assets/components/checkbox-group.tsx +3 -3
  36. package/dist/assets/components/checkbox.stories.tsx +23 -20
  37. package/dist/assets/components/checkbox.tsx +13 -6
  38. package/dist/assets/components/code.stories.tsx +24 -24
  39. package/dist/assets/components/code.tsx +22 -27
  40. package/dist/assets/components/collapsible.stories.tsx +128 -0
  41. package/dist/assets/components/collapsible.tsx +10 -0
  42. package/dist/assets/components/command.stories.tsx +183 -0
  43. package/dist/assets/components/command.tsx +171 -0
  44. package/dist/assets/components/context-menu.stories.tsx +159 -0
  45. package/dist/assets/components/context-menu.tsx +214 -0
  46. package/dist/assets/components/date-input.stories.tsx +9 -9
  47. package/dist/assets/components/date-input.tsx +2 -2
  48. package/dist/assets/components/date-picker.stories.tsx +9 -9
  49. package/dist/assets/components/date-picker.tsx +3 -3
  50. package/dist/assets/components/date-range-picker.stories.tsx +12 -12
  51. package/dist/assets/components/date-range-picker.tsx +3 -3
  52. package/dist/assets/components/dialog.stories.tsx +40 -40
  53. package/dist/assets/components/dialog.tsx +8 -12
  54. package/dist/assets/components/divider.stories.tsx +30 -30
  55. package/dist/assets/components/divider.tsx +34 -35
  56. package/dist/assets/components/drawer.stories.tsx +32 -31
  57. package/dist/assets/components/drawer.tsx +7 -6
  58. package/dist/assets/components/dropdown-menu.tsx +213 -0
  59. package/dist/assets/components/dropdown.stories.tsx +12 -12
  60. package/dist/assets/components/dropdown.tsx +5 -5
  61. package/dist/assets/components/form.stories.tsx +30 -29
  62. package/dist/assets/components/form.tsx +5 -5
  63. package/dist/assets/components/hover-card.stories.tsx +115 -0
  64. package/dist/assets/components/hover-card.tsx +35 -0
  65. package/dist/assets/components/image.stories.tsx +48 -25
  66. package/dist/assets/components/image.tsx +8 -5
  67. package/dist/assets/components/input-otp.stories.tsx +15 -15
  68. package/dist/assets/components/input-otp.tsx +5 -5
  69. package/dist/assets/components/input.stories.tsx +30 -25
  70. package/dist/assets/components/input.tsx +7 -4
  71. package/dist/assets/components/kbd.stories.tsx +34 -34
  72. package/dist/assets/components/kbd.tsx +9 -9
  73. package/dist/assets/components/link.stories.tsx +36 -36
  74. package/dist/assets/components/link.tsx +4 -0
  75. package/dist/assets/components/listbox.stories.tsx +5 -5
  76. package/dist/assets/components/listbox.tsx +4 -4
  77. package/dist/assets/components/menubar.stories.tsx +208 -0
  78. package/dist/assets/components/menubar.tsx +247 -0
  79. package/dist/assets/components/navbar.stories.tsx +24 -24
  80. package/dist/assets/components/navbar.tsx +8 -14
  81. package/dist/assets/components/navigation-menu.stories.tsx +239 -0
  82. package/dist/assets/components/navigation-menu.tsx +135 -0
  83. package/dist/assets/components/number-input.stories.tsx +11 -11
  84. package/dist/assets/components/number-input.tsx +3 -3
  85. package/dist/assets/components/pagination.stories.tsx +13 -13
  86. package/dist/assets/components/pagination.tsx +6 -6
  87. package/dist/assets/components/popover.stories.tsx +35 -35
  88. package/dist/assets/components/popover.tsx +98 -15
  89. package/dist/assets/components/progress.stories.tsx +5 -5
  90. package/dist/assets/components/progress.tsx +5 -5
  91. package/dist/assets/components/radio-group.stories.tsx +7 -7
  92. package/dist/assets/components/radio-group.tsx +3 -3
  93. package/dist/assets/components/range-calendar.stories.tsx +18 -18
  94. package/dist/assets/components/range-calendar.tsx +3 -3
  95. package/dist/assets/components/resizable.stories.tsx +197 -0
  96. package/dist/assets/components/resizable.tsx +47 -0
  97. package/dist/assets/components/scroll-area.stories.tsx +123 -0
  98. package/dist/assets/components/scroll-area.tsx +48 -0
  99. package/dist/assets/components/scroll-shadow.stories.tsx +17 -17
  100. package/dist/assets/components/scroll-shadow.tsx +31 -9
  101. package/dist/assets/components/select.stories.tsx +20 -19
  102. package/dist/assets/components/select.tsx +10 -6
  103. package/dist/assets/components/separator.tsx +32 -0
  104. package/dist/assets/components/sheet.tsx +137 -0
  105. package/dist/assets/components/sidebar.stories.tsx +351 -0
  106. package/dist/assets/components/sidebar.tsx +757 -0
  107. package/dist/assets/components/skeleton.stories.tsx +3 -3
  108. package/dist/assets/components/skeleton.tsx +2 -2
  109. package/dist/assets/components/slider.stories.tsx +6 -6
  110. package/dist/assets/components/slider.tsx +3 -3
  111. package/dist/assets/components/spacer.stories.tsx +11 -11
  112. package/dist/assets/components/spacer.tsx +2 -2
  113. package/dist/assets/components/spinner.stories.tsx +8 -8
  114. package/dist/assets/components/spinner.tsx +5 -5
  115. package/dist/assets/components/switch.stories.tsx +24 -20
  116. package/dist/assets/components/switch.tsx +14 -6
  117. package/dist/assets/components/table.stories.tsx +7 -7
  118. package/dist/assets/components/table.tsx +8 -8
  119. package/dist/assets/components/tabs.stories.tsx +37 -37
  120. package/dist/assets/components/tabs.tsx +3 -3
  121. package/dist/assets/components/textarea.stories.tsx +13 -12
  122. package/dist/assets/components/textarea.tsx +3 -3
  123. package/dist/assets/components/theme-toggle.stories.tsx +31 -30
  124. package/dist/assets/components/theme-toggle.tsx +2 -2
  125. package/dist/assets/components/time-input.stories.tsx +16 -16
  126. package/dist/assets/components/time-input.tsx +2 -2
  127. package/dist/assets/components/toast.stories.tsx +8 -5
  128. package/dist/assets/components/toast.tsx +6 -6
  129. package/dist/assets/components/toggle-group.stories.tsx +153 -0
  130. package/dist/assets/components/toggle-group.tsx +61 -0
  131. package/dist/assets/components/toggle.stories.tsx +77 -0
  132. package/dist/assets/components/toggle.tsx +46 -0
  133. package/dist/assets/components/tooltip.stories.tsx +49 -27
  134. package/dist/assets/components/tooltip.tsx +23 -90
  135. package/dist/assets/components/user.stories.tsx +23 -23
  136. package/dist/assets/components/user.tsx +7 -4
  137. package/dist/assets/dev-tools/SonanceDevTools.tsx +4201 -0
  138. package/dist/assets/dev-tools/index.ts +10 -0
  139. package/dist/assets/globals.css +39 -0
  140. package/dist/assets/logos/40th-anniversary/Sonance_40_Logo_CMYK_BEAM_BLUE_40_AND_BEAM_DARK.png +0 -0
  141. package/dist/assets/logos/Sonance logo dark mode.png +0 -0
  142. package/dist/assets/logos/Sonance logo light mode.png +0 -0
  143. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_2C_Light_RGB_05162025.png +0 -0
  144. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_3C_Dark_RGB_05162025.png +0 -0
  145. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_White_RGB_05162025.png +0 -0
  146. package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Dark_RGB.png +0 -0
  147. package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Light_RGB.png +0 -0
  148. package/dist/assets/logos/james/James_Logo_Black_CMYK.png +0 -0
  149. package/dist/assets/logos/james/James_Logo_Black_RGB.png +0 -0
  150. package/dist/assets/logos/james/James_Logo_LtGray_CMYK.png +0 -0
  151. package/dist/assets/logos/james/James_Logo_LtGray_RGB.png +0 -0
  152. package/dist/assets/logos/james/James_Logo_Polished_RGB.png +0 -0
  153. package/dist/assets/logos/james/James_Logo_Reverse_CMYK.png +0 -0
  154. package/dist/assets/logos/james/James_Logo_Reverse_RGB.png +0 -0
  155. package/dist/assets/logos/james/James_Logo_White_CMYK.png +0 -0
  156. package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Dark_RGB.png +0 -0
  157. package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Light_RGB.png +0 -0
  158. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Dark_RGB.png +0 -0
  159. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Light_RGB.png +0 -0
  160. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Reverse_RGB.png +0 -0
  161. package/dist/assets/logos/my-sonance/My.Sonance_Logo_Black_RGB.png +0 -0
  162. package/dist/assets/logos/my-sonance/My.Sonance_Logo_Reverse_RGB.png +0 -0
  163. package/dist/assets/logos/sonance/Sonance_Logo_2C_Dark_RGB.png +0 -0
  164. package/dist/assets/logos/sonance/Sonance_Logo_2C_Light_RGB.png +0 -0
  165. package/dist/assets/logos/sonance/Sonance_Logo_2C_Reverse_RGB.png +0 -0
  166. package/dist/assets/logos/sonance/Sonance_Logo_Black_RGB.png +0 -0
  167. package/dist/assets/logos/sonance/Sonance_Logo_Grayscale_RGB.png +0 -0
  168. package/dist/assets/logos/sonance/Sonance_Logo_Reverse_RGB.png +0 -0
  169. package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Dark_CMYK.png +0 -0
  170. package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Light_CMYK.png +0 -0
  171. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Dark_RGB.png +0 -0
  172. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Light_RGB.png +0 -0
  173. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Reverse_RGB.png +0 -0
  174. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Black_RGB.png +0 -0
  175. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Grayscale_RGB.png +0 -0
  176. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Reverse_RGB.png +0 -0
  177. package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Dark.png +0 -0
  178. package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Light.png +0 -0
  179. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Dark.png +0 -0
  180. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Light.png +0 -0
  181. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Dark.png +0 -0
  182. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Light.png +0 -0
  183. package/dist/assets/logos/trufig/TrufigLogo_Black.png +0 -0
  184. package/dist/assets/logos/trufig/TrufigLogo_Light.png +0 -0
  185. package/dist/assets/logos/trufig/TrufigWatermark_Black.png +0 -0
  186. package/dist/assets/logos/trufig/TrufigWatermark_Light.png +0 -0
  187. package/dist/assets/styles/brand-overrides.css +37 -0
  188. package/dist/index.js +2055 -15
  189. package/package.json +1 -1
@@ -83,7 +83,7 @@ export function Pagination({
83
83
  const canGoNext = currentPage < totalPages;
84
84
 
85
85
  return (
86
- <nav
86
+ <nav data-sonance-name="pagination"
87
87
  role="navigation"
88
88
  aria-label="Pagination"
89
89
  className={cn("flex items-center gap-1", className)}
@@ -101,7 +101,7 @@ export function Pagination({
101
101
  {pages.map((page, index) => {
102
102
  if (page === "ellipsis") {
103
103
  return (
104
- <span
104
+ <span id="pagination-span" data-sonance-name="pagination"
105
105
  key={`ellipsis-${index}`}
106
106
  className="flex h-9 w-9 items-center justify-center text-foreground-muted"
107
107
  >
@@ -162,7 +162,7 @@ const PaginationButton = forwardRef<HTMLButtonElement, PaginationButtonProps>(
162
162
  : "bg-transparent text-foreground border-border hover:bg-secondary-hover",
163
163
  getButtonStateStyles(state),
164
164
  className
165
- )}
165
+ )} data-sonance-name="pagination"
166
166
  {...props}
167
167
  >
168
168
  {children}
@@ -197,9 +197,9 @@ export function CompactPagination({
197
197
  <ChevronLeft className="h-4 w-4" />
198
198
  </PaginationButton>
199
199
 
200
- <span className="text-sm text-foreground-secondary">
201
- Page <span className="font-medium text-foreground">{currentPage}</span> of{" "}
202
- <span className="font-medium text-foreground">{totalPages}</span>
200
+ <span id="compact-pagination-span" className="text-sm text-foreground-secondary">
201
+ Page <span id="compact-pagination-span-currentpage" className="font-medium text-foreground">{currentPage}</span> of{" "}
202
+ <span id="compact-pagination-span-totalpages" className="font-medium text-foreground">{totalPages}</span>
203
203
  </span>
204
204
 
205
205
  <PaginationButton
@@ -31,9 +31,9 @@ type Story = StoryObj<typeof meta>;
31
31
 
32
32
  export const Default: Story = {
33
33
  render: () => (
34
- <Popover trigger={<Button variant="secondary">Click me</Button>}>
34
+ <Popover trigger={<Button id="default-button-secondary" variant="secondary">Click me</Button>}>
35
35
  <PopoverContent>
36
- <p className="text-foreground-secondary">
36
+ <p id="default-p-this-is-the-popover-" className="text-foreground-secondary">
37
37
  This is the popover content. Click outside to close.
38
38
  </p>
39
39
  </PopoverContent>
@@ -43,9 +43,9 @@ export const Default: Story = {
43
43
 
44
44
  export const OnHover: Story = {
45
45
  render: () => (
46
- <Popover trigger={<Button variant="secondary">Hover me</Button>} triggerOn="hover">
46
+ <Popover trigger={<Button id="on-hover-button-secondary" variant="secondary">Hover me</Button>} triggerOn="hover">
47
47
  <PopoverContent>
48
- <p className="text-foreground-secondary">
48
+ <p id="on-hover-p-this-popover-appears" className="text-foreground-secondary">
49
49
  This popover appears on hover.
50
50
  </p>
51
51
  </PopoverContent>
@@ -55,9 +55,9 @@ export const OnHover: Story = {
55
55
 
56
56
  export const TopPosition: Story = {
57
57
  render: () => (
58
- <Popover trigger={<Button variant="secondary">Open Top</Button>} position="top">
58
+ <Popover trigger={<Button id="top-position-button-secondary" variant="secondary">Open Top</Button>} position="top">
59
59
  <PopoverContent>
60
- <p className="text-foreground-secondary">Popover on top</p>
60
+ <p id="top-position-p-popover-on-top" className="text-foreground-secondary">Popover on top</p>
61
61
  </PopoverContent>
62
62
  </Popover>
63
63
  ),
@@ -65,9 +65,9 @@ export const TopPosition: Story = {
65
65
 
66
66
  export const LeftPosition: Story = {
67
67
  render: () => (
68
- <Popover trigger={<Button variant="secondary">Open Left</Button>} position="left">
68
+ <Popover trigger={<Button id="left-position-button-secondary" variant="secondary">Open Left</Button>} position="left">
69
69
  <PopoverContent>
70
- <p className="text-foreground-secondary">Popover on left</p>
70
+ <p id="left-position-p-popover-on-left" className="text-foreground-secondary">Popover on left</p>
71
71
  </PopoverContent>
72
72
  </Popover>
73
73
  ),
@@ -75,9 +75,9 @@ export const LeftPosition: Story = {
75
75
 
76
76
  export const RightPosition: Story = {
77
77
  render: () => (
78
- <Popover trigger={<Button variant="secondary">Open Right</Button>} position="right">
78
+ <Popover trigger={<Button id="right-position-button-secondary" variant="secondary">Open Right</Button>} position="right">
79
79
  <PopoverContent>
80
- <p className="text-foreground-secondary">Popover on right</p>
80
+ <p id="right-position-p-popover-on-right" className="text-foreground-secondary">Popover on right</p>
81
81
  </PopoverContent>
82
82
  </Popover>
83
83
  ),
@@ -86,24 +86,24 @@ export const RightPosition: Story = {
86
86
  export const AllPositions: Story = {
87
87
  render: () => (
88
88
  <div className="flex items-center gap-8 p-16">
89
- <Popover trigger={<Button variant="secondary" size="sm">Top</Button>} position="top">
89
+ <Popover trigger={<Button id="all-positions-button-secondary" variant="secondary" size="sm">Top</Button>} position="top">
90
90
  <PopoverContent>
91
- <p className="text-sm">Top position</p>
91
+ <p id="all-positions-p-top-position" className="text-sm">Top position</p>
92
92
  </PopoverContent>
93
93
  </Popover>
94
- <Popover trigger={<Button variant="secondary" size="sm">Bottom</Button>} position="bottom">
94
+ <Popover trigger={<Button id="all-positions-button-secondary" variant="secondary" size="sm">Bottom</Button>} position="bottom">
95
95
  <PopoverContent>
96
- <p className="text-sm">Bottom position</p>
96
+ <p id="all-positions-p-bottom-position" className="text-sm">Bottom position</p>
97
97
  </PopoverContent>
98
98
  </Popover>
99
- <Popover trigger={<Button variant="secondary" size="sm">Left</Button>} position="left">
99
+ <Popover trigger={<Button id="all-positions-button-secondary" variant="secondary" size="sm">Left</Button>} position="left">
100
100
  <PopoverContent>
101
- <p className="text-sm">Left position</p>
101
+ <p id="all-positions-p-left-position" className="text-sm">Left position</p>
102
102
  </PopoverContent>
103
103
  </Popover>
104
- <Popover trigger={<Button variant="secondary" size="sm">Right</Button>} position="right">
104
+ <Popover trigger={<Button id="all-positions-button-secondary" variant="secondary" size="sm">Right</Button>} position="right">
105
105
  <PopoverContent>
106
- <p className="text-sm">Right position</p>
106
+ <p id="all-positions-p-right-position" className="text-sm">Right position</p>
107
107
  </PopoverContent>
108
108
  </Popover>
109
109
  </div>
@@ -112,7 +112,7 @@ export const AllPositions: Story = {
112
112
 
113
113
  export const MenuExample: Story = {
114
114
  render: () => (
115
- <Popover trigger={<Button variant="secondary">Actions</Button>}>
115
+ <Popover trigger={<Button id="menu-example-button-secondary" variant="secondary">Actions</Button>}>
116
116
  <div className="py-2">
117
117
  {['Edit', 'Duplicate', 'Archive', 'Delete'].map((action) => (
118
118
  <button
@@ -133,14 +133,14 @@ export const UserMenuExample: Story = {
133
133
  trigger={
134
134
  <button className="flex items-center gap-2 p-2 hover:bg-secondary-hover rounded transition-colors">
135
135
  <div className="w-8 h-8 rounded-full bg-primary" />
136
- <span className="text-sm text-foreground">John Doe</span>
136
+ <span id="user-menu-example-span-john-doe" className="text-sm text-foreground">John Doe</span>
137
137
  </button>
138
138
  }
139
139
  >
140
140
  <div className="w-48">
141
141
  <div className="px-4 py-3 border-b border-border">
142
- <p className="text-sm font-medium text-foreground">John Doe</p>
143
- <p className="text-xs text-foreground-muted">john@example.com</p>
142
+ <p id="user-menu-example-p-john-doe" className="text-sm font-medium text-foreground">John Doe</p>
143
+ <p id="user-menu-example-p-johnexamplecom" className="text-xs text-foreground-muted">john@example.com</p>
144
144
  </div>
145
145
  <div className="py-2">
146
146
  {['Profile', 'Settings', 'Billing'].map((item) => (
@@ -168,36 +168,36 @@ export const ResponsiveMatrix: Story = {
168
168
  <div className="space-y-8">
169
169
  {/* Mobile */}
170
170
  <div>
171
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
171
+ <h4 id="responsive-matrix-h4-mobile-375px" className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
172
172
  <div className="w-[375px] border border-dashed border-border p-4 flex justify-center">
173
- <Popover trigger={<Button variant="secondary" size="sm">Actions</Button>}>
173
+ <Popover trigger={<Button id="responsive-matrix-button-secondary" variant="secondary" size="sm">Actions</Button>}>
174
174
  <PopoverContent>
175
- <p className="text-sm">Popover content</p>
175
+ <p id="responsive-matrix-p-popover-content" className="text-sm">Popover content</p>
176
176
  </PopoverContent>
177
177
  </Popover>
178
178
  </div>
179
179
  </div>
180
180
  {/* Tablet */}
181
181
  <div>
182
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
182
+ <h4 id="responsive-matrix-h4-tablet-768px" className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
183
183
  <div className="w-[768px] border border-dashed border-border p-4 flex justify-center gap-4">
184
- <Popover trigger={<Button variant="secondary">Top</Button>} position="top">
185
- <PopoverContent><p className="text-sm">Top popover</p></PopoverContent>
184
+ <Popover trigger={<Button id="responsive-matrix-button-secondary" variant="secondary">Top</Button>} position="top">
185
+ <PopoverContent><p id="responsive-matrix-p-top-popover" className="text-sm">Top popover</p></PopoverContent>
186
186
  </Popover>
187
- <Popover trigger={<Button variant="secondary">Bottom</Button>} position="bottom">
188
- <PopoverContent><p className="text-sm">Bottom popover</p></PopoverContent>
187
+ <Popover trigger={<Button id="responsive-matrix-button-secondary" variant="secondary">Bottom</Button>} position="bottom">
188
+ <PopoverContent><p id="responsive-matrix-p-bottom-popover" className="text-sm">Bottom popover</p></PopoverContent>
189
189
  </Popover>
190
190
  </div>
191
191
  </div>
192
192
  {/* Desktop */}
193
193
  <div>
194
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
194
+ <h4 id="responsive-matrix-h4-desktop-1280px" className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
195
195
  <div className="w-[1280px] border border-dashed border-border p-4 flex justify-between items-center">
196
- <Popover trigger={<Button variant="secondary">User Menu</Button>}>
196
+ <Popover trigger={<Button id="responsive-matrix-button-secondary" variant="secondary">User Menu</Button>}>
197
197
  <div className="w-48">
198
198
  <div className="px-4 py-3 border-b border-border">
199
- <p className="text-sm font-medium">John Doe</p>
200
- <p className="text-xs text-foreground-muted">john@example.com</p>
199
+ <p id="responsive-matrix-p-john-doe" className="text-sm font-medium">John Doe</p>
200
+ <p id="responsive-matrix-p-johnexamplecom" className="text-xs text-foreground-muted">john@example.com</p>
201
201
  </div>
202
202
  <div className="py-2">
203
203
  <button className="w-full px-4 py-2 text-left text-sm hover:bg-secondary-hover">Profile</button>
@@ -205,7 +205,7 @@ export const ResponsiveMatrix: Story = {
205
205
  </div>
206
206
  </div>
207
207
  </Popover>
208
- <Popover trigger={<Button variant="secondary">Actions</Button>}>
208
+ <Popover trigger={<Button id="responsive-matrix-button-secondary" variant="secondary">Actions</Button>}>
209
209
  <div className="py-2">
210
210
  {['Edit', 'Duplicate', 'Archive', 'Delete'].map((action) => (
211
211
  <button key={action} className="w-full px-4 py-2 text-left text-sm hover:bg-secondary-hover">{action}</button>
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { useState, useRef, useEffect } from "react";
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import { createPortal } from "react-dom";
4
5
  import { cn } from "@/lib/utils";
5
6
 
6
7
  type PopoverPosition = "top" | "bottom" | "left" | "right";
@@ -13,6 +14,11 @@ interface PopoverProps {
13
14
  className?: string;
14
15
  }
15
16
 
17
+ interface PortalPosition {
18
+ top: number;
19
+ left: number;
20
+ }
21
+
16
22
  export function Popover({
17
23
  trigger,
18
24
  children,
@@ -21,22 +27,76 @@ export function Popover({
21
27
  className,
22
28
  }: PopoverProps) {
23
29
  const [isOpen, setIsOpen] = useState(false);
30
+ const [portalPosition, setPortalPosition] = useState<PortalPosition>({ top: 0, left: 0 });
31
+ const [mounted, setMounted] = useState(false);
24
32
  const containerRef = useRef<HTMLDivElement>(null);
33
+ const triggerRef = useRef<HTMLDivElement>(null);
34
+ const popoverRef = useRef<HTMLDivElement>(null);
25
35
  const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
26
36
 
37
+ // Track if component is mounted (for portal)
38
+ useEffect(() => {
39
+ setMounted(true);
40
+ return () => setMounted(false);
41
+ }, []);
42
+
43
+ // Calculate position based on trigger element
44
+ const calculatePosition = useCallback(() => {
45
+ if (!triggerRef.current) return;
46
+
47
+ const triggerRect = triggerRef.current.getBoundingClientRect();
48
+ const popoverWidth = 224; // w-56 = 14rem = 224px
49
+ const gap = 8;
50
+
51
+ let top = 0;
52
+ let left = 0;
53
+
54
+ switch (position) {
55
+ case "top":
56
+ top = triggerRect.top - gap;
57
+ left = triggerRect.left + triggerRect.width / 2 - popoverWidth / 2;
58
+ break;
59
+ case "bottom":
60
+ top = triggerRect.bottom + gap;
61
+ left = triggerRect.left + triggerRect.width / 2 - popoverWidth / 2;
62
+ break;
63
+ case "left":
64
+ top = triggerRect.top + triggerRect.height / 2;
65
+ left = triggerRect.left - popoverWidth - gap;
66
+ break;
67
+ case "right":
68
+ top = triggerRect.top + triggerRect.height / 2;
69
+ left = triggerRect.right + gap;
70
+ break;
71
+ }
72
+
73
+ setPortalPosition({ top, left });
74
+ }, [position]);
75
+
76
+ // Update position when opened
77
+ useEffect(() => {
78
+ if (isOpen) {
79
+ calculatePosition();
80
+ }
81
+ }, [isOpen, calculatePosition]);
82
+
27
83
  // Close on click outside
28
84
  useEffect(() => {
29
- if (triggerOn !== "click") return;
85
+ if (!isOpen || triggerOn !== "click") return;
30
86
 
31
87
  const handleClickOutside = (event: MouseEvent) => {
32
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
88
+ const target = event.target as Node;
89
+ const clickedInTrigger = containerRef.current?.contains(target);
90
+ const clickedInPopover = popoverRef.current?.contains(target);
91
+
92
+ if (!clickedInTrigger && !clickedInPopover) {
33
93
  setIsOpen(false);
34
94
  }
35
95
  };
36
96
 
37
97
  document.addEventListener("mousedown", handleClickOutside);
38
98
  return () => document.removeEventListener("mousedown", handleClickOutside);
39
- }, [triggerOn]);
99
+ }, [isOpen, triggerOn]);
40
100
 
41
101
  // Close on escape
42
102
  useEffect(() => {
@@ -50,6 +110,18 @@ export function Popover({
50
110
  return () => document.removeEventListener("keydown", handleEscape);
51
111
  }, []);
52
112
 
113
+ // Close on scroll
114
+ useEffect(() => {
115
+ if (!isOpen) return;
116
+
117
+ const handleScroll = () => {
118
+ setIsOpen(false);
119
+ };
120
+
121
+ window.addEventListener("scroll", handleScroll, true);
122
+ return () => window.removeEventListener("scroll", handleScroll, true);
123
+ }, [isOpen]);
124
+
53
125
  const handleMouseEnter = () => {
54
126
  if (triggerOn !== "hover") return;
55
127
  if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current);
@@ -63,37 +135,48 @@ export function Popover({
63
135
  }, 150);
64
136
  };
65
137
 
66
- const positionClasses: Record<PopoverPosition, string> = {
67
- top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
68
- bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
69
- left: "right-full top-1/2 -translate-y-1/2 mr-2",
70
- right: "left-full top-1/2 -translate-y-1/2 ml-2",
138
+ // Transform origin based on position
139
+ const transformOrigin: Record<PopoverPosition, string> = {
140
+ top: "bottom center",
141
+ bottom: "top center",
142
+ left: "right center",
143
+ right: "left center",
71
144
  };
72
145
 
73
146
  return (
74
- <div
147
+ <div data-sonance-name="popover"
75
148
  ref={containerRef}
76
149
  className="relative inline-block"
77
150
  onMouseEnter={handleMouseEnter}
78
151
  onMouseLeave={handleMouseLeave}
79
152
  >
80
153
  <div
154
+ ref={triggerRef}
81
155
  onClick={triggerOn === "click" ? () => setIsOpen(!isOpen) : undefined}
82
156
  className="cursor-pointer"
83
157
  >
84
158
  {trigger}
85
159
  </div>
86
- {isOpen && (
160
+ {mounted && isOpen && createPortal(
87
161
  <div
162
+ ref={popoverRef}
88
163
  className={cn(
89
- "absolute z-50 min-w-[200px] bg-card border border-border shadow-lg",
164
+ "fixed z-50 min-w-[200px] bg-card border border-border shadow-lg",
90
165
  "animate-in fade-in zoom-in-95 duration-150",
91
- positionClasses[position],
166
+ position === "left" || position === "right" ? "-translate-y-1/2" : "",
92
167
  className
93
168
  )}
169
+ style={{
170
+ top: portalPosition.top,
171
+ left: portalPosition.left,
172
+ transformOrigin: transformOrigin[position],
173
+ }}
174
+ onMouseEnter={handleMouseEnter}
175
+ onMouseLeave={handleMouseLeave}
94
176
  >
95
177
  {children}
96
- </div>
178
+ </div>,
179
+ document.body
97
180
  )}
98
181
  </div>
99
182
  );
@@ -106,6 +189,6 @@ export function PopoverContent({
106
189
  className?: string;
107
190
  children: React.ReactNode;
108
191
  }) {
109
- return <div className={cn("p-4", className)}>{children}</div>;
192
+ return <div data-sonance-name="popover" className={cn("p-4", className)}>{children}</div>;
110
193
  }
111
194
 
@@ -96,11 +96,11 @@ export const LoadingExample: Story = {
96
96
  render: () => (
97
97
  <div className="space-y-6 w-96">
98
98
  <div>
99
- <h4 className="text-sm font-medium text-foreground mb-2">File Upload</h4>
99
+ <h4 id="loading-example-h4-file-upload" className="text-sm font-medium text-foreground mb-2">File Upload</h4>
100
100
  <Progress value={67} label="document.pdf" showValue />
101
101
  </div>
102
102
  <div>
103
- <h4 className="text-sm font-medium text-foreground mb-2">Installation Progress</h4>
103
+ <h4 id="loading-example-h4-installation-progres" className="text-sm font-medium text-foreground mb-2">Installation Progress</h4>
104
104
  <Progress value={45} label="Installing dependencies..." showValue />
105
105
  </div>
106
106
  </div>
@@ -113,7 +113,7 @@ export const ResponsiveMatrix: Story = {
113
113
  <div className="space-y-8">
114
114
  {/* Mobile */}
115
115
  <div>
116
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
116
+ <h4 id="responsive-matrix-h4-mobile-375px" className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
117
117
  <div className="w-[375px] border border-dashed border-border p-4 space-y-6">
118
118
  <Progress value={65} label="Upload Progress" showValue />
119
119
  <div className="flex justify-center">
@@ -123,7 +123,7 @@ export const ResponsiveMatrix: Story = {
123
123
  </div>
124
124
  {/* Tablet */}
125
125
  <div>
126
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
126
+ <h4 id="responsive-matrix-h4-tablet-768px" className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
127
127
  <div className="w-[768px] border border-dashed border-border p-4">
128
128
  <div className="grid grid-cols-2 gap-8 items-center">
129
129
  <Progress value={45} label="Installation" showValue />
@@ -136,7 +136,7 @@ export const ResponsiveMatrix: Story = {
136
136
  </div>
137
137
  {/* Desktop */}
138
138
  <div>
139
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
139
+ <h4 id="responsive-matrix-h4-desktop-1280px" className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
140
140
  <div className="w-[1280px] border border-dashed border-border p-4">
141
141
  <div className="grid grid-cols-4 gap-8 items-center">
142
142
  <Progress value={25} size="sm" showValue />
@@ -23,16 +23,16 @@ export const Progress = forwardRef<HTMLDivElement, ProgressProps>(
23
23
  };
24
24
 
25
25
  return (
26
- <div ref={ref} className={cn("w-full", className)} {...props}>
26
+ <div data-sonance-name="progress" ref={ref} className={cn("w-full", className)} {...props}>
27
27
  {(label || showValue) && (
28
28
  <div className="mb-2 flex items-center justify-between">
29
29
  {label && (
30
- <span className="text-xs font-medium uppercase tracking-widest text-foreground-muted">
30
+ <span id="progress-span-label" className="text-xs font-medium uppercase tracking-widest text-foreground-muted">
31
31
  {label}
32
32
  </span>
33
33
  )}
34
34
  {showValue && (
35
- <span className="text-sm font-medium text-foreground">{Math.round(percentage)}%</span>
35
+ <span id="progress-span-mathroundpercentage" className="text-sm font-medium text-foreground">{Math.round(percentage)}%</span>
36
36
  )}
37
37
  </div>
38
38
  )}
@@ -89,7 +89,7 @@ export function CircularProgress({
89
89
  const strokeDashoffset = circumference - (percentage / 100) * circumference;
90
90
 
91
91
  return (
92
- <div className={cn("relative inline-flex items-center justify-center", className)}>
92
+ <div data-sonance-name="progress" className={cn("relative inline-flex items-center justify-center", className)}>
93
93
  <svg width={resolvedSize} height={resolvedSize} className="-rotate-90">
94
94
  {/* Background circle */}
95
95
  <circle
@@ -116,7 +116,7 @@ export function CircularProgress({
116
116
  />
117
117
  </svg>
118
118
  {showValue && (
119
- <span className="absolute text-xs font-medium text-foreground">
119
+ <span id="circular-progress-span-mathroundpercentage" className="absolute text-xs font-medium text-foreground">
120
120
  {Math.round(percentage)}%
121
121
  </span>
122
122
  )}
@@ -91,7 +91,7 @@ export const WithDisabledOption: Story = {
91
91
  export const PlanSelection: Story = {
92
92
  render: () => (
93
93
  <div className="w-80">
94
- <h3 className="text-sm font-medium text-foreground mb-4">Select a Plan</h3>
94
+ <h3 id="plan-selection-h3-select-a-plan" className="text-sm font-medium text-foreground mb-4">Select a Plan</h3>
95
95
  <RadioGroup defaultValue="pro">
96
96
  <RadioGroupItem
97
97
  value="free"
@@ -118,12 +118,12 @@ export const StateMatrix: Story = {
118
118
  render: () => {
119
119
  const states: RadioGroupItemState[] = ['default', 'hover', 'focus', 'checked', 'disabled'];
120
120
  return (
121
- <div className="space-y-6">
122
- <h3 className="text-sm font-medium text-foreground-muted">RadioGroupItem States</h3>
121
+ <div data-sonance-name="radio-group.stories" className="space-y-6">
122
+ <h3 id="state-matrix-h3-radiogroupitem-state" className="text-sm font-medium text-foreground-muted">RadioGroupItem States</h3>
123
123
  <div className="space-y-4">
124
124
  {states.map((state) => (
125
125
  <div key={state} className="flex items-center gap-4">
126
- <span className="text-xs font-medium text-foreground-muted uppercase w-20">{state}</span>
126
+ <span id="state-matrix-span-state" className="text-xs font-medium text-foreground-muted uppercase w-20">{state}</span>
127
127
  <RadioGroup>
128
128
  <RadioGroupItem
129
129
  value="demo"
@@ -145,7 +145,7 @@ export const ResponsiveMatrix: Story = {
145
145
  <div className="space-y-8">
146
146
  {/* Mobile */}
147
147
  <div>
148
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
148
+ <h4 id="responsive-matrix-h4-mobile-375px" className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
149
149
  <div className="w-[375px] border border-dashed border-border p-4">
150
150
  <RadioGroup defaultValue="standard">
151
151
  <RadioGroupItem value="standard" label="Standard" description="5-7 business days" />
@@ -156,7 +156,7 @@ export const ResponsiveMatrix: Story = {
156
156
  </div>
157
157
  {/* Tablet */}
158
158
  <div>
159
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
159
+ <h4 id="responsive-matrix-h4-tablet-768px" className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
160
160
  <div className="w-[768px] border border-dashed border-border p-4">
161
161
  <div className="grid grid-cols-2 gap-8">
162
162
  <RadioGroup defaultValue="small">
@@ -173,7 +173,7 @@ export const ResponsiveMatrix: Story = {
173
173
  </div>
174
174
  {/* Desktop */}
175
175
  <div>
176
- <h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
176
+ <h4 id="responsive-matrix-h4-desktop-1280px" className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
177
177
  <div className="w-[1280px] border border-dashed border-border p-4">
178
178
  <RadioGroup defaultValue="pro" orientation="horizontal" className="flex gap-8">
179
179
  <RadioGroupItem value="free" label="Free" description="Basic features" />
@@ -93,7 +93,7 @@ export const RadioGroupItem = forwardRef<HTMLInputElement, RadioGroupItemProps>(
93
93
  const isDisabled = disabled || state === "disabled";
94
94
 
95
95
  return (
96
- <div className="flex items-start gap-3">
96
+ <div data-sonance-name="radio-group" className="flex items-start gap-3">
97
97
  <div className="relative flex items-center justify-center">
98
98
  <input
99
99
  type="radio"
@@ -115,7 +115,7 @@ export const RadioGroupItem = forwardRef<HTMLInputElement, RadioGroupItemProps>(
115
115
  )}
116
116
  {...props}
117
117
  />
118
- <div
118
+ <div data-sonance-name="radio-group"
119
119
  className={cn(
120
120
  "pointer-events-none absolute h-2.5 w-2.5 rounded-full bg-primary",
121
121
  "scale-0 transition-transform duration-150",
@@ -134,7 +134,7 @@ export const RadioGroupItem = forwardRef<HTMLInputElement, RadioGroupItemProps>(
134
134
  </label>
135
135
  )}
136
136
  {description && (
137
- <p className="text-xs text-foreground-muted">{description}</p>
137
+ <p id="p-description" className="text-xs text-foreground-muted">{description}</p>
138
138
  )}
139
139
  </div>
140
140
  )}