flet-box 1.1.2 → 1.1.3

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.
Files changed (154) hide show
  1. package/AGENTS_FOR_CODE_GEN.md +284 -0
  2. package/CONTRIBUTING.md +238 -0
  3. package/README.md +2 -2
  4. package/bin/cli.js +88 -1
  5. package/bin/commands/auditFix.js +181 -27
  6. package/bin/commands/checkApi.js +14 -3
  7. package/bin/commands/checkStructure.js +368 -0
  8. package/bin/commands/checkSyntax.js +293 -0
  9. package/bin/commands/checkUi.js +666 -14
  10. package/bin/commands/create.js +14 -0
  11. package/bin/commands/createBundle.js +2 -2
  12. package/bin/commands/metrics.js +448 -0
  13. package/bin/commands/release.js +18 -0
  14. package/bin/commands/report.js +849 -0
  15. package/bin/commands/tools.js +76 -0
  16. package/bin/commands/unpack.js +39 -11
  17. package/bin/commands/verify.js +879 -0
  18. package/bin/mcp.js +280 -0
  19. package/bin/utils/contract.js +27 -0
  20. package/bin/utils/dtsProps.js +5 -1
  21. package/bin/utils/migrationGuide.js +146 -0
  22. package/bin/utils/moduleExports.js +9 -5
  23. package/bin/utils/templates.js +225 -18
  24. package/bin/utils/toolkit.js +191 -0
  25. package/docs/README.md +5 -1
  26. package/docs/arquitectura.md +1 -1
  27. package/docs/cli/README.md +168 -2
  28. package/docs/guides/README.md +6 -0
  29. package/docs/guides/accessibility.md +2 -1
  30. package/docs/guides/ai-toolkit.md +166 -0
  31. package/docs/guides/compatibility.md +1 -1
  32. package/docs/guides/migration.md +92 -0
  33. package/docs/guides/minification-and-protection.md +2 -1
  34. package/docs/guides/mobile-and-platforms.md +1 -1
  35. package/docs/guides/online-updates.md +2 -0
  36. package/docs/server/deployment.md +1 -1
  37. package/docs/tools/styling.md +2 -2
  38. package/docs/tools/theme.md +139 -4
  39. package/docs/widget/Accordion.md +74 -8
  40. package/docs/widget/AdMob.md +68 -3
  41. package/docs/widget/AdSense.md +67 -2
  42. package/docs/widget/AlertDialog.md +1 -1
  43. package/docs/widget/AnimatedBox.md +80 -39
  44. package/docs/widget/AnimatedText.md +74 -18
  45. package/docs/widget/Audio.md +73 -14
  46. package/docs/widget/Avatar.md +69 -4
  47. package/docs/widget/Badge.md +68 -3
  48. package/docs/widget/BottomSheet.md +2 -2
  49. package/docs/widget/Button.md +69 -3
  50. package/docs/widget/Card.md +69 -4
  51. package/docs/widget/Carousel.md +67 -2
  52. package/docs/widget/Chart.md +67 -2
  53. package/docs/widget/Checkbox.md +74 -8
  54. package/docs/widget/Chip.md +68 -3
  55. package/docs/widget/CircularBar.md +74 -15
  56. package/docs/widget/CircularChart.md +71 -10
  57. package/docs/widget/CodeViewer.md +67 -2
  58. package/docs/widget/Column.md +68 -3
  59. package/docs/widget/Container.md +68 -3
  60. package/docs/widget/DataTable.md +67 -2
  61. package/docs/widget/Divider.md +67 -2
  62. package/docs/widget/DraggBox.md +67 -2
  63. package/docs/widget/Dropdown.md +76 -10
  64. package/docs/widget/DroppBox.md +67 -2
  65. package/docs/widget/FloatingActionButton.md +68 -3
  66. package/docs/widget/GridView.md +67 -2
  67. package/docs/widget/Icon.md +71 -5
  68. package/docs/widget/Image.md +68 -3
  69. package/docs/widget/Input.md +68 -3
  70. package/docs/widget/Inspector.md +2 -2
  71. package/docs/widget/InstallButton.md +73 -13
  72. package/docs/widget/ListTile.md +69 -3
  73. package/docs/widget/ListView.md +67 -2
  74. package/docs/widget/Maps.md +68 -3
  75. package/docs/widget/Markdown.md +74 -15
  76. package/docs/widget/MatrixRain.md +72 -11
  77. package/docs/widget/Menu.md +209 -0
  78. package/docs/widget/Modal.md +3 -3
  79. package/docs/widget/Pagination.md +68 -3
  80. package/docs/widget/ParallaxBox.md +72 -12
  81. package/docs/widget/ProgressBar.md +69 -4
  82. package/docs/widget/QRCode.md +67 -2
  83. package/docs/widget/README.md +54 -50
  84. package/docs/widget/Radio.md +76 -10
  85. package/docs/widget/Rating.md +70 -6
  86. package/docs/widget/Row.md +68 -3
  87. package/docs/widget/Skeleton.md +68 -3
  88. package/docs/widget/Slider.md +75 -9
  89. package/docs/widget/SnackBar.md +2 -2
  90. package/docs/widget/Stack.md +68 -3
  91. package/docs/widget/Stepper.md +68 -3
  92. package/docs/widget/Switch.md +74 -8
  93. package/docs/widget/Text.md +71 -4
  94. package/docs/widget/ThemePicker.md +197 -0
  95. package/docs/widget/Tooltip.md +72 -7
  96. package/docs/widget/TreeView.md +70 -10
  97. package/docs/widget/Video.md +71 -10
  98. package/package.json +10 -3
  99. package/skills/fletbox-design/SKILL.md +89 -5
  100. package/skills/fletbox-motion/SKILL.md +1 -1
  101. package/skills/fletbox-rules/SKILL.md +56 -4
  102. package/src/core/runApp.js +12 -0
  103. package/src/index.d.ts +374 -101
  104. package/src/index.js +9 -0
  105. package/src/navigations/AdaptiveScaffold.js +2 -2
  106. package/src/navigations/BottomNavigation.js +1 -1
  107. package/src/navigations/Scaffold.js +18 -6
  108. package/src/tools/useState.js +11 -4
  109. package/src/utils/colors.js +275 -0
  110. package/src/utils/errorBoundary.js +288 -0
  111. package/src/utils/index.js +10 -2
  112. package/src/utils/stackPosition.js +39 -24
  113. package/src/utils/syntaxHighlight.js +4 -1
  114. package/src/utils/themes.js +279 -3
  115. package/src/widget-factory/addChildren.js +17 -10
  116. package/src/widget-factory/assignProps.js +18 -6
  117. package/src/widget-factory/createWidget.js +7 -0
  118. package/src/widget-factory/deprecations.js +77 -0
  119. package/src/widget-factory/translateProps.js +11 -1
  120. package/src/widget-factory/warnings.js +1 -27
  121. package/src/widget-factory/widgetFactory.js +59 -17
  122. package/src/widgets/Accordion.js +21 -7
  123. package/src/widgets/Badge.js +11 -2
  124. package/src/widgets/Button.js +15 -3
  125. package/src/widgets/Carousel.js +12 -4
  126. package/src/widgets/Chart.js +7 -2
  127. package/src/widgets/Checkbox.js +1 -2
  128. package/src/widgets/Chip.js +24 -10
  129. package/src/widgets/CircularBar.js +9 -3
  130. package/src/widgets/CircularChart.js +5 -3
  131. package/src/widgets/Container.js +6 -0
  132. package/src/widgets/Divider.js +3 -1
  133. package/src/widgets/DraggBox.js +9 -0
  134. package/src/widgets/Dropdown.js +3 -3
  135. package/src/widgets/DroppBox.js +5 -0
  136. package/src/widgets/FloatingActionButton.js +1 -1
  137. package/src/widgets/Icon.js +5 -1
  138. package/src/widgets/InstallButton.js +4 -2
  139. package/src/widgets/Markdown.js +171 -103
  140. package/src/widgets/Menu.js +340 -0
  141. package/src/widgets/Modal.js +7 -1
  142. package/src/widgets/Pagination.js +10 -5
  143. package/src/widgets/ProgressBar.js +5 -4
  144. package/src/widgets/QRCode.js +4 -0
  145. package/src/widgets/Radio.js +1 -2
  146. package/src/widgets/Rating.js +9 -0
  147. package/src/widgets/Slider.js +4 -1
  148. package/src/widgets/SnackBar.js +2 -2
  149. package/src/widgets/Switch.js +1 -2
  150. package/src/widgets/Text.js +9 -5
  151. package/src/widgets/ThemePicker.js +234 -0
  152. package/src/widgets/TreeView.js +2 -1
  153. package/src/widgets/index.js +2 -0
  154. package/docs/CONTRIBUTING.md +0 -156
