@particle-academy/react-fancy 6.7.2 → 6.8.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.
@@ -1,5 +1,6 @@
1
+ import { Button } from './chunk-4GPH7HOR.js';
1
2
  import { cn } from './chunk-MMW2JGKZ.js';
2
- import { forwardRef, useMemo } from 'react';
3
+ import { forwardRef, useMemo, useState, useRef, useEffect, useCallback } from 'react';
3
4
  import { jsxs, jsx } from 'react/jsx-runtime';
4
5
 
5
6
  // src/components/QrCode/qr-tables.ts
@@ -594,6 +595,29 @@ function encodeQr(value, options = {}) {
594
595
  mask: best.mask
595
596
  };
596
597
  }
598
+
599
+ // src/components/QrCode/qr-path.ts
600
+ function modulePath(modules, quietZone) {
601
+ const size = modules.length;
602
+ const parts = [];
603
+ for (let row = 0; row < size; row += 1) {
604
+ const line = modules[row];
605
+ let col = 0;
606
+ while (col < size) {
607
+ if (!line[col]) {
608
+ col += 1;
609
+ continue;
610
+ }
611
+ let run = 1;
612
+ while (col + run < size && line[col + run]) {
613
+ run += 1;
614
+ }
615
+ parts.push(`M${col + quietZone} ${row + quietZone}h${run}v1h-${run}z`);
616
+ col += run;
617
+ }
618
+ }
619
+ return parts.join("");
620
+ }
597
621
  var QrCode = forwardRef(
598
622
  ({
599
623
  value,
@@ -613,26 +637,7 @@ var QrCode = forwardRef(
613
637
  [value, errorCorrection, mode, version]
614
638
  );
615
639
  const extent = symbol.size + quietZone * 2;
616
- const path = useMemo(() => {
617
- const parts = [];
618
- for (let row = 0; row < symbol.size; row += 1) {
619
- const line = symbol.modules[row];
620
- let col = 0;
621
- while (col < symbol.size) {
622
- if (!line[col]) {
623
- col += 1;
624
- continue;
625
- }
626
- let run = 1;
627
- while (col + run < symbol.size && line[col + run]) {
628
- run += 1;
629
- }
630
- parts.push(`M${col + quietZone} ${row + quietZone}h${run}v1h-${run}z`);
631
- col += run;
632
- }
633
- }
634
- return parts.join("");
635
- }, [symbol, quietZone]);
640
+ const path = useMemo(() => modulePath(symbol.modules, quietZone), [symbol, quietZone]);
636
641
  return /* @__PURE__ */ jsxs(
637
642
  "svg",
638
643
  {
@@ -655,6 +660,383 @@ var QrCode = forwardRef(
655
660
  );
656
661
  QrCode.displayName = "QrCode";
657
662
 
658
- export { QrCode, encodeQr, selectMode };
659
- //# sourceMappingURL=chunk-RLE5WYY6.js.map
660
- //# sourceMappingURL=chunk-RLE5WYY6.js.map
663
+ // src/components/QrCode/qr-export.ts
664
+ var DEFAULT_PIXELS_PER_MODULE = 10;
665
+ var MAX_PNG_SIDE = 8192;
666
+ var WHITE = "#ffffff";
667
+ var BLACK = "#000000";
668
+ function fileBackground(colour) {
669
+ return colour === void 0 || colour === "" || colour === "transparent" || colour === "none" ? WHITE : colour;
670
+ }
671
+ function fileForeground(colour) {
672
+ return colour === void 0 || colour === "" || colour === "currentColor" ? BLACK : colour;
673
+ }
674
+ var REPLACEMENT = String.fromCharCode(65533);
675
+ function xmlChars(text) {
676
+ let out = "";
677
+ for (const char of text) {
678
+ const code = char.charCodeAt(0);
679
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
680
+ continue;
681
+ }
682
+ out += char.length === 1 && code >= 55296 && code <= 57343 ? REPLACEMENT : char;
683
+ }
684
+ return out;
685
+ }
686
+ function xml(text) {
687
+ return xmlChars(text).replace(/[&<>"']/g, (char) => {
688
+ switch (char) {
689
+ case "&":
690
+ return "&amp;";
691
+ case "<":
692
+ return "&lt;";
693
+ case ">":
694
+ return "&gt;";
695
+ case '"':
696
+ return "&quot;";
697
+ default:
698
+ return "&#39;";
699
+ }
700
+ });
701
+ }
702
+ function symbolFor(value, options) {
703
+ const symbol = encodeQr(value, {
704
+ errorCorrection: options.errorCorrection,
705
+ mode: options.mode,
706
+ version: options.version
707
+ });
708
+ const quietZone = Math.max(0, Math.floor(options.quietZone ?? 4));
709
+ return { symbol, quietZone, extent: symbol.size + quietZone * 2 };
710
+ }
711
+ function qrCodeSvg(value, options) {
712
+ const { symbol, quietZone, extent } = symbolFor(value, options);
713
+ const size = options.size !== void 0 && Number.isFinite(options.size) && options.size > 0 ? options.size : extent * DEFAULT_PIXELS_PER_MODULE;
714
+ const label = xml(options.label);
715
+ return `<?xml version="1.0" encoding="UTF-8"?>
716
+ <svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${extent} ${extent}" shape-rendering="crispEdges" role="img" aria-label="${label}"><title>${label}</title><rect width="${extent}" height="${extent}" fill="${xml(fileBackground(options.background))}"/><path d="${modulePath(symbol.modules, quietZone)}" fill="${xml(fileForeground(options.foreground))}"/></svg>
717
+ `;
718
+ }
719
+ function pngScale(scale, extent) {
720
+ const wanted = scale === void 0 || Number.isNaN(scale) ? DEFAULT_PIXELS_PER_MODULE : Math.floor(scale);
721
+ const ceiling = Math.max(1, Math.floor(MAX_PNG_SIDE / extent));
722
+ return Math.min(Math.max(1, wanted), ceiling);
723
+ }
724
+ function qrCodePng(value, options) {
725
+ const { symbol, quietZone, extent } = symbolFor(value, options);
726
+ const scale = pngScale(options.scale, extent);
727
+ const canvas = options.canvas ?? document.createElement("canvas");
728
+ canvas.width = extent * scale;
729
+ canvas.height = extent * scale;
730
+ const ctx = canvas.getContext("2d");
731
+ if (!ctx) {
732
+ return Promise.reject(new Error("Canvas 2D is unavailable, so no PNG can be drawn"));
733
+ }
734
+ ctx.imageSmoothingEnabled = false;
735
+ ctx.fillStyle = WHITE;
736
+ ctx.fillStyle = fileBackground(options.background);
737
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
738
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
739
+ ctx.fillStyle = BLACK;
740
+ ctx.fillStyle = fileForeground(options.foreground);
741
+ ctx.fill(new Path2D(modulePath(symbol.modules, quietZone)));
742
+ return new Promise((resolve, reject) => {
743
+ canvas.toBlob((blob) => {
744
+ if (blob) {
745
+ resolve(blob);
746
+ } else {
747
+ reject(new Error("The browser produced no PNG for this canvas"));
748
+ }
749
+ }, "image/png");
750
+ });
751
+ }
752
+ function safeFileName(name, fallback) {
753
+ const replaced = name.slice(0, 1e3).replace(/[^A-Za-z0-9._-]+/g, "-");
754
+ let start = 0;
755
+ let end = replaced.length;
756
+ while (start < end && (replaced[start] === "." || replaced[start] === "-")) {
757
+ start += 1;
758
+ }
759
+ while (end > start && (replaced[end - 1] === "." || replaced[end - 1] === "-")) {
760
+ end -= 1;
761
+ }
762
+ const trimmed = replaced.slice(start, Math.min(end, start + 100));
763
+ return trimmed === "" ? fallback : trimmed;
764
+ }
765
+ var DEFAULT_ACTIONS = ["download-svg", "download-png", "share", "print"];
766
+ var DEFAULT_LABELS = {
767
+ downloadSvg: "Download SVG",
768
+ downloadPng: "Download PNG",
769
+ share: "Share",
770
+ copyLink: "Copy link",
771
+ copy: "Copy",
772
+ print: "Print",
773
+ downloaded: "Downloaded",
774
+ shared: "Shared",
775
+ copied: "Copied",
776
+ printing: "Opened the print dialog",
777
+ failed: "That did not work"
778
+ };
779
+ var ICONS = {
780
+ "download-svg": "download",
781
+ "download-png": "download",
782
+ share: "share-2",
783
+ print: "printer"
784
+ };
785
+ function isWebUrl(value) {
786
+ try {
787
+ const url = new URL(value);
788
+ return url.protocol === "http:" || url.protocol === "https:";
789
+ } catch {
790
+ return false;
791
+ }
792
+ }
793
+ function message(error) {
794
+ return error instanceof Error ? error.message : String(error);
795
+ }
796
+ function isAbort(error) {
797
+ return typeof error === "object" && error !== null && error.name === "AbortError";
798
+ }
799
+ function saveBlob(blob, fileName) {
800
+ const url = URL.createObjectURL(blob);
801
+ const anchor = document.createElement("a");
802
+ anchor.href = url;
803
+ anchor.download = fileName;
804
+ anchor.rel = "noopener";
805
+ anchor.click();
806
+ setTimeout(() => URL.revokeObjectURL(url), 0);
807
+ }
808
+ var PRINT_FRAME = "data-react-fancy-qr-code-print";
809
+ var QrCodePanel = forwardRef(
810
+ ({
811
+ value,
812
+ label,
813
+ size = 192,
814
+ errorCorrection = "M",
815
+ quietZone = 4,
816
+ foreground = "#000000",
817
+ background = "#ffffff",
818
+ mode,
819
+ version,
820
+ actions = DEFAULT_ACTIONS,
821
+ showValue = false,
822
+ fileName = "qr-code",
823
+ pngScale: pngScale2,
824
+ shareTitle,
825
+ printSizeMm = 50,
826
+ labels: labelOverrides,
827
+ onAction,
828
+ buttonSize = "lg",
829
+ className,
830
+ ...props
831
+ }, ref) => {
832
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
833
+ const [status, setStatus] = useState("");
834
+ const [via, setVia] = useState("clipboard");
835
+ const frameRef = useRef(null);
836
+ useEffect(() => {
837
+ setVia(typeof navigator !== "undefined" && typeof navigator.share === "function" ? "share" : "clipboard");
838
+ }, []);
839
+ useEffect(
840
+ () => () => {
841
+ frameRef.current?.remove();
842
+ frameRef.current = null;
843
+ },
844
+ []
845
+ );
846
+ const file = { label, errorCorrection, mode, version, quietZone, foreground, background };
847
+ const base = safeFileName(fileName, "qr-code");
848
+ const link = isWebUrl(value);
849
+ const report = useCallback(
850
+ (event, announcement) => {
851
+ setStatus(announcement);
852
+ onAction?.(event);
853
+ },
854
+ [onAction]
855
+ );
856
+ const downloadSvg = () => {
857
+ const name = `${base}.svg`;
858
+ try {
859
+ saveBlob(new Blob([qrCodeSvg(value, file)], { type: "image/svg+xml" }), name);
860
+ report({ action: "download-svg", outcome: "done", fileName: name }, labels.downloaded);
861
+ } catch (error) {
862
+ report({ action: "download-svg", outcome: "failed", error: message(error) }, labels.failed);
863
+ }
864
+ };
865
+ const downloadPng = async () => {
866
+ const name = `${base}.png`;
867
+ try {
868
+ const blob = await qrCodePng(value, { ...file, scale: pngScale2 });
869
+ saveBlob(blob, name);
870
+ report({ action: "download-png", outcome: "done", fileName: name }, labels.downloaded);
871
+ } catch (error) {
872
+ report({ action: "download-png", outcome: "failed", error: message(error) }, labels.failed);
873
+ }
874
+ };
875
+ const copy = async () => {
876
+ try {
877
+ const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
878
+ if (!clipboard || typeof clipboard.writeText !== "function") {
879
+ throw new Error("The Clipboard API is unavailable");
880
+ }
881
+ await clipboard.writeText(value);
882
+ report({ action: "share", outcome: "done", via: "clipboard" }, labels.copied);
883
+ } catch (error) {
884
+ report({ action: "share", outcome: "failed", via: "clipboard", error: message(error) }, labels.failed);
885
+ }
886
+ };
887
+ const share = async () => {
888
+ const title = shareTitle ?? label;
889
+ const data = link ? { title, url: value } : { title, text: value };
890
+ const canShare = typeof navigator !== "undefined" && typeof navigator.share === "function" && (typeof navigator.canShare !== "function" || navigator.canShare(data));
891
+ if (!canShare) {
892
+ await copy();
893
+ return;
894
+ }
895
+ try {
896
+ await navigator.share(data);
897
+ report({ action: "share", outcome: "done", via: "share" }, labels.shared);
898
+ } catch (error) {
899
+ if (isAbort(error)) {
900
+ report({ action: "share", outcome: "cancelled", via: "share" }, "");
901
+ return;
902
+ }
903
+ await copy();
904
+ }
905
+ };
906
+ const print = () => {
907
+ frameRef.current?.remove();
908
+ document.querySelectorAll(`iframe[${PRINT_FRAME}]`).forEach((stale) => stale.remove());
909
+ const frame = document.createElement("iframe");
910
+ frame.setAttribute(PRINT_FRAME, "");
911
+ frame.setAttribute("aria-hidden", "true");
912
+ frame.tabIndex = -1;
913
+ Object.assign(frame.style, { position: "fixed", right: "0", bottom: "0", width: "0", height: "0", border: "0" });
914
+ document.body.appendChild(frame);
915
+ frameRef.current = frame;
916
+ const doc = frame.contentDocument;
917
+ const win = frame.contentWindow;
918
+ if (!doc || !win) {
919
+ report({ action: "print", outcome: "failed", error: "The print frame has no document" }, labels.failed);
920
+ return;
921
+ }
922
+ doc.title = label;
923
+ Object.assign(doc.body.style, {
924
+ margin: "0",
925
+ padding: "15mm 10mm",
926
+ display: "flex",
927
+ flexDirection: "column",
928
+ alignItems: "center",
929
+ gap: "6mm",
930
+ color: "#000000",
931
+ background: "#ffffff",
932
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
933
+ });
934
+ const parsed = new DOMParser().parseFromString(qrCodeSvg(value, file), "image/svg+xml");
935
+ const svg = doc.importNode(parsed.documentElement, true);
936
+ const millimetres = Number.isFinite(printSizeMm) && printSizeMm > 0 ? printSizeMm : 50;
937
+ svg.setAttribute("width", `${millimetres}mm`);
938
+ svg.setAttribute("height", `${millimetres}mm`);
939
+ doc.body.appendChild(svg);
940
+ if (showValue) {
941
+ const text2 = doc.createElement("p");
942
+ text2.textContent = value;
943
+ Object.assign(text2.style, {
944
+ margin: "0",
945
+ maxWidth: `${Math.max(millimetres, 80)}mm`,
946
+ fontSize: "12pt",
947
+ textAlign: "center",
948
+ overflowWrap: "anywhere"
949
+ });
950
+ doc.body.appendChild(text2);
951
+ }
952
+ win.addEventListener("afterprint", () => {
953
+ if (frameRef.current === frame) {
954
+ frameRef.current = null;
955
+ }
956
+ frame.remove();
957
+ });
958
+ setTimeout(() => {
959
+ try {
960
+ win.print();
961
+ report({ action: "print", outcome: "done" }, labels.printing);
962
+ } catch (error) {
963
+ report({ action: "print", outcome: "failed", error: message(error) }, labels.failed);
964
+ }
965
+ }, 0);
966
+ };
967
+ const run = {
968
+ "download-svg": downloadSvg,
969
+ "download-png": () => void downloadPng(),
970
+ share: () => void share(),
971
+ print
972
+ };
973
+ const text = {
974
+ "download-svg": labels.downloadSvg,
975
+ "download-png": labels.downloadPng,
976
+ share: via === "share" ? labels.share : link ? labels.copyLink : labels.copy,
977
+ print: labels.print
978
+ };
979
+ return /* @__PURE__ */ jsxs(
980
+ "div",
981
+ {
982
+ ref,
983
+ role: "group",
984
+ "aria-label": label,
985
+ "data-react-fancy-qr-code-panel": "",
986
+ className: cn("inline-flex max-w-full flex-col items-center gap-3", className),
987
+ ...props,
988
+ children: [
989
+ /* @__PURE__ */ jsx("div", { "data-react-fancy-qr-code-panel-code": "", className: "leading-none", children: /* @__PURE__ */ jsx(
990
+ QrCode,
991
+ {
992
+ value,
993
+ label,
994
+ size,
995
+ errorCorrection,
996
+ quietZone,
997
+ foreground,
998
+ background,
999
+ mode,
1000
+ version
1001
+ }
1002
+ ) }),
1003
+ showValue && /* @__PURE__ */ jsx(
1004
+ "p",
1005
+ {
1006
+ "data-react-fancy-qr-code-panel-value": "",
1007
+ className: "m-0 max-w-full select-all break-all text-center font-mono text-sm text-zinc-900 dark:text-zinc-100",
1008
+ children: value
1009
+ }
1010
+ ),
1011
+ actions.length > 0 && /* @__PURE__ */ jsx("div", { "data-react-fancy-qr-code-panel-actions": "", className: "flex flex-wrap justify-center gap-2", children: actions.map((action) => /* @__PURE__ */ jsx(
1012
+ Button,
1013
+ {
1014
+ size: buttonSize,
1015
+ icon: action === "share" && via === "clipboard" ? "copy" : ICONS[action],
1016
+ "data-react-fancy-qr-code-action": action,
1017
+ "data-via": action === "share" ? via : void 0,
1018
+ onClick: run[action],
1019
+ children: text[action]
1020
+ },
1021
+ action
1022
+ )) }),
1023
+ /* @__PURE__ */ jsx(
1024
+ "span",
1025
+ {
1026
+ role: "status",
1027
+ "aria-live": "polite",
1028
+ "data-react-fancy-qr-code-panel-status": "",
1029
+ className: "sr-only",
1030
+ children: status
1031
+ }
1032
+ )
1033
+ ]
1034
+ }
1035
+ );
1036
+ }
1037
+ );
1038
+ QrCodePanel.displayName = "QrCodePanel";
1039
+
1040
+ export { QrCode, QrCodePanel, encodeQr, qrCodePng, qrCodeSvg, selectMode };
1041
+ //# sourceMappingURL=chunk-PIG3UTHU.js.map
1042
+ //# sourceMappingURL=chunk-PIG3UTHU.js.map