@opensite/ui 0.9.9 → 1.0.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.
@@ -0,0 +1,74 @@
1
+ import * as React from 'react';
2
+ import { PressableProps } from './pressable.js';
3
+ import 'class-variance-authority';
4
+ import './button-variants-8mtEHxev.js';
5
+ import 'class-variance-authority/types';
6
+
7
+ /**
8
+ * Supported social platform names
9
+ */
10
+ type SocialPlatformName = "instagram" | "linkedin" | "google" | "facebook" | "tiktok" | "youtube" | "yelp" | "spotify" | "apple" | "x";
11
+ /**
12
+ * Props for DynamicIcon that can be passed through
13
+ */
14
+ interface SocialLinkIconDynamicIconProps {
15
+ /**
16
+ * Icon size in pixels
17
+ * @default 20
18
+ */
19
+ iconSize?: number;
20
+ /**
21
+ * Icon color - accepts any valid CSS color
22
+ * Note: When not specified, the icon inherits color from parent via CSS currentColor
23
+ */
24
+ iconColor?: string;
25
+ /**
26
+ * Additional CSS classes for the icon
27
+ */
28
+ iconClassName?: string;
29
+ }
30
+ /**
31
+ * Props for the SocialLinkIcon component
32
+ */
33
+ interface SocialLinkIconProps extends Omit<PressableProps, "children">, SocialLinkIconDynamicIconProps {
34
+ /**
35
+ * The social platform name - determines which icon to display
36
+ */
37
+ platformName: SocialPlatformName;
38
+ /**
39
+ * Optional label for accessibility (defaults to platform name)
40
+ */
41
+ label?: string;
42
+ }
43
+ /**
44
+ * SocialLinkIcon - A reusable social media link icon component.
45
+ *
46
+ * Combines Pressable navigation with DynamicIcon rendering for social media platforms.
47
+ * Supports all major social platforms with proper icon mapping.
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * // Basic usage
52
+ * <SocialLinkIcon platformName="instagram" href="https://instagram.com/company" />
53
+ *
54
+ * // With custom size and styling
55
+ * <SocialLinkIcon
56
+ * platformName="x"
57
+ * href="https://x.com/company"
58
+ * iconSize={24}
59
+ * className="hover:text-primary"
60
+ * />
61
+ *
62
+ * // As a button-styled link
63
+ * <SocialLinkIcon
64
+ * platformName="linkedin"
65
+ * href="https://linkedin.com/company/example"
66
+ * asButton
67
+ * variant="outline"
68
+ * size="icon"
69
+ * />
70
+ * ```
71
+ */
72
+ declare const SocialLinkIcon: React.ForwardRefExoticComponent<SocialLinkIconProps & React.RefAttributes<HTMLAnchorElement | HTMLButtonElement | HTMLSpanElement>>;
73
+
74
+ export { SocialLinkIcon, type SocialLinkIconDynamicIconProps, type SocialLinkIconProps, type SocialPlatformName };
@@ -0,0 +1,584 @@
1
+ "use client";
2
+ import * as React from 'react';
3
+ import { clsx } from 'clsx';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { cva } from 'class-variance-authority';
6
+ import { jsx } from 'react/jsx-runtime';
7
+
8
+ function cn(...inputs) {
9
+ return twMerge(clsx(inputs));
10
+ }
11
+ function normalizePhoneNumber(input) {
12
+ const trimmed = input.trim();
13
+ if (trimmed.toLowerCase().startsWith("tel:")) {
14
+ return trimmed;
15
+ }
16
+ const match = trimmed.match(/^[\s\+\-\(\)]*(\d[\d\s\-\(\)\.]*\d)[\s\-]*(x|ext\.?|extension)?[\s\-]*(\d+)?$/i);
17
+ if (match) {
18
+ const mainNumber = match[1].replace(/[\s\-\(\)\.]/g, "");
19
+ const extension = match[3];
20
+ const normalized = mainNumber.length >= 10 && !trimmed.startsWith("+") ? `+${mainNumber}` : mainNumber;
21
+ const withExtension = extension ? `${normalized};ext=${extension}` : normalized;
22
+ return `tel:${withExtension}`;
23
+ }
24
+ const cleaned = trimmed.replace(/[\s\-\(\)\.]/g, "");
25
+ return `tel:${cleaned}`;
26
+ }
27
+ function normalizeEmail(input) {
28
+ const trimmed = input.trim();
29
+ if (trimmed.toLowerCase().startsWith("mailto:")) {
30
+ return trimmed;
31
+ }
32
+ return `mailto:${trimmed}`;
33
+ }
34
+ function isEmail(input) {
35
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
36
+ return emailRegex.test(input.trim());
37
+ }
38
+ function isPhoneNumber(input) {
39
+ const trimmed = input.trim();
40
+ if (trimmed.toLowerCase().startsWith("tel:")) {
41
+ return true;
42
+ }
43
+ const phoneRegex = /^[\s\+\-\(\)]*\d[\d\s\-\(\)\.]*\d[\s\-]*(x|ext\.?|extension)?[\s\-]*\d*$/i;
44
+ return phoneRegex.test(trimmed);
45
+ }
46
+ function isInternalUrl(href) {
47
+ if (typeof window === "undefined") {
48
+ return href.startsWith("/") && !href.startsWith("//");
49
+ }
50
+ const trimmed = href.trim();
51
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
52
+ return true;
53
+ }
54
+ try {
55
+ const url = new URL(trimmed, window.location.href);
56
+ const currentOrigin = window.location.origin;
57
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
58
+ return normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin);
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+ function toRelativePath(href) {
64
+ if (typeof window === "undefined") {
65
+ return href;
66
+ }
67
+ const trimmed = href.trim();
68
+ if (trimmed.startsWith("/") && !trimmed.startsWith("//")) {
69
+ return trimmed;
70
+ }
71
+ try {
72
+ const url = new URL(trimmed, window.location.href);
73
+ const currentOrigin = window.location.origin;
74
+ const normalizeOrigin = (origin) => origin.replace(/^(https?:\/\/)(www\.)?/, "$1");
75
+ if (normalizeOrigin(url.origin) === normalizeOrigin(currentOrigin)) {
76
+ return url.pathname + url.search + url.hash;
77
+ }
78
+ } catch {
79
+ }
80
+ return trimmed;
81
+ }
82
+ function useNavigation({
83
+ href,
84
+ onClick
85
+ } = {}) {
86
+ const linkType = React.useMemo(() => {
87
+ if (!href || href.trim() === "") {
88
+ return onClick ? "none" : "none";
89
+ }
90
+ const trimmed = href.trim();
91
+ if (trimmed.toLowerCase().startsWith("mailto:") || isEmail(trimmed)) {
92
+ return "mailto";
93
+ }
94
+ if (trimmed.toLowerCase().startsWith("tel:") || isPhoneNumber(trimmed)) {
95
+ return "tel";
96
+ }
97
+ if (isInternalUrl(trimmed)) {
98
+ return "internal";
99
+ }
100
+ try {
101
+ new URL(trimmed, typeof window !== "undefined" ? window.location.href : "http://localhost");
102
+ return "external";
103
+ } catch {
104
+ return "internal";
105
+ }
106
+ }, [href, onClick]);
107
+ const normalizedHref = React.useMemo(() => {
108
+ if (!href || href.trim() === "") {
109
+ return void 0;
110
+ }
111
+ const trimmed = href.trim();
112
+ switch (linkType) {
113
+ case "tel":
114
+ return normalizePhoneNumber(trimmed);
115
+ case "mailto":
116
+ return normalizeEmail(trimmed);
117
+ case "internal":
118
+ return toRelativePath(trimmed);
119
+ case "external":
120
+ return trimmed;
121
+ default:
122
+ return trimmed;
123
+ }
124
+ }, [href, linkType]);
125
+ const target = React.useMemo(() => {
126
+ switch (linkType) {
127
+ case "external":
128
+ return "_blank";
129
+ case "internal":
130
+ return "_self";
131
+ case "mailto":
132
+ case "tel":
133
+ return void 0;
134
+ default:
135
+ return void 0;
136
+ }
137
+ }, [linkType]);
138
+ const rel = React.useMemo(() => {
139
+ if (linkType === "external") {
140
+ return "noopener noreferrer";
141
+ }
142
+ return void 0;
143
+ }, [linkType]);
144
+ const isExternal = linkType === "external";
145
+ const isInternal = linkType === "internal";
146
+ const shouldUseRouter = isInternal && typeof normalizedHref === "string" && normalizedHref.startsWith("/");
147
+ const handleClick = React.useCallback(
148
+ (event) => {
149
+ if (onClick) {
150
+ try {
151
+ onClick(event);
152
+ } catch (error) {
153
+ console.error("Error in user onClick handler:", error);
154
+ }
155
+ }
156
+ if (event.defaultPrevented) {
157
+ return;
158
+ }
159
+ if (shouldUseRouter && normalizedHref && event.button === 0 && // left-click only
160
+ !event.metaKey && !event.altKey && !event.ctrlKey && !event.shiftKey) {
161
+ if (typeof window !== "undefined") {
162
+ const handler = window.__opensiteNavigationHandler;
163
+ if (typeof handler === "function") {
164
+ try {
165
+ const handled = handler(normalizedHref, event.nativeEvent || event);
166
+ if (handled !== false) {
167
+ event.preventDefault();
168
+ }
169
+ } catch (error) {
170
+ console.error("Error in navigation handler:", error);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ },
176
+ [onClick, shouldUseRouter, normalizedHref]
177
+ );
178
+ return {
179
+ linkType,
180
+ normalizedHref,
181
+ target,
182
+ rel,
183
+ isExternal,
184
+ isInternal,
185
+ shouldUseRouter,
186
+ handleClick
187
+ };
188
+ }
189
+ var baseStyles = [
190
+ // Layout
191
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap shrink-0",
192
+ // Typography - using CSS variables with sensible defaults
193
+ "font-[var(--button-font-family,inherit)]",
194
+ "font-[var(--button-font-weight,500)]",
195
+ "tracking-[var(--button-letter-spacing,0)]",
196
+ "leading-[var(--button-line-height,1.25)]",
197
+ "[text-transform:var(--button-text-transform,none)]",
198
+ "text-sm",
199
+ // Border radius
200
+ "rounded-[var(--button-radius,var(--radius,0.375rem))]",
201
+ // Smooth transition - using [transition:...] to set full shorthand property (not just transition-property)
202
+ "[transition:var(--button-transition,all_250ms_cubic-bezier(0.4,0,0.2,1))]",
203
+ // Box shadow (master level) - using [box-shadow:...] for complex multi-value shadows
204
+ "[box-shadow:var(--button-shadow,none)]",
205
+ "hover:[box-shadow:var(--button-shadow-hover,var(--button-shadow,none))]",
206
+ // Disabled state
207
+ "disabled:pointer-events-none disabled:opacity-50",
208
+ // SVG handling
209
+ "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
210
+ // Focus styles
211
+ "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
212
+ // Invalid state
213
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive"
214
+ ].join(" ");
215
+ var buttonVariants = cva(baseStyles, {
216
+ variants: {
217
+ variant: {
218
+ // Default (Primary) variant - full customization
219
+ default: [
220
+ "bg-[var(--button-default-bg,hsl(var(--primary)))]",
221
+ "text-[var(--button-default-fg,hsl(var(--primary-foreground)))]",
222
+ "border-[length:var(--button-default-border-width,0px)]",
223
+ "border-[color:var(--button-default-border,transparent)]",
224
+ "[box-shadow:var(--button-default-shadow,var(--button-shadow,none))]",
225
+ "hover:bg-[var(--button-default-hover-bg,hsl(var(--primary)/0.9))]",
226
+ "hover:text-[var(--button-default-hover-fg,var(--button-default-fg,hsl(var(--primary-foreground))))]",
227
+ "hover:border-[color:var(--button-default-hover-border,var(--button-default-border,transparent))]",
228
+ "hover:[box-shadow:var(--button-default-shadow-hover,var(--button-shadow-hover,var(--button-default-shadow,var(--button-shadow,none))))]"
229
+ ].join(" "),
230
+ // Destructive variant - full customization
231
+ destructive: [
232
+ "bg-[var(--button-destructive-bg,hsl(var(--destructive)))]",
233
+ "text-[var(--button-destructive-fg,white)]",
234
+ "border-[length:var(--button-destructive-border-width,0px)]",
235
+ "border-[color:var(--button-destructive-border,transparent)]",
236
+ "[box-shadow:var(--button-destructive-shadow,var(--button-shadow,none))]",
237
+ "hover:bg-[var(--button-destructive-hover-bg,hsl(var(--destructive)/0.9))]",
238
+ "hover:text-[var(--button-destructive-hover-fg,var(--button-destructive-fg,white))]",
239
+ "hover:border-[color:var(--button-destructive-hover-border,var(--button-destructive-border,transparent))]",
240
+ "hover:[box-shadow:var(--button-destructive-shadow-hover,var(--button-shadow-hover,var(--button-destructive-shadow,var(--button-shadow,none))))]",
241
+ "focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
242
+ "dark:bg-destructive/60"
243
+ ].join(" "),
244
+ // Outline variant - full customization with proper border handling
245
+ outline: [
246
+ "bg-[var(--button-outline-bg,hsl(var(--background)))]",
247
+ "text-[var(--button-outline-fg,inherit)]",
248
+ "border-[length:var(--button-outline-border-width,1px)]",
249
+ "border-[color:var(--button-outline-border,hsl(var(--border)))]",
250
+ "[box-shadow:var(--button-outline-shadow,var(--button-shadow,0_1px_2px_0_rgb(0_0_0/0.05)))]",
251
+ "hover:bg-[var(--button-outline-hover-bg,hsl(var(--accent)))]",
252
+ "hover:text-[var(--button-outline-hover-fg,hsl(var(--accent-foreground)))]",
253
+ "hover:border-[color:var(--button-outline-hover-border,var(--button-outline-border,hsl(var(--border))))]",
254
+ "hover:[box-shadow:var(--button-outline-shadow-hover,var(--button-shadow-hover,var(--button-outline-shadow,var(--button-shadow,none))))]",
255
+ "dark:bg-input/30 dark:border-input dark:hover:bg-input/50"
256
+ ].join(" "),
257
+ // Secondary variant - full customization
258
+ secondary: [
259
+ "bg-[var(--button-secondary-bg,hsl(var(--secondary)))]",
260
+ "text-[var(--button-secondary-fg,hsl(var(--secondary-foreground)))]",
261
+ "border-[length:var(--button-secondary-border-width,0px)]",
262
+ "border-[color:var(--button-secondary-border,transparent)]",
263
+ "[box-shadow:var(--button-secondary-shadow,var(--button-shadow,none))]",
264
+ "hover:bg-[var(--button-secondary-hover-bg,hsl(var(--secondary)/0.8))]",
265
+ "hover:text-[var(--button-secondary-hover-fg,var(--button-secondary-fg,hsl(var(--secondary-foreground))))]",
266
+ "hover:border-[color:var(--button-secondary-hover-border,var(--button-secondary-border,transparent))]",
267
+ "hover:[box-shadow:var(--button-secondary-shadow-hover,var(--button-shadow-hover,var(--button-secondary-shadow,var(--button-shadow,none))))]"
268
+ ].join(" "),
269
+ // Ghost variant - full customization
270
+ ghost: [
271
+ "bg-[var(--button-ghost-bg,transparent)]",
272
+ "text-[var(--button-ghost-fg,inherit)]",
273
+ "border-[length:var(--button-ghost-border-width,0px)]",
274
+ "border-[color:var(--button-ghost-border,transparent)]",
275
+ "[box-shadow:var(--button-ghost-shadow,var(--button-shadow,none))]",
276
+ "hover:bg-[var(--button-ghost-hover-bg,hsl(var(--accent)))]",
277
+ "hover:text-[var(--button-ghost-hover-fg,hsl(var(--accent-foreground)))]",
278
+ "hover:border-[color:var(--button-ghost-hover-border,var(--button-ghost-border,transparent))]",
279
+ "hover:[box-shadow:var(--button-ghost-shadow-hover,var(--button-shadow-hover,var(--button-ghost-shadow,var(--button-shadow,none))))]",
280
+ "dark:hover:bg-accent/50"
281
+ ].join(" "),
282
+ // Link variant - full customization
283
+ link: [
284
+ "bg-[var(--button-link-bg,transparent)]",
285
+ "text-[var(--button-link-fg,hsl(var(--primary)))]",
286
+ "border-[length:var(--button-link-border-width,0px)]",
287
+ "border-[color:var(--button-link-border,transparent)]",
288
+ "[box-shadow:var(--button-link-shadow,none)]",
289
+ "hover:bg-[var(--button-link-hover-bg,transparent)]",
290
+ "hover:text-[var(--button-link-hover-fg,var(--button-link-fg,hsl(var(--primary))))]",
291
+ "hover:[box-shadow:var(--button-link-shadow-hover,none)]",
292
+ "underline-offset-4 hover:underline"
293
+ ].join(" ")
294
+ },
295
+ size: {
296
+ default: [
297
+ "h-[var(--button-height-md,2.25rem)]",
298
+ "px-[var(--button-padding-x-md,1rem)]",
299
+ "py-[var(--button-padding-y-md,0.5rem)]",
300
+ "has-[>svg]:px-[calc(var(--button-padding-x-md,1rem)*0.75)]"
301
+ ].join(" "),
302
+ sm: [
303
+ "h-[var(--button-height-sm,2rem)]",
304
+ "px-[var(--button-padding-x-sm,0.75rem)]",
305
+ "py-[var(--button-padding-y-sm,0.25rem)]",
306
+ "gap-1.5",
307
+ "has-[>svg]:px-[calc(var(--button-padding-x-sm,0.75rem)*0.83)]"
308
+ ].join(" "),
309
+ md: [
310
+ "h-[var(--button-height-md,2.25rem)]",
311
+ "px-[var(--button-padding-x-md,1rem)]",
312
+ "py-[var(--button-padding-y-md,0.5rem)]",
313
+ "has-[>svg]:px-[calc(var(--button-padding-x-md,1rem)*0.75)]"
314
+ ].join(" "),
315
+ lg: [
316
+ "h-[var(--button-height-lg,2.5rem)]",
317
+ "px-[var(--button-padding-x-lg,1.5rem)]",
318
+ "py-[var(--button-padding-y-lg,0.5rem)]",
319
+ "has-[>svg]:px-[calc(var(--button-padding-x-lg,1.5rem)*0.67)]"
320
+ ].join(" "),
321
+ icon: "size-[var(--button-height-md,2.25rem)]",
322
+ "icon-sm": "size-[var(--button-height-sm,2rem)]",
323
+ "icon-lg": "size-[var(--button-height-lg,2.5rem)]"
324
+ }
325
+ },
326
+ defaultVariants: {
327
+ variant: "default",
328
+ size: "default"
329
+ }
330
+ });
331
+ var Pressable = React.forwardRef(
332
+ ({
333
+ children,
334
+ className,
335
+ href,
336
+ onClick,
337
+ variant,
338
+ size,
339
+ asButton = false,
340
+ fallbackComponentType = "span",
341
+ componentType,
342
+ "aria-label": ariaLabel,
343
+ "aria-describedby": ariaDescribedby,
344
+ id,
345
+ ...props
346
+ }, ref) => {
347
+ const navigation = useNavigation({ href, onClick });
348
+ const {
349
+ normalizedHref,
350
+ target,
351
+ rel,
352
+ linkType,
353
+ isInternal,
354
+ handleClick
355
+ } = navigation;
356
+ const shouldRenderLink = normalizedHref && linkType !== "none";
357
+ const shouldRenderButton = !shouldRenderLink && onClick;
358
+ const effectiveComponentType = componentType || (shouldRenderLink ? "a" : shouldRenderButton ? "button" : fallbackComponentType);
359
+ const finalComponentType = isInternal && shouldRenderLink ? "a" : effectiveComponentType;
360
+ const shouldApplyButtonStyles = asButton || variant || size;
361
+ const combinedClassName = cn(
362
+ shouldApplyButtonStyles && buttonVariants({ variant, size }),
363
+ className
364
+ );
365
+ const dataProps = Object.fromEntries(
366
+ Object.entries(props).filter(([key]) => key.startsWith("data-"))
367
+ );
368
+ const buttonDataAttributes = shouldApplyButtonStyles ? {
369
+ "data-slot": "button",
370
+ "data-variant": variant ?? "default",
371
+ "data-size": size ?? "default"
372
+ } : {};
373
+ const commonProps = {
374
+ className: combinedClassName,
375
+ onClick: handleClick,
376
+ "aria-label": ariaLabel,
377
+ "aria-describedby": ariaDescribedby,
378
+ id,
379
+ ...dataProps,
380
+ ...buttonDataAttributes
381
+ };
382
+ if (finalComponentType === "a" && shouldRenderLink) {
383
+ return /* @__PURE__ */ jsx(
384
+ "a",
385
+ {
386
+ ref,
387
+ href: normalizedHref,
388
+ target,
389
+ rel,
390
+ ...commonProps,
391
+ ...props,
392
+ children
393
+ }
394
+ );
395
+ }
396
+ if (finalComponentType === "button") {
397
+ return /* @__PURE__ */ jsx(
398
+ "button",
399
+ {
400
+ ref,
401
+ type: props.type || "button",
402
+ ...commonProps,
403
+ ...props,
404
+ children
405
+ }
406
+ );
407
+ }
408
+ if (finalComponentType === "div") {
409
+ return /* @__PURE__ */ jsx(
410
+ "div",
411
+ {
412
+ ref,
413
+ ...commonProps,
414
+ children
415
+ }
416
+ );
417
+ }
418
+ return /* @__PURE__ */ jsx(
419
+ "span",
420
+ {
421
+ ref,
422
+ ...commonProps,
423
+ children
424
+ }
425
+ );
426
+ }
427
+ );
428
+ Pressable.displayName = "Pressable";
429
+ var svgCache = /* @__PURE__ */ new Map();
430
+ function DynamicIcon({
431
+ name,
432
+ size = 28,
433
+ color,
434
+ className,
435
+ alt
436
+ }) {
437
+ const [svgContent, setSvgContent] = React.useState(null);
438
+ const [isLoading, setIsLoading] = React.useState(true);
439
+ const [error, setError] = React.useState(null);
440
+ const { url, iconName } = React.useMemo(() => {
441
+ const separator = name.includes("/") ? "/" : ":";
442
+ const [prefix, iconName2] = name.split(separator);
443
+ const baseUrl = `https://icons.opensite.ai/api/icon/${prefix}/${iconName2}?format=svg&width=${size}&height=${size}`;
444
+ return {
445
+ url: baseUrl,
446
+ iconName: iconName2
447
+ };
448
+ }, [name, size]);
449
+ React.useEffect(() => {
450
+ let isMounted = true;
451
+ const fetchSvg = async () => {
452
+ const cached = svgCache.get(url);
453
+ if (cached) {
454
+ if (isMounted) {
455
+ setSvgContent(cached);
456
+ setIsLoading(false);
457
+ }
458
+ return;
459
+ }
460
+ try {
461
+ setIsLoading(true);
462
+ setError(null);
463
+ const response = await fetch(url);
464
+ if (!response.ok) {
465
+ throw new Error(`Failed to fetch icon: ${response.status}`);
466
+ }
467
+ let svg = await response.text();
468
+ svg = processSvgForCurrentColor(svg);
469
+ svgCache.set(url, svg);
470
+ if (isMounted) {
471
+ setSvgContent(svg);
472
+ setIsLoading(false);
473
+ }
474
+ } catch (err) {
475
+ if (isMounted) {
476
+ setError(err instanceof Error ? err.message : "Failed to load icon");
477
+ setIsLoading(false);
478
+ }
479
+ }
480
+ };
481
+ fetchSvg();
482
+ return () => {
483
+ isMounted = false;
484
+ };
485
+ }, [url]);
486
+ if (isLoading) {
487
+ return /* @__PURE__ */ jsx(
488
+ "span",
489
+ {
490
+ className: cn("inline-block", className),
491
+ style: { width: size, height: size },
492
+ "aria-hidden": "true"
493
+ }
494
+ );
495
+ }
496
+ if (error || !svgContent) {
497
+ return /* @__PURE__ */ jsx(
498
+ "span",
499
+ {
500
+ className: cn("inline-block", className),
501
+ style: { width: size, height: size },
502
+ role: "img",
503
+ "aria-label": alt || iconName
504
+ }
505
+ );
506
+ }
507
+ return /* @__PURE__ */ jsx(
508
+ "span",
509
+ {
510
+ className: cn("inline-flex items-center justify-center", className),
511
+ style: {
512
+ width: size,
513
+ height: size,
514
+ color: color || "inherit"
515
+ },
516
+ role: "img",
517
+ "aria-label": alt || iconName,
518
+ dangerouslySetInnerHTML: { __html: svgContent }
519
+ }
520
+ );
521
+ }
522
+ function processSvgForCurrentColor(svg) {
523
+ let processed = svg;
524
+ processed = processed.replace(
525
+ /stroke=["'](#000000|#000|black)["']/gi,
526
+ 'stroke="currentColor"'
527
+ );
528
+ processed = processed.replace(
529
+ /fill=["'](#000000|#000|black)["']/gi,
530
+ 'fill="currentColor"'
531
+ );
532
+ return processed;
533
+ }
534
+ var platformIconMap = {
535
+ instagram: "cib/instagram",
536
+ linkedin: "cib/linkedin",
537
+ google: "cib/google",
538
+ facebook: "cib/facebook",
539
+ tiktok: "cib/tiktok",
540
+ youtube: "cib/youtube",
541
+ yelp: "cib/yelp",
542
+ spotify: "cib/spotify",
543
+ apple: "cib/apple",
544
+ x: "line-md/twitter-x"
545
+ };
546
+ var SocialLinkIcon = React.forwardRef(
547
+ ({
548
+ platformName,
549
+ label,
550
+ iconSize = 20,
551
+ iconColor,
552
+ iconClassName,
553
+ className,
554
+ ...pressableProps
555
+ }, ref) => {
556
+ const iconName = platformIconMap[platformName];
557
+ const accessibleLabel = label || platformName;
558
+ return /* @__PURE__ */ jsx(
559
+ Pressable,
560
+ {
561
+ ref,
562
+ "aria-label": accessibleLabel,
563
+ className: cn(
564
+ "inline-flex items-center justify-center transition-colors",
565
+ className
566
+ ),
567
+ ...pressableProps,
568
+ children: /* @__PURE__ */ jsx(
569
+ DynamicIcon,
570
+ {
571
+ name: iconName,
572
+ size: iconSize,
573
+ color: iconColor,
574
+ className: iconClassName,
575
+ alt: accessibleLabel
576
+ }
577
+ )
578
+ }
579
+ );
580
+ }
581
+ );
582
+ SocialLinkIcon.displayName = "SocialLinkIcon";
583
+
584
+ export { SocialLinkIcon };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensite/ui",
3
- "version": "0.9.9",
3
+ "version": "1.0.1",
4
4
  "description": "Foundational UI component library for OpenSite Semantic Site Builder with tree-shakable exports and abstract styling",
5
5
  "keywords": [
6
6
  "react",
@@ -3226,6 +3226,11 @@
3226
3226
  "import": "./dist/sheet.js",
3227
3227
  "require": "./dist/sheet.cjs"
3228
3228
  },
3229
+ "./components/social-link-icon": {
3230
+ "types": "./dist/social-link-icon.d.ts",
3231
+ "import": "./dist/social-link-icon.js",
3232
+ "require": "./dist/social-link-icon.cjs"
3233
+ },
3229
3234
  "./components/switch": {
3230
3235
  "types": "./dist/switch.d.ts",
3231
3236
  "import": "./dist/switch.js",