@colixsystems/widget-sdk 0.131.0 → 0.133.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 CHANGED
@@ -70,7 +70,17 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
70
70
 
71
71
  ## Status
72
72
 
73
- `v0.131.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**.
73
+ `v0.133.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.133.0 (contract 1.102.0)
76
+
77
+ **A required `tableRef` needs a `datastoreTemplate` table that answers to its NAME, not just another table in the list (sc-6965).** 0.57.0's publish gate `manifest.requiredTableRefsHaveTemplate` compared COUNTS — it passed as soon as `datastoreTemplate.tables` plus any host-supplied tables outnumbered the `required` `tableRef` props. So a widget with two required props published on any two template tables, including the case where both of them name the SAME prop and the other has nothing: the installer, which binds by name, then had no table for it and fell back to whichever one was left over — your widget wired to a table its code was never written against. The gate now runs the installer's own matcher over your template, per property: a table's `suffix` must answer to the property's name (`ordersTableId` needs suffix `Orders`), and the failure names only the properties nothing answers to, with the suffix each one wants. **What still passes unchanged:** the conventional bare `tableId`, which carries no name to match and takes the first table still free; a property marked `sharedTable: true`, which seeds nothing by design (0.106.0); and a standalone submit with no template at all, which fails exactly as it did. **What to change if you are newly rejected:** name the table after the property it is for — that is the pairing that was always going to decide the binding. Every first-party widget passes unchanged. No export, type, hook, or manifest field changed shape — a publish-gate tightening plus documentation. `CONTRACT` is unchanged (no new field).
78
+
79
+ ### What's new in 0.132.0 (contract 1.102.0)
80
+
81
+ **BREAKING: the top bar has no divider — `resolveTopBarTokens` drops `borderColor` and `borderWidth` (sc-7360).** The bar and its tab row are one surface; a line under the bar ruled it off from its own menu, and a line under the tabs ruled the menu off from the page it navigates. Neither host draws one any more, the Design page offers no colour or width for one, and `TopBarTokens` no longer carries the two fields. A stored `topBar.borderColor` / `borderWidth` is **inert, not migrated** — nothing reads it, and the theme coercer drops it from a saved look or a Mason `set_theme`. The rail's, footer's and site header's dividers are unchanged.
82
+
83
+ - `resolveSidebarTokens` / `resolveFooterTokens` are unchanged. No `CONTRACT` change.
74
84
 
75
85
  ### What's new in 0.131.0 (contract 1.102.0)
76
86
 
@@ -1382,7 +1392,7 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
1382
1392
  - **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.
1383
1393
  - **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.
1384
1394
  - **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.
1385
- - **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.
1395
+ - **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. When a field carries an icon beside it (a search glyph, a clear button), the border belongs on the WRAPPER row and the `TextInput` inside it goes borderless and transparent with `flex: 1, minWidth: 0` (the `minWidth: 0` stops a long value pushing the border past its container) — React Native has no `:focus-within`, so drive the wrapper's `borderColor` between `colors.border` and `colors.primary` from the input's own `onFocus` / `onBlur`. A border left on the input rings only its own `<input>` box on web, leaving the icon outside the ring; never absolutely-position the icon over the field to work around it.
1386
1396
  - **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.
