@bwp-web/components 1.0.9 → 1.1.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.
package/README.md CHANGED
@@ -49,6 +49,7 @@ For `BiampTable` only:
49
49
  | `SegmentedButton` | Individual toggle button for use inside `SegmentedButtonGroup` |
50
50
  | `BiampTable` | Composable data table with sorting, selection, pagination, and more |
51
51
  | `UserInitialsIcon` | Avatar-style icon showing a user's initials with a deterministic color |
52
+ | `DynamicSvgIcon` | Renders a remotely-fetched SVG with a skeleton loader and required fallback |
52
53
 
53
54
  ## Usage
54
55
 
@@ -247,6 +248,36 @@ import { UserInitialsIcon } from '@bwp-web/components';
247
248
  | `sx` | `SxProps` | — | MUI `sx` style overrides |
248
249
  | `...` | `BoxProps` | — | All other MUI `Box` props are forwarded to the root element |
249
250
 
251
+ ### DynamicSvgIcon
252
+
253
+ Fetches an SVG from a URL and renders it as a MUI `SvgIcon`. Shows a `Skeleton` placeholder while loading and a required fallback on error. The `width` and `height` props are enforced across all three states so the layout never shifts.
254
+
255
+ ```tsx
256
+ import { DynamicSvgIcon } from '@bwp-web/components';
257
+ import BrokenImageIcon from '@mui/icons-material/BrokenImage';
258
+
259
+ <DynamicSvgIcon
260
+ url="https://example.com/icon.svg"
261
+ width={32}
262
+ height={32}
263
+ fallback={<BrokenImageIcon />}
264
+ />;
265
+ ```
266
+
267
+ #### DynamicSvgIcon Props
268
+
269
+ | Prop | Type | Default | Description |
270
+ | ------------------- | ------------------------------------------ | ------------ | -------------------------------------------------------------- |
271
+ | `url` | `string` | — | URL of the SVG to fetch |
272
+ | `fallback` | `React.ReactNode` | **required** | Element shown when loading fails |
273
+ | `width` | `number` | `24` | Width in pixels for icon, skeleton, and fallback |
274
+ | `height` | `number` | `24` | Height in pixels for icon, skeleton, and fallback |
275
+ | `replaceColors` | `boolean` | `false` | Replace all fill/stroke colors with `currentColor` for theming |
276
+ | `skeletonVariant` | `'circular' \| 'rectangular' \| 'rounded'` | `'circular'` | Skeleton shape during loading |
277
+ | `skeletonAnimation` | `'pulse' \| 'wave' \| false` | `'pulse'` | Skeleton animation type |
278
+ | `onLoad` | `() => void` | — | Called when the SVG loads successfully |
279
+ | `onError` | `(error: string) => void` | — | Called when loading fails |
280
+
250
281
  ### BiampTable
251
282
 
252
283
  A composable data table built on TanStack React Table v8 with support for sorting, row selection, pagination, column visibility, global search, column filters, and CSV export.
@@ -267,3 +298,4 @@ Detailed per-component docs are available in the repository's [`/docs`](../../do
267
298
  | [biamp-global-search.md](../../docs/biamp-global-search.md) | `BiampGlobalSearch` — options, filtering, async loading, navigation |
268
299
  | [biamp-table.md](../../docs/biamp-table.md) | `BiampTable` — columns, sorting, selection, pagination, filters, export |
269
300
  | [user-initials-icon.md](../../docs/user-initials-icon.md) | `UserInitialsIcon` — props, color seeding, sizing, edge cases |
