sonance-brand-mcp 1.2.4 → 1.3.1

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 (90) hide show
  1. package/dist/assets/components/alert-dialog.stories.tsx +142 -0
  2. package/dist/assets/components/alert-dialog.tsx +142 -0
  3. package/dist/assets/components/aspect-ratio.stories.tsx +67 -0
  4. package/dist/assets/components/aspect-ratio.tsx +8 -0
  5. package/dist/assets/components/avatar.tsx +64 -20
  6. package/dist/assets/components/carousel.stories.tsx +158 -0
  7. package/dist/assets/components/carousel.tsx +262 -0
  8. package/dist/assets/components/chart.stories.tsx +376 -0
  9. package/dist/assets/components/chart.tsx +384 -0
  10. package/dist/assets/components/checkbox.tsx +12 -2
  11. package/dist/assets/components/code.tsx +22 -20
  12. package/dist/assets/components/collapsible.stories.tsx +128 -0
  13. package/dist/assets/components/collapsible.tsx +10 -0
  14. package/dist/assets/components/command.stories.tsx +183 -0
  15. package/dist/assets/components/command.tsx +170 -0
  16. package/dist/assets/components/context-menu.stories.tsx +159 -0
  17. package/dist/assets/components/context-menu.tsx +218 -0
  18. package/dist/assets/components/divider.tsx +38 -35
  19. package/dist/assets/components/dropdown-menu.tsx +217 -0
  20. package/dist/assets/components/hover-card.stories.tsx +113 -0
  21. package/dist/assets/components/hover-card.tsx +35 -0
  22. package/dist/assets/components/kbd.tsx +6 -6
  23. package/dist/assets/components/menubar.stories.tsx +208 -0
  24. package/dist/assets/components/menubar.tsx +251 -0
  25. package/dist/assets/components/navigation-menu.stories.tsx +237 -0
  26. package/dist/assets/components/navigation-menu.tsx +135 -0
  27. package/dist/assets/components/resizable.stories.tsx +197 -0
  28. package/dist/assets/components/resizable.tsx +47 -0
  29. package/dist/assets/components/scroll-area.stories.tsx +123 -0
  30. package/dist/assets/components/scroll-area.tsx +48 -0
  31. package/dist/assets/components/scroll-shadow.tsx +29 -7
  32. package/dist/assets/components/separator.tsx +32 -0
  33. package/dist/assets/components/sheet.tsx +141 -0
  34. package/dist/assets/components/sidebar.stories.tsx +351 -0
  35. package/dist/assets/components/sidebar.tsx +760 -0
  36. package/dist/assets/components/toggle-group.stories.tsx +153 -0
  37. package/dist/assets/components/toggle-group.tsx +61 -0
  38. package/dist/assets/components/toggle.stories.tsx +77 -0
  39. package/dist/assets/components/toggle.tsx +46 -0
  40. package/dist/assets/components/tooltip.tsx +23 -90
  41. package/dist/assets/globals.css +30 -0
  42. package/dist/assets/logos/40th-anniversary/Sonance_40_Logo_CMYK_BEAM_BLUE_40_AND_BEAM_DARK.png +0 -0
  43. package/dist/assets/logos/Sonance logo dark mode.png +0 -0
  44. package/dist/assets/logos/Sonance logo light mode.png +0 -0
  45. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_2C_Light_RGB_05162025.png +0 -0
  46. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_3C_Dark_RGB_05162025.png +0 -0
  47. package/dist/assets/logos/blaze/BlazeBySonance_Logo_Lockup_White_RGB_05162025.png +0 -0
  48. package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Dark_RGB.png +0 -0
  49. package/dist/assets/logos/iport/IPORT_Sonance_LockUp_2C_Light_RGB.png +0 -0
  50. package/dist/assets/logos/james/James_Logo_Black_CMYK.png +0 -0
  51. package/dist/assets/logos/james/James_Logo_Black_RGB.png +0 -0
  52. package/dist/assets/logos/james/James_Logo_LtGray_CMYK.png +0 -0
  53. package/dist/assets/logos/james/James_Logo_LtGray_RGB.png +0 -0
  54. package/dist/assets/logos/james/James_Logo_Polished_RGB.png +0 -0
  55. package/dist/assets/logos/james/James_Logo_Reverse_CMYK.png +0 -0
  56. package/dist/assets/logos/james/James_Logo_Reverse_RGB.png +0 -0
  57. package/dist/assets/logos/james/James_Logo_White_CMYK.png +0 -0
  58. package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Dark_RGB.png +0 -0
  59. package/dist/assets/logos/life-is-better/Sonance_LifeisBetter_Light_RGB.png +0 -0
  60. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Dark_RGB.png +0 -0
  61. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Light_RGB.png +0 -0
  62. package/dist/assets/logos/my-sonance/My.Sonance_Logo_2C_Reverse_RGB.png +0 -0
  63. package/dist/assets/logos/my-sonance/My.Sonance_Logo_Black_RGB.png +0 -0
  64. package/dist/assets/logos/my-sonance/My.Sonance_Logo_Reverse_RGB.png +0 -0
  65. package/dist/assets/logos/sonance/Sonance_Logo_2C_Dark_RGB.png +0 -0
  66. package/dist/assets/logos/sonance/Sonance_Logo_2C_Light_RGB.png +0 -0
  67. package/dist/assets/logos/sonance/Sonance_Logo_2C_Reverse_RGB.png +0 -0
  68. package/dist/assets/logos/sonance/Sonance_Logo_Black_RGB.png +0 -0
  69. package/dist/assets/logos/sonance/Sonance_Logo_Grayscale_RGB.png +0 -0
  70. package/dist/assets/logos/sonance/Sonance_Logo_Reverse_RGB.png +0 -0
  71. package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Dark_CMYK.png +0 -0
  72. package/dist/assets/logos/sonance-academy/SonanceAcademy_Logo_Light_CMYK.png +0 -0
  73. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Dark_RGB.png +0 -0
  74. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Light_RGB.png +0 -0
  75. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_3C_Reverse_RGB.png +0 -0
  76. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Black_RGB.png +0 -0
  77. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Grayscale_RGB.png +0 -0
  78. package/dist/assets/logos/sonance-iport/Sonance_IPORT_LockUp_Reverse_RGB.png +0 -0
  79. package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Dark.png +0 -0
  80. package/dist/assets/logos/sonance-james/Sonance_James_Lockup_Light.png +0 -0
  81. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Dark.png +0 -0
  82. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_LockupStacked_Light.png +0 -0
  83. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Dark.png +0 -0
  84. package/dist/assets/logos/sonance-james-iport/Sonance_James_IPORT_Lockup_Light.png +0 -0
  85. package/dist/assets/logos/trufig/TrufigLogo_Black.png +0 -0
  86. package/dist/assets/logos/trufig/TrufigLogo_Light.png +0 -0
  87. package/dist/assets/logos/trufig/TrufigWatermark_Black.png +0 -0
  88. package/dist/assets/logos/trufig/TrufigWatermark_Light.png +0 -0
  89. package/dist/index.js +416 -17
  90. package/package.json +1 -1
