@colixsystems/widget-sdk 0.85.1 → 0.86.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 +10 -1
- package/dist/linter.cjs +69 -0
- package/dist/linter.js +87 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -61,7 +61,16 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
61
61
|
|
|
62
62
|
## Status
|
|
63
63
|
|
|
64
|
-
`v0.
|
|
64
|
+
`v0.86.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
65
|
+
|
|
66
|
+
### What's new in 0.86.0 (contract unchanged)
|
|
67
|
+
|
|
68
|
+
**New linter rule `measured-width-ignores-padding` — a measured width includes the measuring element's own padding (sc-4913).**
|
|
69
|
+
|
|
70
|
+
- **`measured-width-ignores-padding` (severity `warning`, non-blocking).** A widget that puts `onLayout` on an element which sets its own `padding` (or `paddingHorizontal`/`Left`/`Right`), and then sizes grid cells from the measured number, is flagged. `onLayout` reports the element's **frame** width and padding sits inside that frame, so the space the children really get is `width - paddingLeft - paddingRight`. Cells sized to fill the raw measurement overflow the content box, the last one wraps, and the widget ships a whole empty column of whitespace beside its cards — on both the web Player and the native Expo export, with nothing in the console. Author fix: spread `onLayout` on an **unpadded** element (keep the padding on a parent, or measure an inner `<View>` inside the padded root) so the number you hold is the usable width. Better still for content-sized cells: skip the measurement entirely and wrap with flex — a `{ flexDirection: "row", flexWrap: "wrap", gap }` row whose cards take `{ flexGrow: 1, flexBasis: CARD_MIN }` splits the row's real content width itself and can never leave a leftover band.
|
|
71
|
+
- **Why a warning.** The rule fires only when the measured value feeds sizing arithmetic — a padded box measured just to pick a wide/narrow form (`isNarrowWidth(width)`) is off by one padding pair and stays silent. A widget that already subtracts its own padding by hand matches too, because no text scan can verify the subtraction; that is deliberate — the remediation is correct for it as well and retires the arithmetic. Comments are not scanned, so documenting the anti-pattern is safe.
|
|
72
|
+
|
|
73
|
+
`CONTRACT` is unchanged (no new field), and no export changed signature.
|
|
65
74
|
|
|
66
75
|
### What's new in 0.85.1 (contract 1.60.1)
|
|
67
76
|
|
package/dist/linter.cjs
CHANGED
|
@@ -877,6 +877,72 @@ function _imagePercentHeightRules(source) {
|
|
|
877
877
|
return findings;
|
|
878
878
|
}
|
|
879
879
|
|
|
880
|
+
// sc-4913 — a measured frame INCLUDES the element's own padding, so cells sized
|
|
881
|
+
// from it overflow the content box and the last one wraps into an empty column.
|
|
882
|
+
// Mirror of linter.js (see there for the full rationale).
|
|
883
|
+
const _JSX_TAG_RE = /<([A-Z][\w.]*)\b/g;
|
|
884
|
+
const _ON_LAYOUT_ATTR_RE = /\bonLayout\s*=/;
|
|
885
|
+
const _OWN_PADDING_RE = /\bpadding(?:Horizontal|Left|Right|Start|End)?\s*:/;
|
|
886
|
+
const _WIDTH_HOLDER_RE =
|
|
887
|
+
/const\s*\[\s*([\w$]+)\s*,\s*([\w$]+)\s*\]\s*=\s*(useState|useContainerWidth)\b/g;
|
|
888
|
+
const _LAYOUT_FEED_WINDOW = 80;
|
|
889
|
+
|
|
890
|
+
function _measuredWidthNames(code) {
|
|
891
|
+
const names = [];
|
|
892
|
+
_WIDTH_HOLDER_RE.lastIndex = 0;
|
|
893
|
+
let held;
|
|
894
|
+
while ((held = _WIDTH_HOLDER_RE.exec(code))) {
|
|
895
|
+
const [, value, setter, hook] = held;
|
|
896
|
+
if (hook === "useContainerWidth") {
|
|
897
|
+
names.push(value);
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const fedByLayout = new RegExp(
|
|
901
|
+
`\\b${setter}\\s*\\([\\s\\S]{0,${_LAYOUT_FEED_WINDOW}}?\\blayout\\.width\\b`,
|
|
902
|
+
);
|
|
903
|
+
if (fedByLayout.test(code)) names.push(value);
|
|
904
|
+
}
|
|
905
|
+
return names;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function _sizesFromMeasuredWidth(code, names) {
|
|
909
|
+
return names.some((name) =>
|
|
910
|
+
new RegExp(`\\b${name}\\s*[-/*]|[-/*]\\s*\\b${name}\\b`).test(code),
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function _measuredPaddingRules(source) {
|
|
915
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
916
|
+
const measured = _measuredWidthNames(code);
|
|
917
|
+
if (!_sizesFromMeasuredWidth(code, measured)) return [];
|
|
918
|
+
const sourceLines = source.split(/\r?\n/);
|
|
919
|
+
const findings = [];
|
|
920
|
+
_JSX_TAG_RE.lastIndex = 0;
|
|
921
|
+
let tag;
|
|
922
|
+
while ((tag = _JSX_TAG_RE.exec(code))) {
|
|
923
|
+
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
924
|
+
const attrs = code.slice(tag.index, end);
|
|
925
|
+
_JSX_TAG_RE.lastIndex = end;
|
|
926
|
+
if (!_ON_LAYOUT_ATTR_RE.test(attrs)) continue;
|
|
927
|
+
const padded = _OWN_PADDING_RE.exec(attrs);
|
|
928
|
+
if (!padded) continue;
|
|
929
|
+
const line = code
|
|
930
|
+
.slice(0, tag.index + padded.index)
|
|
931
|
+
.split(/\r?\n/).length;
|
|
932
|
+
findings.push({
|
|
933
|
+
rule: "measured-width-ignores-padding",
|
|
934
|
+
severity: "warning",
|
|
935
|
+
label:
|
|
936
|
+
`<${tag[1]}> has onLayout AND its own padding: the measured width ` +
|
|
937
|
+
`INCLUDES that padding, so cells sized from it overflow and the last ` +
|
|
938
|
+
`wraps, leaving an empty column. Measure an unpadded inner View.`,
|
|
939
|
+
line,
|
|
940
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
return findings;
|
|
944
|
+
}
|
|
945
|
+
|
|
880
946
|
// Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
881
947
|
// ships to, so `import-platform-mismatch` lints each file against what it
|
|
882
948
|
// actually targets. Mirror of linter.js.
|
|
@@ -939,6 +1005,9 @@ function lintSource(source, options) {
|
|
|
939
1005
|
findings.push(..._lucideIconRules(source));
|
|
940
1006
|
findings.push(..._reactInScopeRules(source));
|
|
941
1007
|
findings.push(..._imagePercentHeightRules(source));
|
|
1008
|
+
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
1009
|
+
// padding wraps the last grid column into an empty one.
|
|
1010
|
+
findings.push(..._measuredPaddingRules(source));
|
|
942
1011
|
// sc-4650 — soft warning: every payment refusal reported as "try again".
|
|
943
1012
|
findings.push(..._paymentCurrencyRules(source));
|
|
944
1013
|
findings.push(..._hardcodedCurrencyLabelRules(source));
|
package/dist/linter.js
CHANGED
|
@@ -1013,6 +1013,90 @@ function _imagePercentHeightRules(source) {
|
|
|
1013
1013
|
return findings;
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
|
+
// sc-4913 — a measured frame INCLUDES the element's own padding.
|
|
1017
|
+
//
|
|
1018
|
+
// `onLayout` reports the frame width, and padding sits inside that frame. A
|
|
1019
|
+
// widget that measures its padded root and then sizes fixed-pixel grid columns
|
|
1020
|
+
// to fill the measured number overflows its own content box by left+right
|
|
1021
|
+
// padding, so the last column wraps: a whole empty column of whitespace beside
|
|
1022
|
+
// the cards, on both hosts. Nothing else catches it — the widget renders and
|
|
1023
|
+
// every other gate passes.
|
|
1024
|
+
//
|
|
1025
|
+
// Fires only when the measurement feeds sizing arithmetic. A padded box
|
|
1026
|
+
// measured just to branch narrow/wide is off by one padding pair and correct
|
|
1027
|
+
// enough; `isNarrowWidth` is the sanctioned reader for that. A widget that DID
|
|
1028
|
+
// subtract its own padding matches too — no text scan can verify the
|
|
1029
|
+
// subtraction — and that is deliberate: the remediation (measure an unpadded
|
|
1030
|
+
// inner View) is correct for it as well, and retires the arithmetic entirely.
|
|
1031
|
+
const _JSX_TAG_RE = /<([A-Z][\w.]*)\b/g;
|
|
1032
|
+
const _ON_LAYOUT_ATTR_RE = /\bonLayout\s*=/;
|
|
1033
|
+
const _OWN_PADDING_RE = /\bpadding(?:Horizontal|Left|Right|Start|End)?\s*:/;
|
|
1034
|
+
const _WIDTH_HOLDER_RE =
|
|
1035
|
+
/const\s*\[\s*([\w$]+)\s*,\s*([\w$]+)\s*\]\s*=\s*(useState|useContainerWidth)\b/g;
|
|
1036
|
+
// A setter fed a layout width within this many chars of the call is measuring:
|
|
1037
|
+
// covers `setW(e.nativeEvent.layout.width)` and `setW(Math.round(…))` alike.
|
|
1038
|
+
const _LAYOUT_FEED_WINDOW = 80;
|
|
1039
|
+
|
|
1040
|
+
function _measuredWidthNames(code) {
|
|
1041
|
+
const names = [];
|
|
1042
|
+
_WIDTH_HOLDER_RE.lastIndex = 0;
|
|
1043
|
+
let held;
|
|
1044
|
+
while ((held = _WIDTH_HOLDER_RE.exec(code))) {
|
|
1045
|
+
const [, value, setter, hook] = held;
|
|
1046
|
+
if (hook === "useContainerWidth") {
|
|
1047
|
+
names.push(value);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const fedByLayout = new RegExp(
|
|
1051
|
+
`\\b${setter}\\s*\\([\\s\\S]{0,${_LAYOUT_FEED_WINDOW}}?\\blayout\\.width\\b`,
|
|
1052
|
+
);
|
|
1053
|
+
if (fedByLayout.test(code)) names.push(value);
|
|
1054
|
+
}
|
|
1055
|
+
return names;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function _sizesFromMeasuredWidth(code, names) {
|
|
1059
|
+
return names.some((name) =>
|
|
1060
|
+
new RegExp(`\\b${name}\\s*[-/*]|[-/*]\\s*\\b${name}\\b`).test(code),
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function _measuredPaddingRules(source) {
|
|
1065
|
+
// Comments are blanked so this rule's own documented anti-pattern, and a
|
|
1066
|
+
// commented-out draft, never fire.
|
|
1067
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
1068
|
+
const measured = _measuredWidthNames(code);
|
|
1069
|
+
if (!_sizesFromMeasuredWidth(code, measured)) return [];
|
|
1070
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1071
|
+
const findings = [];
|
|
1072
|
+
_JSX_TAG_RE.lastIndex = 0;
|
|
1073
|
+
let tag;
|
|
1074
|
+
while ((tag = _JSX_TAG_RE.exec(code))) {
|
|
1075
|
+
const end = _jsxOpenTagEnd(code, tag.index + tag[0].length);
|
|
1076
|
+
const attrs = code.slice(tag.index, end);
|
|
1077
|
+
_JSX_TAG_RE.lastIndex = end;
|
|
1078
|
+
if (!_ON_LAYOUT_ATTR_RE.test(attrs)) continue;
|
|
1079
|
+
const padded = _OWN_PADDING_RE.exec(attrs);
|
|
1080
|
+
if (!padded) continue;
|
|
1081
|
+
// Report the padding, not the tag: on a multi-line open tag the style line
|
|
1082
|
+
// is the actionable one.
|
|
1083
|
+
const line = code
|
|
1084
|
+
.slice(0, tag.index + padded.index)
|
|
1085
|
+
.split(/\r?\n/).length;
|
|
1086
|
+
findings.push({
|
|
1087
|
+
rule: "measured-width-ignores-padding",
|
|
1088
|
+
severity: "warning",
|
|
1089
|
+
label:
|
|
1090
|
+
`<${tag[1]}> has onLayout AND its own padding: the measured width ` +
|
|
1091
|
+
`INCLUDES that padding, so cells sized from it overflow and the last ` +
|
|
1092
|
+
`wraps, leaving an empty column. Measure an unpadded inner View.`,
|
|
1093
|
+
line,
|
|
1094
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
return findings;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1016
1100
|
/**
|
|
1017
1101
|
* Narrow a split-impl widget's manifest to the platform a single bundle file
|
|
1018
1102
|
* ships to, so `import-platform-mismatch` lints each file against what it
|
|
@@ -1083,6 +1167,9 @@ export function lintSource(source, options) {
|
|
|
1083
1167
|
findings.push(..._reactInScopeRules(source));
|
|
1084
1168
|
// sc-3493 — soft warning: percentage height on an <Image> collapses to 0.
|
|
1085
1169
|
findings.push(..._imagePercentHeightRules(source));
|
|
1170
|
+
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
1171
|
+
// padding wraps the last grid column into an empty one.
|
|
1172
|
+
findings.push(..._measuredPaddingRules(source));
|
|
1086
1173
|
// sc-4650 — soft warning: every payment refusal reported as "try again".
|
|
1087
1174
|
findings.push(..._paymentCurrencyRules(source));
|
|
1088
1175
|
findings.push(..._hardcodedCurrencyLabelRules(source));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js"
|
|
51
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-filestore-file.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-payments.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-section-empty.test.js src/__tests__/hooks-widget-event.test.js src/__tests__/hooks-widget-input.test.js src/__tests__/hooks-identification.test.js src/__tests__/hooks-subscription.test.js src/__tests__/hooks-volatile-query-key.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/linter-translation-api.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-measured-padding.test.js src/__tests__/linter-payment-error.test.js src/__tests__/linter-platform.test.js src/__tests__/linter-react-import.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/lucideIconName.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/hooks-translate.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/theme-components-parity.test.js src/__tests__/theme-depth-tokens.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|