@colixsystems/widget-sdk 0.126.0 → 0.128.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 +24 -2
- package/dist/contract.cjs +48 -1
- package/dist/contract.js +48 -1
- package/dist/dev-shims.js +8 -0
- package/dist/linter.cjs +34 -0
- package/dist/linter.js +58 -0
- package/package.json +2 -2
- package/src/dev-shims.js +8 -0
package/README.md
CHANGED
|
@@ -70,7 +70,27 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
70
70
|
|
|
71
71
|
## Status
|
|
72
72
|
|
|
73
|
-
`v0.
|
|
73
|
+
`v0.128.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**.
|
|
74
|
+
|
|
75
|
+
### What's new in 0.128.0 (contract 1.99.0 — unchanged)
|
|
76
|
+
|
|
77
|
+
**New linter rule `flex-basis-percent` — a percentage `flexBasis` sizes the HEIGHT in a column (sc-7274).** A generated form carried one shared field helper, `style={{ flexGrow: 1, flexBasis: wide ? "100%" : 220 }}`, used both as a row cell (where the number is exactly right) and as a full-width field stacked in a column (where the percentage is a trap). `flex-basis` sizes the **main** axis, and a column's main axis is the **height**: each wide field was asking for its parent's entire height. It rendered correctly in the builder canvas — an auto-height ancestor leaves the percentage indefinite, so it degrades to `content` — and broke the moment the page shipped, because a Grid cell stretches its child on the web Player and made that height definite. Measured on the published page: the group was 633px tall and every wide field inside it was **also** 633px, so with `View`'s default `flex-shrink: 0` they could not shrink back and overflowed 633px and 1266px down, painting a blank band mid-form and three fields on top of the section below and the submit row.
|
|
78
|
+
|
|
79
|
+
- **`flex-basis-percent` (severity `warning`, non-blocking).** A literal percentage `flexBasis` — `"100%"`, `"50%"`, and the responsive ternary `flexBasis: stacked ? "100%" : CARD_WIDE` — is flagged. Author fix: **`width: "100%"`**, which is the direction-agnostic way to say "full width": it fills the row in a column parent AND takes its own line in a wrap row, so nothing is lost by switching. Keep a **number** (`flexBasis: 220`) where you mean a wrap threshold. It is a **warning**, not an error, because in a ROW parent the percentage IS correct and an AST-free scan cannot see the parent's `flexDirection` — the offending style usually lives in a shared cell helper far from its parent. Scope is the literal inline form; a basis threaded through a variable is beyond the scan, and the designer skill's form guidance remains the first guard. Comments are not scanned, so documenting the anti-pattern is safe.
|
|
80
|
+
|
|
81
|
+
### What's new in 0.127.0 (contract 1.99.0)
|
|
82
|
+
|
|
83
|
+
**Widgets can do maths now — five pure-JS packages join the vetted import allowlist (sc-7191).** The list had 31 entries and exactly one non-UI utility (`date-fns`), so anything numeric a widget needed had to be hand-rolled in a sibling file. Two gaps in particular:
|
|
84
|
+
|
|
85
|
+
- `decimal.js` — exact decimal arithmetic. The platform has payments, invoicing and VAT, and a total accumulated in IEEE-754 floats drifts from what the backend actually charged. `new Decimal(a).plus(b).toFixed(2)` does not.
|
|
86
|
+
- `d3-scale` + `d3-shape` + `d3-array` — the scale, path-generator and domain maths a bespoke chart needs. `d3-shape` emits path strings you hand straight to the already-vetted `react-native-svg`'s `<Path d={…} />`, so a custom line/area/donut chart is ONE source file that renders identically in the Player and the Expo export.
|
|
87
|
+
- `simple-statistics` — mean/median/quantile/regression/correlation, for summarising a datastore table without shipping a maths framework.
|
|
88
|
+
|
|
89
|
+
All five are `platforms: ["web", "native"]` with no native module, so this is **full parity**, not a §8 native-only case. Each is host-shimmed in the Player and pinned in the export for the same reason `date-fns` is: an AI-agent widget is transpiled rather than bundled, so its bare import has to resolve at runtime on both hosts.
|
|
90
|
+
|
|
91
|
+
`mathjs` was considered and deliberately left off — 9.4 MB unpacked with nine transitive dependencies, and a web-vetted package is bundled into the Studio.
|
|
92
|
+
|
|
93
|
+
Additive: no existing entry, hook, primitive or `propertySchema` type changed shape. `CONTRACT.version` → `1.99.0`.
|
|
74
94
|
|
|
75
95
|
### What's new in 0.126.0 (contract 1.98.0)
|
|
76
96
|
|
|
@@ -1322,7 +1342,7 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
|
|
|
1322
1342
|
- **Spend one gradient.** `<Gradient colors={[theme.colors.primary, theme.colors.primaryStrong]} angle={160} style={…}>` is a `View` that paints a gradient behind its children, so it replaces the `View` you'd otherwise give a flat `backgroundColor`. `angle` is CSS degrees (0 = to top, 90 = to right, default 180); text on it uses `colors.onPrimary`. Exactly **one** per widget — on the focal element — and never behind body text. Both hosts render it identically (web paints CSS, native uses `expo-linear-gradient`), so there is no per-platform branching to write; don't import `expo-linear-gradient` yourself and don't write a `backgroundImage` string.
|
|
1323
1343
|
- **Answer the touch.** Every tappable card, row and list entry lifts while the pointer is over it (web) or it is pressed (touch). One declaration does both: give the Pressable a style FUNCTION and spread `pressableLift` — `<Pressable onPress={open} style={(state) => [styles.card, ...pressableLift(state)]}>`. The lift is a -2px nudge plus one elevation step from `theme.interaction`, with the web transition built in. Never hand-write hover logic or your own pressed shadows, and never fake feedback with `opacity` — a dimmed surface reads as disabling itself.
|
|
1324
1344
|
- **Size to your container — measure it, don't stretch into it.** The same widget sits in a full-width desktop section (~1400px), a half-width grid cell (~700px) and a phone (~360px), so layout built only from `flex: 1` stretches to fill whatever it is handed — a month calendar ends up with 200px day cells and swallows the page. Measure your own width with `onLayout={(e) => setWidth(e.nativeEvent.layout.width)}` on the root `View` (a React Native primitive, so it behaves identically on both hosts), render nothing size-dependent while `width === 0`, and compute every threshold from the measured value: a widget gets no declared breakpoint prop, but it can always measure. **Cap a repeating cell** rather than giving a grid `flex: 1` — `const cell = Math.max(32, Math.min(Math.floor((usable - gap * (columns - 1)) / columns), 64));`, with a calendar day cell topping out at 56–72px on `aspectRatio: 1`, and the grid given its exact computed width plus `alignSelf: 'center'` when the cap leaves slack. **Split two co-equal surfaces above ~720px measured width** (`flexDirection: width >= 720 ? 'row' : 'column'`, each half `{ flex: 1, minWidth: 0 }`) — a picker beside the form it feeds on a wide canvas, stacked in reading order below it. Never hardcode a width, never put `flex: 1` / `height: '100%'` on a content widget's root, and don't read the screen with `Dimensions` — the screen is not the widget.
|
|
1325
|
-
- **Compose forms — pair fields into rows, don't stack one per row.** Put short, related fields side by side (first + last name, city + postal code, expiry + CVC): a row of `{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.md }` with each field cell `{ flexGrow: 1, flexBasis: 160 }` splits the width on a wide card and wraps to stacked on a narrow phone — the right recipe for field pairs because it needs no measurement (a fixed-width column overflows a phone; when a layout needs a real column count instead of wrapping, measure your width as above). Keep wide fields (email, address, notes) full-width
|
|
1345
|
+
- **Compose forms — pair fields into rows, don't stack one per row.** Put short, related fields side by side (first + last name, city + postal code, expiry + CVC): a row of `{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.md }` with each field cell `{ flexGrow: 1, flexBasis: 160 }` splits the width on a wide card and wraps to stacked on a narrow phone — the right recipe for field pairs because it needs no measurement (a fixed-width column overflows a phone; when a layout needs a real column count instead of wrapping, measure your width as above). Keep wide fields (email, address, notes) full-width with `width: '100%'` — NEVER `flexBasis: '100%'`, which sizes the main axis and therefore claims the parent's whole HEIGHT in a column (sc-7274) — cap it at two–three per row, group a long form into labelled sections, and label every input above it (not placeholder-only). Give a `multiline` field BOTH a floor and a ceiling (`{ minHeight: 150, maxHeight: 260 }`) so a long value scrolls inside the box instead of growing past its card, and keep the Save / Cancel row in normal flow below the fields, never positioned over them.
|
|
1326
1346
|
- **Respond to touch.** Give every `Pressable` the lift via the function-style `style={(state) => [base, ...pressableLift(state)]}` — see "Answer the touch" above. Never dim with `opacity`, which reads as the surface disabling itself.
|
|
1327
1347
|
- **Drag and drop — show what is being dragged.** A drag where the item stays put reads as broken. Three things change the moment a drag starts: the **drag proxy** (the item lifts and follows the finger — `...theme.elevation.lg`, `{ scale: 1.03 }`, `opacity: 0.9`; for a tall or full-width item drag a compact `primarySoft` pill with its icon + one line of label instead), the **source placeholder** (the vacated slot keeps its height as a quiet `colors.surfaceMuted` block so the list doesn't collapse), and the **drop target** (one slot at a time highlighted with `primarySoft` or a 2px `colors.primary` border). Always animate the release — settle into the new slot, or `Animated.spring(pan, { toValue: { x: 0, y: 0 }, useNativeDriver: false })` back to the origin on cancel. Build it with `Animated` + `PanResponder` from `react-native` (the only mechanism that behaves identically on both hosts) — never HTML5 drag events (`draggable` / `onDragStart` / `dataTransfer` are web-only, and `document` / `window` are banned) — and start the drag from a `GripVertical` grip handle whenever the row is also tappable or sits in a `ScrollView`.
|
|
1328
1348
|
- **Use icons for clarity.** Pair a `lucide-react-native` icon with its label at a consistent size, coloured from the theme. The label never repeats the icon as a character — with a `Plus` icon the button says "Add item", never "+ Add item" (that renders a doubled plus).
|
|
@@ -1584,6 +1604,8 @@ Two rules keep a widget's look reachable from the Studio (sc-6455). `no-hardcode
|
|
|
1584
1604
|
|
|
1585
1605
|
Pass `--manifest` to enable `style-field-unread` — it needs the manifest, and it needs every source at once so a split-impl widget's per-host field reads are seen together.
|
|
1586
1606
|
|
|
1607
|
+
`flex-basis-percent` flags a literal percentage `flexBasis` (sc-7274) — `"100%"`, `"50%"`, or the ternary a responsive cell writes. `flex-basis` sizes the MAIN axis, so in a column parent it asks for the parent's HEIGHT: under an ancestor that makes that height definite (a Grid cell stretches its child on the web Player) the child fills the container and, since a `View` never shrinks, overflows over everything below it. Say full-width with `width: "100%"` — correct in a column AND in a wrap row — and keep a NUMBER for a wrap threshold. A warning, because the percentage is right in a row and a text scan cannot see the parent.
|
|
1608
|
+
|
|
1587
1609
|
`no-html-in-content` flags an HTML tag inside a **string** (sc-6970) — `"<strong>"`, `"<br>"`, `"<p>…</p>"`. There is no HTML renderer on either host, so the tag reaches the reader as literal text; author formatted text with `<MarkdownInput>` and render it with `<RichText>` instead. Only string and template content is scanned, so your own `<View>` / `<Text>` JSX can never trip it. A warning for a human author, blocking for the AI widget agent.
|
|
1588
1610
|
|
|
1589
1611
|
## Local dev loop (`appstudio-widget dev`)
|
package/dist/contract.cjs
CHANGED
|
@@ -2677,6 +2677,41 @@ const VETTED_IMPORTS = [
|
|
|
2677
2677
|
description:
|
|
2678
2678
|
"Device motion hardware on the Expo export: Accelerometer, Gyroscope, Magnetometer, DeviceMotion, Barometer, Pedometer and LightSensor, each read as an addListener subscription with setUpdateInterval — always remove the subscription on unmount, a sensor left running drains the battery. Expo SDK 56 ships 56.0.x. Native-only on purpose: the package's own web build derives acceleration from deviceorientation ANGLES rather than real motion, so a shake or tilt threshold tuned on one host would read differently on the other. Author it in widget.native.jsx and pair it with a widget.web.jsx reading window.DeviceMotionEvent (accelerationIncludingGravity / rotationRate), the browser API the same hardware exposes. Both hosts need a user gesture before readings start, and iOS Safari additionally needs an explicit DeviceMotionEvent.requestPermission() grant — so gate the reading behind a Pressable, never start it on mount.",
|
|
2679
2679
|
},
|
|
2680
|
+
{
|
|
2681
|
+
specifier: "decimal.js",
|
|
2682
|
+
platforms: ["web", "native"],
|
|
2683
|
+
category: "utility",
|
|
2684
|
+
description:
|
|
2685
|
+
"Arbitrary-precision decimal arithmetic. Reach for it whenever a widget computes MONEY: JavaScript numbers are binary floats, so a price total, a VAT line or a discount accumulated in `+`/`*` drifts by fractions of a cent and disagrees with what the backend charged. `new Decimal(a).plus(b).toFixed(2)` does not. Pure JS with zero dependencies, identical on both platforms. Not for general maths — it is slower than a number and only earns its cost where exactness is the point.",
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
specifier: "d3-scale",
|
|
2689
|
+
platforms: ["web", "native"],
|
|
2690
|
+
category: "utility",
|
|
2691
|
+
description:
|
|
2692
|
+
"Maps data values to pixel positions — `scaleLinear`, `scaleTime`, `scaleBand`, `scaleOrdinal`, plus the `.ticks()` an axis is labelled from. The maths half of a custom chart; pair it with d3-shape for the path and react-native-svg to draw. Pure JS (its only deps are other d3 modules), so one implementation covers both platforms.",
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
specifier: "d3-shape",
|
|
2696
|
+
platforms: ["web", "native"],
|
|
2697
|
+
category: "drawing",
|
|
2698
|
+
description:
|
|
2699
|
+
"SVG path generators — `line`, `area`, `arc`, `pie`, `curve*`. Each returns a path string you hand to the vetted react-native-svg's `<Path d={…} />`, so a bespoke line/area/donut chart renders identically in the Player and the Expo export from ONE source file. Pure JS.",
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
specifier: "d3-array",
|
|
2703
|
+
platforms: ["web", "native"],
|
|
2704
|
+
category: "utility",
|
|
2705
|
+
description:
|
|
2706
|
+
"Array statistics and binning — `extent`, `min`/`max`, `bisect`, `bin`, `group`, `rollup`. Chiefly how you compute the domain d3-scale expects from a dataset. Pure JS.",
|
|
2707
|
+
},
|
|
2708
|
+
{
|
|
2709
|
+
specifier: "simple-statistics",
|
|
2710
|
+
platforms: ["web", "native"],
|
|
2711
|
+
category: "utility",
|
|
2712
|
+
description:
|
|
2713
|
+
"Descriptive statistics and simple models — mean/median/mode, standard deviation, quantiles, linear regression, correlation. Small and pure JS, so a widget can summarise a datastore table without shipping a maths framework. For exact decimal arithmetic (money) use decimal.js instead: this operates on JS numbers.",
|
|
2714
|
+
},
|
|
2680
2715
|
];
|
|
2681
2716
|
|
|
2682
2717
|
// sc-1064: CORE React infrastructure specifiers the host RESOLVES at runtime
|
|
@@ -3721,7 +3756,19 @@ const CONTRACT = deepFreeze({
|
|
|
3721
3756
|
// capability is the direction §8 forbids. The slice is OPTIONAL, so a host
|
|
3722
3757
|
// that brokers nothing degrades the hook to supported:false rather than
|
|
3723
3758
|
// throwing. Minor bump on the pre-1.0 channel.
|
|
3724
|
-
|
|
3759
|
+
// 1.99.0: additive (sc-7191) — the vetted import allowlist gains five PURE-JS
|
|
3760
|
+
// packages, the first non-UI maths available to a widget: `decimal.js`
|
|
3761
|
+
// (exact decimal arithmetic — the platform has payments, invoicing and
|
|
3762
|
+
// VAT, and float money math disagrees with what the backend charged),
|
|
3763
|
+
// `d3-scale` + `d3-shape` + `d3-array` (the scale/path/domain maths a
|
|
3764
|
+
// bespoke chart needs, drawn through the already-vetted react-native-svg),
|
|
3765
|
+
// and `simple-statistics`. All five are `["web", "native"]` with no native
|
|
3766
|
+
// module, so this is full parity, not a §8 native-only case. Each is
|
|
3767
|
+
// host-shimmed in widgetLoader.js and pinned in the export for the same
|
|
3768
|
+
// reason date-fns is: an AI-agent widget is transpiled, never bundled, so
|
|
3769
|
+
// its bare import must resolve at runtime on both hosts. No existing entry
|
|
3770
|
+
// changed shape — minor bump on the pre-1.0 channel.
|
|
3771
|
+
version: "1.99.0",
|
|
3725
3772
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3726
3773
|
hooks: HOOKS,
|
|
3727
3774
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -2677,6 +2677,41 @@ const VETTED_IMPORTS = [
|
|
|
2677
2677
|
description:
|
|
2678
2678
|
"Device motion hardware on the Expo export: Accelerometer, Gyroscope, Magnetometer, DeviceMotion, Barometer, Pedometer and LightSensor, each read as an addListener subscription with setUpdateInterval — always remove the subscription on unmount, a sensor left running drains the battery. Expo SDK 56 ships 56.0.x. Native-only on purpose: the package's own web build derives acceleration from deviceorientation ANGLES rather than real motion, so a shake or tilt threshold tuned on one host would read differently on the other. Author it in widget.native.jsx and pair it with a widget.web.jsx reading window.DeviceMotionEvent (accelerationIncludingGravity / rotationRate), the browser API the same hardware exposes. Both hosts need a user gesture before readings start, and iOS Safari additionally needs an explicit DeviceMotionEvent.requestPermission() grant — so gate the reading behind a Pressable, never start it on mount.",
|
|
2679
2679
|
},
|
|
2680
|
+
{
|
|
2681
|
+
specifier: "decimal.js",
|
|
2682
|
+
platforms: ["web", "native"],
|
|
2683
|
+
category: "utility",
|
|
2684
|
+
description:
|
|
2685
|
+
"Arbitrary-precision decimal arithmetic. Reach for it whenever a widget computes MONEY: JavaScript numbers are binary floats, so a price total, a VAT line or a discount accumulated in `+`/`*` drifts by fractions of a cent and disagrees with what the backend charged. `new Decimal(a).plus(b).toFixed(2)` does not. Pure JS with zero dependencies, identical on both platforms. Not for general maths — it is slower than a number and only earns its cost where exactness is the point.",
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
specifier: "d3-scale",
|
|
2689
|
+
platforms: ["web", "native"],
|
|
2690
|
+
category: "utility",
|
|
2691
|
+
description:
|
|
2692
|
+
"Maps data values to pixel positions — `scaleLinear`, `scaleTime`, `scaleBand`, `scaleOrdinal`, plus the `.ticks()` an axis is labelled from. The maths half of a custom chart; pair it with d3-shape for the path and react-native-svg to draw. Pure JS (its only deps are other d3 modules), so one implementation covers both platforms.",
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
specifier: "d3-shape",
|
|
2696
|
+
platforms: ["web", "native"],
|
|
2697
|
+
category: "drawing",
|
|
2698
|
+
description:
|
|
2699
|
+
"SVG path generators — `line`, `area`, `arc`, `pie`, `curve*`. Each returns a path string you hand to the vetted react-native-svg's `<Path d={…} />`, so a bespoke line/area/donut chart renders identically in the Player and the Expo export from ONE source file. Pure JS.",
|
|
2700
|
+
},
|
|
2701
|
+
{
|
|
2702
|
+
specifier: "d3-array",
|
|
2703
|
+
platforms: ["web", "native"],
|
|
2704
|
+
category: "utility",
|
|
2705
|
+
description:
|
|
2706
|
+
"Array statistics and binning — `extent`, `min`/`max`, `bisect`, `bin`, `group`, `rollup`. Chiefly how you compute the domain d3-scale expects from a dataset. Pure JS.",
|
|
2707
|
+
},
|
|
2708
|
+
{
|
|
2709
|
+
specifier: "simple-statistics",
|
|
2710
|
+
platforms: ["web", "native"],
|
|
2711
|
+
category: "utility",
|
|
2712
|
+
description:
|
|
2713
|
+
"Descriptive statistics and simple models — mean/median/mode, standard deviation, quantiles, linear regression, correlation. Small and pure JS, so a widget can summarise a datastore table without shipping a maths framework. For exact decimal arithmetic (money) use decimal.js instead: this operates on JS numbers.",
|
|
2714
|
+
},
|
|
2680
2715
|
];
|
|
2681
2716
|
|
|
2682
2717
|
// sc-1064: CORE React infrastructure specifiers the host RESOLVES at runtime
|
|
@@ -3721,7 +3756,19 @@ const CONTRACT = deepFreeze({
|
|
|
3721
3756
|
// capability is the direction §8 forbids. The slice is OPTIONAL, so a host
|
|
3722
3757
|
// that brokers nothing degrades the hook to supported:false rather than
|
|
3723
3758
|
// throwing. Minor bump on the pre-1.0 channel.
|
|
3724
|
-
|
|
3759
|
+
// 1.99.0: additive (sc-7191) — the vetted import allowlist gains five PURE-JS
|
|
3760
|
+
// packages, the first non-UI maths available to a widget: `decimal.js`
|
|
3761
|
+
// (exact decimal arithmetic — the platform has payments, invoicing and
|
|
3762
|
+
// VAT, and float money math disagrees with what the backend charged),
|
|
3763
|
+
// `d3-scale` + `d3-shape` + `d3-array` (the scale/path/domain maths a
|
|
3764
|
+
// bespoke chart needs, drawn through the already-vetted react-native-svg),
|
|
3765
|
+
// and `simple-statistics`. All five are `["web", "native"]` with no native
|
|
3766
|
+
// module, so this is full parity, not a §8 native-only case. Each is
|
|
3767
|
+
// host-shimmed in widgetLoader.js and pinned in the export for the same
|
|
3768
|
+
// reason date-fns is: an AI-agent widget is transpiled, never bundled, so
|
|
3769
|
+
// its bare import must resolve at runtime on both hosts. No existing entry
|
|
3770
|
+
// changed shape — minor bump on the pre-1.0 channel.
|
|
3771
|
+
version: "1.99.0",
|
|
3725
3772
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3726
3773
|
hooks: HOOKS,
|
|
3727
3774
|
primitives: PRIMITIVES,
|
package/dist/dev-shims.js
CHANGED
|
@@ -117,6 +117,14 @@ const HOST_EXTERNAL_SPECIFIERS = [
|
|
|
117
117
|
// bundle shares one RN-web (StyleSheet/context) instead of inlining a second
|
|
118
118
|
// copy. On native, Metro resolves the real react-native in the export.
|
|
119
119
|
"react-native",
|
|
120
|
+
// sc-7191: the vetted pure-JS maths set. Externalised for the same reason as
|
|
121
|
+
// date-fns — an AI-agent widget is transpiled, never bundled, so the host
|
|
122
|
+
// must resolve the bare import at runtime.
|
|
123
|
+
"decimal.js",
|
|
124
|
+
"d3-scale",
|
|
125
|
+
"d3-shape",
|
|
126
|
+
"d3-array",
|
|
127
|
+
"simple-statistics",
|
|
120
128
|
];
|
|
121
129
|
|
|
122
130
|
/**
|
package/dist/linter.cjs
CHANGED
|
@@ -1201,6 +1201,37 @@ function _imagePercentHeightRules(source) {
|
|
|
1201
1201
|
return findings;
|
|
1202
1202
|
}
|
|
1203
1203
|
|
|
1204
|
+
// sc-7274 — a percentage `flexBasis` sizes the MAIN axis, which in a column is
|
|
1205
|
+
// the HEIGHT: under a definite-height parent (a Grid cell stretches its child
|
|
1206
|
+
// on the web Player) the child fills the container and, with `flex-shrink: 0`,
|
|
1207
|
+
// overflows over the content below. `width: "100%"` is the direction-agnostic
|
|
1208
|
+
// replacement. Mirror of linter.js (see there for the full rationale).
|
|
1209
|
+
const _FLEX_BASIS_PERCENT_RE =
|
|
1210
|
+
/\bflexBasis\s*:[^,}\n]*?(["'])\s*(\d+(?:\.\d+)?)\s*%\s*\1/g;
|
|
1211
|
+
|
|
1212
|
+
function _flexBasisPercentRules(source) {
|
|
1213
|
+
const findings = [];
|
|
1214
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
1215
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1216
|
+
_FLEX_BASIS_PERCENT_RE.lastIndex = 0;
|
|
1217
|
+
let hit;
|
|
1218
|
+
while ((hit = _FLEX_BASIS_PERCENT_RE.exec(code))) {
|
|
1219
|
+
const line = code.slice(0, hit.index).split(/\r?\n/).length;
|
|
1220
|
+
findings.push({
|
|
1221
|
+
rule: "flex-basis-percent",
|
|
1222
|
+
severity: "warning",
|
|
1223
|
+
label:
|
|
1224
|
+
`flexBasis "${hit[2]}%" sizes the MAIN axis — in a column that is the ` +
|
|
1225
|
+
`HEIGHT, so under a definite-height parent it fills the container and ` +
|
|
1226
|
+
`overflows below. Use width: "100%"; a NUMBER (flexBasis: 220) is a ` +
|
|
1227
|
+
`wrap threshold.`,
|
|
1228
|
+
line,
|
|
1229
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
return findings;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1204
1235
|
// sc-4913 — a measured frame INCLUDES the element's own padding, so cells sized
|
|
1205
1236
|
// from it overflow the content box and the last one wraps into an empty column.
|
|
1206
1237
|
// Mirror of linter.js (see there for the full rationale).
|
|
@@ -1491,6 +1522,9 @@ function lintSource(source, options) {
|
|
|
1491
1522
|
findings.push(..._lucideIconRules(source));
|
|
1492
1523
|
findings.push(..._selfContainedReferenceRules(source));
|
|
1493
1524
|
findings.push(..._imagePercentHeightRules(source));
|
|
1525
|
+
// sc-7274 — soft warning: a percentage flexBasis sizes the main axis, so in a
|
|
1526
|
+
// column it claims the parent's whole height and overflows.
|
|
1527
|
+
findings.push(..._flexBasisPercentRules(source));
|
|
1494
1528
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
1495
1529
|
// padding wraps the last grid column into an empty one.
|
|
1496
1530
|
findings.push(..._measuredPaddingRules(source));
|
package/dist/linter.js
CHANGED
|
@@ -1226,6 +1226,61 @@ function _imagePercentHeightRules(source) {
|
|
|
1226
1226
|
return findings;
|
|
1227
1227
|
}
|
|
1228
1228
|
|
|
1229
|
+
// sc-7274 — a percentage `flexBasis` sizes the MAIN axis, and in a column that
|
|
1230
|
+
// is the HEIGHT.
|
|
1231
|
+
//
|
|
1232
|
+
// `flexBasis: "100%"` written to mean "full width" is only that in a ROW. In a
|
|
1233
|
+
// column parent it asks for the parent's whole HEIGHT, and the moment an
|
|
1234
|
+
// ancestor makes that height definite — a Grid cell stretches its child on the
|
|
1235
|
+
// web Player — every such sibling becomes as tall as the container. `View`
|
|
1236
|
+
// defaults to `flex-shrink: 0`, so they cannot shrink back: they overflow
|
|
1237
|
+
// hundreds of pixels down over whatever follows. That shipped a customer form
|
|
1238
|
+
// with a blank band mid-layout and three fields painted across the submit row,
|
|
1239
|
+
// visible only once published (the builder canvas leaves the height auto,
|
|
1240
|
+
// where the percentage degrades to `content` and the form looks right).
|
|
1241
|
+
//
|
|
1242
|
+
// `width: "100%"` is the direction-agnostic replacement — it fills the row in a
|
|
1243
|
+
// column parent AND takes its own line in a wrap row — so a percentage basis is
|
|
1244
|
+
// never the only way to say it.
|
|
1245
|
+
//
|
|
1246
|
+
// `severity: "warning"`, like `image-percent-height`: in a row parent the value
|
|
1247
|
+
// IS correct, and an AST-free scan cannot see the parent's `flexDirection`
|
|
1248
|
+
// (the style usually lives in a shared field/cell helper). A warning still
|
|
1249
|
+
// drives an AI-agent repair turn, which is the point of the rule.
|
|
1250
|
+
//
|
|
1251
|
+
// Scope is the literal inline form, including the ternary a responsive cell
|
|
1252
|
+
// writes (`flexBasis: stacked ? "100%" : CARD`). A basis threaded through a
|
|
1253
|
+
// variable stays beyond the scan; the designer skill remains the first guard.
|
|
1254
|
+
const _FLEX_BASIS_PERCENT_RE =
|
|
1255
|
+
/\bflexBasis\s*:[^,}\n]*?(["'])\s*(\d+(?:\.\d+)?)\s*%\s*\1/g;
|
|
1256
|
+
|
|
1257
|
+
function _flexBasisPercentRules(source) {
|
|
1258
|
+
const findings = [];
|
|
1259
|
+
// Comments blanked, strings kept: the value under test IS a string literal,
|
|
1260
|
+
// so this rule's own documentation must not flag itself.
|
|
1261
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
1262
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1263
|
+
_FLEX_BASIS_PERCENT_RE.lastIndex = 0;
|
|
1264
|
+
let hit;
|
|
1265
|
+
while ((hit = _FLEX_BASIS_PERCENT_RE.exec(code))) {
|
|
1266
|
+
const line = code.slice(0, hit.index).split(/\r?\n/).length;
|
|
1267
|
+
findings.push({
|
|
1268
|
+
rule: "flex-basis-percent",
|
|
1269
|
+
severity: "warning",
|
|
1270
|
+
// Kept under FINDING_LABEL_BUDGET: the repair turn truncates the whole
|
|
1271
|
+
// message at 300 chars, which would otherwise eat the fix and the line.
|
|
1272
|
+
label:
|
|
1273
|
+
`flexBasis "${hit[2]}%" sizes the MAIN axis — in a column that is the ` +
|
|
1274
|
+
`HEIGHT, so under a definite-height parent it fills the container and ` +
|
|
1275
|
+
`overflows below. Use width: "100%"; a NUMBER (flexBasis: 220) is a ` +
|
|
1276
|
+
`wrap threshold.`,
|
|
1277
|
+
line,
|
|
1278
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
return findings;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1229
1284
|
// sc-4913 — a measured frame INCLUDES the element's own padding.
|
|
1230
1285
|
//
|
|
1231
1286
|
// `onLayout` reports the frame width, and padding sits inside that frame. A
|
|
@@ -1543,6 +1598,9 @@ export function lintSource(source, options) {
|
|
|
1543
1598
|
findings.push(..._selfContainedReferenceRules(source));
|
|
1544
1599
|
// sc-3493 — soft warning: percentage height on an <Image> collapses to 0.
|
|
1545
1600
|
findings.push(..._imagePercentHeightRules(source));
|
|
1601
|
+
// sc-7274 — soft warning: a percentage flexBasis sizes the main axis, so in a
|
|
1602
|
+
// column it claims the parent's whole height and overflows.
|
|
1603
|
+
findings.push(..._flexBasisPercentRules(source));
|
|
1546
1604
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
1547
1605
|
// padding wraps the last grid column into an empty one.
|
|
1548
1606
|
findings.push(..._measuredPaddingRules(source));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.128.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
],
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "node scripts/build.js",
|
|
52
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.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-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-hardcoded-design.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__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js"
|
|
52
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/vetted-imports-audit.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-invites.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-page-url.test.js src/__tests__/linter-image-height.test.js src/__tests__/linter-flex-basis-percent.test.js src/__tests__/linter-hardcoded-design.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__/flatten-entry.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js src/__tests__/property-schema-resolve.test.js src/__tests__/corner-radius.test.js src/__tests__/theme-components-parity.test.js src/__tests__/navigation-parity.test.js src/__tests__/theme-depth-tokens.test.js src/__tests__/interaction-lift.test.js src/__tests__/toast-host.test.js src/__tests__/overlay-tokens.test.js src/__tests__/hooks-domain-error-mapping.test.js src/__tests__/linter-datastore-error.test.js src/__tests__/linter-write-gating.test.js src/__tests__/hooks-speech-to-text.test.js src/__tests__/hooks-camera.test.js src/__tests__/hooks-image-editor.test.js src/__tests__/hooks-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js src/__tests__/linter-html-in-content.test.js src/__tests__/markdown.test.js src/__tests__/markdown-edit.test.js src/__tests__/richtext-tokens.test.js"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=18"
|
package/src/dev-shims.js
CHANGED
|
@@ -117,6 +117,14 @@ const HOST_EXTERNAL_SPECIFIERS = [
|
|
|
117
117
|
// bundle shares one RN-web (StyleSheet/context) instead of inlining a second
|
|
118
118
|
// copy. On native, Metro resolves the real react-native in the export.
|
|
119
119
|
"react-native",
|
|
120
|
+
// sc-7191: the vetted pure-JS maths set. Externalised for the same reason as
|
|
121
|
+
// date-fns — an AI-agent widget is transpiled, never bundled, so the host
|
|
122
|
+
// must resolve the bare import at runtime.
|
|
123
|
+
"decimal.js",
|
|
124
|
+
"d3-scale",
|
|
125
|
+
"d3-shape",
|
|
126
|
+
"d3-array",
|
|
127
|
+
"simple-statistics",
|
|
120
128
|
];
|
|
121
129
|
|
|
122
130
|
/**
|