301
+ | [dynamic-svg-icon.md](../../docs/dynamic-svg-icon.md) | `DynamicSvgIcon` — props, hook API, caching, skeleton, fallback |
@@ -0,0 +1,98 @@
1
+ import type { SvgIconProps } from '@mui/material';
2
+ /** Clear the internal SVG fetch cache. Useful for testing or forcing a refetch. */
3
+ export declare function clearDynamicSvgIconCache(): void;
4
+ export interface UseDynamicSvgIconOptions {
5
+ /**
6
+ * When `true`, all `fill` and `stroke` attribute values (except `"none"` and
7
+ * `"currentColor"`) are replaced with `"currentColor"`. This makes the SVG
8
+ * fully themeable via the CSS `color` property or MUI's `sx={{ color }}`.
9
+ *
10
+ * @default false
11
+ */
12
+ replaceColors?: boolean;
13
+ /** Called when the SVG loads successfully */
14
+ onLoad?: () => void;
15
+ /** Called when loading fails */
16
+ onError?: (error: string) => void;
17
+ }
18
+ export interface UseDynamicSvgIconResult {
19
+ /** Whether the SVG is currently being fetched */
20
+ loading: boolean;
21
+ /** Error message if fetching failed, null otherwise */
22
+ error: string | null;
23
+ /** The inner SVG content (paths, groups, etc.) */
24
+ svgContent: string | null;
25
+ /** The viewBox extracted from the source SVG */
26
+ svgViewBox: string | null;
27
+ }
28
+ /**
29
+ * Hook that fetches an SVG from a URL and returns the parsed content.
30
+ * The SVG is rendered as-is by default. Pass `replaceColors: true` to replace
31
+ * all fill/stroke colors with `currentColor` for full theming support.
32
+ * Results are cached in-memory so subsequent renders with the same URL are instant.
33
+ *
34
+ * @param url - URL of the SVG to fetch (supports any URL that `fetch` can handle, including data URLs)
35
+ * @param options - Optional callbacks for load/error events
36
+ * @returns Object with `loading`, `error`, `svgContent`, and `svgViewBox` fields
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const { loading, error, svgContent, svgViewBox } = useDynamicSvgIcon(
41
+ * 'https://example.com/icon.svg',
42
+ * { onError: (msg) => console.warn(msg) },
43
+ * );
44
+ * ```
45
+ */
46
+ export declare function useDynamicSvgIcon(url: string, options?: UseDynamicSvgIconOptions): UseDynamicSvgIconResult;
47
+ export interface DynamicSvgIconProps extends Omit<SvgIconProps, 'children' | 'onLoad' | 'onError' | 'width' | 'height'> {
48
+ /** URL of the SVG to load */
49
+ url: string;
50
+ /** Fallback element shown when loading fails */
51
+ fallback: React.ReactNode;
52
+ /** Width in pixels — applied to icon, skeleton, and fallback (default: 24) */
53
+ width?: number;
54
+ /** Height in pixels — applied to icon, skeleton, and fallback (default: 24) */
55
+ height?: number;
56
+ /**
57
+ * Replace all fill/stroke colors (except `"none"` and `"currentColor"`) with
58
+ * `"currentColor"`, making the icon fully themeable via CSS `color`.
59
+ * Set to `false` to preserve the SVG's original colors.
60
+ *
61
+ * @default false
62
+ */
63
+ replaceColors?: boolean;
64
+ /** Skeleton shape shown during loading (default: 'circular') */
65
+ skeletonVariant?: 'circular' | 'rectangular' | 'rounded';
66
+ /** Skeleton animation type (default: 'pulse') */
67
+ skeletonAnimation?: 'pulse' | 'wave' | false;
68
+ /** Called when the SVG loads successfully */
69
+ onLoad?: () => void;
70
+ /** Called when loading fails */
71
+ onError?: (error: string) => void;
72
+ }
73
+ /**
74
+ * Renders an SVG icon fetched from a URL with a MUI Skeleton placeholder during
75
+ * loading and a required fallback on error. The `width` and `height` props
76
+ * control the dimensions of all three states (skeleton, icon, fallback).
77
+ *
78
+ * The SVG is rendered as-is — fill, stroke, and viewBox are preserved from the
79
+ * source. Paths without an explicit fill will inherit `currentColor` from MUI
80
+ * SvgIcon's CSS. Set `replaceColors` to force **all** fills and strokes to
81
+ * `"currentColor"`, making the icon fully themeable via CSS `color`.
82
+ *
83
+ * Fetched SVGs are cached in-memory; the same URL will only be fetched once
84
+ * per page session. Use {@link clearDynamicSvgIconCache} to force a refetch.
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * <DynamicSvgIcon
89
+ * url="https://example.com/icon.svg"
90
+ * width={32}
91
+ * height={32}
92
+ * fallback={<BrokenImageIcon />}
93
+ * onError={(msg) => console.warn(msg)}
94
+ * />
95
+ * ```
96
+ */
97
+ export declare function DynamicSvgIcon({ url, fallback, width, height, replaceColors, skeletonVariant, skeletonAnimation, onLoad, onError, sx, ...svgIconProps }: DynamicSvgIconProps): import("react/jsx-runtime").JSX.Element;
98
+ //# sourceMappingURL=DynamicSvgIcon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DynamicSvgIcon.d.ts","sourceRoot":"","sources":["../../src/DynamicSvgIcon/DynamicSvgIcon.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAUlD,mFAAmF;AACnF,wBAAgB,wBAAwB,SAEvC;AAYD,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,gCAAgC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,uDAAuD;IACvD,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,kDAAkD;IAClD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gDAAgD;IAChD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,wBAA6B,GACrC,uBAAuB,CA0FzB;AAID,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAC/C,YAAY,EACZ,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CACvD;IACC,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gEAAgE;IAChE,eAAe,CAAC,EAAE,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;IACzD,iDAAiD;IACjD,iBAAiB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IAC7C,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,gCAAgC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,GAAG,EACH,QAAQ,EACR,KAAoB,EACpB,MAAqB,EACrB,aAAa,EACb,eAA4B,EAC5B,iBAA2B,EAC3B,MAAM,EACN,OAAO,EACP,EAAE,EACF,GAAG,YAAY,EAChB,EAAE,mBAAmB,2CAsDrB"}
@@ -0,0 +1,3 @@
1
+ export { DynamicSvgIcon, useDynamicSvgIcon, clearDynamicSvgIconCache, } from './DynamicSvgIcon';
2
+ export type { DynamicSvgIconProps, UseDynamicSvgIconOptions, UseDynamicSvgIconResult, } from './DynamicSvgIcon';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/DynamicSvgIcon/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,mBAAmB,EACnB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,kBAAkB,CAAC"}
package/dist/index.cjs CHANGED
@@ -438,10 +438,12 @@ __export(index_exports, {
438
438
  BiampTableToolbarSearch: () => BiampTableToolbarSearch,
439
439
  BiampTableTruncatedCell: () => BiampTableTruncatedCell,
440
440
  BiampWrapper: () => BiampWrapper,
441
+ DynamicSvgIcon: () => DynamicSvgIcon,
441
442
  SegmentedButton: () => SegmentedButton,
442
443
  SegmentedButtonGroup: () => SegmentedButtonGroup,
443
444
  UserInitialsIcon: () => UserInitialsIcon,
444
445
  buildCsvString: () => buildCsvString,
446
+ clearDynamicSvgIconCache: () => clearDynamicSvgIconCache,
445
447
  exportToCsv: () => exportToCsv,
446
448
  getColumnVisibilityDirtyCount: () => getColumnVisibilityDirtyCount,
447
449
  getDefaultColumnVisibility: () => getDefaultColumnVisibility,
@@ -454,7 +456,8 @@ __export(index_exports, {
454
456
  sortingToOrder: () => sortingToOrder,
455
457
  toVisibilityState: () => toVisibilityState,
456
458
  useBiampServerSideTable: () => useBiampServerSideTable,
457
- useDebouncedCallback: () => useDebouncedCallback
459
+ useDebouncedCallback: () => useDebouncedCallback,
460
+ useDynamicSvgIcon: () => useDynamicSvgIcon
458
461
  });
459
462
  module.exports = __toCommonJS(index_exports);
460
463
 
@@ -2759,6 +2762,156 @@ var getInitials = (name) => {
2759
2762
  const initials = words.filter(Boolean).slice(0, 2).map((word) => word[0].toUpperCase()).join("");
2760
2763
  return initials;
2761
2764
  };
2765
+
2766
+ // src/DynamicSvgIcon/DynamicSvgIcon.tsx
2767
+ var import_material23 = require("@mui/material");
2768
+ var import_react12 = require("react");
2769
+ var import_jsx_runtime26 = require("react/jsx-runtime");
2770
+ var svgCache = /* @__PURE__ */ new Map();
2771
+ function clearDynamicSvgIconCache() {
2772
+ svgCache.clear();
2773
+ }
2774
+ function applyCurrentColor(svg) {
2775
+ return svg.replace(/fill="(?!none|currentColor)[^"]*"/g, 'fill="currentColor"').replace(/stroke="(?!none|currentColor)[^"]*"/g, 'stroke="currentColor"');
2776
+ }
2777
+ function useDynamicSvgIcon(url, options = {}) {
2778
+ const { replaceColors = false, onLoad, onError } = options;
2779
+ const transform = replaceColors ? applyCurrentColor : (s) => s;
2780
+ const [svgContent, setSvgContent] = (0, import_react12.useState)(() => {
2781
+ const cached = svgCache.get(url);
2782
+ return cached ? transform(cached.innerContent) : null;
2783
+ });
2784
+ const [svgViewBox, setSvgViewBox] = (0, import_react12.useState)(() => {
2785
+ const cached = svgCache.get(url);
2786
+ return cached?.viewBox ?? null;
2787
+ });
2788
+ const [loading, setLoading] = (0, import_react12.useState)(() => !svgCache.has(url));
2789
+ const [error, setError] = (0, import_react12.useState)(null);
2790
+ (0, import_react12.useEffect)(() => {
2791
+ if (!url) {
2792
+ setLoading(false);
2793
+ setError("No URL provided");
2794
+ setSvgContent(null);
2795
+ setSvgViewBox(null);
2796
+ return;
2797
+ }
2798
+ let cancelled = false;
2799
+ const cached = svgCache.get(url);
2800
+ if (cached) {
2801
+ setSvgContent(transform(cached.innerContent));
2802
+ setSvgViewBox(cached.viewBox);
2803
+ setLoading(false);
2804
+ setError(null);
2805
+ onLoad?.();
2806
+ return;
2807
+ }
2808
+ setLoading(true);
2809
+ setError(null);
2810
+ setSvgContent(null);
2811
+ setSvgViewBox(null);
2812
+ (async () => {
2813
+ try {
2814
+ const response = await fetch(url);
2815
+ if (!response.ok) {
2816
+ throw new Error(
2817
+ `Failed to fetch SVG: ${response.status} ${response.statusText}`
2818
+ );
2819
+ }
2820
+ const contentType = response.headers.get("content-type") ?? "";
2821
+ const text = await response.text();
2822
+ if (!text.includes("<svg") && !contentType.includes("svg")) {
2823
+ throw new Error("Response is not an SVG");
2824
+ }
2825
+ const viewBoxMatch = text.match(/viewBox="([^"]*)"/);
2826
+ const viewBox = viewBoxMatch ? viewBoxMatch[1] : null;
2827
+ const svgMatch = text.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);
2828
+ const innerContent = svgMatch ? svgMatch[1] : text;
2829
+ svgCache.set(url, { innerContent, viewBox });
2830
+ if (!cancelled) {
2831
+ setSvgContent(transform(innerContent));
2832
+ setSvgViewBox(viewBox);
2833
+ setLoading(false);
2834
+ onLoad?.();
2835
+ }
2836
+ } catch (err) {
2837
+ if (!cancelled) {
2838
+ const message = err instanceof Error ? err.message : "Failed to load SVG";
2839
+ setError(message);
2840
+ setLoading(false);
2841
+ onError?.(message);
2842
+ }
2843
+ }
2844
+ })();
2845
+ return () => {
2846
+ cancelled = true;
2847
+ };
2848
+ }, [url]);
2849
+ return { loading, error, svgContent, svgViewBox };
2850
+ }
2851
+ var DEFAULT_SIZE2 = 24;
2852
+ function DynamicSvgIcon({
2853
+ url,
2854
+ fallback,
2855
+ width = DEFAULT_SIZE2,
2856
+ height = DEFAULT_SIZE2,
2857
+ replaceColors,
2858
+ skeletonVariant = "circular",
2859
+ skeletonAnimation = "pulse",
2860
+ onLoad,
2861
+ onError,
2862
+ sx,
2863
+ ...svgIconProps
2864
+ }) {
2865
+ const { loading, error, svgContent, svgViewBox } = useDynamicSvgIcon(url, {
2866
+ replaceColors,
2867
+ onLoad,
2868
+ onError
2869
+ });
2870
+ if (loading) {
2871
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2872
+ import_material23.Skeleton,
2873
+ {
2874
+ variant: skeletonVariant,
2875
+ animation: skeletonAnimation,
2876
+ sx: { width, height }
2877
+ }
2878
+ );
2879
+ }
2880
+ if (error || !svgContent) {
2881
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2882
+ import_material23.Box,
2883
+ {
2884
+ sx: {
2885
+ width,
2886
+ height,
2887
+ display: "inline-flex",
2888
+ alignItems: "center",
2889
+ justifyContent: "center",
2890
+ overflow: "hidden",
2891
+ flexShrink: 0,
2892
+ "& > svg, & > .MuiSvgIcon-root": {
2893
+ width: "100%",
2894
+ height: "100%"
2895
+ }
2896
+ },
2897
+ children: fallback
2898
+ }
2899
+ );
2900
+ }
2901
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2902
+ import_material23.SvgIcon,
2903
+ {
2904
+ ...svgIconProps,
2905
+ ...svgViewBox && { viewBox: svgViewBox },
2906
+ sx: {
2907
+ ...typeof sx === "object" && sx !== null && !Array.isArray(sx) ? sx : void 0,
2908
+ width,
2909
+ height
2910
+ },
2911
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("g", { dangerouslySetInnerHTML: { __html: svgContent } })
2912
+ }
2913
+ );
2914
+ }
2762
2915
  // Annotate the CommonJS export names for ESM import in node:
2763
2916
  0 && (module.exports = {
2764
2917
  BIAMP_TABLE_DEBOUNCE_DELAY,
@@ -2799,10 +2952,12 @@ var getInitials = (name) => {
2799
2952
  BiampTableToolbarSearch,
2800
2953
  BiampTableTruncatedCell,
2801
2954
  BiampWrapper,
2955
+ DynamicSvgIcon,
2802
2956
  SegmentedButton,
2803
2957
  SegmentedButtonGroup,
2804
2958
  UserInitialsIcon,
2805
2959
  buildCsvString,
2960
+ clearDynamicSvgIconCache,
2806
2961
  exportToCsv,
2807
2962
  getColumnVisibilityDirtyCount,
2808
2963
  getDefaultColumnVisibility,
@@ -2815,6 +2970,7 @@ var getInitials = (name) => {
2815
2970
  sortingToOrder,
2816
2971
  toVisibilityState,
2817
2972
  useBiampServerSideTable,
2818
- useDebouncedCallback
2973
+ useDebouncedCallback,
2974
+ useDynamicSvgIcon
2819
2975
  });
2820
2976
  //# sourceMappingURL=index.cjs.map