1387
1397
  - **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`.
1388
1398
  - **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).
package/dist/host.d.ts CHANGED
@@ -222,13 +222,12 @@ export function resolveSidebarTokens(theme: unknown): SidebarTokens;
222
222
 
223
223
  /** The top app bar's resolved tokens. `tintColor` and `titleColor` are separate
224
224
  * because an unthemed bar paints its icons slate and its app name in the brand
225
- * colour; an authored `topBar.textColor` drives both. */
225
+ * colour; an authored `topBar.textColor` drives both. The bar carries NO
226
+ * divider (sc-7360): a stored `topBar.borderColor` / `borderWidth` is inert. */
226
227
  export interface TopBarTokens {
227
228
  backgroundColor: string;
228
229
  tintColor: string;
229
230
  titleColor: string;
230
- borderColor: string | null;
231
- borderWidth: number | null;
232
231
  /** The current page's mark — the active link's label, and an active tab's
233
232
  * label plus its indicator. Falls back to the RAIL's `activeColor` and then
234
233
  * to the brand, so an app states its navigation colour once. */
@@ -281,7 +281,9 @@ function resolveSidebarTokens(theme) {
281
281
  }
282
282
 
283
283
  /**
284
- * The top app bar's surface, its two text colours and its opt-in divider.
284
+ * The top app bar's surface and its two text colours. It has NO divider
285
+ * (sc-7360): the bar and its tab row sit in one surface, so a stored divider
286
+ * colour or width on the block is inert on both hosts.
285
287
  *
286
288
  * `tintColor` and `titleColor` are SEPARATE because the web bar has always
287
289
  * painted them differently when the author names nothing: the hamburger and
@@ -295,13 +297,11 @@ function resolveSidebarTokens(theme) {
295
297
  * @param {unknown} theme — the whole `theme_config`; the app's
296
298
  * `backgroundColor`/`backgroundGradient` are read because an unset surface
297
299
  * resolves to the page's, as an `attached` tab's already did (sc-6596).
298
- * @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
299
- * borderColor: string|null, borderWidth: number|null }}
300
+ * @returns {{ backgroundColor: string, tintColor: string, titleColor: string }}
300
301
  */
301
302
  function resolveTopBarTokens(theme) {
302
303
  const config = isPlainObject(theme) ? theme : {};
303
304
  const topBar = isPlainObject(config.topBar) ? config.topBar : {};
304
- const borderColor = hexOrNull(topBar.borderColor);
305
305
  // An explicit colour drives BOTH slots; only the unset case splits.
306
306
  const authored = hexOrNull(topBar.textColor);
307
307
  return {
@@ -324,13 +324,10 @@ function resolveTopBarTokens(theme) {
324
324
  topBar.activeColor,
325
325
  hexOr(sidebarBlock(config).activeColor, brandPrimary(config)),
326
326
  ),
327
- borderColor,
328
- borderWidth: borderColor ? borderWidthOr(topBar.borderWidth) : null,
329
327
  // REQ-NAV-STRUCTURE: how the tab row marks its current page.
330
328
  // `underline` keeps the tab in the bar's surface and marks it with an
331
329
  // indicator. `attached` makes it a real folder tab: it takes the CONTENT's
332
- // surface and sits over the row's divider, so the tab and the page beneath
333
- // read as one plane. Meaningless on a shape that draws no tab row — a host
330
+ // surface and meets the page beneath it, so the two read as one plane. Meaningless on a shape that draws no tab row a host
334
331
  // reads it only where it has tabs to draw.
335
332
  tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
336
333
  // The surface an `attached` tab AND its content panel share. One value for
@@ -272,7 +272,9 @@ export function resolveSidebarTokens(theme) {
272
272
  }
273
273
 
274
274
  /**
275
- * The top app bar's surface, its two text colours and its opt-in divider.
275
+ * The top app bar's surface and its two text colours. It has NO divider
276
+ * (sc-7360): the bar and its tab row sit in one surface, so a stored divider
277
+ * colour or width on the block is inert on both hosts.
276
278
  *
277
279
  * `tintColor` and `titleColor` are SEPARATE because the web bar has always
278
280
  * painted them differently when the author names nothing: the hamburger and
@@ -286,13 +288,11 @@ export function resolveSidebarTokens(theme) {
286
288
  * @param {unknown} theme — the whole `theme_config`; the app's
287
289
  * `backgroundColor`/`backgroundGradient` are read because an unset surface
288
290
  * resolves to the page's, as an `attached` tab's already did (sc-6596).
289
- * @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
290
- * borderColor: string|null, borderWidth: number|null }}
291
+ * @returns {{ backgroundColor: string, tintColor: string, titleColor: string }}
291
292
  */
292
293
  export function resolveTopBarTokens(theme) {
293
294
  const config = isPlainObject(theme) ? theme : {};
294
295
  const topBar = isPlainObject(config.topBar) ? config.topBar : {};
295
- const borderColor = hexOrNull(topBar.borderColor);
296
296
  // An explicit colour drives BOTH slots; only the unset case splits.
297
297
  const authored = hexOrNull(topBar.textColor);
298
298
  return {
@@ -315,13 +315,10 @@ export function resolveTopBarTokens(theme) {
315
315
  topBar.activeColor,
316
316
  hexOr(sidebarBlock(config).activeColor, brandPrimary(config)),
317
317
  ),
318
- borderColor,
319
- borderWidth: borderColor ? borderWidthOr(topBar.borderWidth) : null,
320
318
  // REQ-NAV-STRUCTURE: how the tab row marks its current page.
321
319
  // `underline` keeps the tab in the bar's surface and marks it with an
322
320
  // indicator. `attached` makes it a real folder tab: it takes the CONTENT's
323
- // surface and sits over the row's divider, so the tab and the page beneath
324
- // read as one plane. Meaningless on a shape that draws no tab row — a host
321
+ // surface and meets the page beneath it, so the two read as one plane. Meaningless on a shape that draws no tab row a host
325
322
  // reads it only where it has tabs to draw.
326
323
  tabStyle: topBar.tabStyle === "attached" ? "attached" : "underline",
327
324
  // The surface an `attached` tab AND its content panel share. One value for
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.131.0",
3
+ "version": "0.133.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",