@colixsystems/widget-sdk 0.290.0 → 0.291.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 +8 -1
- package/dist/linter.cjs +75 -0
- package/dist/linter.js +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
90
90
|
|
|
91
91
|
## Status
|
|
92
92
|
|
|
93
|
-
`v0.
|
|
93
|
+
`v0.291.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**.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.291.0 (contract 1.238.0)
|
|
96
|
+
|
|
97
|
+
**New linter rule `grid-cell-too-narrow` (severity `error`, blocking).** A `<Grid minCellWidth={N}>` with `N` under 200 whose cell holds more than one `Text` is refused, naming the cell width and the default of 240: on cards carrying a title, a duration and a price it makes five narrow columns at desktop width, with broken words and the price wrapped onto two lines. A cell of a single `Text` (a chip, an avatar, an icon tile) passes at any width, and a Grid without `minCellWidth` passes. A width given as a `const` is followed; one computed at runtime is beyond the scan. See *Design & visual polish*.
|
|
94
98
|
|
|
95
99
|
### What's new in 0.290.0 (contract 1.238.0)
|
|
96
100
|
|
|
@@ -3492,6 +3496,7 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
|
|
|
3492
3496
|
- **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.
|
|
3493
3497
|
- **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.
|
|
3494
3498
|
- **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 `const [width, onLayout] = useContainerWidth();`, spreading `onLayout` on an unpadded root `View` (a React Native callback, so it behaves identically on both hosts). A width of `0` means "not yet measured" and the SDK helpers answer it with the wide form, so do not special-case it — except a count or size computed from the width (a calendar day, a chart, a card grid's cards per line), which renders nothing size-dependent until the width arrives: a widget gets no declared breakpoint prop, but it can always measure. **Every form, list, table and filter bar lays out in three tiers**: `const [width, onLayout] = useContainerWidth();` then branch on `widthTier(width)` — a form is one column narrow, paired short fields at medium, a 2–3 column grid capped near 1000px wide; a list is stacked cards, one-line cards, then real columns under a header; a filter bar is one toolbar row (search, a dropdown per facet, sort) with the active filters as removable chips beneath, and a single "Filters" button opening a panel when narrow. **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 in the wide tier** (`flexDirection: widthTier(width) === 'wide' ? '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. **Whatever you narrow, you centre** — a block that caps its own measure carries all three of `{ width: '100%', maxWidth: FORM_MAX, alignSelf: 'center' }`, because `alignSelf` alone stops the block stretching and sizes it to its content instead, and `maxWidth` alone leaves the widget flush left with dead space beside it; a capped wrap row lays its cells out along the MAIN axis, so it centres with `justifyContent: 'center'` on the row instead — but a card grid or a tile row is never capped. **Lay out a card grid, a tile row or a grid of capped cells with `<Grid>` by default** — it measures its own unpadded box and applies the rules below; lay one out yourself when it needs what `<Grid>` cannot express, such as masonry or cells that span lines. **A card grid fills its lines, it never wraps freely** — a free `flexWrap` strands the remainder (six cards that fit five render 5 + 1). Count the cards that fit the measured width (`const max = Math.max(1, Math.floor((width + gap) / (CARD_MIN + gap)));`), take `const lines = Math.ceil(count / max); const cols = Math.ceil(count / lines);`, and render each slice of `cols` cards as a row of `{ flex: 1, minWidth: 0 }` cells, padding the last line with empty `{ flex: 1 }` spacers so its cards keep the width of the cards above (six that fit five are 3 + 3, ten that fit four are 4 + 4 + 2). **A short value never breaks across lines** — a time, date, price, count, percentage or code takes `numberOfLines={1}` and `flexShrink: 0`, never a fixed width narrower than it, and a narrow slot re-arranges or steps the font down instead. **A metric / stat-tile row FILLS ITS ROW** — when you know the count, balance the lines (`const lines = Math.ceil(count / max); const base = Math.floor(count / lines); const extra = count % lines; const rows = Array.from({ length: lines }, (_, i) => tiles.slice(i * base + Math.min(i, extra), (i + 1) * base + Math.min(i + 1, extra)));`, with `max` 4 / 3 / 2 by tier, each tile `{ flex: 1, minWidth: 0 }`), and when the count comes from data wrap it (`{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.md }`, each tile `{ flexGrow: 1, flexBasis: TILE_MIN }`, never a `maxWidth` — capped tiles cluster to one side), because a bare `flexDirection: 'row'` has no way to break and a React Native flex child defaults to `flexShrink: 0` (unlike CSS), so the tiles cannot give ground either: the row runs past its container, spilling out of the card natively and cut off by the scrolling page column on web, with nothing thrown and nothing logged, once there are enough of them (four fit where five do not). The basis is also the per-line ceiling — `Math.floor((usable + gap) / (TILE_MIN + gap))` tiles fit a line, so 240 tops out at five across a full-width ~1400px section and 320 at four; past four or five a line the tiles stop reading as cards.
|
|
3499
|
+
- **A Grid cell that holds more than one `Text` is at least 200 wide.** A cell that holds more than one Text is at least 200 wide: `<Grid minCellWidth={150}>` makes five narrow columns at desktop width, and a card carrying a title, a duration and a price breaks words and wraps the price onto two lines there. Leave `minCellWidth` off (the default is 240) or set it to 240 or more; a width under 200 is for cells of a single `Text` (chips, avatars, icon tiles), and the publish gate refuses (`grid-cell-too-narrow`) a narrower Grid whose cell holds more than one `Text`. An action inside a card is a `<Button>`, never a `Pressable` around text.
|
|
3495
3500
|
- **Compose forms — pair fields into rows, don't stack one per row.** Lay a form out with `<Form>` by default (above), which does the following for you; when it needs what `<Form>` cannot do (a multi-step wizard, a form beside a live preview, fields laid over a picture), lay the form out in three tiers and cap it near 1000px, centred with the three declarations above — never a 560–720px column alone in a wide box, which leaves a form that IS the page as a narrow strip in empty space. 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 — 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. A search box is `<SearchField>`, which already carries its glyph and clear control. Any other icon goes inside the field through `leadingIcon` / `trailingIcon`; never wrap a `<TextField>` in a bordered row, since it draws its own border. Put a unit beside the field as a sibling in a row (`flex: 1, minWidth: 0` on the field, so a long value shrinks instead of pushing the row past its container) rather than rebuilding the field to hold it: a hand-built bordered wrapper around a bare `TextInput` loses the app's input look and its label, focus and error handling.
|
|
3496
3501
|
- **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.
|
|
3497
3502
|
- **Drag and drop — show what is being dragged.** Use `<SortableList>` for a list the user reorders and `<Board>` for cards moved between columns by default: they do everything this item describes, on both hosts, and give a user who cannot drag the same moves from the handle. When the request needs what they cannot do (free-form positioning on a canvas, a drag between two widgets), build your own as follows. 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`. **A board (kanban, pipeline) drops onto ANOTHER column**, which needs four more things: the board's root renders the one proxy (a column clips whatever leaves it); you hit-test the drop yourself — `measureInWindow` every column and the board when the drag starts and compare the gesture's window `moveX` (never `onLayout` frames, which are relative to a padded or scrolled parent) — highlighting exactly one target; the drop WRITES (`update(id, { [statusField]: value })` through `useDatastoreMutation`, optimistic until `refetch()`, a toast and a spring-back on failure); and three or more columns go in a `horizontal` `ScrollView` when `widthTier(width) === "narrow"`, with the grip claiming the gesture (`onStartShouldSetPanResponder`, `onPanResponderTerminationRequest: () => false`) and carrying `touchAction: 'none'` on web so a finger drag moves the card instead of scrolling the page. And because `useRef(PanResponder.create(...))` runs ONCE, everything its callbacks read — the `useCanWrite` flag included, which starts `false` — goes through a ref refreshed every render (`latest.current.canDrag`); the publish gate `widget.staleResponder` rejects a value read directly.
|
|
@@ -3904,6 +3909,8 @@ Pass `--manifest` to enable `style-field-unread` and the three editability rules
|
|
|
3904
3909
|
|
|
3905
3910
|
`flex-basis-percent` flags a literal percentage `flexBasis` — `"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.
|
|
3906
3911
|
|
|
3912
|
+
`grid-cell-too-narrow` refuses a `<Grid minCellWidth={N}>` with `N` under 200 whose cell holds more than one `Text`, naming the cell width and the default of 240: a card with a title and a price breaks its words at that width. A cell of a single `Text` passes at any width, and a Grid without `minCellWidth` passes. An error.
|
|
3913
|
+
|
|
3907
3914
|
`row-action-not-gated` flags a widget that calls `update` or `delete` from `useDatastoreMutation` without a per-row permission check: no `_capabilities` read and no `useCanWrite(tableId, { recordId })`. The table-level `useCanWrite(tableId)` only answers "may create", so a list gated on it shows Edit / Delete on rows the server will refuse. A warning.
|
|
3908
3915
|
|
|
3909
3916
|
`no-html-in-content` flags an HTML tag inside a **string** — `"<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.
|
package/dist/linter.cjs
CHANGED
|
@@ -2165,6 +2165,79 @@ function _fieldInBorderedRowRules(source) {
|
|
|
2165
2165
|
return findings;
|
|
2166
2166
|
}
|
|
2167
2167
|
|
|
2168
|
+
// sc-15754 — a Grid cell narrower than the default fits one Text, not a card.
|
|
2169
|
+
//
|
|
2170
|
+
// `<Grid minCellWidth={150}>` on cards carrying a title, a duration and a
|
|
2171
|
+
// price made five narrow columns at desktop width, with broken words and the
|
|
2172
|
+
// prices wrapped onto two lines; the agent cannot see that until it renders.
|
|
2173
|
+
// A cell of a single Text (a chip, an avatar, an icon tile) is fine at any
|
|
2174
|
+
// width, so only a cell holding more than one Text is refused. A width read
|
|
2175
|
+
// from a `const` is followed; one computed at runtime stays beyond the scan.
|
|
2176
|
+
const _GRID_DEFAULT_MIN_CELL_WIDTH = 240; // grid-logic.js DEFAULT_MIN_CELL_WIDTH
|
|
2177
|
+
const _GRID_NARROW_CELL_WIDTH = 200;
|
|
2178
|
+
const _MIN_CELL_WIDTH_ATTR_RE =
|
|
2179
|
+
/\bminCellWidth\s*=\s*\{\s*(\d+(?:\.\d+)?|[A-Za-z_$][\w$]*)\s*\}/;
|
|
2180
|
+
|
|
2181
|
+
function _gridMinCellWidth(structure, attrs) {
|
|
2182
|
+
const hit = _MIN_CELL_WIDTH_ATTR_RE.exec(attrs);
|
|
2183
|
+
if (!hit) return NaN;
|
|
2184
|
+
if (/^\d/.test(hit[1])) return Number(hit[1]);
|
|
2185
|
+
const declared = new RegExp(`\\bconst\\s+${hit[1]}\\s*=\\s*(\\d+(?:\\.\\d+)?)\\s*;`).exec(structure);
|
|
2186
|
+
return declared ? Number(declared[1]) : NaN;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function _gridCellWidthRules(source) {
|
|
2190
|
+
const findings = [];
|
|
2191
|
+
const structure = _stripNonCode(source);
|
|
2192
|
+
const index = _jsxScanIndex(structure);
|
|
2193
|
+
const sourceLines = source.split(/\r?\n/);
|
|
2194
|
+
const lineAt = _lineCounter(structure);
|
|
2195
|
+
const open = _openTagStack();
|
|
2196
|
+
_JSX_TOKEN_RE.lastIndex = 0;
|
|
2197
|
+
let token;
|
|
2198
|
+
while ((token = _JSX_TOKEN_RE.exec(structure))) {
|
|
2199
|
+
const [, closing, name] = token;
|
|
2200
|
+
const parent = open.top();
|
|
2201
|
+
if (!name) {
|
|
2202
|
+
if (structure[token.index + 1 + closing.length] !== ">") continue;
|
|
2203
|
+
// A fragment is transparent: its children are its parent's children.
|
|
2204
|
+
if (closing) open.pop();
|
|
2205
|
+
else open.push({ name: "", narrow: parent && parent.narrow, cell: parent && parent.cell });
|
|
2206
|
+
continue;
|
|
2207
|
+
}
|
|
2208
|
+
if (closing) {
|
|
2209
|
+
open.close(name);
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
const end = index.tagEnd(token.index);
|
|
2213
|
+
if (end === -1) break;
|
|
2214
|
+
const tag = { name, narrow: undefined, cell: parent && parent.cell };
|
|
2215
|
+
if (parent && parent.narrow !== undefined) tag.cell = { width: parent.narrow, at: token.index, texts: 0 };
|
|
2216
|
+
if (name === "Grid") {
|
|
2217
|
+
const width = _gridMinCellWidth(structure, structure.slice(token.index, end));
|
|
2218
|
+
if (width < _GRID_NARROW_CELL_WIDTH) tag.narrow = width;
|
|
2219
|
+
}
|
|
2220
|
+
if (name === "Text" && tag.cell && ++tag.cell.texts === 2) {
|
|
2221
|
+
const line = lineAt(tag.cell.at);
|
|
2222
|
+
findings.push({
|
|
2223
|
+
rule: "grid-cell-too-narrow",
|
|
2224
|
+
severity: "error",
|
|
2225
|
+
label:
|
|
2226
|
+
`<Grid minCellWidth={${tag.cell.width}}> makes each cell ${tag.cell.width}px wide, and a cell ` +
|
|
2227
|
+
`holding more than one Text breaks its words and wraps a price or a time onto two ` +
|
|
2228
|
+
`lines at that width. Leave minCellWidth off (the default is ${_GRID_DEFAULT_MIN_CELL_WIDTH}) ` +
|
|
2229
|
+
`or set it to ${_GRID_DEFAULT_MIN_CELL_WIDTH} or more; a width under ${_GRID_NARROW_CELL_WIDTH} ` +
|
|
2230
|
+
`fits only a cell of a single Text, such as a chip, an avatar or an icon tile.`,
|
|
2231
|
+
line,
|
|
2232
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
if (!_selfClosing(structure, end)) open.push(tag);
|
|
2236
|
+
_JSX_TOKEN_RE.lastIndex = end + 1;
|
|
2237
|
+
}
|
|
2238
|
+
return findings;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2168
2241
|
// sc-4913 — a measured frame INCLUDES the element's own padding.
|
|
2169
2242
|
//
|
|
2170
2243
|
// `onLayout` reports the frame width, and padding sits inside that frame. A
|
|
@@ -2896,6 +2969,8 @@ function lintSource(source, options) {
|
|
|
2896
2969
|
findings.push(..._fullWidthMarginRules(source));
|
|
2897
2970
|
// sc-12232 — soft warning: a field inside a bordered row draws two borders.
|
|
2898
2971
|
findings.push(..._fieldInBorderedRowRules(source));
|
|
2972
|
+
// sc-15754 — a Grid cell under 200px holding more than one Text is refused.
|
|
2973
|
+
findings.push(..._gridCellWidthRules(source));
|
|
2899
2974
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
2900
2975
|
// padding wraps the last grid column into an empty one.
|
|
2901
2976
|
findings.push(..._measuredPaddingRules(source));
|
package/dist/linter.js
CHANGED
|
@@ -2160,6 +2160,79 @@ function _fieldInBorderedRowRules(source) {
|
|
|
2160
2160
|
return findings;
|
|
2161
2161
|
}
|
|
2162
2162
|
|
|
2163
|
+
// sc-15754 — a Grid cell narrower than the default fits one Text, not a card.
|
|
2164
|
+
//
|
|
2165
|
+
// `<Grid minCellWidth={150}>` on cards carrying a title, a duration and a
|
|
2166
|
+
// price made five narrow columns at desktop width, with broken words and the
|
|
2167
|
+
// prices wrapped onto two lines; the agent cannot see that until it renders.
|
|
2168
|
+
// A cell of a single Text (a chip, an avatar, an icon tile) is fine at any
|
|
2169
|
+
// width, so only a cell holding more than one Text is refused. A width read
|
|
2170
|
+
// from a `const` is followed; one computed at runtime stays beyond the scan.
|
|
2171
|
+
const _GRID_DEFAULT_MIN_CELL_WIDTH = 240; // grid-logic.js DEFAULT_MIN_CELL_WIDTH
|
|
2172
|
+
const _GRID_NARROW_CELL_WIDTH = 200;
|
|
2173
|
+
const _MIN_CELL_WIDTH_ATTR_RE =
|
|
2174
|
+
/\bminCellWidth\s*=\s*\{\s*(\d+(?:\.\d+)?|[A-Za-z_$][\w$]*)\s*\}/;
|
|
2175
|
+
|
|
2176
|
+
function _gridMinCellWidth(structure, attrs) {
|
|
2177
|
+
const hit = _MIN_CELL_WIDTH_ATTR_RE.exec(attrs);
|
|
2178
|
+
if (!hit) return NaN;
|
|
2179
|
+
if (/^\d/.test(hit[1])) return Number(hit[1]);
|
|
2180
|
+
const declared = new RegExp(`\\bconst\\s+${hit[1]}\\s*=\\s*(\\d+(?:\\.\\d+)?)\\s*;`).exec(structure);
|
|
2181
|
+
return declared ? Number(declared[1]) : NaN;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
function _gridCellWidthRules(source) {
|
|
2185
|
+
const findings = [];
|
|
2186
|
+
const structure = _stripNonCode(source);
|
|
2187
|
+
const index = _jsxScanIndex(structure);
|
|
2188
|
+
const sourceLines = source.split(/\r?\n/);
|
|
2189
|
+
const lineAt = _lineCounter(structure);
|
|
2190
|
+
const open = _openTagStack();
|
|
2191
|
+
_JSX_TOKEN_RE.lastIndex = 0;
|
|
2192
|
+
let token;
|
|
2193
|
+
while ((token = _JSX_TOKEN_RE.exec(structure))) {
|
|
2194
|
+
const [, closing, name] = token;
|
|
2195
|
+
const parent = open.top();
|
|
2196
|
+
if (!name) {
|
|
2197
|
+
if (structure[token.index + 1 + closing.length] !== ">") continue;
|
|
2198
|
+
// A fragment is transparent: its children are its parent's children.
|
|
2199
|
+
if (closing) open.pop();
|
|
2200
|
+
else open.push({ name: "", narrow: parent && parent.narrow, cell: parent && parent.cell });
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
if (closing) {
|
|
2204
|
+
open.close(name);
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
const end = index.tagEnd(token.index);
|
|
2208
|
+
if (end === -1) break;
|
|
2209
|
+
const tag = { name, narrow: undefined, cell: parent && parent.cell };
|
|
2210
|
+
if (parent && parent.narrow !== undefined) tag.cell = { width: parent.narrow, at: token.index, texts: 0 };
|
|
2211
|
+
if (name === "Grid") {
|
|
2212
|
+
const width = _gridMinCellWidth(structure, structure.slice(token.index, end));
|
|
2213
|
+
if (width < _GRID_NARROW_CELL_WIDTH) tag.narrow = width;
|
|
2214
|
+
}
|
|
2215
|
+
if (name === "Text" && tag.cell && ++tag.cell.texts === 2) {
|
|
2216
|
+
const line = lineAt(tag.cell.at);
|
|
2217
|
+
findings.push({
|
|
2218
|
+
rule: "grid-cell-too-narrow",
|
|
2219
|
+
severity: "error",
|
|
2220
|
+
label:
|
|
2221
|
+
`<Grid minCellWidth={${tag.cell.width}}> makes each cell ${tag.cell.width}px wide, and a cell ` +
|
|
2222
|
+
`holding more than one Text breaks its words and wraps a price or a time onto two ` +
|
|
2223
|
+
`lines at that width. Leave minCellWidth off (the default is ${_GRID_DEFAULT_MIN_CELL_WIDTH}) ` +
|
|
2224
|
+
`or set it to ${_GRID_DEFAULT_MIN_CELL_WIDTH} or more; a width under ${_GRID_NARROW_CELL_WIDTH} ` +
|
|
2225
|
+
`fits only a cell of a single Text, such as a chip, an avatar or an icon tile.`,
|
|
2226
|
+
line,
|
|
2227
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
if (!_selfClosing(structure, end)) open.push(tag);
|
|
2231
|
+
_JSX_TOKEN_RE.lastIndex = end + 1;
|
|
2232
|
+
}
|
|
2233
|
+
return findings;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2163
2236
|
// sc-4913 — a measured frame INCLUDES the element's own padding.
|
|
2164
2237
|
//
|
|
2165
2238
|
// `onLayout` reports the frame width, and padding sits inside that frame. A
|
|
@@ -2891,6 +2964,8 @@ export function lintSource(source, options) {
|
|
|
2891
2964
|
findings.push(..._fullWidthMarginRules(source));
|
|
2892
2965
|
// sc-12232 — soft warning: a field inside a bordered row draws two borders.
|
|
2893
2966
|
findings.push(..._fieldInBorderedRowRules(source));
|
|
2967
|
+
// sc-15754 — a Grid cell under 200px holding more than one Text is refused.
|
|
2968
|
+
findings.push(..._gridCellWidthRules(source));
|
|
2894
2969
|
// sc-4913 — soft warning: a measured width that includes the widget's own
|
|
2895
2970
|
// padding wraps the last grid column into an empty one.
|
|
2896
2971
|
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.291.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",
|