@@ -0,0 +1,384 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ // Format: { THEME_NAME: CSS_SELECTOR }
8
+ const THEMES = { light: "", dark: ".dark" } as const;
9
+
10
+ export type ChartConfig = {
11
+ [k in string]: {
12
+ label?: React.ReactNode;
13
+ icon?: React.ComponentType;
14
+ } & (
15
+ | { color?: string; theme?: never }
16
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
17
+ );
18
+ };
19
+
20
+ type ChartContextProps = {
21
+ config: ChartConfig;
22
+ };
23
+
24
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
25
+
26
+ function useChart() {
27
+ const context = React.useContext(ChartContext);
28
+
29
+ if (!context) {
30
+ throw new Error("useChart must be used within a <ChartContainer />");
31
+ }
32
+
33
+ return context;
34
+ }
35
+
36
+ const ChartContainer = React.forwardRef<
37
+ HTMLDivElement,
38
+ React.ComponentProps<"div"> & {
39
+ config: ChartConfig;
40
+ children: React.ComponentProps<
41
+ typeof RechartsPrimitive.ResponsiveContainer
42
+ >["children"];
43
+ }
44
+ >(({ id, className, children, config, ...props }, ref) => {
45
+ const uniqueId = React.useId();
46
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
47
+
48
+ return (
49
+ <ChartContext.Provider value={{ config }}>
50
+ <div
51
+ data-chart={chartId}
52
+ ref={ref}
53
+ className={cn(
54
+ "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-foreground-muted [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-secondary-hover [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-secondary-hover [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
55
+ className
56
+ )}
57
+ {...props}
58
+ >
59
+ <ChartStyle id={chartId} config={config} />
60
+ <RechartsPrimitive.ResponsiveContainer>
61
+ {children}
62
+ </RechartsPrimitive.ResponsiveContainer>
63
+ </div>
64
+ </ChartContext.Provider>
65
+ );
66
+ });
67
+ ChartContainer.displayName = "Chart";
68
+
69
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
70
+ const colorConfig = Object.entries(config).filter(
71
+ ([, config]) => config.theme || config.color
72
+ );
73
+
74
+ if (!colorConfig.length) {
75
+ return null;
76
+ }
77
+
78
+ return (
79
+ <style
80
+ dangerouslySetInnerHTML={{
81
+ __html: Object.entries(THEMES)
82
+ .map(
83
+ ([theme, prefix]) => `
84
+ ${prefix} [data-chart=${id}] {
85
+ ${colorConfig
86
+ .map(([key, itemConfig]) => {
87
+ const color =
88
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
89
+ itemConfig.color;
90
+ return color ? ` --color-${key}: ${color};` : null;
91
+ })
92
+ .join("\n")}
93
+ }
94
+ `
95
+ )
96
+ .join("\n"),
97
+ }}
98
+ />
99
+ );
100
+ };
101
+
102
+ const ChartTooltip = RechartsPrimitive.Tooltip;
103
+
104
+ interface ChartTooltipContentProps extends React.HTMLAttributes<HTMLDivElement> {
105
+ active?: boolean;
106
+ payload?: Array<{
107
+ name?: string;
108
+ value?: number | string;
109
+ dataKey?: string | number;
110
+ color?: string;
111
+ payload?: Record<string, unknown>;
112
+ fill?: string;
113
+ }>;
114
+ label?: string;
115
+ hideLabel?: boolean;
116
+ hideIndicator?: boolean;
117
+ indicator?: "line" | "dot" | "dashed";
118
+ nameKey?: string;
119
+ labelKey?: string;
120
+ labelFormatter?: (value: unknown, payload: unknown[]) => React.ReactNode;
121
+ labelClassName?: string;
122
+ formatter?: (
123
+ value: unknown,
124
+ name: unknown,
125
+ item: unknown,
126
+ index: number,
127
+ payload: unknown
128
+ ) => React.ReactNode;
129
+ color?: string;
130
+ }
131
+
132
+ const ChartTooltipContent = React.forwardRef<HTMLDivElement, ChartTooltipContentProps>(
133
+ (
134
+ {
135
+ active,
136
+ payload,
137
+ className,
138
+ indicator = "dot",
139
+ hideLabel = false,
140
+ hideIndicator = false,
141
+ label,
142
+ labelFormatter,
143
+ labelClassName,
144
+ formatter,
145
+ color,
146
+ nameKey,
147
+ labelKey,
148
+ },
149
+ ref
150
+ ) => {
151
+ const { config } = useChart();
152
+
153
+ const tooltipLabel = React.useMemo(() => {
154
+ if (hideLabel || !payload?.length) {
155
+ return null;
156
+ }
157
+
158
+ const [item] = payload;
159
+ const key = `${labelKey || item.dataKey || item.name || "value"}`;
160
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
161
+ const value =
162
+ !labelKey && typeof label === "string"
163
+ ? config[label as keyof typeof config]?.label || label
164
+ : itemConfig?.label;
165
+
166
+ if (labelFormatter) {
167
+ return (
168
+ <div className={cn("font-medium", labelClassName)}>
169
+ {labelFormatter(value, payload)}
170
+ </div>
171
+ );
172
+ }
173
+
174
+ if (!value) {
175
+ return null;
176
+ }
177
+
178
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
179
+ }, [
180
+ label,
181
+ labelFormatter,
182
+ payload,
183
+ hideLabel,
184
+ labelClassName,
185
+ config,
186
+ labelKey,
187
+ ]);
188
+
189
+ if (!active || !payload?.length) {
190
+ return null;
191
+ }
192
+
193
+ const nestLabel = payload.length === 1 && indicator !== "dot";
194
+
195
+ return (
196
+ <div
197
+ ref={ref}
198
+ className={cn(
199
+ "grid min-w-[8rem] items-start gap-1.5 rounded-sm border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
200
+ className
201
+ )}
202
+ >
203
+ {!nestLabel ? tooltipLabel : null}
204
+ <div className="grid gap-1.5">
205
+ {payload.map((item, index) => {
206
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
207
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
208
+ const indicatorColor = color || (item.payload as Record<string, unknown>)?.fill as string || item.color;
209
+
210
+ return (
211
+ <div
212
+ key={String(item.dataKey)}
213
+ className={cn(
214
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-foreground-muted",
215
+ indicator === "dot" && "items-center"
216
+ )}
217
+ >
218
+ {formatter && item?.value !== undefined && item.name ? (
219
+ formatter(item.value, item.name, item, index, item.payload)
220
+ ) : (
221
+ <>
222
+ {itemConfig?.icon ? (
223
+ <itemConfig.icon />
224
+ ) : (
225
+ !hideIndicator && (
226
+ <div
227
+ className={cn(
228
+ "shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
229
+ {
230
+ "h-2.5 w-2.5": indicator === "dot",
231
+ "w-1": indicator === "line",
232
+ "w-0 border-[1.5px] border-dashed bg-transparent":
233
+ indicator === "dashed",
234
+ "my-0.5": nestLabel && indicator === "dashed",
235
+ }
236
+ )}
237
+ style={
238
+ {
239
+ "--color-bg": indicatorColor,
240
+ "--color-border": indicatorColor,
241
+ } as React.CSSProperties
242
+ }
243
+ />
244
+ )
245
+ )}
246
+ <div
247
+ className={cn(
248
+ "flex flex-1 justify-between leading-none",
249
+ nestLabel ? "items-end" : "items-center"
250
+ )}
251
+ >
252
+ <div className="grid gap-1.5">
253
+ {nestLabel ? tooltipLabel : null}
254
+ <span className="text-foreground-muted">
255
+ {itemConfig?.label || item.name}
256
+ </span>
257
+ </div>
258
+ {item.value && (
259
+ <span className="font-mono font-medium tabular-nums text-foreground">
260
+ {typeof item.value === 'number' ? item.value.toLocaleString() : item.value}
261
+ </span>
262
+ )}
263
+ </div>
264
+ </>
265
+ )}
266
+ </div>
267
+ );
268
+ })}
269
+ </div>
270
+ </div>
271
+ );
272
+ }
273
+ );
274
+ ChartTooltipContent.displayName = "ChartTooltip";
275
+
276
+ const ChartLegend = RechartsPrimitive.Legend;
277
+
278
+ interface ChartLegendContentProps extends React.HTMLAttributes<HTMLDivElement> {
279
+ payload?: Array<{
280
+ value?: string;
281
+ dataKey?: string | number;
282
+ color?: string;
283
+ }>;
284
+ verticalAlign?: "top" | "bottom";
285
+ hideIcon?: boolean;
286
+ nameKey?: string;
287
+ }
288
+
289
+ const ChartLegendContent = React.forwardRef<HTMLDivElement, ChartLegendContentProps>(
290
+ (
291
+ { className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
292
+ ref
293
+ ) => {
294
+ const { config } = useChart();
295
+
296
+ if (!payload?.length) {
297
+ return null;
298
+ }
299
+
300
+ return (
301
+ <div
302
+ ref={ref}
303
+ className={cn(
304
+ "flex items-center justify-center gap-4",
305
+ verticalAlign === "top" ? "pb-3" : "pt-3",
306
+ className
307
+ )}
308
+ >
309
+ {payload.map((item) => {
310
+ const key = `${nameKey || item.dataKey || "value"}`;
311
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
312
+
313
+ return (
314
+ <div
315
+ key={item.value}
316
+ className={cn(
317
+ "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-foreground-muted"
318
+ )}
319
+ >
320
+ {itemConfig?.icon && !hideIcon ? (
321
+ <itemConfig.icon />
322
+ ) : (
323
+ <div
324
+ className="h-2 w-2 shrink-0 rounded-[2px]"
325
+ style={{
326
+ backgroundColor: item.color,
327
+ }}
328
+ />
329
+ )}
330
+ {itemConfig?.label}
331
+ </div>
332
+ );
333
+ })}
334
+ </div>
335
+ );
336
+ }
337
+ );
338
+ ChartLegendContent.displayName = "ChartLegend";
339
+
340
+ // Helper to extract item config from a payload.
341
+ function getPayloadConfigFromPayload(
342
+ config: ChartConfig,
343
+ payload: unknown,
344
+ key: string
345
+ ) {
346
+ if (typeof payload !== "object" || payload === null) {
347
+ return undefined;
348
+ }
349
+
350
+ const payloadPayload =
351
+ "payload" in payload &&
352
+ typeof payload.payload === "object" &&
353
+ payload.payload !== null
354
+ ? payload.payload
355
+ : undefined;
356
+
357
+ let configLabelKey: string = key;
358
+
359
+ if (
360
+ key in payload &&
361
+ typeof (payload as Record<string, unknown>)[key] === "string"
362
+ ) {
363
+ configLabelKey = (payload as Record<string, unknown>)[key] as string;
364
+ } else if (
365
+ payloadPayload &&
366
+ key in payloadPayload &&
367
+ typeof (payloadPayload as Record<string, unknown>)[key] === "string"
368
+ ) {
369
+ configLabelKey = (payloadPayload as Record<string, unknown>)[key] as string;
370
+ }
371
+
372
+ return configLabelKey in config
373
+ ? config[configLabelKey]
374
+ : config[key as keyof typeof config];
375
+ }
376
+
377
+ export {
378
+ ChartContainer,
379
+ ChartTooltip,
380
+ ChartTooltipContent,
381
+ ChartLegend,
382
+ ChartLegendContent,
383
+ ChartStyle,
384
+ };
@@ -32,17 +32,27 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
32
32
  const uniqueId = useId();
33
33
  const inputId = id || `checkbox-${uniqueId}`;
34
34
  const isDisabled = disabled || state === "disabled";
35
- const isChecked = checked || state === "checked";
35
+
36
+ // Resolve checked state:
37
+ // 1. If visual state is "checked", force true.
38
+ // 2. Else use provided checked prop (undefined = uncontrolled).
39
+ const isVisualChecked = state === "checked";
40
+ const resolvedChecked = isVisualChecked ? true : checked;
41
+
42
+ // If visual state forces checked but no handler, mark readOnly to avoid React warnings
43
+ const isReadOnly = isVisualChecked && !props.onChange && !props.readOnly;
36
44
 
37
45
  return (
38
46
  <div className="flex items-start gap-3">
39
47
  <div className="relative flex items-center justify-center">
40
48
  <input
49
+ key={isVisualChecked ? "visual-checked" : "normal"}
41
50
  type="checkbox"
42
51
  id={inputId}
43
52
  ref={ref}
44
53
  disabled={isDisabled}
45
- checked={isChecked}
54
+ checked={resolvedChecked}
55
+ readOnly={isReadOnly || props.readOnly}
46
56
  className={cn(
47
57
  "peer h-5 w-5 shrink-0 appearance-none border border-border bg-input",
48
58
  "hover:border-border-hover",
@@ -20,7 +20,8 @@ export const Code = forwardRef<HTMLElement, CodeProps>(
20
20
  <code
21
21
  ref={ref}
22
22
  className={cn(
23
- "px-1.5 py-0.5 font-mono text-sm rounded-sm",
23
+ "px-1 py-0.5 font-mono text-xs sm:text-sm rounded-sm",
24
+ "break-words",
24
25
  variantClasses[variant],
25
26
  className
26
27
  )}
@@ -61,12 +62,12 @@ export function CodeBlock({
61
62
  const lines = code.split("\n");
62
63
 
63
64
  return (
64
- <div className={cn("relative group", className)}>
65
+ <div className={cn("relative group w-full max-w-full overflow-hidden", className)}>
65
66
  {filename && (
66
- <div className="flex items-center justify-between px-4 py-2 border-b border-border bg-background-tertiary">
67
- <span className="text-xs font-medium text-foreground-muted">{filename}</span>
67
+ <div className="flex items-center justify-between px-3 sm:px-4 py-2 border-b border-border bg-background-tertiary">
68
+ <span className="text-xs font-medium text-foreground-muted truncate">{filename}</span>
68
69
  {language && (
69
- <span className="text-xs uppercase tracking-wider text-foreground-subtle">
70
+ <span className="text-xs uppercase tracking-wider text-foreground-subtle flex-shrink-0 ml-2">
70
71
  {language}
71
72
  </span>
72
73
  )}
@@ -75,22 +76,22 @@ export function CodeBlock({
75
76
  <div className="relative">
76
77
  <pre
77
78
  className={cn(
78
- "overflow-x-auto bg-background-secondary p-4",
79
- "font-mono text-sm text-foreground-secondary",
79
+ "overflow-x-auto bg-background-secondary p-3 sm:p-4 w-full max-w-full",
80
+ "font-mono text-xs sm:text-sm text-foreground-secondary",
80
81
  filename && "rounded-t-none"
81
82
  )}
82
83
  >
83
- <code>
84
+ <code className="block">
84
85
  {showLineNumbers ? (
85
86
  lines.map((line, index) => (
86
87
  <div
87
88
  key={index}
88
89
  className={cn(
89
90
  "flex",
90
- highlightLines.includes(index + 1) && "bg-primary/10 -mx-4 px-4"
91
+ highlightLines.includes(index + 1) && "bg-primary/10 -mx-3 sm:-mx-4 px-3 sm:px-4"
91
92
  )}
92
93
  >
93
- <span className="mr-4 inline-block w-8 select-none text-right text-foreground-subtle">
94
+ <span className="mr-3 sm:mr-4 inline-block w-6 sm:w-8 select-none text-right text-foreground-subtle">
94
95
  {index + 1}
95
96
  </span>
96
97
  <span>{line}</span>
@@ -104,7 +105,7 @@ export function CodeBlock({
104
105
  <button
105
106
  onClick={copyToClipboard}
106
107
  className={cn(
107
- "absolute right-2 top-2 p-2",
108
+ "absolute right-1.5 sm:right-2 top-1.5 sm:top-2 p-1.5 sm:p-2",
108
109
  "bg-background border border-border rounded-sm",
109
110
  "text-foreground-muted hover:text-foreground",
110
111
  "opacity-0 group-hover:opacity-100 transition-opacity",
@@ -113,9 +114,9 @@ export function CodeBlock({
113
114
  aria-label={copied ? "Copied!" : "Copy code"}
114
115
  >
115
116
  {copied ? (
116
- <Check className="h-4 w-4 text-success" />
117
+ <Check className="h-3.5 w-3.5 sm:h-4 sm:w-4 text-success" />
117
118
  ) : (
118
- <Copy className="h-4 w-4" />
119
+ <Copy className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
119
120
  )}
120
121
  </button>
121
122
  </div>
@@ -148,24 +149,25 @@ export function Snippet({
148
149
  return (
149
150
  <div
150
151
  className={cn(
151
- "inline-flex items-center gap-2 px-3 py-2",
152
+ "inline-flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2",
152
153
  "bg-background-secondary border border-border rounded-sm",
153
- "font-mono text-sm",
154
+ "font-mono text-xs sm:text-sm",
155
+ "max-w-full overflow-hidden",
154
156
  className
155
157
  )}
156
158
  >
157
- {symbol && <span className="text-foreground-muted">{symbol}</span>}
158
- <code className="text-foreground">{text}</code>
159
+ {symbol && <span className="text-foreground-muted flex-shrink-0">{symbol}</span>}
160
+ <code className="text-foreground truncate">{text}</code>
159
161
  {!hideCopyButton && (
160
162
  <button
161
163
  onClick={copyToClipboard}
162
- className="ml-2 p-1 text-foreground-muted hover:text-foreground transition-colors"
164
+ className="ml-1 sm:ml-2 p-1 text-foreground-muted hover:text-foreground transition-colors flex-shrink-0"
163
165
  aria-label={copied ? "Copied!" : "Copy"}
164
166
  >
165
167
  {copied ? (
166
- <Check className="h-3.5 w-3.5 text-success" />
168
+ <Check className="h-3 w-3 sm:h-3.5 sm:w-3.5 text-success" />
167
169
  ) : (
168
- <Copy className="h-3.5 w-3.5" />
170
+ <Copy className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
169
171
  )}
170
172
  </button>
171
173
  )}
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import type { Meta, StoryObj } from "@storybook/react";
4
+ import { useState } from "react";
5
+ import { ChevronDown } from "lucide-react";
6
+ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "./collapsible";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ const meta: Meta<typeof Collapsible> = {
10
+ title: "Components/Data Display/Collapsible",
11
+ component: Collapsible,
12
+ parameters: {
13
+ layout: "centered",
14
+ },
15
+ tags: ["autodocs"],
16
+ };
17
+
18
+ export default meta;
19
+ type Story = StoryObj<typeof Collapsible>;
20
+
21
+ function CollapsibleDemo() {
22
+ const [isOpen, setIsOpen] = useState(false);
23
+
24
+ return (
25
+ <Collapsible
26
+ open={isOpen}
27
+ onOpenChange={setIsOpen}
28
+ className="w-[350px] space-y-2"
29
+ >
30
+ <div className="flex items-center justify-between space-x-4 px-4">
31
+ <h4 className="text-sm font-medium text-foreground">
32
+ @peduarte starred 3 repositories
33
+ </h4>
34
+ <CollapsibleTrigger asChild>
35
+ <button className="inline-flex h-8 w-8 items-center justify-center rounded-sm border border-border bg-transparent hover:bg-secondary-hover transition-colors">
36
+ <ChevronDown
37
+ className={cn(
38
+ "h-4 w-4 transition-transform duration-200",
39
+ isOpen && "rotate-180"
40
+ )}
41
+ />
42
+ <span className="sr-only">Toggle</span>
43
+ </button>
44
+ </CollapsibleTrigger>
45
+ </div>
46
+ <div className="rounded-sm border border-border px-4 py-3 text-sm text-foreground">
47
+ @radix-ui/primitives
48
+ </div>
49
+ <CollapsibleContent className="space-y-2">
50
+ <div className="rounded-sm border border-border px-4 py-3 text-sm text-foreground">
51
+ @radix-ui/colors
52
+ </div>
53
+ <div className="rounded-sm border border-border px-4 py-3 text-sm text-foreground">
54
+ @stitches/react
55
+ </div>
56
+ </CollapsibleContent>
57
+ </Collapsible>
58
+ );
59
+ }
60
+
61
+ export const Default: Story = {
62
+ render: () => <CollapsibleDemo />,
63
+ };
64
+
65
+ function CollapsibleFAQDemo() {
66
+ const [openItems, setOpenItems] = useState<string[]>([]);
67
+
68
+ const toggleItem = (item: string) => {
69
+ setOpenItems((prev) =>
70
+ prev.includes(item) ? prev.filter((i) => i !== item) : [...prev, item]
71
+ );
72
+ };
73
+
74
+ const faqs = [
75
+ {
76
+ id: "1",
77
+ question: "What is Sonance?",
78
+ answer:
79
+ "Sonance is a premium audio brand specializing in architectural speakers and outdoor audio solutions.",
80
+ },
81
+ {
82
+ id: "2",
83
+ question: "How do I install outdoor speakers?",
84
+ answer:
85
+ "Outdoor speakers should be installed by a certified integrator to ensure proper weatherproofing and optimal audio performance.",
86
+ },
87
+ {
88
+ id: "3",
89
+ question: "Do you offer warranties?",
90
+ answer:
91
+ "Yes, all Sonance products come with comprehensive warranty coverage. Please refer to your product documentation for specific terms.",
92
+ },
93
+ ];
94
+
95
+ return (
96
+ <div className="w-[400px] space-y-2">
97
+ {faqs.map((faq) => (
98
+ <Collapsible
99
+ key={faq.id}
100
+ open={openItems.includes(faq.id)}
101
+ onOpenChange={() => toggleItem(faq.id)}
102
+ >
103
+ <CollapsibleTrigger asChild>
104
+ <button className="flex w-full items-center justify-between rounded-sm border border-border bg-card px-4 py-3 text-left text-sm font-medium text-foreground hover:bg-card-hover transition-colors">
105
+ {faq.question}
106
+ <ChevronDown
107
+ className={cn(
108
+ "h-4 w-4 shrink-0 transition-transform duration-200",
109
+ openItems.includes(faq.id) && "rotate-180"
110
+ )}
111
+ />
112
+ </button>
113
+ </CollapsibleTrigger>
114
+ <CollapsibleContent>
115
+ <div className="border border-t-0 border-border bg-background px-4 py-3 text-sm text-foreground-secondary">
116
+ {faq.answer}
117
+ </div>
118
+ </CollapsibleContent>
119
+ </Collapsible>
120
+ ))}
121
+ </div>
122
+ );
123
+ }
124
+
125
+ export const FAQ: Story = {
126
+ render: () => <CollapsibleFAQDemo />,
127
+ };
128
+
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
+
5
+ const Collapsible = CollapsiblePrimitive.Root;
6
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
7
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
8
+
9
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
10
+