@@ -613,12 +613,14 @@ const lineOf = (offsets, index) => {
613
613
  const isLargeText = (props) => {
614
614
  let size = null;
615
615
  for (const key of ["fontSize", "size"]) {
616
- const numbers = numbersIn(props.get(key)?.value ?? "");
616
+ const entry = props.get(key);
617
+ const numbers = numbersIn(entry?.resolved ?? entry?.value ?? "");
617
618
  if (numbers.length === 1) size = numbers[0].number;
618
619
  }
619
620
  if (size === null) return false;
620
621
  if (size >= LARGE_TEXT_SIZE) return true;
621
- return size >= 18 && /bold|['"]?(600|700)['"]?/.test(props.get("weight")?.value ?? "");
622
+ const weightEntry = props.get("weight");
623
+ return size >= 18 && /bold|['"]?(600|700)['"]?/.test(weightEntry?.resolved ?? weightEntry?.value ?? "");
622
624
  };
623
625
 
624
626
  /** The props that paint the plate a label sits on, in the order they win. */
@@ -636,10 +638,11 @@ const fillOf = (props, tokens) => {
636
638
  for (const key of FILL_KEYS) {
637
639
  const entry = props.get(key);
638
640
  if (!entry) continue;
641
+ const value = entry.resolved ?? entry.value;
639
642
  const resolved =
640
643
  key === "gradient"
641
- ? gradientStop(entry.value, tokens)
642
- : resolveColor(entry.value, tokens);
644
+ ? gradientStop(value, tokens)
645
+ : resolveColor(value, tokens);
643
646
  if (resolved) return resolved;
644
647
  }
645
648
  return null;
@@ -739,10 +742,10 @@ const checkContrast = (widget, props, scopeProps, ctx, line) => {
739
742
  /** The theme whose answer is hardest to read, plus the numbers to print. */
740
743
  let worst = null;
741
744
  for (const theme of themes) {
742
- const fg = resolveColor(fgEntry.value, theme.tokens);
745
+ const fg = resolveColor(fgEntry.resolved ?? fgEntry.value, theme.tokens);
743
746
  if (!fg) continue;
744
747
  if (accent) {
745
- const bg = resolveColor(accent.value, theme.tokens);
748
+ const bg = resolveColor(accent.resolved ?? accent.value, theme.tokens);
746
749
  if (!bg) continue;
747
750
  const ratio = ctx.contrast(fg, bg);
748
751
  if (!worst || ratio < worst.ratio) worst = { ratio, fg, bg, theme: theme.name };
@@ -897,10 +900,651 @@ export const functionBodies = (code) => {
897
900
  return bodies;
898
901
  };
899
902
 
903
+ // --------------------------------------------------------------------------- //
904
+ // Const resolution for the clean-UI pass. A value written as an identifier that
905
+ // names a module-level or block-level `const` is resolved to the literal it
906
+ // stands for, so `padding: PAD` where `const PAD = 13;` is still reported as an
907
+ // off-scale spacing. Bindings that are not a simple const (parameters, catch
908
+ // variables, loop variables, `let`/`var`, destructuring) shadow the name and
909
+ // make it unresolvable, so a runtime value is never mistaken for a literal.
910
+ // --------------------------------------------------------------------------- //
911
+
912
+ const matchingBracket = (code, i) => {
913
+ const open = code[i];
914
+ const close = open === "{" ? "}" : open === "(" ? ")" : open === "[" ? "]" : null;
915
+ if (!close) return -1;
916
+ let depth = 0;
917
+ let str = false;
918
+ let quote = "";
919
+ for (let j = i; j < code.length; j++) {
920
+ const ch = code[j];
921
+ if (str) {
922
+ if (ch === "\\\\") {
923
+ j++;
924
+ continue;
925
+ }
926
+ if (ch === quote) str = false;
927
+ continue;
928
+ }
929
+ if (isQuote(ch)) {
930
+ str = true;
931
+ quote = ch;
932
+ continue;
933
+ }
934
+ if (ch === open) depth++;
935
+ else if (ch === close) {
936
+ depth--;
937
+ if (depth === 0) return j;
938
+ }
939
+ }
940
+ return -1;
941
+ };
942
+
943
+ const splitTopLevel = (s, sep) => {
944
+ const out = [];
945
+ let depth = 0;
946
+ let str = false;
947
+ let quote = "";
948
+ let cur = "";
949
+ for (let i = 0; i < s.length; i++) {
950
+ const ch = s[i];
951
+ if (str) {
952
+ cur += ch;
953
+ if (ch === "\\\\") {
954
+ cur += s[++i] ?? "";
955
+ continue;
956
+ }
957
+ if (ch === quote) str = false;
958
+ continue;
959
+ }
960
+ if (isQuote(ch)) {
961
+ str = true;
962
+ quote = ch;
963
+ cur += ch;
964
+ continue;
965
+ }
966
+ if (ch === "{" || ch === "(" || ch === "[") {
967
+ depth++;
968
+ cur += ch;
969
+ continue;
970
+ }
971
+ if (ch === "}" || ch === ")" || ch === "]") {
972
+ depth--;
973
+ cur += ch;
974
+ continue;
975
+ }
976
+ if (depth === 0 && ch === sep) {
977
+ out.push(cur);
978
+ cur = "";
979
+ continue;
980
+ }
981
+ cur += ch;
982
+ }
983
+ if (cur.trim() !== "") out.push(cur);
984
+ return out;
985
+ };
986
+
987
+ const findTopLevelEquals = (s) => {
988
+ let depth = 0;
989
+ let str = false;
990
+ let quote = "";
991
+ for (let i = 0; i < s.length; i++) {
992
+ const ch = s[i];
993
+ if (str) {
994
+ if (ch === "\\\\") {
995
+ i++;
996
+ continue;
997
+ }
998
+ if (ch === quote) str = false;
999
+ continue;
1000
+ }
1001
+ if (isQuote(ch)) {
1002
+ str = true;
1003
+ quote = ch;
1004
+ continue;
1005
+ }
1006
+ if (ch === "{" || ch === "(" || ch === "[") {
1007
+ depth++;
1008
+ continue;
1009
+ }
1010
+ if (ch === "}" || ch === ")" || ch === "]") {
1011
+ depth--;
1012
+ continue;
1013
+ }
1014
+ if (depth === 0 && ch === "=" && s[i + 1] !== "=" && s[i - 1] !== "=" && s[i + 1] !== ">") {
1015
+ return i;
1016
+ }
1017
+ }
1018
+ return -1;
1019
+ };
1020
+
1021
+ const findTopLevelColon = (s) => {
1022
+ let depth = 0;
1023
+ let str = false;
1024
+ let quote = "";
1025
+ for (let i = 0; i < s.length; i++) {
1026
+ const ch = s[i];
1027
+ if (str) {
1028
+ if (ch === "\\\\") {
1029
+ i++;
1030
+ continue;
1031
+ }
1032
+ if (ch === quote) str = false;
1033
+ continue;
1034
+ }
1035
+ if (isQuote(ch)) {
1036
+ str = true;
1037
+ quote = ch;
1038
+ continue;
1039
+ }
1040
+ if (ch === "{" || ch === "(" || ch === "[") {
1041
+ depth++;
1042
+ continue;
1043
+ }
1044
+ if (ch === "}" || ch === ")" || ch === "]") {
1045
+ depth--;
1046
+ continue;
1047
+ }
1048
+ if (depth === 0 && ch === ":") return i;
1049
+ }
1050
+ return -1;
1051
+ };
1052
+
1053
+ const extractBindingNames = (pattern) => {
1054
+ const p = pattern.trim();
1055
+ if (!p) return [];
1056
+ if (/^[A-Za-z_$][\w$]*$/.test(p)) return [p];
1057
+ if (p.startsWith("{") && p.endsWith("}")) {
1058
+ const inner = p.slice(1, -1);
1059
+ const parts = splitTopLevel(inner, ",");
1060
+ const names = [];
1061
+ for (const part of parts) {
1062
+ let pp = part.trim();
1063
+ if (!pp) continue;
1064
+ if (pp.startsWith("...")) pp = pp.slice(3).trim();
1065
+ const colon = findTopLevelColon(pp);
1066
+ if (colon !== -1) {
1067
+ const valuePart = pp.slice(colon + 1).trim();
1068
+ const eq = findTopLevelEquals(valuePart);
1069
+ const target = eq !== -1 ? valuePart.slice(0, eq).trim() : valuePart;
1070
+ names.push(...extractBindingNames(target));
1071
+ } else {
1072
+ const eq = findTopLevelEquals(pp);
1073
+ const target = eq !== -1 ? pp.slice(0, eq).trim() : pp;
1074
+ names.push(...extractBindingNames(target));
1075
+ }
1076
+ }
1077
+ return names;
1078
+ }
1079
+ if (p.startsWith("[") && p.endsWith("]")) {
1080
+ const inner = p.slice(1, -1);
1081
+ const parts = splitTopLevel(inner, ",");
1082
+ const names = [];
1083
+ for (const part of parts) {
1084
+ let pp = part.trim();
1085
+ if (!pp) continue;
1086
+ if (pp.startsWith("...")) pp = pp.slice(3).trim();
1087
+ const eq = findTopLevelEquals(pp);
1088
+ const target = eq !== -1 ? pp.slice(0, eq).trim() : pp;
1089
+ names.push(...extractBindingNames(target));
1090
+ }
1091
+ return names;
1092
+ }
1093
+ return [];
1094
+ };
1095
+
1096
+ const parseParamNames = (s) => {
1097
+ const parts = splitTopLevel(s, ",");
1098
+ const names = [];
1099
+ for (const part of parts) {
1100
+ let pp = part.trim();
1101
+ if (!pp) continue;
1102
+ const eq = findTopLevelEquals(pp);
1103
+ if (eq !== -1) pp = pp.slice(0, eq).trim();
1104
+ if (pp.startsWith("...")) pp = pp.slice(3).trim();
1105
+ names.push(...extractBindingNames(pp));
1106
+ }
1107
+ return names;
1108
+ };
1109
+
1110
+ const extractBindingNamesFromFor = (s) => {
1111
+ let main = s;
1112
+ const ofMatch = /\s+(?:of|in)\s+/.exec(s);
1113
+ if (ofMatch) main = s.slice(0, ofMatch.index);
1114
+ const parts = splitTopLevel(main, ",");
1115
+ const names = [];
1116
+ for (const part of parts) {
1117
+ let pp = part.trim();
1118
+ const eq = findTopLevelEquals(pp);
1119
+ if (eq !== -1) pp = pp.slice(0, eq).trim();
1120
+ names.push(...extractBindingNames(pp));
1121
+ }
1122
+ return names;
1123
+ };
1124
+
1125
+ export const paramsBeforeBrace = (code, brace) => {
1126
+ let i = brace - 1;
1127
+ while (i >= 0 && /\s/.test(code[i])) i--;
1128
+ let arrow = false;
1129
+ if (i >= 1 && code[i] === ">" && code[i - 1] === "=") {
1130
+ arrow = true;
1131
+ i -= 2;
1132
+ }
1133
+ while (i >= 0 && /\s/.test(code[i])) i--;
1134
+ if (i < 0 || code[i] !== ")") return null;
1135
+ const closeIdx = i;
1136
+ let depth = 0;
1137
+ let start = -1;
1138
+ for (let j = closeIdx; j >= 0; j--) {
1139
+ const ch = code[j];
1140
+ if (ch === ")") depth++;
1141
+ else if (ch === "(") {
1142
+ depth--;
1143
+ if (depth === 0) {
1144
+ start = j;
1145
+ break;
1146
+ }
1147
+ }
1148
+ }
1149
+ if (start === -1) return null;
1150
+ let k = start - 1;
1151
+ let _d = 0;
1152
+ while (k >= 0) {
1153
+ const ch = code[k];
1154
+ if (ch === ")" || ch === "]") _d++;
1155
+ else if (ch === "(" || ch === "[") {
1156
+ if (_d === 0) break;
1157
+ _d--;
1158
+ } else if (_d === 0 && (ch === ";" || ch === "{" || ch === "}" || ch === "\n")) {
1159
+ break;
1160
+ }
1161
+ k--;
1162
+ }
1163
+ const before = code.slice(k + 1, start).trim();
1164
+ let kind = null;
1165
+ if (/\bcatch$/.test(before)) kind = "catch";
1166
+ else if (/\bfor$/.test(before)) kind = "for";
1167
+ else if (/\bfunction\b/.test(before)) kind = "function";
1168
+ else if (before.endsWith("=>")) kind = "function";
1169
+ else if (/[A-Za-z_$][\w$]*\s*$/.test(before) && !/\b(if|for|while|switch|with|do|else|try|catch)$/.test(before)) kind = "function";
1170
+ else if (arrow) kind = "function";
1171
+ if (!kind) return null;
1172
+
1173
+ const paramText = code.slice(start + 1, closeIdx);
1174
+ if (kind === "for") {
1175
+ const names = extractBindingNamesFromFor(paramText);
1176
+ return names.length ? names : null;
1177
+ }
1178
+ return parseParamNames(paramText);
1179
+ };
1180
+
1181
+ export const arrowParams = (code, arrowIdx) => {
1182
+ let i = arrowIdx - 1;
1183
+ while (i >= 0 && /\s/.test(code[i])) i--;
1184
+ if (i < 0) return null;
1185
+ if (/[A-Za-z_$]/.test(code[i])) {
1186
+ const m = /[A-Za-z_$][\w$]*$/.exec(code.slice(0, i + 1));
1187
+ if (m) return [m[0]];
1188
+ return null;
1189
+ }
1190
+ if (code[i] !== ")") return null;
1191
+ const closeIdx = i;
1192
+ let depth = 0;
1193
+ let start = -1;
1194
+ for (let j = closeIdx; j >= 0; j--) {
1195
+ const ch = code[j];
1196
+ if (ch === ")") depth++;
1197
+ else if (ch === "(") {
1198
+ depth--;
1199
+ if (depth === 0) {
1200
+ start = j;
1201
+ break;
1202
+ }
1203
+ }
1204
+ }
1205
+ if (start === -1) return null;
1206
+ return parseParamNames(code.slice(start + 1, closeIdx));
1207
+ };
1208
+
1209
+ const exprExtent = (code, j, hi) => {
1210
+ let depth = 0;
1211
+ let str = false;
1212
+ let quote = "";
1213
+ let i = j;
1214
+ while (i < hi) {
1215
+ const ch = code[i];
1216
+ if (str) {
1217
+ if (ch === "\\\\") {
1218
+ i += 2;
1219
+ continue;
1220
+ }
1221
+ if (ch === quote) str = false;
1222
+ i++;
1223
+ continue;
1224
+ }
1225
+ if (isQuote(ch)) {
1226
+ str = true;
1227
+ quote = ch;
1228
+ i++;
1229
+ continue;
1230
+ }
1231
+ if (ch === "{" || ch === "(" || ch === "[") {
1232
+ depth++;
1233
+ i++;
1234
+ continue;
1235
+ }
1236
+ if (ch === "}" || ch === ")" || ch === "]") {
1237
+ if (depth === 0) break;
1238
+ depth--;
1239
+ i++;
1240
+ continue;
1241
+ }
1242
+ if (depth === 0 && (ch === ";" || ch === ",")) break;
1243
+ if (depth === 0 && ch === "\n") {
1244
+ const soFar = code.slice(j, i).trimEnd();
1245
+ const last = soFar[soFar.length - 1];
1246
+ if (last && /[+\-*/%&|^,(:[{]/.test(last)) {
1247
+ i++;
1248
+ continue;
1249
+ }
1250
+ break;
1251
+ }
1252
+ i++;
1253
+ }
1254
+ return i;
1255
+ };
1256
+
1257
+ const isConstKeyword = (code, i) => {
1258
+ if (i > 0 && /[A-Za-z0-9_$]/.test(code[i - 1])) return false;
1259
+ if (!code.startsWith("const", i)) return false;
1260
+ const after = code[i + 5];
1261
+ if (after && /[A-Za-z0-9_$]/.test(after)) return false;
1262
+ return true;
1263
+ };
1264
+
1265
+ const isLetOrVarKeyword = (code, i) => {
1266
+ for (const kw of ["let", "var"]) {
1267
+ if (!code.startsWith(kw, i)) continue;
1268
+ if (i > 0 && /[A-Za-z0-9_$]/.test(code[i - 1])) continue;
1269
+ const after = code[i + kw.length];
1270
+ if (after && /[A-Za-z0-9_$]/.test(after)) continue;
1271
+ return kw;
1272
+ }
1273
+ return null;
1274
+ };
1275
+
1276
+ const parseConstDecl = (code, start, hi, kind) => {
1277
+ const kw = kind === "const" ? "const" : kind;
1278
+ let i = start + kw.length;
1279
+ const decls = [];
1280
+ const parseOne = () => {
1281
+ while (i < hi && /\s/.test(code[i])) i++;
1282
+ if (i >= hi) return false;
1283
+ if (code[i] === ";" || code[i] === "}" || code[i] === "\n") {
1284
+ if (code[i] === ";") i++;
1285
+ return false;
1286
+ }
1287
+ const patStart = i;
1288
+ let pattern;
1289
+ if (code[i] === "{" || code[i] === "[") {
1290
+ const close = matchingBracket(code, i);
1291
+ if (close === -1 || close >= hi) return false;
1292
+ pattern = code.slice(i, close + 1);
1293
+ i = close + 1;
1294
+ } else {
1295
+ const m = /^[A-Za-z_$][\w$]*/.exec(code.slice(i));
1296
+ if (!m) return false;
1297
+ pattern = m[0];
1298
+ i += m[0].length;
1299
+ }
1300
+ while (i < hi && /\s/.test(code[i])) i++;
1301
+ let init = null;
1302
+ let initStart = null;
1303
+ if (i < hi && code[i] === "=" && code[i + 1] !== "=" && code[i + 1] !== ">") {
1304
+ i++;
1305
+ while (i < hi && /\s/.test(code[i])) i++;
1306
+ initStart = i;
1307
+ let depth = 0;
1308
+ let str = false;
1309
+ let quote = "";
1310
+ while (i < hi) {
1311
+ const ch = code[i];
1312
+ if (str) {
1313
+ if (ch === "\\\\") {
1314
+ i += 2;
1315
+ continue;
1316
+ }
1317
+ if (ch === quote) str = false;
1318
+ i++;
1319
+ continue;
1320
+ }
1321
+ if (isQuote(ch)) {
1322
+ str = true;
1323
+ quote = ch;
1324
+ i++;
1325
+ continue;
1326
+ }
1327
+ if (ch === "{" || ch === "(" || ch === "[") {
1328
+ depth++;
1329
+ i++;
1330
+ continue;
1331
+ }
1332
+ if (ch === "}" || ch === ")" || ch === "]") {
1333
+ if (depth === 0) break;
1334
+ depth--;
1335
+ i++;
1336
+ continue;
1337
+ }
1338
+ if (depth === 0 && (ch === "," || ch === ";")) break;
1339
+ if (depth === 0 && ch === "\n") {
1340
+ const soFar = code.slice(initStart, i).trimEnd();
1341
+ const last = soFar[soFar.length - 1];
1342
+ if (!last || !/[+\-*/%&|^,(:[{]/.test(last)) break;
1343
+ }
1344
+ i++;
1345
+ }
1346
+ init = code.slice(initStart, i).trim();
1347
+ }
1348
+ decls.push({ pattern, name: /^[A-Za-z_$][\w$]*$/.test(pattern) ? pattern : null, init, declOffset: patStart, kind, initStart, initEnd: init !== null ? i : null });
1349
+ while (i < hi && /\s/.test(code[i])) i++;
1350
+ if (i < hi && code[i] === ",") {
1351
+ i++;
1352
+ return true;
1353
+ }
1354
+ return false;
1355
+ };
1356
+ let more = true;
1357
+ while (more && i < hi) {
1358
+ more = parseOne();
1359
+ }
1360
+ return { decls, endOffset: i };
1361
+ };
1362
+
1363
+ export const processRange = (code, lo, hi, parent, params) => {
1364
+ const scope = {
1365
+ parent,
1366
+ lo,
1367
+ hi,
1368
+ bindings: new Map(),
1369
+ children: [],
1370
+ };
1371
+ for (const p of params || []) {
1372
+ if (/^[A-Za-z_$][\w$]*$/.test(p)) {
1373
+ scope.bindings.set(p, { kind: "param", init: null, declOffset: lo });
1374
+ }
1375
+ }
1376
+ let i = lo;
1377
+ let parenDepth = 0;
1378
+ while (i < hi) {
1379
+ if (isQuote(code[i])) {
1380
+ i = skipString(code, i);
1381
+ continue;
1382
+ }
1383
+ if (parenDepth > 0) {
1384
+ if (code[i] === "(") parenDepth++;
1385
+ else if (code[i] === ")") parenDepth--;
1386
+ i++;
1387
+ continue;
1388
+ }
1389
+ if (code[i] === "(") {
1390
+ parenDepth++;
1391
+ i++;
1392
+ continue;
1393
+ }
1394
+ if (code[i] === "{") {
1395
+ const close = matchingBracket(code, i);
1396
+ if (close === -1 || close >= hi) {
1397
+ i++;
1398
+ continue;
1399
+ }
1400
+ let childParams = null;
1401
+ const pb = paramsBeforeBrace(code, i);
1402
+ if (pb) childParams = pb;
1403
+ const child = processRange(code, i + 1, close, scope, childParams);
1404
+ scope.children.push(child);
1405
+ i = close + 1;
1406
+ continue;
1407
+ }
1408
+ if (code[i] === "=" && code[i + 1] === ">") {
1409
+ const ps = arrowParams(code, i);
1410
+ let j = i + 2;
1411
+ while (j < hi && /\s/.test(code[j])) j++;
1412
+ if (j < hi && code[j] !== "{") {
1413
+ const bodyEnd = exprExtent(code, j, hi);
1414
+ const child = processRange(code, j, bodyEnd, scope, ps || []);
1415
+ scope.children.push(child);
1416
+ i = bodyEnd;
1417
+ continue;
1418
+ }
1419
+ i += 2;
1420
+ continue;
1421
+ }
1422
+ const letKw = isLetOrVarKeyword(code, i);
1423
+ if (letKw) {
1424
+ const kind = letKw;
1425
+ const parsed = parseConstDecl(code, i, hi, kind);
1426
+ for (const d of parsed.decls) {
1427
+ const names = d.name ? [d.name] : extractBindingNames(d.pattern);
1428
+ for (const n of names) {
1429
+ scope.bindings.set(n, { kind, init: null, declOffset: d.declOffset });
1430
+ }
1431
+ if (d.initStart !== null && d.initEnd !== null && d.initEnd > d.initStart) {
1432
+ const child = processRange(code, d.initStart, d.initEnd, scope, []);
1433
+ scope.children.push(child);
1434
+ }
1435
+ }
1436
+ i = parsed.endOffset;
1437
+ continue;
1438
+ }
1439
+ if (isConstKeyword(code, i)) {
1440
+ const parsed = parseConstDecl(code, i, hi, "const");
1441
+ for (const d of parsed.decls) {
1442
+ if (d.name) {
1443
+ scope.bindings.set(d.name, { kind: "const", init: d.init, declOffset: d.declOffset });
1444
+ } else {
1445
+ const names = extractBindingNames(d.pattern);
1446
+ for (const n of names) {
1447
+ scope.bindings.set(n, { kind: "other", init: null, declOffset: d.declOffset });
1448
+ }
1449
+ }
1450
+ if (d.initStart !== null && d.initEnd !== null && d.initEnd > d.initStart) {
1451
+ const child = processRange(code, d.initStart, d.initEnd, scope, []);
1452
+ scope.children.push(child);
1453
+ }
1454
+ }
1455
+ i = parsed.endOffset;
1456
+ continue;
1457
+ }
1458
+ i++;
1459
+ }
1460
+ return scope;
1461
+ };
1462
+
1463
+ export const scopeAt = (root, offset) => {
1464
+ let cur = root;
1465
+ let changed = true;
1466
+ while (changed) {
1467
+ changed = false;
1468
+ for (const child of cur.children) {
1469
+ if (offset >= child.lo && offset < child.hi) {
1470
+ cur = child;
1471
+ changed = true;
1472
+ break;
1473
+ }
1474
+ }
1475
+ }
1476
+ return cur;
1477
+ };
1478
+
1479
+ const resolveExpr = (text, scope, pos, visited) => {
1480
+ const t = text.trim();
1481
+ if (!t) return text;
1482
+ if (isQuote(t[0])) {
1483
+ const end = skipString(t, 0);
1484
+ if (end === t.length) return text;
1485
+ }
1486
+ if (/^-?\d+(?:\.\d+)?$/.test(t)) return text;
1487
+ if (/^colors\.[A-Za-z_$][\w$]*$/.test(t)) return text;
1488
+ if (/^[A-Za-z_$][\w$]*$/.test(t)) {
1489
+ const r = resolveIdent(t, scope, pos, visited || new Set());
1490
+ return r === null ? text : r;
1491
+ }
1492
+ let out = "";
1493
+ let i = 0;
1494
+ const v = visited || new Set();
1495
+ while (i < t.length) {
1496
+ const ch = t[i];
1497
+ if (isQuote(ch)) {
1498
+ const end = skipString(t, i);
1499
+ out += t.slice(i, end);
1500
+ i = end;
1501
+ continue;
1502
+ }
1503
+ if (/[A-Za-z_$]/.test(ch)) {
1504
+ const m = /^[A-Za-z_$][\w$]*/.exec(t.slice(i));
1505
+ const ident = m[0];
1506
+ const before = i > 0 ? t[i - 1] : "";
1507
+ const after = t[i + ident.length] || "";
1508
+ if (before !== "." && after !== ".") {
1509
+ const r = resolveIdent(ident, scope, pos, v);
1510
+ out += r === null ? ident : r;
1511
+ } else {
1512
+ out += ident;
1513
+ }
1514
+ i += ident.length;
1515
+ continue;
1516
+ }
1517
+ out += ch;
1518
+ i++;
1519
+ }
1520
+ return out;
1521
+ };
1522
+
1523
+ const resolveIdent = (name, scope, pos, visited) => {
1524
+ let cur = scope;
1525
+ while (cur) {
1526
+ const b = cur.bindings.get(name);
1527
+ if (b) {
1528
+ if (b.declOffset > pos) return null;
1529
+ if (b.kind !== "const") return null;
1530
+ if (b.init == null) return null;
1531
+ const key = name + "@" + (cur.lo || 0);
1532
+ if (visited.has(key)) return null;
1533
+ visited.add(key);
1534
+ const resolved = resolveExpr(b.init, cur, b.declOffset, visited);
1535
+ visited.delete(key);
1536
+ return resolved === null ? b.init : resolved;
1537
+ }
1538
+ cur = cur.parent;
1539
+ }
1540
+ return null;
1541
+ };
1542
+
900
1543
  const checkFile = (file, ctx) => {
901
1544
  const findings = [];
902
1545
  const src = fs.readFileSync(file, "utf8");
903
1546
  const code = blankComments(src);
1547
+ const rootScope = processRange(code, 0, code.length, null, []);
904
1548
  const offsets = lineOffsets(src);
905
1549
  const tokenFile = isTokenFile(file);
906
1550
  const push = (line, codeName, message, detail) =>
@@ -914,7 +1558,15 @@ const checkFile = (file, ctx) => {
914
1558
  const calls = findCalls(code).map((call) => {
915
1559
  const props = new Map();
916
1560
  for (const entry of entriesOf(code, call.open, call.close)) {
917
- if (!props.has(entry.key)) props.set(entry.key, entry);
1561
+ if (!props.has(entry.key)) {
1562
+ entry.resolved = resolveExpr(
1563
+ entry.value,
1564
+ scopeAt(rootScope, entry.valueStart),
1565
+ entry.valueStart,
1566
+ new Set(),
1567
+ );
1568
+ props.set(entry.key, entry);
1569
+ }
918
1570
  }
919
1571
  return { ...call, props };
920
1572
  });
@@ -959,7 +1611,7 @@ const checkFile = (file, ctx) => {
959
1611
  for (const key of TEXT_KEYS) {
960
1612
  const entry = call.props.get(key);
961
1613
  if (!entry) continue;
962
- const match = /^['"`]([\s\S]*)['"`]$/.exec(entry.value.trim());
1614
+ const match = /^['"`]([\s\S]*)['"`]$/.exec((entry.resolved ?? entry.value).trim());
963
1615
  return match ? match[1] : null;
964
1616
  }
965
1617
  return null;
@@ -1018,11 +1670,11 @@ const checkFile = (file, ctx) => {
1018
1670
  if (SKIP_VALUE_KEYS.has(key)) continue;
1019
1671
  const kind = ctx.propKinds.get(key);
1020
1672
  const valueLine = lineOf(offsets, entry.valueStart);
1021
- const measured = measuredNumbers(entry.value);
1673
+ const measured = measuredNumbers(entry.resolved ?? entry.value);
1022
1674
  const numbers = measured.numbers;
1023
1675
 
1024
1676
  if (kind === "color" || COLOR_FALLBACK_KEYS.has(key)) {
1025
- const literal = colorLiteralIn(entry.value);
1677
+ const literal = colorLiteralIn(entry.resolved ?? entry.value);
1026
1678
  if (literal && !tokenFile) {
1027
1679
  push(
1028
1680
  valueLine,
@@ -1030,7 +1682,7 @@ const checkFile = (file, ctx) => {
1030
1682
  `${call.name} sets \`${key}\` to the literal ${literal}.`,
1031
1683
  "Color comes from a token (`colors.text`, `colors.primary`). A typed hex is the first sign a screen drifted from the rest; if a token is missing, add it to src/database/themes.js.",
1032
1684
  );
1033
- } else if (/colors\.[A-Za-z0-9_]+\s*\+\s*['"][0-9a-fA-F]{2}['"]/.test(entry.value)) {
1685
+ } else if (/colors\.[A-Za-z0-9_]+\s*\+\s*['"][0-9a-fA-F]{2}['"]/.test(entry.resolved ?? entry.value)) {
1034
1686
  push(
1035
1687
  valueLine,
1036
1688
  "HAND_MIXED_COLOR",
@@ -1093,9 +1745,9 @@ const checkFile = (file, ctx) => {
1093
1745
  // is 40x40 and legal, while `size: 16` alone is the glyph and nothing else.
1094
1746
  // A padding this pass cannot resolve makes the target unmeasured.
1095
1747
  const padded = props.has("paddingVertical")
1096
- ? measuredNumbers(props.get("paddingVertical").value)
1748
+ ? measuredNumbers(props.get("paddingVertical").resolved ?? props.get("paddingVertical").value)
1097
1749
  : props.has("padding")
1098
- ? measuredNumbers(props.get("padding").value)
1750
+ ? measuredNumbers(props.get("padding").resolved ?? props.get("padding").value)
1099
1751
  : { measured: true, numbers: [0] };
1100
1752
  const vertical = padded.measured ? Math.abs(padded.numbers[0] ?? 0) : null;
1101
1753
  for (const n of numbers) {
@@ -1116,7 +1768,7 @@ const checkFile = (file, ctx) => {
1116
1768
  if (
1117
1769
  tappable &&
1118
1770
  key === "size" &&
1119
- /(['"])small\1/.test(entry.value) &&
1771
+ /(['"])small\1/.test(entry.resolved ?? entry.value) &&
1120
1772
  !props.has("height") &&
1121
1773
  !props.has("minHeight")
1122
1774
  ) {