@colixsystems/widget-sdk 0.155.0 → 0.157.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 +20 -5
- package/dist/container-width.js +27 -0
- package/dist/contract.cjs +27 -2
- package/dist/contract.js +27 -2
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -1
- package/dist/index.native.js +6 -1
- package/dist/navigation.cjs +29 -7
- package/dist/navigation.js +29 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
31
31
|
| **CORE** | `useFill()` | `boolean` | `ctx.fill` — no scope. `true` when the host sized this widget to fill its page-grid tile's reserved height (containers + media fill by default; the author can override per tile). Media-style widgets switch to a `flex: 1` / `height: "100%"` layout; others ignore it. Defaults `false`. |
|
|
32
32
|
| **CORE** | `useContainerWidth()` | `[width, onLayout]` | No context slice, no scope. Measures the width the widget's OWN box has, so it can lay itself out for the space it is in rather than for the screen. Spread the handler onto your outermost primitive. Use it for any widget with a wide form and a narrow one (a table, a toolbar, a row of tiles) — never switch on the device or window width, because a widget in a one-of-three grid cell on desktop has phone-width room and a widget filling a phone page does not. Width is 0 until the first layout: render the WIDE form then. One implementation covers web (react-native-web) and the native export. |
|
|
33
33
|
| **CORE** | `isNarrowWidth(width)` | `boolean` | No context slice, no scope. True when a MEASURED width is below `NARROW_WIDTH_PX` (480) — the one threshold every widget switches at, so a page reflows together rather than raggedly. An unmeasured width (0) is NOT narrow, so nothing flashes through the narrow form on first paint. |
|
|
34
|
+
| **CORE** | `widthTier(width)` | `"narrow" \| "medium" \| "wide"` | No context slice, no scope. Which of THREE layouts a measured width calls for: narrow below `NARROW_WIDTH_PX` (480), wide at or above `WIDE_WIDTH_PX` (960), medium between. `widthTier(w) === "narrow"` exactly when `isNarrowWidth(w)`; an unmeasured width (0) is "wide". |
|
|
34
35
|
| **CORE** | `useSectionEmpty(isEmpty)` | `void` | `ctx.section.reportEmpty` — no scope. Declares that the widget has NO content to show, so the host drops its layout slot instead of reserving space (and its parent's `gap`) for it. Returning `null` is not enough: the host wraps every widget in an entrance element, so a widget rendering nothing still leaves an empty box the parent stack gaps around. For a CONDITIONALLY ABSENT section (a per-record child collection with no rows for this record), never to suppress a genuine empty state. Stays mounted while collapsed, so passing `false` brings it back. Authoring surfaces never collapse. No-op on a host that doesn't implement it. |
|
|
35
36
|
| **CORE** | `useScreenTitle(title)` | `void` | `ctx.screen.reportTitle` — no scope. Names the screen the widget is on, so the host paints that name in the app header beside the back control instead of the app brand. The page's own name is the default; only the widget knows the screen is really "Order #1423" rather than "Orders". The MOST RECENT call wins — with several widgets on one screen the last to report owns the title, and on unmount (or an empty title) the host falls back to the most recent surviving report and finally to the page name. Only a screen showing the back control paints a title; a root page keeps the brand. No-op on a host that doesn't implement it. |
|
|
36
37
|
| **CORE** | `useRefresh(handler)` | `void` | `ctx.refresh.subscribe` — no scope. Subscribes the handler to the page-level refresh tick (pull-to-refresh on mobile). Handler may return a Promise — the host waits for `allSettled` before clearing the spinner. The three datastore hooks auto-subscribe their own `refetch`; widgets only call this directly to re-run non-datastore work. No-op on a host that doesn't implement refresh. |
|
|
@@ -72,7 +73,21 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
72
73
|
|
|
73
74
|
## Status
|
|
74
75
|
|
|
75
|
-
`v0.
|
|
76
|
+
`v0.157.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**.
|
|
77
|
+
|
|
78
|
+
### What's new in 0.157.0 (contract 1.122.0 — unchanged)
|
|
79
|
+
|
|
80
|
+
**The app header's row has its own text colour (sc-8518).** `resolveTopStripeLayout(theme)` now returns **`tintColor`** and **`titleColor`** beside the resolved `backgroundColor`, each reading `theme.sidebar.stripe.textColor` and falling back through `resolveTopBarTokens` when it is unset — exactly as the surface already falls back. One authored colour drives both, as the bar's own does; unset they part, the icons taking slate and the app name the brand.
|
|
81
|
+
|
|
82
|
+
Before this the row's ink lived on `topBar.textColor`, which also paints the tab row every top navigation bar draws — so recolouring the app header moved every one of those tab rows with it. What the two colours reach is what `slots` describes: the phone header, the desktop strip, the full-width bar, and the export's screen header. The drawer's own account rows, the sign-in control, the notification bell and the site-mode sheet are not that row and keep reading the bar.
|
|
83
|
+
|
|
84
|
+
Additive — nothing in the existing `./host` surface changed, an unset value resolves to what it resolved to before, and the contract is untouched.
|
|
85
|
+
|
|
86
|
+
### What's new in 0.156.0 (contract 1.122.0)
|
|
87
|
+
|
|
88
|
+
**A third layout tier: `widthTier(width)` and `WIDE_WIDTH_PX` (sc-8508).** `isNarrowWidth` splits widths into narrow and everything else, so a widget alone on a desktop page rendered the same layout it uses in a half-width cell and left most of a 1200px box empty. `widthTier` returns `"narrow"`, `"medium"` or `"wide"` — lay a form out in one column, then paired fields, then two or three columns; a list as stacked cards, then one-line cards, then rows with real columns.
|
|
89
|
+
|
|
90
|
+
Additive: `isNarrowWidth` and `NARROW_WIDTH_PX` are unchanged, and `widthTier(w) === "narrow"` exactly when `isNarrowWidth(w)` is true.
|
|
76
91
|
|
|
77
92
|
### What's new in 0.155.0 (contract 1.121.0)
|
|
78
93
|
|
|
@@ -104,7 +119,7 @@ Behaviour: a bare date given to a `mode="datetime"` picker now shows local midni
|
|
|
104
119
|
|
|
105
120
|
**The app header's row is arrangeable, and `./host` publishes the resolver both hosts read (sc-8023).** The row runs across the top of the content outside the side panel, and is drawn three times — as the phone header where the panel is a drawer, as the desktop strip, and as the full-width bar when one is switched on. Each surface hard-coded its own copy of it, so an author could colour the row but never move anything in it.
|
|
106
121
|
|
|
107
|
-
- **`resolveTopStripeLayout(theme)`** reads `theme.sidebar.stripe` and returns the arrangement fully resolved: `slots` (already filtered and sequenced, so no host re-derives it), `order`, the box (`height`, `paddingX`, `contentOffsetY`), the resolved `backgroundColor
|
|
122
|
+
- **`resolveTopStripeLayout(theme)`** reads `theme.sidebar.stripe` and returns the arrangement fully resolved: `slots` (already filtered and sequenced, so no host re-derives it), `order`, the box (`height`, `paddingX`, `contentOffsetY`), the resolved `backgroundColor` (joined in 0.155.0 by `tintColor` and `titleColor`), and `menuSide` — which edge the drawer must arrive from, since the brand grows and a burger ordered after it sits at the trailing edge. The web Player picks the drawer's side with it; the export makes it `drawerPosition`.
|
|
108
123
|
- **`TOP_STRIPE_SLOTS`** is the vocabulary (`back`, `menu`, `brand`, `notifications`) and **`TOP_STRIPE_ALWAYS_SHOWN_SLOTS`** the slots that move but never hide. The burger is the only member: it is drawn only where the panel is a drawer, and there it is the sole route into the menu, so hiding it would strand the visitor. It keeps its place in `order` regardless.
|
|
109
124
|
- **`topStripeShowKey(slot)`** derives the `theme_config` key that shows or hides one slot, in one place, so a control, the write path and the resolver cannot spell it differently.
|
|
110
125
|
- **The bounds travel with the resolver** — `MIN`/`MAX_TOP_STRIPE_HEIGHT_PX`, `MIN`/`MAX_TOP_STRIPE_PADDING_X_PX`, `MAX_TOP_STRIPE_OFFSET_Y_PX` — so a control offers exactly the range the resolver clamps to. `DEFAULT_TOP_STRIPE_HEIGHT_PX` / `DEFAULT_TOP_STRIPE_PADDING_X_PX` are what the DESKTOP row measures with nothing stored, published so a control can show the number an author is departing from; the resolver still answers `null`, because each surface keeps its own box until one is named.
|
|
@@ -1746,10 +1761,10 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
|
|
|
1746
1761
|
- **Tint the supporting cast.** `colors.primarySoft` is a tint of the workspace accent over the surface and `colors.onPrimarySoft` is guaranteed readable on it (WCAG AA, on light and dark themes alike). Use the pair for chips, secondary buttons, progress tracks, icon badges and selected rows. One saturated accent moment surrounded by several pale echoes of the same hue is what reads as designed — a row of grey-outlined buttons reads as a form. Never hand-mix a tint with `rgba(...)` or a translucent overlay.
|
|
1747
1762
|
- **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.
|
|
1748
1763
|
- **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.
|
|
1749
|
-
- **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=
|
|
1750
|
-
- **Compose forms — pair fields into rows, don't stack one per row.**
|
|
1764
|
+
- **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 cell whose definite size is computed from the width (a calendar day, a chart), 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** (sc-8508): `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. **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.
|
|
1765
|
+
- **Compose forms — pair fields into rows, don't stack one per row.** 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 (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.
|
|
1751
1766
|
- **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.
|
|
1752
|
-
- **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`. **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`
|
|
1767
|
+
- **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`. **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.
|
|
1753
1768
|
- **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).
|
|
1754
1769
|
- **Use imagery deliberately.** Render pictures with the `Image` primitive (`source` takes a URL or `{ uri }`); resolve workspace assets via `useAsset()`. Give every image a sized, `radii`-clipped container so it never renders as a raw rectangle, and never hardcode a credentialed image URL — expose an `image`-type property instead. The frame is your decision, never the picture's: size it for the role (a 40–56 square avatar, a 72–96 square row thumbnail, a `16 / 9` card cover, a 160–240 tall band) and let `resizeMode="cover"` crop the photo into it — photos arrive at every size and ratio, so one left to its own proportions breaks the layout. Keep `contain` for art whose whole subject must stay visible (a logo, a diagram), inside a fixed frame.
|
|
1755
1770
|
- **Design the empty, loading, and error states.** A blank box on a fresh install reads as broken — show a short helper line when a list is empty, a calm loading line, and a single human sentence in `colors.danger` on error.
|
package/dist/container-width.js
CHANGED
|
@@ -26,3 +26,30 @@ export const NARROW_WIDTH_PX = 480;
|
|
|
26
26
|
export function isNarrowWidth(width) {
|
|
27
27
|
return typeof width === "number" && width > 0 && width < NARROW_WIDTH_PX;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* sc-8508 — the width above which a widget has room for its WIDE form: a form
|
|
32
|
+
* in two or three columns, a list whose rows carry real columns, a filter bar
|
|
33
|
+
* on one toolbar row. Twice the narrow threshold, which is the three-column
|
|
34
|
+
* break the Developer guide already showed as `NARROW_WIDTH_PX * 2`.
|
|
35
|
+
*/
|
|
36
|
+
export const WIDE_WIDTH_PX = 960;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Which of the three layouts a MEASURED width calls for: "narrow" below
|
|
40
|
+
* `NARROW_WIDTH_PX`, "wide" at or above `WIDE_WIDTH_PX`, "medium" between.
|
|
41
|
+
*
|
|
42
|
+
* Two tiers were not enough. With only "narrow" and "not narrow", a widget
|
|
43
|
+
* alone on a desktop page rendered the layout it would use in a half-width
|
|
44
|
+
* cell and left the rest of its box empty.
|
|
45
|
+
*
|
|
46
|
+
* An unmeasured width (0, before the first `onLayout`) is "wide", for the same
|
|
47
|
+
* reason `isNarrowWidth(0)` is false: the wide rendering is the historical one,
|
|
48
|
+
* and a widget using both helpers must never get two different answers.
|
|
49
|
+
*/
|
|
50
|
+
export function widthTier(width) {
|
|
51
|
+
if (typeof width !== "number" || !(width > 0)) return "wide";
|
|
52
|
+
if (width < NARROW_WIDTH_PX) return "narrow";
|
|
53
|
+
if (width < WIDE_WIDTH_PX) return "medium";
|
|
54
|
+
return "wide";
|
|
55
|
+
}
|
package/dist/contract.cjs
CHANGED
|
@@ -771,7 +771,7 @@ const HOOKS = [
|
|
|
771
771
|
"callback, so ONE implementation serves the web Player (through " +
|
|
772
772
|
"react-native-web) and the native export. Before the first layout the " +
|
|
773
773
|
"width is 0: treat that as \"not yet measured\" and render the wide " +
|
|
774
|
-
"form. Pair it with `isNarrowWidth(width)` so every widget switches at " +
|
|
774
|
+
"form. Pair it with `widthTier(width)` (or `isNarrowWidth(width)`) so every widget switches at " +
|
|
775
775
|
"the same threshold. Takes no host context, so it is safe everywhere " +
|
|
776
776
|
"including the Studio canvas.",
|
|
777
777
|
returnShape: {
|
|
@@ -796,6 +796,25 @@ const HOOKS = [
|
|
|
796
796
|
requiredContextSlice: [],
|
|
797
797
|
scopes: null,
|
|
798
798
|
},
|
|
799
|
+
{
|
|
800
|
+
name: "widthTier",
|
|
801
|
+
signature: "widthTier(width)",
|
|
802
|
+
description:
|
|
803
|
+
"Which of the THREE layouts a MEASURED width calls for: \"narrow\" " +
|
|
804
|
+
"below `NARROW_WIDTH_PX` (480), \"wide\" at or above `WIDE_WIDTH_PX` " +
|
|
805
|
+
"(960), \"medium\" between. A widget alone on a desktop page has far " +
|
|
806
|
+
"more room than one in a half-width cell, and two tiers treated both " +
|
|
807
|
+
"the same, so the lone widget left most of its box empty. Branch on " +
|
|
808
|
+
"this for every list, form, table and filter bar: a form goes one " +
|
|
809
|
+
"column, then paired fields, then two or three columns; a list goes " +
|
|
810
|
+
"stacked cards, then one-line cards, then rows with real columns. An " +
|
|
811
|
+
"unmeasured width (0) is \"wide\", matching `isNarrowWidth(0)`.",
|
|
812
|
+
returnShape: {
|
|
813
|
+
"(returns)": "\"narrow\" | \"medium\" | \"wide\"",
|
|
814
|
+
},
|
|
815
|
+
requiredContextSlice: [],
|
|
816
|
+
scopes: null,
|
|
817
|
+
},
|
|
799
818
|
{
|
|
800
819
|
name: "useSectionEmpty",
|
|
801
820
|
signature: "useSectionEmpty(isEmpty)",
|
|
@@ -4309,7 +4328,13 @@ const CONTRACT = deepFreeze({
|
|
|
4309
4328
|
// untouched and renders byte-identically, and an unresolvable binding
|
|
4310
4329
|
// fails the existing hex guard, dropping the wash rather than painting it
|
|
4311
4330
|
// blank.
|
|
4312
|
-
|
|
4331
|
+
// 1.122.0: additive (sc-8508) — `widthTier(width)` + `WIDE_WIDTH_PX`, a
|
|
4332
|
+
// third layout tier. `isNarrowWidth` split widths into "phone" and
|
|
4333
|
+
// "everything else", so a widget alone on a desktop page rendered the
|
|
4334
|
+
// layout it would use in a half-width cell. Additive: `isNarrowWidth` and
|
|
4335
|
+
// `NARROW_WIDTH_PX` are unchanged, and `widthTier(w) === "narrow"` exactly
|
|
4336
|
+
// when `isNarrowWidth(w)` is true.
|
|
4337
|
+
version: "1.122.0",
|
|
4313
4338
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4314
4339
|
hooks: HOOKS,
|
|
4315
4340
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -771,7 +771,7 @@ const HOOKS = [
|
|
|
771
771
|
"callback, so ONE implementation serves the web Player (through " +
|
|
772
772
|
"react-native-web) and the native export. Before the first layout the " +
|
|
773
773
|
"width is 0: treat that as \"not yet measured\" and render the wide " +
|
|
774
|
-
"form. Pair it with `isNarrowWidth(width)` so every widget switches at " +
|
|
774
|
+
"form. Pair it with `widthTier(width)` (or `isNarrowWidth(width)`) so every widget switches at " +
|
|
775
775
|
"the same threshold. Takes no host context, so it is safe everywhere " +
|
|
776
776
|
"including the Studio canvas.",
|
|
777
777
|
returnShape: {
|
|
@@ -796,6 +796,25 @@ const HOOKS = [
|
|
|
796
796
|
requiredContextSlice: [],
|
|
797
797
|
scopes: null,
|
|
798
798
|
},
|
|
799
|
+
{
|
|
800
|
+
name: "widthTier",
|
|
801
|
+
signature: "widthTier(width)",
|
|
802
|
+
description:
|
|
803
|
+
"Which of the THREE layouts a MEASURED width calls for: \"narrow\" " +
|
|
804
|
+
"below `NARROW_WIDTH_PX` (480), \"wide\" at or above `WIDE_WIDTH_PX` " +
|
|
805
|
+
"(960), \"medium\" between. A widget alone on a desktop page has far " +
|
|
806
|
+
"more room than one in a half-width cell, and two tiers treated both " +
|
|
807
|
+
"the same, so the lone widget left most of its box empty. Branch on " +
|
|
808
|
+
"this for every list, form, table and filter bar: a form goes one " +
|
|
809
|
+
"column, then paired fields, then two or three columns; a list goes " +
|
|
810
|
+
"stacked cards, then one-line cards, then rows with real columns. An " +
|
|
811
|
+
"unmeasured width (0) is \"wide\", matching `isNarrowWidth(0)`.",
|
|
812
|
+
returnShape: {
|
|
813
|
+
"(returns)": "\"narrow\" | \"medium\" | \"wide\"",
|
|
814
|
+
},
|
|
815
|
+
requiredContextSlice: [],
|
|
816
|
+
scopes: null,
|
|
817
|
+
},
|
|
799
818
|
{
|
|
800
819
|
name: "useSectionEmpty",
|
|
801
820
|
signature: "useSectionEmpty(isEmpty)",
|
|
@@ -4305,7 +4324,13 @@ const CONTRACT = deepFreeze({
|
|
|
4305
4324
|
// untouched and renders byte-identically, and an unresolvable binding
|
|
4306
4325
|
// fails the existing hex guard, dropping the wash rather than painting it
|
|
4307
4326
|
// blank.
|
|
4308
|
-
|
|
4327
|
+
// 1.122.0: additive (sc-8508) — `widthTier(width)` + `WIDE_WIDTH_PX`, a
|
|
4328
|
+
// third layout tier. `isNarrowWidth` split widths into "phone" and
|
|
4329
|
+
// "everything else", so a widget alone on a desktop page rendered the
|
|
4330
|
+
// layout it would use in a half-width cell. Additive: `isNarrowWidth` and
|
|
4331
|
+
// `NARROW_WIDTH_PX` are unchanged, and `widthTier(w) === "narrow"` exactly
|
|
4332
|
+
// when `isNarrowWidth(w)` is true.
|
|
4333
|
+
version: "1.122.0",
|
|
4309
4334
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
4310
4335
|
hooks: HOOKS,
|
|
4311
4336
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -1563,6 +1563,16 @@ export const NARROW_WIDTH_PX: number;
|
|
|
1563
1563
|
*/
|
|
1564
1564
|
export function isNarrowWidth(width: number): boolean;
|
|
1565
1565
|
|
|
1566
|
+
/** The width at or above which a widget has room for its wide form (960). */
|
|
1567
|
+
export const WIDE_WIDTH_PX: number;
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* Which of the three layouts a MEASURED width calls for: `"narrow"` below
|
|
1571
|
+
* `NARROW_WIDTH_PX`, `"wide"` at or above `WIDE_WIDTH_PX`, `"medium"` between.
|
|
1572
|
+
* An unmeasured width (0) is `"wide"`, matching `isNarrowWidth(0) === false`.
|
|
1573
|
+
*/
|
|
1574
|
+
export function widthTier(width: number): "narrow" | "medium" | "wide";
|
|
1575
|
+
|
|
1566
1576
|
/** Pass-through options for `useGeolocation().getCurrentPosition(...)`. */
|
|
1567
1577
|
export interface GeolocationOptions {
|
|
1568
1578
|
enableHighAccuracy?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,12 @@ export {
|
|
|
91
91
|
BarcodeError,
|
|
92
92
|
WidgetTree,
|
|
93
93
|
} from "./hooks.js";
|
|
94
|
-
export {
|
|
94
|
+
export {
|
|
95
|
+
isNarrowWidth,
|
|
96
|
+
NARROW_WIDTH_PX,
|
|
97
|
+
widthTier,
|
|
98
|
+
WIDE_WIDTH_PX,
|
|
99
|
+
} from "./container-width.js";
|
|
95
100
|
// REQ-WSDK-PLATFORM §6 — Tier A hooks. Each ships in a per-platform file
|
|
96
101
|
// (./clipboard.js / .native.js, ./toast.js / .native.js); index.js picks
|
|
97
102
|
// the web variant and index.native.js picks the native variant.
|
package/dist/index.native.js
CHANGED
|
@@ -91,7 +91,12 @@ export {
|
|
|
91
91
|
BarcodeError,
|
|
92
92
|
WidgetTree,
|
|
93
93
|
} from "./hooks.js";
|
|
94
|
-
export {
|
|
94
|
+
export {
|
|
95
|
+
isNarrowWidth,
|
|
96
|
+
NARROW_WIDTH_PX,
|
|
97
|
+
widthTier,
|
|
98
|
+
WIDE_WIDTH_PX,
|
|
99
|
+
} from "./container-width.js";
|
|
95
100
|
// REQ-WSDK-PLATFORM §6 — Tier A hooks (native variants).
|
|
96
101
|
export { useClipboard, ClipboardError } from "./clipboard.native.js";
|
|
97
102
|
export { useToast } from "./toast.native.js";
|
package/dist/navigation.cjs
CHANGED
|
@@ -403,15 +403,17 @@ function topStripeOrder(value) {
|
|
|
403
403
|
* bar left holding nothing is not drawn at all.
|
|
404
404
|
*
|
|
405
405
|
* @param {unknown} theme — the whole `theme_config`.
|
|
406
|
-
* @returns {{ backgroundColor: string,
|
|
407
|
-
*
|
|
408
|
-
* slots: string[], menuSide: "start"|"end" }}
|
|
406
|
+
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
|
|
407
|
+
* height: number|null, paddingX: number|null, contentOffsetY: number,
|
|
408
|
+
* order: string[], slots: string[], menuSide: "start"|"end" }}
|
|
409
409
|
*/
|
|
410
410
|
function resolveTopStripeLayout(theme) {
|
|
411
411
|
const config = isPlainObject(theme) ? theme : {};
|
|
412
412
|
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
413
413
|
const stripe = isPlainObject(sidebar.stripe) ? sidebar.stripe : {};
|
|
414
414
|
const order = topStripeOrder(stripe.order);
|
|
415
|
+
// Resolved once: the row's colours all fall back through the app-header bar.
|
|
416
|
+
const barTokens = resolveTopBarTokens(theme);
|
|
415
417
|
const shown = (slot) => {
|
|
416
418
|
if (TOP_STRIPE_ALWAYS_SHOWN_SLOTS.includes(slot)) return true;
|
|
417
419
|
const value = stripe[topStripeShowKey(slot)];
|
|
@@ -449,10 +451,30 @@ function resolveTopStripeLayout(theme) {
|
|
|
449
451
|
// page's top navigation bar no longer repaints the stripe above the panel by
|
|
450
452
|
// accident. Fully resolved here, not raw-or-null, for the reason sc-8033
|
|
451
453
|
// resolved the footer's: a default that lives in each host drifts.
|
|
452
|
-
backgroundColor: hexOr(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
454
|
+
backgroundColor: hexOr(stripe.backgroundColor, barTokens.backgroundColor),
|
|
455
|
+
// sc-8518: the row's OWN text, the half sc-8023 left on the bar. An author
|
|
456
|
+
// recolouring the app header used to move every top navigation bar's tab
|
|
457
|
+
// labels with it, because both read `topBar.textColor` — and had to open a
|
|
458
|
+
// panel named after the other part to do it.
|
|
459
|
+
//
|
|
460
|
+
// One authored colour drives BOTH slots, exactly as the bar's own does:
|
|
461
|
+
// unset they part (icons take slate, the app name takes the brand), and
|
|
462
|
+
// naming one is a decision about the whole row. Unset inherits the bar,
|
|
463
|
+
// so an app that never opens this field is unmoved.
|
|
464
|
+
//
|
|
465
|
+
// The row is what `slots` above describes — the desktop bar, the phone
|
|
466
|
+
// header, and native's screen header. The drawer's own account rows, the
|
|
467
|
+
// sign-in control and the site-mode sheet are NOT this row and keep
|
|
468
|
+
// reading the bar; so does the SITE header, which is shown instead of this
|
|
469
|
+
// row rather than beside it.
|
|
470
|
+
//
|
|
471
|
+
// The notification bell is the one member of `slots` this does not reach.
|
|
472
|
+
// It keeps its own resting colour on both hosts — web reads the authored
|
|
473
|
+
// `topBar.textColor` and the export tints it not at all — and moving it
|
|
474
|
+
// here would widen that difference rather than close it. Whoever closes it
|
|
475
|
+
// should move both hosts at once.
|
|
476
|
+
tintColor: hexOr(stripe.textColor, barTokens.tintColor),
|
|
477
|
+
titleColor: hexOr(stripe.textColor, barTokens.titleColor),
|
|
456
478
|
order,
|
|
457
479
|
slots,
|
|
458
480
|
// sc-8023: which edge the panel the burger opens should come in from. The
|
package/dist/navigation.js
CHANGED
|
@@ -394,15 +394,17 @@ function topStripeOrder(value) {
|
|
|
394
394
|
* bar left holding nothing is not drawn at all.
|
|
395
395
|
*
|
|
396
396
|
* @param {unknown} theme — the whole `theme_config`.
|
|
397
|
-
* @returns {{ backgroundColor: string,
|
|
398
|
-
*
|
|
399
|
-
* slots: string[], menuSide: "start"|"end" }}
|
|
397
|
+
* @returns {{ backgroundColor: string, tintColor: string, titleColor: string,
|
|
398
|
+
* height: number|null, paddingX: number|null, contentOffsetY: number,
|
|
399
|
+
* order: string[], slots: string[], menuSide: "start"|"end" }}
|
|
400
400
|
*/
|
|
401
401
|
export function resolveTopStripeLayout(theme) {
|
|
402
402
|
const config = isPlainObject(theme) ? theme : {};
|
|
403
403
|
const sidebar = isPlainObject(config.sidebar) ? config.sidebar : {};
|
|
404
404
|
const stripe = isPlainObject(sidebar.stripe) ? sidebar.stripe : {};
|
|
405
405
|
const order = topStripeOrder(stripe.order);
|
|
406
|
+
// Resolved once: the row's colours all fall back through the app-header bar.
|
|
407
|
+
const barTokens = resolveTopBarTokens(theme);
|
|
406
408
|
const shown = (slot) => {
|
|
407
409
|
if (TOP_STRIPE_ALWAYS_SHOWN_SLOTS.includes(slot)) return true;
|
|
408
410
|
const value = stripe[topStripeShowKey(slot)];
|
|
@@ -440,10 +442,30 @@ export function resolveTopStripeLayout(theme) {
|
|
|
440
442
|
// page's top navigation bar no longer repaints the stripe above the panel by
|
|
441
443
|
// accident. Fully resolved here, not raw-or-null, for the reason sc-8033
|
|
442
444
|
// resolved the footer's: a default that lives in each host drifts.
|
|
443
|
-
backgroundColor: hexOr(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
445
|
+
backgroundColor: hexOr(stripe.backgroundColor, barTokens.backgroundColor),
|
|
446
|
+
// sc-8518: the row's OWN text, the half sc-8023 left on the bar. An author
|
|
447
|
+
// recolouring the app header used to move every top navigation bar's tab
|
|
448
|
+
// labels with it, because both read `topBar.textColor` — and had to open a
|
|
449
|
+
// panel named after the other part to do it.
|
|
450
|
+
//
|
|
451
|
+
// One authored colour drives BOTH slots, exactly as the bar's own does:
|
|
452
|
+
// unset they part (icons take slate, the app name takes the brand), and
|
|
453
|
+
// naming one is a decision about the whole row. Unset inherits the bar,
|
|
454
|
+
// so an app that never opens this field is unmoved.
|
|
455
|
+
//
|
|
456
|
+
// The row is what `slots` above describes — the desktop bar, the phone
|
|
457
|
+
// header, and native's screen header. The drawer's own account rows, the
|
|
458
|
+
// sign-in control and the site-mode sheet are NOT this row and keep
|
|
459
|
+
// reading the bar; so does the SITE header, which is shown instead of this
|
|
460
|
+
// row rather than beside it.
|
|
461
|
+
//
|
|
462
|
+
// The notification bell is the one member of `slots` this does not reach.
|
|
463
|
+
// It keeps its own resting colour on both hosts — web reads the authored
|
|
464
|
+
// `topBar.textColor` and the export tints it not at all — and moving it
|
|
465
|
+
// here would widen that difference rather than close it. Whoever closes it
|
|
466
|
+
// should move both hosts at once.
|
|
467
|
+
tintColor: hexOr(stripe.textColor, barTokens.tintColor),
|
|
468
|
+
titleColor: hexOr(stripe.textColor, barTokens.titleColor),
|
|
447
469
|
order,
|
|
448
470
|
slots,
|
|
449
471
|
// sc-8023: which edge the panel the burger opens should come in from. The
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.157.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",
|