uilint-react 0.1.23 → 0.1.25

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,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-XPAUSE5Z.js";
4
+ } from "./chunk-45MPASAN.js";
5
5
 
6
6
  // src/components/ui-lint/UILintToolbar.tsx
7
7
  import { useState, useRef, useEffect } from "react";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  useUILintContext
4
- } from "./chunk-XPAUSE5Z.js";
4
+ } from "./chunk-45MPASAN.js";
5
5
 
6
6
  // src/components/ui-lint/LocatorOverlay.tsx
7
7
  import { useState, useEffect, useMemo } from "react";
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  buildEditorUrl,
4
4
  useUILintContext
5
- } from "./chunk-XPAUSE5Z.js";
5
+ } from "./chunk-45MPASAN.js";
6
6
 
7
7
  // src/components/ui-lint/InspectionPanel.tsx
8
- import { useState, useEffect, useCallback } from "react";
8
+ import { useState, useEffect, useCallback, useMemo } from "react";
9
9
  import { createPortal } from "react-dom";
10
10
 
11
11
  // src/components/ui-lint/source-fetcher.ts
@@ -558,12 +558,58 @@ function SourceTab({ element }) {
558
558
  ] });
559
559
  }
560
560
  function ScanTab({ element }) {
561
+ const { elementIssuesCache, autoScanState } = useUILintContext();
561
562
  const [scanning, setScanning] = useState(false);
562
563
  const [error, setError] = useState(null);
563
564
  const [fixPrompt, setFixPrompt] = useState(null);
564
565
  const [copied, setCopied] = useState(false);
565
566
  const componentName = element.componentStack[0]?.name || element.element.tagName.toLowerCase();
566
567
  const componentLine = element.source?.lineNumber;
568
+ const cachedIssue = useMemo(() => {
569
+ if (element.scannedElementId) {
570
+ const cached = elementIssuesCache.get(element.scannedElementId);
571
+ if (cached) return cached;
572
+ }
573
+ if (element.source) {
574
+ for (const [, issue] of elementIssuesCache) {
575
+ const scannedElement = autoScanState.elements.find(
576
+ (el) => el.id === issue.elementId
577
+ );
578
+ if (scannedElement?.source?.fileName === element.source.fileName) {
579
+ return issue;
580
+ }
581
+ }
582
+ }
583
+ return null;
584
+ }, [
585
+ element.scannedElementId,
586
+ element.source,
587
+ elementIssuesCache,
588
+ autoScanState.elements
589
+ ]);
590
+ const generateFixPrompt = useCallback(
591
+ (issues, relativePath) => {
592
+ if (issues.length === 0) {
593
+ return `No style issues found in the \`${componentName}\` component in \`${relativePath}\`. The component appears to follow the styleguide.`;
594
+ }
595
+ const issueList = issues.map((issue) => {
596
+ const lineInfo = issue.line ? `Line ${issue.line}: ` : "";
597
+ return `- ${lineInfo}${issue.message}`;
598
+ }).join("\n");
599
+ return `Fix the following style issues in the \`${componentName}\` component in \`${relativePath}\`:
600
+
601
+ Issues found:
602
+ ${issueList}
603
+
604
+ Please update this component to match our styleguide.`;
605
+ },
606
+ [componentName]
607
+ );
608
+ const cachedFixPrompt = useMemo(() => {
609
+ if (!cachedIssue || cachedIssue.status !== "complete") return null;
610
+ const relativePath = element.source?.fileName || "unknown";
611
+ return generateFixPrompt(cachedIssue.issues, relativePath);
612
+ }, [cachedIssue, element.source, generateFixPrompt]);
567
613
  const handleScan = useCallback(async () => {
568
614
  if (!element.source) {
569
615
  setError("No source information available");
@@ -574,7 +620,9 @@ function ScanTab({ element }) {
574
620
  setFixPrompt(null);
575
621
  try {
576
622
  const sourceResponse = await fetch(
577
- `/api/.uilint/source?path=${encodeURIComponent(element.source.fileName)}`
623
+ `/api/.uilint/source?path=${encodeURIComponent(
624
+ element.source.fileName
625
+ )}`
578
626
  );
579
627
  if (!sourceResponse.ok) {
580
628
  throw new Error("Failed to fetch source code");
@@ -597,24 +645,7 @@ function ScanTab({ element }) {
597
645
  }
598
646
  const result = await analyzeResponse.json();
599
647
  const issues = result.issues || [];
600
- if (issues.length === 0) {
601
- setFixPrompt(
602
- `No style issues found in the \`${componentName}\` component in \`${relativePath}\`. The component appears to follow the styleguide.`
603
- );
604
- } else {
605
- const issueList = issues.map((issue) => {
606
- const lineInfo = issue.line ? `Line ${issue.line}: ` : "";
607
- return `- ${lineInfo}${issue.message}`;
608
- }).join("\n");
609
- setFixPrompt(
610
- `Fix the following style issues in the \`${componentName}\` component in \`${relativePath}\`:
611
-
612
- Issues found:
613
- ${issueList}
614
-
615
- Please update this component to match our styleguide.`
616
- );
617
- }
648
+ setFixPrompt(generateFixPrompt(issues, relativePath));
618
649
  } catch (err) {
619
650
  setError(
620
651
  err instanceof Error ? err.message : "An error occurred during scanning"
@@ -622,19 +653,282 @@ Please update this component to match our styleguide.`
622
653
  } finally {
623
654
  setScanning(false);
624
655
  }
625
- }, [element.source, componentName, componentLine]);
626
- const handleCopy = useCallback(async () => {
627
- if (!fixPrompt) return;
656
+ }, [element.source, componentName, componentLine, generateFixPrompt]);
657
+ const handleCopy = useCallback(async (text) => {
628
658
  try {
629
- await navigator.clipboard.writeText(fixPrompt);
659
+ await navigator.clipboard.writeText(text);
630
660
  setCopied(true);
631
661
  setTimeout(() => setCopied(false), 2e3);
632
662
  } catch {
633
663
  setError("Failed to copy to clipboard");
634
664
  }
635
- }, [fixPrompt]);
665
+ }, []);
666
+ const showCachedScanning = cachedIssue?.status === "scanning" && !scanning && !fixPrompt;
667
+ const showCachedPending = cachedIssue?.status === "pending" && !scanning && !fixPrompt;
668
+ const showCachedError = cachedIssue?.status === "error" && !scanning && !fixPrompt;
669
+ const showCachedResult = cachedFixPrompt && !fixPrompt && !scanning;
670
+ const showScanButton = !cachedIssue && !fixPrompt && !scanning;
671
+ const showManualResult = fixPrompt && !scanning;
636
672
  return /* @__PURE__ */ jsxs("div", { style: { padding: "16px" }, children: [
637
- !fixPrompt && !scanning && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "24px 0" }, children: [
673
+ showCachedScanning && /* @__PURE__ */ jsxs(
674
+ "div",
675
+ {
676
+ style: {
677
+ display: "flex",
678
+ flexDirection: "column",
679
+ alignItems: "center",
680
+ justifyContent: "center",
681
+ padding: "48px 24px",
682
+ gap: "16px"
683
+ },
684
+ children: [
685
+ /* @__PURE__ */ jsx(
686
+ "div",
687
+ {
688
+ style: {
689
+ width: "32px",
690
+ height: "32px",
691
+ border: `3px solid ${STYLES.border}`,
692
+ borderTopColor: STYLES.success,
693
+ borderRadius: "50%",
694
+ animation: "uilint-spin 1s linear infinite"
695
+ }
696
+ }
697
+ ),
698
+ /* @__PURE__ */ jsx("div", { style: { color: STYLES.textMuted, fontSize: "13px" }, children: "Auto-scan in progress..." })
699
+ ]
700
+ }
701
+ ),
702
+ showCachedPending && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "24px 0" }, children: [
703
+ /* @__PURE__ */ jsxs(
704
+ "div",
705
+ {
706
+ style: {
707
+ display: "inline-flex",
708
+ alignItems: "center",
709
+ gap: "8px",
710
+ padding: "12px 24px",
711
+ borderRadius: "8px",
712
+ backgroundColor: STYLES.bgSurface,
713
+ color: STYLES.textMuted,
714
+ fontSize: "13px"
715
+ },
716
+ children: [
717
+ /* @__PURE__ */ jsx(
718
+ "div",
719
+ {
720
+ style: {
721
+ width: "8px",
722
+ height: "8px",
723
+ borderRadius: "50%",
724
+ backgroundColor: "rgba(156, 163, 175, 0.5)"
725
+ }
726
+ }
727
+ ),
728
+ "Waiting in scan queue..."
729
+ ]
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx("div", { style: { marginTop: "16px" }, children: /* @__PURE__ */ jsx(
733
+ "button",
734
+ {
735
+ onClick: handleScan,
736
+ disabled: !element.source,
737
+ style: {
738
+ padding: "8px 16px",
739
+ borderRadius: "6px",
740
+ border: `1px solid ${STYLES.border}`,
741
+ backgroundColor: "transparent",
742
+ color: STYLES.textMuted,
743
+ fontSize: "12px",
744
+ cursor: element.source ? "pointer" : "not-allowed",
745
+ transition: "all 0.15s"
746
+ },
747
+ onMouseEnter: (e) => {
748
+ if (element.source) {
749
+ e.currentTarget.style.borderColor = STYLES.accent;
750
+ e.currentTarget.style.color = STYLES.text;
751
+ }
752
+ },
753
+ onMouseLeave: (e) => {
754
+ e.currentTarget.style.borderColor = STYLES.border;
755
+ e.currentTarget.style.color = STYLES.textMuted;
756
+ },
757
+ children: "Scan Now"
758
+ }
759
+ ) })
760
+ ] }),
761
+ showCachedError && /* @__PURE__ */ jsxs("div", { children: [
762
+ /* @__PURE__ */ jsx(
763
+ "div",
764
+ {
765
+ style: {
766
+ padding: "16px",
767
+ backgroundColor: "rgba(239, 68, 68, 0.1)",
768
+ border: "1px solid rgba(239, 68, 68, 0.3)",
769
+ borderRadius: "8px",
770
+ color: "#EF4444",
771
+ fontSize: "13px",
772
+ marginBottom: "16px"
773
+ },
774
+ children: "Auto-scan failed for this element"
775
+ }
776
+ ),
777
+ /* @__PURE__ */ jsx("div", { style: { textAlign: "center" }, children: /* @__PURE__ */ jsxs(
778
+ "button",
779
+ {
780
+ onClick: handleScan,
781
+ disabled: !element.source,
782
+ style: {
783
+ display: "inline-flex",
784
+ alignItems: "center",
785
+ gap: "8px",
786
+ padding: "10px 20px",
787
+ borderRadius: "8px",
788
+ border: "none",
789
+ backgroundColor: element.source ? STYLES.success : STYLES.textDim,
790
+ color: "#FFFFFF",
791
+ fontSize: "13px",
792
+ fontWeight: 600,
793
+ cursor: element.source ? "pointer" : "not-allowed"
794
+ },
795
+ children: [
796
+ /* @__PURE__ */ jsx(ScanIcon, {}),
797
+ "Retry Scan"
798
+ ]
799
+ }
800
+ ) })
801
+ ] }),
802
+ showCachedResult && /* @__PURE__ */ jsxs("div", { children: [
803
+ /* @__PURE__ */ jsxs(
804
+ "div",
805
+ {
806
+ style: {
807
+ display: "flex",
808
+ alignItems: "center",
809
+ gap: "8px",
810
+ marginBottom: "12px",
811
+ padding: "8px 12px",
812
+ backgroundColor: "rgba(16, 185, 129, 0.1)",
813
+ borderRadius: "6px",
814
+ fontSize: "11px",
815
+ color: STYLES.success
816
+ },
817
+ children: [
818
+ /* @__PURE__ */ jsx(CheckIconSmall, {}),
819
+ "Results from auto-scan"
820
+ ]
821
+ }
822
+ ),
823
+ /* @__PURE__ */ jsxs(
824
+ "div",
825
+ {
826
+ style: {
827
+ display: "flex",
828
+ alignItems: "center",
829
+ justifyContent: "space-between",
830
+ marginBottom: "12px"
831
+ },
832
+ children: [
833
+ /* @__PURE__ */ jsx(
834
+ "div",
835
+ {
836
+ style: {
837
+ fontSize: "12px",
838
+ fontWeight: 600,
839
+ color: STYLES.text
840
+ },
841
+ children: "Fix Prompt"
842
+ }
843
+ ),
844
+ /* @__PURE__ */ jsx(
845
+ "button",
846
+ {
847
+ onClick: () => handleCopy(cachedFixPrompt),
848
+ style: {
849
+ display: "flex",
850
+ alignItems: "center",
851
+ gap: "6px",
852
+ padding: "6px 12px",
853
+ borderRadius: "6px",
854
+ border: "none",
855
+ backgroundColor: copied ? STYLES.success : STYLES.accent,
856
+ color: "#FFFFFF",
857
+ fontSize: "11px",
858
+ fontWeight: 500,
859
+ cursor: "pointer",
860
+ transition: "all 0.15s"
861
+ },
862
+ children: copied ? /* @__PURE__ */ jsxs(Fragment, { children: [
863
+ /* @__PURE__ */ jsx(CheckIcon, {}),
864
+ "Copied!"
865
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
866
+ /* @__PURE__ */ jsx(CopyIcon, {}),
867
+ "Copy to Clipboard"
868
+ ] })
869
+ }
870
+ )
871
+ ]
872
+ }
873
+ ),
874
+ /* @__PURE__ */ jsx(
875
+ "div",
876
+ {
877
+ style: {
878
+ padding: "12px",
879
+ backgroundColor: STYLES.bgSurface,
880
+ border: `1px solid ${STYLES.border}`,
881
+ borderRadius: "8px",
882
+ fontFamily: STYLES.fontMono,
883
+ fontSize: "12px",
884
+ lineHeight: 1.6,
885
+ whiteSpace: "pre-wrap",
886
+ color: STYLES.text,
887
+ maxHeight: "300px",
888
+ overflow: "auto"
889
+ },
890
+ children: cachedFixPrompt
891
+ }
892
+ ),
893
+ /* @__PURE__ */ jsx(
894
+ "div",
895
+ {
896
+ style: {
897
+ marginTop: "12px",
898
+ fontSize: "11px",
899
+ color: STYLES.textMuted,
900
+ textAlign: "center"
901
+ },
902
+ children: "Paste this prompt into Cursor to fix the issues"
903
+ }
904
+ ),
905
+ /* @__PURE__ */ jsx("div", { style: { textAlign: "center", marginTop: "16px" }, children: /* @__PURE__ */ jsx(
906
+ "button",
907
+ {
908
+ onClick: handleScan,
909
+ style: {
910
+ padding: "8px 16px",
911
+ borderRadius: "6px",
912
+ border: `1px solid ${STYLES.border}`,
913
+ backgroundColor: "transparent",
914
+ color: STYLES.textMuted,
915
+ fontSize: "12px",
916
+ cursor: "pointer",
917
+ transition: "all 0.15s"
918
+ },
919
+ onMouseEnter: (e) => {
920
+ e.currentTarget.style.borderColor = STYLES.accent;
921
+ e.currentTarget.style.color = STYLES.text;
922
+ },
923
+ onMouseLeave: (e) => {
924
+ e.currentTarget.style.borderColor = STYLES.border;
925
+ e.currentTarget.style.color = STYLES.textMuted;
926
+ },
927
+ children: "Rescan"
928
+ }
929
+ ) })
930
+ ] }),
931
+ showScanButton && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "24px 0" }, children: [
638
932
  /* @__PURE__ */ jsxs(
639
933
  "button",
640
934
  {
@@ -731,12 +1025,13 @@ Please update this component to match our styleguide.`
731
1025
  border: "1px solid rgba(239, 68, 68, 0.3)",
732
1026
  borderRadius: "8px",
733
1027
  color: "#EF4444",
734
- fontSize: "13px"
1028
+ fontSize: "13px",
1029
+ marginTop: "16px"
735
1030
  },
736
1031
  children: error
737
1032
  }
738
1033
  ),
739
- fixPrompt && /* @__PURE__ */ jsxs("div", { children: [
1034
+ showManualResult && /* @__PURE__ */ jsxs("div", { children: [
740
1035
  /* @__PURE__ */ jsxs(
741
1036
  "div",
742
1037
  {
@@ -761,7 +1056,7 @@ Please update this component to match our styleguide.`
761
1056
  /* @__PURE__ */ jsx(
762
1057
  "button",
763
1058
  {
764
- onClick: handleCopy,
1059
+ onClick: () => handleCopy(fixPrompt),
765
1060
  style: {
766
1061
  display: "flex",
767
1062
  alignItems: "center",
@@ -850,6 +1145,18 @@ Please update this component to match our styleguide.`
850
1145
  ] })
851
1146
  ] });
852
1147
  }
1148
+ function CheckIconSmall() {
1149
+ return /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx(
1150
+ "path",
1151
+ {
1152
+ d: "M20 6L9 17l-5-5",
1153
+ stroke: "currentColor",
1154
+ strokeWidth: "2",
1155
+ strokeLinecap: "round",
1156
+ strokeLinejoin: "round"
1157
+ }
1158
+ ) });
1159
+ }
853
1160
  function Section({
854
1161
  title,
855
1162
  children
package/dist/index.d.ts CHANGED
@@ -88,6 +88,8 @@ interface InspectedElement {
88
88
  source: SourceLocation | null;
89
89
  componentStack: ComponentInfo[];
90
90
  rect: DOMRect;
91
+ /** Optional ID from auto-scan to link to cached results */
92
+ scannedElementId?: string;
91
93
  }
92
94
  /**
93
95
  * Context value provided by UILintProvider
@@ -157,6 +159,7 @@ declare const DATA_UILINT_ID = "data-ui-lint-id";
157
159
 
158
160
  /**
159
161
  * Hook to access UILint context
162
+ * For backwards compatibility - delegates to Zustand store
160
163
  */
161
164
  declare function useUILintContext(): UILintContextValue;
162
165
  /**
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import {
3
3
  UILintToolbar
4
- } from "./chunk-7M2NF7HT.js";
4
+ } from "./chunk-ORYG2TNM.js";
5
5
  import {
6
6
  InspectionPanel,
7
7
  clearSourceCache,
@@ -9,10 +9,10 @@ import {
9
9
  fetchSourceWithContext,
10
10
  getCachedSource,
11
11
  prefetchSources
12
- } from "./chunk-7BJIS7PI.js";
12
+ } from "./chunk-SIVHTQ2P.js";
13
13
  import {
14
14
  LocatorOverlay
15
- } from "./chunk-ZMGUWRAO.js";
15
+ } from "./chunk-RA27RIJ2.js";
16
16
  import {
17
17
  DATA_UILINT_ID,
18
18
  DEFAULT_SETTINGS,
@@ -31,7 +31,7 @@ import {
31
31
  scanDOMForSources,
32
32
  updateElementRects,
33
33
  useUILintContext
34
- } from "./chunk-XPAUSE5Z.js";
34
+ } from "./chunk-45MPASAN.js";
35
35
 
36
36
  // src/consistency/snapshot.ts
37
37
  var DATA_ELEMENTS_ATTR = "data-elements";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uilint-react",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "React component for AI-powered UI consistency checking",
5
5
  "author": "Peter Suggate",
6
6
  "repository": {
@@ -34,7 +34,8 @@
34
34
  "node": ">=20.0.0"
35
35
  },
36
36
  "dependencies": {
37
- "uilint-core": "^0.1.23"
37
+ "uilint-core": "^0.1.25",
38
+ "zustand": "^5.0.5"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "react": "^19.0.0",