@bwp-web/components 1.0.9 → 1.1.0

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,35 @@ 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
+ | `skeletonVariant` | `'circular' \| 'rectangular' \| 'rounded'` | `'circular'` | Skeleton shape during loading |
276
+ | `skeletonAnimation` | `'pulse' \| 'wave' \| false` | `'pulse'` | Skeleton animation type |
277
+ | `onLoad` | `() => void` | — | Called when the SVG loads successfully |
278
+ | `onError` | `(error: string) => void` | — | Called when loading fails |
279
+
250
280
  ### BiampTable
251
281
 
252
282
  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 +297,4 @@ Detailed per-component docs are available in the repository's [`/docs`](../../do
267
297
  | [biamp-global-search.md](../../docs/biamp-global-search.md) | `BiampGlobalSearch` — options, filtering, async loading, navigation |
268
298
  | [biamp-table.md](../../docs/biamp-table.md) | `BiampTable` — columns, sorting, selection, pagination, filters, export |
269
299
  | [user-initials-icon.md](../../docs/user-initials-icon.md) | `UserInitialsIcon` — props, color seeding, sizing, edge cases |
300
+ | [dynamic-svg-icon.md](../../docs/dynamic-svg-icon.md) | `DynamicSvgIcon` — props, hook API, caching, skeleton, fallback |
@@ -0,0 +1,80 @@
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
+ /** Called when the SVG loads successfully */
6
+ onLoad?: () => void;
7
+ /** Called when loading fails */
8
+ onError?: (error: string) => void;
9
+ }
10
+ export interface UseDynamicSvgIconResult {
11
+ /** Whether the SVG is currently being fetched */
12
+ loading: boolean;
13
+ /** Error message if fetching failed, null otherwise */
14
+ error: string | null;
15
+ /** The inner SVG content (paths, groups, etc.) */
16
+ svgContent: string | null;
17
+ /** The viewBox extracted from the source SVG */
18
+ svgViewBox: string | null;
19
+ }
20
+ /**
21
+ * Hook that fetches an SVG from a URL and returns the parsed content.
22
+ * The SVG is rendered as-is — colors and viewBox are preserved from the source.
23
+ * Results are cached in-memory so subsequent renders with the same URL are instant.
24
+ *
25
+ * @param url - URL of the SVG to fetch (supports any URL that `fetch` can handle, including data URLs)
26
+ * @param options - Optional callbacks for load/error events
27
+ * @returns Object with `loading`, `error`, `svgContent`, and `svgViewBox` fields
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * const { loading, error, svgContent, svgViewBox } = useDynamicSvgIcon(
32
+ * 'https://example.com/icon.svg',
33
+ * { onError: (msg) => console.warn(msg) },
34
+ * );
35
+ * ```
36
+ */
37
+ export declare function useDynamicSvgIcon(url: string, options?: UseDynamicSvgIconOptions): UseDynamicSvgIconResult;
38
+ export interface DynamicSvgIconProps extends Omit<SvgIconProps, 'children' | 'onLoad' | 'onError' | 'width' | 'height'> {
39
+ /** URL of the SVG to load */
40
+ url: string;
41
+ /** Fallback element shown when loading fails */
42
+ fallback: React.ReactNode;
43
+ /** Width in pixels — applied to icon, skeleton, and fallback (default: 24) */
44
+ width?: number;
45
+ /** Height in pixels — applied to icon, skeleton, and fallback (default: 24) */
46
+ height?: number;
47
+ /** Skeleton shape shown during loading (default: 'circular') */
48
+ skeletonVariant?: 'circular' | 'rectangular' | 'rounded';
49
+ /** Skeleton animation type (default: 'pulse') */
50
+ skeletonAnimation?: 'pulse' | 'wave' | false;
51
+ /** Called when the SVG loads successfully */
52
+ onLoad?: () => void;
53
+ /** Called when loading fails */
54
+ onError?: (error: string) => void;
55
+ }
56
+ /**
57
+ * Renders an SVG icon fetched from a URL with a MUI Skeleton placeholder during
58
+ * loading and a required fallback on error. The `width` and `height` props
59
+ * control the dimensions of all three states (skeleton, icon, fallback).
60
+ *
61
+ * The SVG is rendered as-is — fill, stroke, and viewBox are preserved from the
62
+ * source. Paths without an explicit fill will inherit `currentColor` from MUI
63
+ * SvgIcon's CSS, so they respond to the parent's text color.
64
+ *
65
+ * Fetched SVGs are cached in-memory; the same URL will only be fetched once
66
+ * per page session. Use {@link clearDynamicSvgIconCache} to force a refetch.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * <DynamicSvgIcon
71
+ * url="https://example.com/icon.svg"
72
+ * width={32}
73
+ * height={32}
74
+ * fallback={<BrokenImageIcon />}
75
+ * onError={(msg) => console.warn(msg)}
76
+ * />
77
+ * ```
78
+ */
79
+ export declare function DynamicSvgIcon({ url, fallback, width, height, skeletonVariant, skeletonAnimation, onLoad, onError, sx, ...svgIconProps }: DynamicSvgIconProps): import("react/jsx-runtime").JSX.Element;
80
+ //# 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;AAED,MAAM,WAAW,wBAAwB;IACvC,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;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,wBAA6B,GACrC,uBAAuB,CAwFzB;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,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;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,GAAG,EACH,QAAQ,EACR,KAAoB,EACpB,MAAqB,EACrB,eAA4B,EAC5B,iBAA2B,EAC3B,MAAM,EACN,OAAO,EACP,EAAE,EACF,GAAG,YAAY,EAChB,EAAE,mBAAmB,2CAqDrB"}
@@ -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,150 @@ 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 useDynamicSvgIcon(url, options = {}) {
2775
+ const { onLoad, onError } = options;
2776
+ const [svgContent, setSvgContent] = (0, import_react12.useState)(() => {
2777
+ const cached = svgCache.get(url);
2778
+ return cached ? cached.innerContent : null;
2779
+ });
2780
+ const [svgViewBox, setSvgViewBox] = (0, import_react12.useState)(() => {
2781
+ const cached = svgCache.get(url);
2782
+ return cached?.viewBox ?? null;
2783
+ });
2784
+ const [loading, setLoading] = (0, import_react12.useState)(() => !svgCache.has(url));
2785
+ const [error, setError] = (0, import_react12.useState)(null);
2786
+ (0, import_react12.useEffect)(() => {
2787
+ if (!url) {
2788
+ setLoading(false);
2789
+ setError("No URL provided");
2790
+ setSvgContent(null);
2791
+ setSvgViewBox(null);
2792
+ return;
2793
+ }
2794
+ let cancelled = false;
2795
+ const cached = svgCache.get(url);
2796
+ if (cached) {
2797
+ setSvgContent(cached.innerContent);
2798
+ setSvgViewBox(cached.viewBox);
2799
+ setLoading(false);
2800
+ setError(null);
2801
+ onLoad?.();
2802
+ return;
2803
+ }
2804
+ setLoading(true);
2805
+ setError(null);
2806
+ setSvgContent(null);
2807
+ setSvgViewBox(null);
2808
+ (async () => {
2809
+ try {
2810
+ const response = await fetch(url);
2811
+ if (!response.ok) {
2812
+ throw new Error(
2813
+ `Failed to fetch SVG: ${response.status} ${response.statusText}`
2814
+ );
2815
+ }
2816
+ const contentType = response.headers.get("content-type") ?? "";
2817
+ const text = await response.text();
2818
+ if (!text.includes("<svg") && !contentType.includes("svg")) {
2819
+ throw new Error("Response is not an SVG");
2820
+ }
2821
+ const viewBoxMatch = text.match(/viewBox="([^"]*)"/);
2822
+ const viewBox = viewBoxMatch ? viewBoxMatch[1] : null;
2823
+ const svgMatch = text.match(/<svg[^>]*>([\s\S]*?)<\/svg>/);
2824
+ const innerContent = svgMatch ? svgMatch[1] : text;
2825
+ svgCache.set(url, { innerContent, viewBox });
2826
+ if (!cancelled) {
2827
+ setSvgContent(innerContent);
2828
+ setSvgViewBox(viewBox);
2829
+ setLoading(false);
2830
+ onLoad?.();
2831
+ }
2832
+ } catch (err) {
2833
+ if (!cancelled) {
2834
+ const message = err instanceof Error ? err.message : "Failed to load SVG";
2835
+ setError(message);
2836
+ setLoading(false);
2837
+ onError?.(message);
2838
+ }
2839
+ }
2840
+ })();
2841
+ return () => {
2842
+ cancelled = true;
2843
+ };
2844
+ }, [url]);
2845
+ return { loading, error, svgContent, svgViewBox };
2846
+ }
2847
+ var DEFAULT_SIZE2 = 24;
2848
+ function DynamicSvgIcon({
2849
+ url,
2850
+ fallback,
2851
+ width = DEFAULT_SIZE2,
2852
+ height = DEFAULT_SIZE2,
2853
+ skeletonVariant = "circular",
2854
+ skeletonAnimation = "pulse",
2855
+ onLoad,
2856
+ onError,
2857
+ sx,
2858
+ ...svgIconProps
2859
+ }) {
2860
+ const { loading, error, svgContent, svgViewBox } = useDynamicSvgIcon(url, {
2861
+ onLoad,
2862
+ onError
2863
+ });
2864
+ if (loading) {
2865
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2866
+ import_material23.Skeleton,
2867
+ {
2868
+ variant: skeletonVariant,
2869
+ animation: skeletonAnimation,
2870
+ sx: { width, height }
2871
+ }
2872
+ );
2873
+ }
2874
+ if (error || !svgContent) {
2875
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2876
+ import_material23.Box,
2877
+ {
2878
+ sx: {
2879
+ width,
2880
+ height,
2881
+ display: "inline-flex",
2882
+ alignItems: "center",
2883
+ justifyContent: "center",
2884
+ overflow: "hidden",
2885
+ flexShrink: 0,
2886
+ "& > svg, & > .MuiSvgIcon-root": {
2887
+ width: "100%",
2888
+ height: "100%"
2889
+ }
2890
+ },
2891
+ children: fallback
2892
+ }
2893
+ );
2894
+ }
2895
+ return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
2896
+ import_material23.SvgIcon,
2897
+ {
2898
+ ...svgIconProps,
2899
+ ...svgViewBox && { viewBox: svgViewBox },
2900
+ sx: {
2901
+ ...typeof sx === "object" && sx !== null && !Array.isArray(sx) ? sx : void 0,
2902
+ width,
2903
+ height
2904
+ },
2905
+ children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("g", { dangerouslySetInnerHTML: { __html: svgContent } })
2906
+ }
2907
+ );
2908
+ }
2762
2909
  // Annotate the CommonJS export names for ESM import in node:
2763
2910
  0 && (module.exports = {
2764
2911
  BIAMP_TABLE_DEBOUNCE_DELAY,
@@ -2799,10 +2946,12 @@ var getInitials = (name) => {
2799
2946
  BiampTableToolbarSearch,
2800
2947
  BiampTableTruncatedCell,
2801
2948
  BiampWrapper,
2949
+ DynamicSvgIcon,
2802
2950
  SegmentedButton,
2803
2951
  SegmentedButtonGroup,
2804
2952
  UserInitialsIcon,
2805
2953
  buildCsvString,
2954
+ clearDynamicSvgIconCache,
2806
2955
  exportToCsv,
2807
2956
  getColumnVisibilityDirtyCount,
2808
2957
  getDefaultColumnVisibility,
@@ -2815,6 +2964,7 @@ var getInitials = (name) => {
2815
2964
  sortingToOrder,
2816
2965
  toVisibilityState,
2817
2966
  useBiampServerSideTable,
2818
- useDebouncedCallback
2967
+ useDebouncedCallback,
2968
+ useDynamicSvgIcon
2819
2969
  });
2820
2970
  //# sourceMappingURL=index.cjs.map