@colixsystems/widget-sdk 0.115.0 → 0.116.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 +15 -0
- package/dist/contract.cjs +34 -2
- package/dist/contract.js +34 -2
- package/dist/index.d.ts +30 -0
- package/dist/index.js +4 -0
- package/dist/index.native.js +2 -0
- package/dist/overlay-tokens.js +64 -0
- package/dist/overlay-view.js +109 -0
- package/dist/overlay.js +13 -0
- package/dist/overlay.native.js +11 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,20 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
72
72
|
|
|
73
73
|
`v0.112.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
74
|
|
|
75
|
+
### What's new in 0.116.0 (contract 1.88.0)
|
|
76
|
+
|
|
77
|
+
**New primitive `<Overlay>` — a widget can finally open something over the SCREEN (sc-6607).** Until now a widget could only paint an overlay inside its own root, where the host's layout containers clip it: a PDF preview, a lightbox, or a confirm dialog opened trapped inside the widget's tile, and no amount of `zIndex` fixed it (`overflow: "hidden"` clips regardless, and `position: "fixed"` does not exist on native).
|
|
78
|
+
|
|
79
|
+
```jsx
|
|
80
|
+
<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">
|
|
81
|
+
<ScrollView>{renderPreview(preview)}</ScrollView>
|
|
82
|
+
</Overlay>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`<Overlay>` renders its children OUTSIDE the widget's layout box on both hosts — the web Player portals them to the document root, the exported Expo app hands them to the OS modal — while they stay in your own React tree, so their state and hooks are untouched. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme.
|
|
86
|
+
|
|
87
|
+
An anchored dropdown or popover is the one overlay kind that still belongs inside your own root — `<Overlay>` centres on the screen rather than on a trigger.
|
|
88
|
+
|
|
75
89
|
### What's new in 0.115.0 (contract unchanged)
|
|
76
90
|
|
|
77
91
|
**`write-not-gated-on-user` now accepts a `useCanWrite()` gate — a widget may be opened to logged-out visitors (sc-6593).** The rule (added in 0.89.0, below) flagged any `useDatastoreMutation` write that carried no identity guard, and only a `.id` / `groupIds` / `roles` check counted as one. That encoded "a write needs a signed-in app user" as a platform fact, which it is not: a table whose permissions grant **Create** to *Everyone (anonymous + signed-in)* accepts a write from a logged-out visitor, and `useCanWrite(tableId)` answers `true` for them.
|
|
@@ -1090,6 +1104,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
1090
1104
|
- `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `useWidgetInput`, `usePayments`, `useSendNotification`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useRouteParams`, `usePageContext`, `useWidgetRoute`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError` carrying `code`, the server's user-safe `message`, and `retryable` (`false` = this charge cannot succeed until the workspace, manifest, or amount changes — show the message, not a retry). `useSendNotification()` returns `{ send, sending, error }` and requires the `notifications.send:appUser` scope; `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace (cross-workspace `recipient_user_id` is rejected), must be called from an event handler rather than render, and rejects with a `NotificationError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (camelCase — the host-built context object, not a wire payload; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute, openLink }` for internal page navigation; `openLink(link)` follows an author- or data-supplied link of unknown shape through the host's shared resolver (in-app page → internal route, off-app http(s) → opened outside, unsafe → refused) and is the safe choice for any value your code did not construct, while `Linking.openURL(url)` is for an external URL you built yourself. `useRouteParams()` returns the current route's params object (`currentRoute.params`) — the flat master→detail accessor; read a param off it (e.g. `recordId`), never call it. `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case). `useWidgetInput(inputName)` returns the latest payload a sibling widget published on the event the page author wired to this widget's declared `inputs` entry (`undefined` when unwired or not yet published).
|
|
1091
1105
|
- `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
|
|
1092
1106
|
- `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
|
|
1107
|
+
- `Overlay` — the screen-level overlay (sc-6607). `<Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">…</Overlay>` renders its children OUTSIDE the widget's layout box on both hosts (web portals it to the document root, native uses the OS modal), so no clipping card, `ScrollView` or neighbouring widget can cut off a document/media preview, lightbox or confirm dialog — which absolute positioning inside the widget cannot achieve on either platform. Children stay in your own React tree, so state and hooks work normally. `onRequestClose` carries the backdrop press, Escape on web, and the Android back button; `size` is `sm` | `md` (default) | `lg` | `full`; the scrim, surface, radius, padding and elevation come from the workspace theme. An anchored dropdown still belongs inside your own root.
|
|
1093
1108
|
- `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
|
|
1094
1109
|
|
|
1095
1110
|
## Design & visual polish
|
package/dist/contract.cjs
CHANGED
|
@@ -272,6 +272,16 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
272
272
|
radius: "buttonRadius",
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
// sc-6602: Image frames media with the flat `radius`/`borderColor`/`background`
|
|
276
|
+
// trio rather than the `card*` names, so the card scope reaches it through this
|
|
277
|
+
// map. Bound by id, so `shadow` is safe to carry here.
|
|
278
|
+
const MEDIA_FRAME_FIELDS = Object.freeze({
|
|
279
|
+
background: "background",
|
|
280
|
+
borderColor: "borderColor",
|
|
281
|
+
radius: "radius",
|
|
282
|
+
shadow: "shadow",
|
|
283
|
+
});
|
|
284
|
+
|
|
275
285
|
// REQ-THEME-WIDGET: the card fields whose NAMES are unambiguous, so they bind to
|
|
276
286
|
// ANY widget that reads them -- including a Mason-generated one, whose id can
|
|
277
287
|
// never appear in a hand-maintained allowlist. That allowlist is why "make the
|
|
@@ -280,7 +290,8 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
280
290
|
// `shadow` is deliberately ABSENT: its name is bare and shared with the button
|
|
281
291
|
// scope, so binding it by name would cross the scopes. The bare names stay on
|
|
282
292
|
// the allowlist for exactly that reason -- `appstudio.image` also reads a
|
|
283
|
-
// `background` field, and the button scope must not leak into it.
|
|
293
|
+
// `background` field, and the button scope must not leak into it. The media
|
|
294
|
+
// surfaces get themed elevation by ID instead (sc-6602); keep it out of here.
|
|
284
295
|
const CARD_UNIVERSAL_FIELDS = Object.freeze({
|
|
285
296
|
background: "cardBackground",
|
|
286
297
|
borderColor: "cardBorderColor",
|
|
@@ -367,6 +378,12 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
367
378
|
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
368
379
|
"appstudio.user-management": CARD_SURFACE_FIELDS,
|
|
369
380
|
"appstudio.link": CARD_SURFACE_FIELDS,
|
|
381
|
+
// sc-6602: the media surfaces. They paint a card exactly as the widgets
|
|
382
|
+
// above do, so a themed elevation must reach them — a reference design's
|
|
383
|
+
// lifted hero image was unthemeable while these three were absent.
|
|
384
|
+
"appstudio.sound": CARD_SURFACE_FIELDS,
|
|
385
|
+
"appstudio.video": CARD_SURFACE_FIELDS,
|
|
386
|
+
"appstudio.image": MEDIA_FRAME_FIELDS,
|
|
370
387
|
}),
|
|
371
388
|
}),
|
|
372
389
|
text: Object.freeze({
|
|
@@ -1770,6 +1787,17 @@ const PRIMITIVES = [
|
|
|
1770
1787
|
rnComponent: null,
|
|
1771
1788
|
docsUrl: null,
|
|
1772
1789
|
},
|
|
1790
|
+
// sc-6607 — the SCREEN-level overlay. Widgets could previously only paint an
|
|
1791
|
+
// overlay inside their own box, so a preview or dialog was clipped by the
|
|
1792
|
+
// layout container the widget sits in; this is the one primitive that leaves
|
|
1793
|
+
// that box on both hosts.
|
|
1794
|
+
{
|
|
1795
|
+
name: "Overlay",
|
|
1796
|
+
description:
|
|
1797
|
+
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget — a preview (PDF, image, video), a lightbox, a confirm dialog, a full detail panel. It renders OUTSIDE the widget layout box on both hosts (react-native-web portals it to the document root; native uses the OS modal), so no `overflow: hidden` card, scroll container or sibling widget can clip or cover it — which absolute positioning inside the widget cannot achieve on either host. Children stay in your own React tree, so state and hooks work normally. Props: `visible` (boolean, required to show), `onRequestClose` (fired by the backdrop press, the Escape key on web, and the Android back button — always wire it or the overlay cannot be closed), `size` ("sm" | "md" | "lg" | "full", default "md"; `full` runs edge to edge for media), `dismissOnBackdropPress` (default true), `accessibilityLabel`, and `style` for extra panel styles. The scrim, surface, radius, padding and elevation come from the workspace theme — never re-style them. Keep it for screen-level surfaces: a dropdown anchored to its trigger still belongs inside the widget.',
|
|
1798
|
+
rnComponent: null,
|
|
1799
|
+
docsUrl: null,
|
|
1800
|
+
},
|
|
1773
1801
|
];
|
|
1774
1802
|
|
|
1775
1803
|
const CATEGORIES = [
|
|
@@ -3468,7 +3496,11 @@ const CONTRACT = deepFreeze({
|
|
|
3468
3496
|
// plus the `pressableLift` primitive that resolves it per input — hover on
|
|
3469
3497
|
// web, press on native. Tappable layout containers and widget surfaces lift
|
|
3470
3498
|
// from this ONE table.
|
|
3471
|
-
|
|
3499
|
+
// 1.88.0: additive (sc-6607) — the `Overlay` primitive: the first surface a
|
|
3500
|
+
// widget can open at SCREEN level. Everything before it was clipped by the
|
|
3501
|
+
// layout container the widget sits in, so a preview or dialog could not leave
|
|
3502
|
+
// the widget's own tile on either host.
|
|
3503
|
+
version: "1.88.0",
|
|
3472
3504
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3473
3505
|
hooks: HOOKS,
|
|
3474
3506
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -272,6 +272,16 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
272
272
|
radius: "buttonRadius",
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
// sc-6602: Image frames media with the flat `radius`/`borderColor`/`background`
|
|
276
|
+
// trio rather than the `card*` names, so the card scope reaches it through this
|
|
277
|
+
// map. Bound by id, so `shadow` is safe to carry here.
|
|
278
|
+
const MEDIA_FRAME_FIELDS = Object.freeze({
|
|
279
|
+
background: "background",
|
|
280
|
+
borderColor: "borderColor",
|
|
281
|
+
radius: "radius",
|
|
282
|
+
shadow: "shadow",
|
|
283
|
+
});
|
|
284
|
+
|
|
275
285
|
// REQ-THEME-WIDGET: the card fields whose NAMES are unambiguous, so they bind to
|
|
276
286
|
// ANY widget that reads them -- including a Mason-generated one, whose id can
|
|
277
287
|
// never appear in a hand-maintained allowlist. That allowlist is why "make the
|
|
@@ -280,7 +290,8 @@ const LINK_ACTION_FIELDS = Object.freeze({
|
|
|
280
290
|
// `shadow` is deliberately ABSENT: its name is bare and shared with the button
|
|
281
291
|
// scope, so binding it by name would cross the scopes. The bare names stay on
|
|
282
292
|
// the allowlist for exactly that reason -- `appstudio.image` also reads a
|
|
283
|
-
// `background` field, and the button scope must not leak into it.
|
|
293
|
+
// `background` field, and the button scope must not leak into it. The media
|
|
294
|
+
// surfaces get themed elevation by ID instead (sc-6602); keep it out of here.
|
|
284
295
|
const CARD_UNIVERSAL_FIELDS = Object.freeze({
|
|
285
296
|
background: "cardBackground",
|
|
286
297
|
borderColor: "cardBorderColor",
|
|
@@ -367,6 +378,12 @@ const THEME_COMPONENTS = Object.freeze({
|
|
|
367
378
|
"appstudio.form-builder": CARD_SURFACE_FIELDS,
|
|
368
379
|
"appstudio.user-management": CARD_SURFACE_FIELDS,
|
|
369
380
|
"appstudio.link": CARD_SURFACE_FIELDS,
|
|
381
|
+
// sc-6602: the media surfaces. They paint a card exactly as the widgets
|
|
382
|
+
// above do, so a themed elevation must reach them — a reference design's
|
|
383
|
+
// lifted hero image was unthemeable while these three were absent.
|
|
384
|
+
"appstudio.sound": CARD_SURFACE_FIELDS,
|
|
385
|
+
"appstudio.video": CARD_SURFACE_FIELDS,
|
|
386
|
+
"appstudio.image": MEDIA_FRAME_FIELDS,
|
|
370
387
|
}),
|
|
371
388
|
}),
|
|
372
389
|
text: Object.freeze({
|
|
@@ -1770,6 +1787,17 @@ const PRIMITIVES = [
|
|
|
1770
1787
|
rnComponent: null,
|
|
1771
1788
|
docsUrl: null,
|
|
1772
1789
|
},
|
|
1790
|
+
// sc-6607 — the SCREEN-level overlay. Widgets could previously only paint an
|
|
1791
|
+
// overlay inside their own box, so a preview or dialog was clipped by the
|
|
1792
|
+
// layout container the widget sits in; this is the one primitive that leaves
|
|
1793
|
+
// that box on both hosts.
|
|
1794
|
+
{
|
|
1795
|
+
name: "Overlay",
|
|
1796
|
+
description:
|
|
1797
|
+
'Screen-level overlay. `<Overlay visible={open} onRequestClose={() => setOpen(false)} size="md">…panel…</Overlay>`. THE way to open anything that must cover the SCREEN rather than the widget — a preview (PDF, image, video), a lightbox, a confirm dialog, a full detail panel. It renders OUTSIDE the widget layout box on both hosts (react-native-web portals it to the document root; native uses the OS modal), so no `overflow: hidden` card, scroll container or sibling widget can clip or cover it — which absolute positioning inside the widget cannot achieve on either host. Children stay in your own React tree, so state and hooks work normally. Props: `visible` (boolean, required to show), `onRequestClose` (fired by the backdrop press, the Escape key on web, and the Android back button — always wire it or the overlay cannot be closed), `size` ("sm" | "md" | "lg" | "full", default "md"; `full` runs edge to edge for media), `dismissOnBackdropPress` (default true), `accessibilityLabel`, and `style` for extra panel styles. The scrim, surface, radius, padding and elevation come from the workspace theme — never re-style them. Keep it for screen-level surfaces: a dropdown anchored to its trigger still belongs inside the widget.',
|
|
1798
|
+
rnComponent: null,
|
|
1799
|
+
docsUrl: null,
|
|
1800
|
+
},
|
|
1773
1801
|
];
|
|
1774
1802
|
|
|
1775
1803
|
const CATEGORIES = [
|
|
@@ -3468,7 +3496,11 @@ const CONTRACT = deepFreeze({
|
|
|
3468
3496
|
// plus the `pressableLift` primitive that resolves it per input — hover on
|
|
3469
3497
|
// web, press on native. Tappable layout containers and widget surfaces lift
|
|
3470
3498
|
// from this ONE table.
|
|
3471
|
-
|
|
3499
|
+
// 1.88.0: additive (sc-6607) — the `Overlay` primitive: the first surface a
|
|
3500
|
+
// widget can open at SCREEN level. Everything before it was clipped by the
|
|
3501
|
+
// layout container the widget sits in, so a preview or dialog could not leave
|
|
3502
|
+
// the widget's own tile on either host.
|
|
3503
|
+
version: "1.88.0",
|
|
3472
3504
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
3473
3505
|
hooks: HOOKS,
|
|
3474
3506
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -2151,6 +2151,36 @@ export const Gradient: (props: {
|
|
|
2151
2151
|
children?: ReactNode;
|
|
2152
2152
|
}) => any;
|
|
2153
2153
|
|
|
2154
|
+
/**
|
|
2155
|
+
* sc-6607 — screen-level overlay. Renders OUTSIDE the widget's layout box on
|
|
2156
|
+
* both hosts (web portals it to the document root, native uses the OS modal),
|
|
2157
|
+
* so no clipping card, scroll container or neighbouring widget can cut it off.
|
|
2158
|
+
* Use it for anything that takes over the screen — a document/media preview, a
|
|
2159
|
+
* lightbox, a confirm dialog. An anchored dropdown still belongs inside the
|
|
2160
|
+
* widget's own root.
|
|
2161
|
+
*
|
|
2162
|
+
* @example
|
|
2163
|
+
* <Overlay visible={!!preview} onRequestClose={() => setPreview(null)} size="full">
|
|
2164
|
+
* <ScrollView>{renderPreview(preview)}</ScrollView>
|
|
2165
|
+
* </Overlay>
|
|
2166
|
+
*/
|
|
2167
|
+
export const Overlay: (props: {
|
|
2168
|
+
/** Nothing renders (and nothing is mounted) while this is false. */
|
|
2169
|
+
visible?: boolean;
|
|
2170
|
+
/** Backdrop press, Escape (web) and the Android back button all fire this. */
|
|
2171
|
+
onRequestClose?: () => void;
|
|
2172
|
+
/** Panel width tier. `full` runs edge to edge. Defaults to `md`. */
|
|
2173
|
+
size?: "sm" | "md" | "lg" | "full";
|
|
2174
|
+
/** Set false when only an explicit control may close the overlay. */
|
|
2175
|
+
dismissOnBackdropPress?: boolean;
|
|
2176
|
+
accessibilityLabel?: string;
|
|
2177
|
+
/** Label for the backdrop's dismiss target. Defaults to "Close". */
|
|
2178
|
+
closeAccessibilityLabel?: string;
|
|
2179
|
+
/** Extra styles merged onto the themed panel. */
|
|
2180
|
+
style?: any;
|
|
2181
|
+
children?: ReactNode;
|
|
2182
|
+
}) => any;
|
|
2183
|
+
|
|
2154
2184
|
// ------------------------------------------------------- theme derivation
|
|
2155
2185
|
// sc-3696: the colour maths both hosts resolve `useTheme()` with. Exported so
|
|
2156
2186
|
// the Player (frontend/src/services/widgetTheme.js) and the exported app's
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,10 @@ export { isNarrowWidth, NARROW_WIDTH_PX } from "./container-width.js";
|
|
|
90
90
|
// the web variant and index.native.js picks the native variant.
|
|
91
91
|
export { useClipboard, ClipboardError } from "./clipboard.js";
|
|
92
92
|
export { useToast } from "./toast.js";
|
|
93
|
+
// sc-6607 — `<Overlay>` is one shared component (./overlay-view.js) bound to
|
|
94
|
+
// each platform's primitives, so a widget's modal escapes its container box
|
|
95
|
+
// identically on the Player and the Expo export.
|
|
96
|
+
export { Overlay } from "./overlay.js";
|
|
93
97
|
export {
|
|
94
98
|
Text,
|
|
95
99
|
View,
|
package/dist/index.native.js
CHANGED
|
@@ -88,6 +88,8 @@ export { isNarrowWidth, NARROW_WIDTH_PX } from "./container-width.js";
|
|
|
88
88
|
// REQ-WSDK-PLATFORM §6 — Tier A hooks (native variants).
|
|
89
89
|
export { useClipboard, ClipboardError } from "./clipboard.native.js";
|
|
90
90
|
export { useToast } from "./toast.native.js";
|
|
91
|
+
// sc-6607 — see the note on the web mirror in ./index.js.
|
|
92
|
+
export { Overlay } from "./overlay.native.js";
|
|
91
93
|
export {
|
|
92
94
|
Text,
|
|
93
95
|
View,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// sc-6607 — the values `<Overlay>` paints one screen-level overlay with.
|
|
2
|
+
//
|
|
3
|
+
// Presentation-free and platform-free, the same way `toast-host.js` is: both
|
|
4
|
+
// hosts render the overlay from the SAME component (`overlay-view.js`), and
|
|
5
|
+
// this module is where the theme becomes numbers. Keeping the maths here makes
|
|
6
|
+
// it unit-testable without a renderer, and makes the scrim/panel contract a
|
|
7
|
+
// thing tests can pin rather than a literal buried in JSX.
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
10
|
+
|
|
11
|
+
export const OVERLAY_SIZES = Object.freeze(["sm", "md", "lg", "full"]);
|
|
12
|
+
|
|
13
|
+
export const OVERLAY_DEFAULTS = Object.freeze({
|
|
14
|
+
size: "md",
|
|
15
|
+
// Slate-black at 55%: dark enough to mute a busy page behind the panel,
|
|
16
|
+
// light enough that the page still reads as "still there, just behind".
|
|
17
|
+
scrim: "rgba(15, 23, 42, 0.55)",
|
|
18
|
+
// Breathing room between the panel and the screen edge on a phone.
|
|
19
|
+
screenPadding: 16,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// `full` deliberately has no ceiling — a media/PDF preview wants the screen.
|
|
23
|
+
const SIZE_MAX_WIDTH = Object.freeze({
|
|
24
|
+
sm: 360,
|
|
25
|
+
md: 520,
|
|
26
|
+
lg: 760,
|
|
27
|
+
full: null,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export function normaliseOverlaySize(size) {
|
|
31
|
+
return OVERLAY_SIZES.indexOf(size) === -1 ? OVERLAY_DEFAULTS.size : size;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the values one overlay is painted with, from the workspace theme.
|
|
36
|
+
*
|
|
37
|
+
* Returns primitives only, so the panel a widget opens carries the workspace's
|
|
38
|
+
* own surface, radius, spacing and elevation instead of a hard-coded card.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} theme — the resolved workspace theme (`useTheme()` shape).
|
|
41
|
+
* @param {string} size — `sm` | `md` | `lg` | `full`.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveOverlayTokens(theme, size) {
|
|
44
|
+
const safeSize = normaliseOverlaySize(size);
|
|
45
|
+
const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
|
|
46
|
+
const fallback = DEFAULT_THEME_TOKENS;
|
|
47
|
+
const colors = base.colors || fallback.colors;
|
|
48
|
+
const radii = base.radii || fallback.radii;
|
|
49
|
+
const spacing = base.spacing || fallback.spacing;
|
|
50
|
+
const elevation = (base.elevation || fallback.elevation).lg || {};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
size: safeSize,
|
|
54
|
+
scrim: OVERLAY_DEFAULTS.scrim,
|
|
55
|
+
surface: colors.surface || fallback.colors.surface,
|
|
56
|
+
radius: safeSize === "full" ? 0 : radii.lg || fallback.radii.lg,
|
|
57
|
+
padding: spacing.lg || fallback.spacing.lg,
|
|
58
|
+
// A `full` overlay runs edge to edge; every other size keeps a margin so
|
|
59
|
+
// the scrim stays visible and the panel reads as a layer, not a screen.
|
|
60
|
+
screenPadding: safeSize === "full" ? 0 : OVERLAY_DEFAULTS.screenPadding,
|
|
61
|
+
maxWidth: SIZE_MAX_WIDTH[safeSize],
|
|
62
|
+
elevation,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// sc-6607 — the ONE implementation behind the `<Overlay>` SDK primitive.
|
|
2
|
+
//
|
|
3
|
+
// A widget that opens a preview, a lightbox, or a confirm dialog used to have
|
|
4
|
+
// no way to paint over the SCREEN: it rendered the panel as an absolutely
|
|
5
|
+
// positioned child of its own root, where the Player's layout containers
|
|
6
|
+
// (`overflow: hidden` cards, scroll containers) clip it and where React Native
|
|
7
|
+
// has no `position: "fixed"` at all. `<Overlay>` fixes that for both hosts at
|
|
8
|
+
// once by rendering into React Native's `Modal`, which leaves the parent's
|
|
9
|
+
// layout on BOTH platforms — react-native-web portals it to `document.body`,
|
|
10
|
+
// native hands it to the OS — while keeping the children in the widget's own
|
|
11
|
+
// React tree, so their state, hooks and WidgetContext all survive.
|
|
12
|
+
//
|
|
13
|
+
// The two platform bindings (`overlay.js` / `overlay.native.js`) differ ONLY in
|
|
14
|
+
// where the primitives come from; the component itself is defined once here so
|
|
15
|
+
// the hosts cannot drift (CLAUDE.md §3, §8).
|
|
16
|
+
|
|
17
|
+
import React from "react";
|
|
18
|
+
import { useHostTheme } from "./hooks.js";
|
|
19
|
+
import { resolveOverlayTokens } from "./overlay-tokens.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bind the shared overlay component to one platform's primitives.
|
|
23
|
+
*
|
|
24
|
+
* @param {{Modal: any, View: any, Pressable: any, StyleSheet: any}} rn
|
|
25
|
+
*/
|
|
26
|
+
export function makeOverlay(rn) {
|
|
27
|
+
const { Modal, View, Pressable, StyleSheet } = rn;
|
|
28
|
+
|
|
29
|
+
function Overlay({
|
|
30
|
+
visible = false,
|
|
31
|
+
onRequestClose,
|
|
32
|
+
size = "md",
|
|
33
|
+
dismissOnBackdropPress = true,
|
|
34
|
+
accessibilityLabel,
|
|
35
|
+
closeAccessibilityLabel = "Close",
|
|
36
|
+
style,
|
|
37
|
+
children,
|
|
38
|
+
}) {
|
|
39
|
+
const theme = useHostTheme();
|
|
40
|
+
const tokens = React.useMemo(
|
|
41
|
+
() => resolveOverlayTokens(theme, size),
|
|
42
|
+
[theme, size],
|
|
43
|
+
);
|
|
44
|
+
const requestClose = React.useCallback(() => {
|
|
45
|
+
if (typeof onRequestClose === "function") onRequestClose();
|
|
46
|
+
}, [onRequestClose]);
|
|
47
|
+
|
|
48
|
+
// Unmounted while closed: `Modal` appends a host container the moment it
|
|
49
|
+
// renders, so every widget on the page would otherwise leave one behind.
|
|
50
|
+
if (!visible) return null;
|
|
51
|
+
|
|
52
|
+
const backdrop = {
|
|
53
|
+
flex: 1,
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
padding: tokens.screenPadding,
|
|
57
|
+
backgroundColor: tokens.scrim,
|
|
58
|
+
};
|
|
59
|
+
const panel = {
|
|
60
|
+
width: "100%",
|
|
61
|
+
// Capped to the padded screen box so a tall panel scrolls inside its own
|
|
62
|
+
// ScrollView instead of overflowing past the viewport unreachably.
|
|
63
|
+
maxHeight: "100%",
|
|
64
|
+
flexShrink: 1,
|
|
65
|
+
...(tokens.maxWidth ? { maxWidth: tokens.maxWidth } : { flex: 1 }),
|
|
66
|
+
backgroundColor: tokens.surface,
|
|
67
|
+
borderRadius: tokens.radius,
|
|
68
|
+
padding: tokens.padding,
|
|
69
|
+
...tokens.elevation,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return React.createElement(
|
|
73
|
+
Modal,
|
|
74
|
+
{
|
|
75
|
+
visible: true,
|
|
76
|
+
transparent: true,
|
|
77
|
+
// Not animated on purpose: the web host arms the overlay's dialog role
|
|
78
|
+
// and its Escape handler when the entrance animation ENDS, so a fade
|
|
79
|
+
// leaves a window where the keyboard cannot dismiss it — and a dropped
|
|
80
|
+
// animation event would leave it un-dismissable for good.
|
|
81
|
+
animationType: "none",
|
|
82
|
+
onRequestClose: requestClose,
|
|
83
|
+
},
|
|
84
|
+
React.createElement(
|
|
85
|
+
View,
|
|
86
|
+
{ style: backdrop },
|
|
87
|
+
dismissOnBackdropPress
|
|
88
|
+
? React.createElement(Pressable, {
|
|
89
|
+
style: StyleSheet.absoluteFill,
|
|
90
|
+
onPress: requestClose,
|
|
91
|
+
accessibilityRole: "button",
|
|
92
|
+
accessibilityLabel: closeAccessibilityLabel,
|
|
93
|
+
})
|
|
94
|
+
: null,
|
|
95
|
+
React.createElement(
|
|
96
|
+
View,
|
|
97
|
+
{
|
|
98
|
+
style: [panel, style],
|
|
99
|
+
accessibilityLabel,
|
|
100
|
+
},
|
|
101
|
+
children,
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Overlay.displayName = "Overlay";
|
|
108
|
+
return Overlay;
|
|
109
|
+
}
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// sc-6607 — `<Overlay>` (web). Binds the shared implementation in
|
|
2
|
+
// ./overlay-view.js to react-native-web's primitives.
|
|
3
|
+
//
|
|
4
|
+
// The import is `react-native-web` rather than the bare `react-native`
|
|
5
|
+
// specifier for the rolldown optional-peer-dep reason spelled out at the top of
|
|
6
|
+
// ./primitives.js. react-native-web's `Modal` portals its subtree into a
|
|
7
|
+
// `document.body` container at `position: fixed`, which is exactly what makes
|
|
8
|
+
// the overlay escape the widget's clipping ancestors.
|
|
9
|
+
|
|
10
|
+
import * as ReactNative from "react-native-web";
|
|
11
|
+
import { makeOverlay } from "./overlay-view.js";
|
|
12
|
+
|
|
13
|
+
export const Overlay = makeOverlay(ReactNative);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// sc-6607 — `<Overlay>` (native). Binds the shared implementation in
|
|
2
|
+
// ./overlay-view.js to React Native's own primitives.
|
|
3
|
+
//
|
|
4
|
+
// RN's `Modal` renders outside the parent's layout and routes the Android
|
|
5
|
+
// hardware back button to `onRequestClose`, matching what react-native-web's
|
|
6
|
+
// portal does on the web (./overlay.js). Only the import differs.
|
|
7
|
+
|
|
8
|
+
import { Modal, View, Pressable, StyleSheet } from "react-native";
|
|
9
|
+
import { makeOverlay } from "./overlay-view.js";
|
|
10
|
+
|
|
11
|
+
export const Overlay = makeOverlay({ Modal, View, Pressable, StyleSheet });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.116.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
],
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node scripts/build.js",
|
|
51
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/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__/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__/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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
|
|
51
|
+
"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__/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-bound-columns.test.js src/__tests__/hooks-stable-query.test.js src/__tests__/hooks-can-write.test.js src/__tests__/widget-route.test.js"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=18"
|