@colixsystems/widget-sdk 0.221.1 → 0.223.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 +45 -4
- package/dist/card-photo.js +26 -0
- package/dist/card-photo.native.js +32 -0
- package/dist/card-style.js +72 -0
- package/dist/card-view.js +82 -8
- package/dist/card.js +2 -1
- package/dist/card.native.js +2 -1
- package/dist/contract.cjs +10 -2
- package/dist/contract.js +10 -2
- package/dist/hooks.js +11 -7
- package/dist/index.d.ts +26 -4
- package/package.json +1 -1
- package/src/contract.js +10 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
24
24
|
| **CORE** | `useUser()` | `{ id, email, displayName, photoUrl, roles, groupIds }` | `ctx.user` (host-built context, **camelCase** — not a wire payload; `id` null when anonymous) — no scope. `photoUrl` is the user's own profile photo as an absolute URL, or `null`. |
|
|
25
25
|
| **CORE** | `useUserPhoto()` | `{ url, canEdit, upload, remove, uploading, error }` | `ctx.user` + the optional `ctx.account` — no scope. Changes the signed-in user's OWN profile photo. `canEdit` is false when nobody is signed in or the host cannot change it; `upload` / `remove` then reject. |
|
|
26
26
|
| **CORE** | `useAnonymousAccount()` | `{ supported, canCreate, creating, error, create }` | The optional `ctx.account.anonymous` — no scope. Offers an account with a generated login code and password and no email. `create()` opens the app's own dialog, which confirms, creates the account, shows the pair once and signs the visitor in; the widget never receives the pair and `create()` resolves `{ created: true }`. `supported` is false where the workspace has not turned anonymous accounts on, on the Builder canvas, in a preview and for a badge counter; `canCreate` is false while someone is signed in. |
|
|
27
|
-
| **CORE** | `useNavigation()` | `{ goTo, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
27
|
+
| **CORE** | `useNavigation()` | `{ goTo, goToStartPage, goBack, push, replace, back, currentRoute, openLink }` | `ctx.navigation` — no scope. `goTo(pageIdOrSlug, params?)` accepts a page UUID (a `pageRef` prop) **or its slug** (a row's page-key field) — both hosts resolve either; `goToStartPage()` opens the signed-in user's own start page (`openLink` for a link of unknown shape; a known external URL can also use the `Linking` primitive) |
|
|
28
28
|
| **CORE** | `useFilePreview(file)` | `{ uri }` | No scope. A temporary address an `<Image>` can show for a file the person picked but has not uploaded yet (a photo in a composer). Native picks use their own uri; on the web the SDK creates and releases an object URL, which widgets may not do themselves. `uri` is null for no file. |
|
|
29
29
|
| **CORE** | `usePageBadge(count)` | `{ supported }` | `ctx.navigation.setBadge` — no scope. Puts `count` on the menu entry or tab of the page the widget is on (a chat's unread messages). The host sums the page's widgets and keeps the last count after the widget leaves the screen, until it reports again; `0` clears this widget's share. `supported` is false where the host draws no page badges (the Builder canvas). |
|
|
30
30
|
| **CORE** | `useRouteParams()` | `{ [paramKey]: value }` | `ctx.navigation.currentRoute.params` — no scope. The nav params the previous page passed via `goTo(pageId, params)`; the flat accessor for master→detail (read `recordId` on a detail page). Empty object when none. |
|
|
@@ -84,7 +84,48 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
84
84
|
|
|
85
85
|
## Status
|
|
86
86
|
|
|
87
|
-
`v0.
|
|
87
|
+
`v0.223.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**.
|
|
88
|
+
|
|
89
|
+
### What's new in 0.223.0 (contract 1.179.0)
|
|
90
|
+
|
|
91
|
+
**A widget can send the user back to their start page — `useNavigation().goToStartPage()`.** An app opens on a start page that can differ per user, because page access is restricted per group. A "Back to start" or "Home" button should take each user to their own start page rather than to one hard-coded page they may not be allowed to open.
|
|
92
|
+
|
|
93
|
+
```jsx
|
|
94
|
+
import { useNavigation, Button } from "@colixsystems/widget-sdk";
|
|
95
|
+
|
|
96
|
+
export default function BackToStart() {
|
|
97
|
+
const { goToStartPage } = useNavigation();
|
|
98
|
+
return <Button title="Back to the start page" onPress={() => goToStartPage()} />;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The web Player and the exported app both go to the page the app opens on for the signed-in user. It takes no arguments and carries no params. It is a no-op where there is no live router (the Studio canvas, a badge counter). Additive.
|
|
103
|
+
|
|
104
|
+
### What's new in 0.222.0 (contract 1.178.0)
|
|
105
|
+
|
|
106
|
+
**A card can carry its picture as its background — `<Card image>`.** A small image with its title in a box underneath reads as a caption under a thumbnail. The same content with the picture filling the card and the title on it reads as designed, and until now a widget could only build that by hand: an absolutely positioned `Image`, its own translucent overlay, hand-picked light text. `<Card>` now does it.
|
|
107
|
+
|
|
108
|
+
```jsx
|
|
109
|
+
<Card
|
|
110
|
+
as={Pressable}
|
|
111
|
+
onPress={open}
|
|
112
|
+
image={urls ? urls.card : null}
|
|
113
|
+
scrim="bottom"
|
|
114
|
+
style={{ aspectRatio: 1, justifyContent: "flex-end" }}
|
|
115
|
+
>
|
|
116
|
+
{({ colors }) => <Text style={{ color: colors.onSurface }}>{record.name}</Text>}
|
|
117
|
+
</Card>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **`image`** takes a URL or `{ uri }`. The picture fills the card to every edge, inside the card's own corner radius; the card's padding insets the content, not the picture.
|
|
121
|
+
- **`scrim`** names the edge the text sits on: `"bottom"` (default), `"top"`, `"left"`, `"right"`, `"full"` for centred text, or `"none"`. It is a light gradient that is clear over most of the picture.
|
|
122
|
+
- **`imagePosition`** (`"center"` default, `"top"`, `"bottom"`, `"left"`, `"right"`) keeps that part of the picture in frame when the crop is tight.
|
|
123
|
+
- **Children may be a function.** It receives `{ colors, onImage }`: the theme colours as they read where the content sits. Over a picture `colors.onSurface` and `colors.onSurfaceMuted` are light; on a card with no image they are the theme's own. A `<Button>` inside the card reads its backdrop without help.
|
|
124
|
+
- **No image, no change.** `image={null}` renders the ordinary card, so a record with an empty file cell needs no branch.
|
|
125
|
+
- **A photo card is a `View` or a `Pressable`.** Put a scroller inside it, never as it (`as={ScrollView}`): the picture would scroll away with the content.
|
|
126
|
+
- **Give it a size.** Nothing picture-shaped sizes a photo card, so `style` carries an `aspectRatio` or a `minHeight`. The keys that arrange content (`justifyContent`, `alignItems`, `gap`, `flexDirection`) apply to the content laid over the picture, also under `as={Pressable}`.
|
|
127
|
+
|
|
128
|
+
Both hosts paint it: the web Player with a CSS background, the exported app with `expo-image`, which every export already bundles. Additive: a card given no `image` renders exactly as before.
|
|
88
129
|
|
|
89
130
|
### What's new in 0.221.1 (contract 1.177.1)
|
|
90
131
|
|
|
@@ -2726,7 +2767,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
|
|
|
2726
2767
|
|
|
2727
2768
|
- `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
|
|
2728
2769
|
- `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
|
|
2729
|
-
- `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).
|
|
2770
|
+
- `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, goToStartPage, goBack, push, replace, back, currentRoute, openLink }` for internal page navigation; `goToStartPage()` opens the signed-in user's own start page (the page the app opens on for them); `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).
|
|
2730
2771
|
- `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.
|
|
2731
2772
|
- `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.
|
|
2732
2773
|
- `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.
|
|
@@ -2779,7 +2820,7 @@ A widget that works but looks unfinished is only half done. `useTheme()` is the
|
|
|
2779
2820
|
- **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.
|
|
2780
2821
|
- **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.
|
|
2781
2822
|
- **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).
|
|
2782
|
-
- **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
|
|
2823
|
+
- **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 square photo tile, a `16 / 9` photo card, 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. When an item has a photograph, make it the card's background with the title on it — `<Card image={url} scrim="bottom">` with a function child for the colours — rather than a small picture with its text in a box underneath; keep the stacked form for a card whose text is long or dense.
|
|
2783
2824
|
- **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.
|
|
2784
2825
|
|
|
2785
2826
|
**Honest ceilings:** the styling surface is React Native style objects, not full CSS. Gradients come from the `<Gradient>` primitive (not a CSS `linear-gradient` string), depth comes from `theme.elevation` (not arbitrary `box-shadow` stacks), and there are no custom CSS keyframe animations or `transition` strings (the hover/press lift comes built into `pressableLift` — never write your own), no `filter` / `backdrop-filter` / `clip-path` / `mask` / blend modes, and no opacity-faked tints (that's what `primarySoft` is for). Aim for clean, confident, professional polish within those bounds — lifted surfaces, generous corners, one accent moment.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The picture behind a photo-backed `<Card>` (web). A CSS background, because
|
|
2
|
+
// `background-position` is what keeps the chosen part of a crop in frame.
|
|
3
|
+
|
|
4
|
+
import React from "react";
|
|
5
|
+
import * as ReactNative from "react-native-web";
|
|
6
|
+
|
|
7
|
+
// A quote, backslash or newline would end the CSS `url("…")` early.
|
|
8
|
+
function cssUrl(uri) {
|
|
9
|
+
const safe = uri.replace(/[\n\r\f]/g, "").replace(/\\/g, "%5C").replace(/"/g, "%22");
|
|
10
|
+
return `url("${safe}")`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Photo({ uri, position, style }) {
|
|
14
|
+
return React.createElement(ReactNative.View, {
|
|
15
|
+
"aria-hidden": true,
|
|
16
|
+
style: [
|
|
17
|
+
style,
|
|
18
|
+
{
|
|
19
|
+
backgroundImage: cssUrl(uri),
|
|
20
|
+
backgroundSize: "cover",
|
|
21
|
+
backgroundPosition: position,
|
|
22
|
+
backgroundRepeat: "no-repeat",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The picture behind a photo-backed `<Card>` (native). expo-image, because
|
|
2
|
+
// `contentPosition` is the only native equivalent of CSS background-position;
|
|
3
|
+
// React Native's own Image can only centre a `cover` crop.
|
|
4
|
+
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Image } from "react-native";
|
|
7
|
+
|
|
8
|
+
// Pinned in every export; only a dev client older than the pin lacks it.
|
|
9
|
+
let ExpoImage = null;
|
|
10
|
+
try {
|
|
11
|
+
ExpoImage = require("expo-image").Image;
|
|
12
|
+
} catch (_) {
|
|
13
|
+
ExpoImage = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Photo({ uri, position, style }) {
|
|
17
|
+
if (!ExpoImage) {
|
|
18
|
+
return React.createElement(Image, {
|
|
19
|
+
source: { uri },
|
|
20
|
+
resizeMode: "cover",
|
|
21
|
+
style,
|
|
22
|
+
accessible: false,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return React.createElement(ExpoImage, {
|
|
26
|
+
source: { uri },
|
|
27
|
+
contentFit: "cover",
|
|
28
|
+
contentPosition: position,
|
|
29
|
+
style,
|
|
30
|
+
accessible: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
package/dist/card-style.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
normaliseComponentGradient,
|
|
12
12
|
componentGradientColors,
|
|
13
13
|
componentGradientMidpoint,
|
|
14
|
+
deriveSurfaceTokens,
|
|
14
15
|
} from "./contract.js";
|
|
15
16
|
import { normaliseCornerRadius, cornerRadiusStyle } from "./corner-radius.js";
|
|
16
17
|
import { normaliseSpacing, spacingStyle, isZeroSpacing } from "./spacing.js";
|
|
@@ -160,3 +161,74 @@ function isOpaqueHex(value) {
|
|
|
160
161
|
export function cardGradientProps(style) {
|
|
161
162
|
return gradientProps(style && style.cardGradient);
|
|
162
163
|
}
|
|
164
|
+
|
|
165
|
+
// ---- The photo-backed card: a picture edge to edge, content laid over it ----
|
|
166
|
+
|
|
167
|
+
export const CARD_SCRIMS = Object.freeze(["bottom", "top", "left", "right", "full", "none"]);
|
|
168
|
+
export const DEFAULT_CARD_SCRIM = "bottom";
|
|
169
|
+
|
|
170
|
+
export const CARD_IMAGE_POSITIONS = Object.freeze(["center", "top", "bottom", "left", "right"]);
|
|
171
|
+
|
|
172
|
+
// What content over a photo is inked against, and what shows while it loads.
|
|
173
|
+
export const CARD_PHOTO_BACKDROP = "#111827";
|
|
174
|
+
|
|
175
|
+
// The dark end sits on the named edge, where the text is.
|
|
176
|
+
const SCRIM_ANGLES = Object.freeze({ bottom: 180, top: 0, left: 270, right: 90 });
|
|
177
|
+
// Clear over most of the picture, so the photo stays a photo.
|
|
178
|
+
const SCRIM_EDGE = Object.freeze(["#00000000", "#00000026", "#000000b8"]);
|
|
179
|
+
const SCRIM_FULL = Object.freeze(["#00000066", "#00000066"]);
|
|
180
|
+
|
|
181
|
+
/** The picture's address from a URL or an RN `{ uri }` source, else null. */
|
|
182
|
+
export function cardPhotoUri(image) {
|
|
183
|
+
const raw = image && typeof image === "object" ? image.uri : image;
|
|
184
|
+
if (typeof raw !== "string") return null;
|
|
185
|
+
const uri = raw.trim();
|
|
186
|
+
return uri ? uri : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The `<Gradient>` props that keep text readable on a photo; null for none. */
|
|
190
|
+
export function cardScrimProps(scrim) {
|
|
191
|
+
const name = CARD_SCRIMS.includes(scrim) ? scrim : DEFAULT_CARD_SCRIM;
|
|
192
|
+
if (name === "none") return null;
|
|
193
|
+
if (name === "full") return { colors: [...SCRIM_FULL], angle: 180 };
|
|
194
|
+
return { colors: [...SCRIM_EDGE], angle: SCRIM_ANGLES[name] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Which part of the picture a cropped card keeps in frame. */
|
|
198
|
+
export function cardImagePosition(value) {
|
|
199
|
+
return CARD_IMAGE_POSITIONS.includes(value) ? value : "center";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Theme colours as content over a photo reads them, for a card with no host. */
|
|
203
|
+
export function cardPhotoColors(colors) {
|
|
204
|
+
return {
|
|
205
|
+
...colors,
|
|
206
|
+
...deriveSurfaceTokens(CARD_PHOTO_BACKDROP),
|
|
207
|
+
backdrop: CARD_PHOTO_BACKDROP,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const RADIUS_FIELDS = Object.freeze([
|
|
212
|
+
"borderRadius", "borderTopLeftRadius", "borderTopRightRadius",
|
|
213
|
+
"borderBottomRightRadius", "borderBottomLeftRadius",
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The layer that holds a photo card's picture: it fills the card behind the
|
|
218
|
+
* content and rounds the picture to the card's corners. Its own layer, since
|
|
219
|
+
* iOS drops the shadow of a view that clips.
|
|
220
|
+
*
|
|
221
|
+
* @param {object|Array} styles - the card's composed container style.
|
|
222
|
+
*/
|
|
223
|
+
export function cardPhotoClip(styles) {
|
|
224
|
+
const flat = flattenStyle(styles);
|
|
225
|
+
const clip = {
|
|
226
|
+
position: "absolute", top: 0, right: 0, bottom: 0, left: 0,
|
|
227
|
+
overflow: "hidden",
|
|
228
|
+
pointerEvents: "none",
|
|
229
|
+
};
|
|
230
|
+
for (const key of RADIUS_FIELDS) {
|
|
231
|
+
if (flat[key] !== undefined) clip[key] = flat[key];
|
|
232
|
+
}
|
|
233
|
+
return clip;
|
|
234
|
+
}
|
package/dist/card-view.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// A gradient is a COMPONENT on both hosts, never a style value, so the element
|
|
6
6
|
// swap lives here once rather than in each widget.
|
|
7
|
+
//
|
|
8
|
+
// A card given an `image` is a PHOTO card: the same element, with the picture
|
|
9
|
+
// and a readability scrim on a layer behind its content.
|
|
7
10
|
|
|
8
11
|
import React from "react";
|
|
9
12
|
import { useHostTheme, BackdropScope } from "./hooks.js";
|
|
@@ -15,6 +18,12 @@ import {
|
|
|
15
18
|
cardGradientProps,
|
|
16
19
|
cardLayoutStyle,
|
|
17
20
|
cardBackdrop,
|
|
21
|
+
cardPhotoUri,
|
|
22
|
+
cardScrimProps,
|
|
23
|
+
cardImagePosition,
|
|
24
|
+
cardPhotoClip,
|
|
25
|
+
cardPhotoColors,
|
|
26
|
+
CARD_PHOTO_BACKDROP,
|
|
18
27
|
} from "./card-style.js";
|
|
19
28
|
import { normaliseThemeComponents } from "./theme-components.js";
|
|
20
29
|
import { flattenStyle } from "./flatten-style.js";
|
|
@@ -31,6 +40,12 @@ const FILL_IN_GRADIENT = {
|
|
|
31
40
|
|
|
32
41
|
const CLIP_TO_RADIUS = { overflow: "hidden" };
|
|
33
42
|
|
|
43
|
+
// The fill shows while the picture loads, so light text never sits on white.
|
|
44
|
+
const PHOTO_BOX = { backgroundColor: CARD_PHOTO_BACKDROP };
|
|
45
|
+
const PHOTO_LAYER = { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 };
|
|
46
|
+
// The theme's hairline reads as a stray outline around a photograph.
|
|
47
|
+
const NO_BORDER = { borderWidth: 0 };
|
|
48
|
+
|
|
34
49
|
const CARD_FIELDS = CONTRACT.themeComponents.card.universalFields;
|
|
35
50
|
|
|
36
51
|
// The convenience props, each named for the `card*` field it sets.
|
|
@@ -61,6 +76,19 @@ function themedFields(theme) {
|
|
|
61
76
|
return out;
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
// Inside the card's scope, so a function child reads the card's own inks.
|
|
80
|
+
function CardContent({ onImage, children }) {
|
|
81
|
+
const hosted = useHostTheme();
|
|
82
|
+
if (typeof children !== "function") return children;
|
|
83
|
+
// No host means no scope to re-ink through; the photo is dark regardless.
|
|
84
|
+
const colors = hosted
|
|
85
|
+
? hosted.colors
|
|
86
|
+
: onImage
|
|
87
|
+
? cardPhotoColors(DEFAULT_THEME_TOKENS.colors)
|
|
88
|
+
: DEFAULT_THEME_TOKENS.colors;
|
|
89
|
+
return children({ colors, onImage });
|
|
90
|
+
}
|
|
91
|
+
|
|
64
92
|
function themedBase(theme) {
|
|
65
93
|
const { colors, radii, spacing } = theme;
|
|
66
94
|
return {
|
|
@@ -75,10 +103,10 @@ function themedBase(theme) {
|
|
|
75
103
|
/**
|
|
76
104
|
* Bind the shared card to one platform's primitives.
|
|
77
105
|
*
|
|
78
|
-
* @param {{View: any, Gradient: any}} rn
|
|
106
|
+
* @param {{View: any, Gradient: any, Photo: any}} rn
|
|
79
107
|
*/
|
|
80
108
|
export function makeCard(rn) {
|
|
81
|
-
const { View, Gradient } = rn;
|
|
109
|
+
const { View, Gradient, Photo } = rn;
|
|
82
110
|
|
|
83
111
|
/**
|
|
84
112
|
* @param {object|Array} [props.style] — `card*` fields, `shadow`, `margin`
|
|
@@ -101,6 +129,16 @@ export function makeCard(rn) {
|
|
|
101
129
|
* element receiving the other props, so a Pressable's feedback lands.
|
|
102
130
|
* @param {boolean} [props.withoutMargin] — leave the margin off this surface
|
|
103
131
|
* for a widget that rides it on an outer root.
|
|
132
|
+
* @param {string|{uri: string}} [props.image] — a picture painted edge to
|
|
133
|
+
* edge BEHIND the content. With none (a record without a photo) the card
|
|
134
|
+
* is an ordinary card, so one widget serves both.
|
|
135
|
+
* @param {string} [props.imagePosition] — the part kept in frame when
|
|
136
|
+
* cropped (`CARD_IMAGE_POSITIONS`).
|
|
137
|
+
* @param {string} [props.scrim] — the edge the readability gradient darkens
|
|
138
|
+
* (`CARD_SCRIMS`); the edge the text sits on.
|
|
139
|
+
* @param {React.ReactNode|Function} [props.children] — a function is called
|
|
140
|
+
* with `{ colors, onImage }`: the theme colours as they read where the
|
|
141
|
+
* content sits, light over a photo and the theme's own without one.
|
|
104
142
|
*/
|
|
105
143
|
function Card(props) {
|
|
106
144
|
const {
|
|
@@ -110,6 +148,9 @@ export function makeCard(rn) {
|
|
|
110
148
|
fillInGradient = false,
|
|
111
149
|
containerStyle,
|
|
112
150
|
withoutMargin = false,
|
|
151
|
+
image,
|
|
152
|
+
imagePosition,
|
|
153
|
+
scrim,
|
|
113
154
|
children,
|
|
114
155
|
...others
|
|
115
156
|
} = props;
|
|
@@ -118,6 +159,7 @@ export function makeCard(rn) {
|
|
|
118
159
|
if (!(key in PROP_FIELDS)) rest[key] = value;
|
|
119
160
|
}
|
|
120
161
|
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
162
|
+
const uri = cardPhotoUri(image);
|
|
121
163
|
|
|
122
164
|
const explicit = {};
|
|
123
165
|
for (const [prop, field] of Object.entries(PROP_FIELDS)) {
|
|
@@ -137,17 +179,49 @@ export function makeCard(rn) {
|
|
|
137
179
|
const layout = standalone ? cardLayoutStyle(own) : null;
|
|
138
180
|
|
|
139
181
|
const overlay = withoutMargin ? cardSurfaceStyle(fields) : cardOverlay(fields);
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
182
|
+
|
|
183
|
+
const gradient = uri ? null : cardGradientProps(fields);
|
|
184
|
+
const basis = uri
|
|
185
|
+
? CARD_PHOTO_BACKDROP
|
|
186
|
+
: cardBackdrop(
|
|
187
|
+
[base, overlay, typeof containerStyle === "function" ? null : containerStyle],
|
|
188
|
+
gradient,
|
|
189
|
+
);
|
|
190
|
+
const scoped = React.createElement(
|
|
191
|
+
BackdropScope,
|
|
192
|
+
{ basis, always: Boolean(uri) },
|
|
193
|
+
React.createElement(CardContent, { onImage: Boolean(uri) }, children),
|
|
144
194
|
);
|
|
145
|
-
const scoped = React.createElement(BackdropScope, { basis }, children);
|
|
146
195
|
|
|
147
196
|
if (!gradient) {
|
|
197
|
+
const box = [
|
|
198
|
+
base,
|
|
199
|
+
overlay,
|
|
200
|
+
layout,
|
|
201
|
+
uri ? PHOTO_BOX : null,
|
|
202
|
+
uri && !fields.cardBorderColor ? NO_BORDER : null,
|
|
203
|
+
];
|
|
204
|
+
const scrimProps = uri ? cardScrimProps(scrim) : null;
|
|
205
|
+
const ownStyle = typeof containerStyle === "function" ? null : containerStyle;
|
|
206
|
+
// The layer keeps its slot with no photo, so content survives one arriving.
|
|
207
|
+
const layer = uri
|
|
208
|
+
? React.createElement(
|
|
209
|
+
View,
|
|
210
|
+
{ style: cardPhotoClip([box, ownStyle]) },
|
|
211
|
+
React.createElement(Photo, {
|
|
212
|
+
uri,
|
|
213
|
+
position: cardImagePosition(imagePosition),
|
|
214
|
+
style: PHOTO_LAYER,
|
|
215
|
+
}),
|
|
216
|
+
scrimProps
|
|
217
|
+
? React.createElement(Gradient, { ...scrimProps, style: PHOTO_LAYER })
|
|
218
|
+
: null,
|
|
219
|
+
)
|
|
220
|
+
: null;
|
|
148
221
|
return React.createElement(
|
|
149
222
|
Container,
|
|
150
|
-
{ style: composeStyle(
|
|
223
|
+
{ style: composeStyle(box, containerStyle), ...rest },
|
|
224
|
+
layer,
|
|
151
225
|
scoped,
|
|
152
226
|
);
|
|
153
227
|
}
|
package/dist/card.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import * as ReactNative from "react-native-web";
|
|
5
5
|
import { Gradient } from "./gradient.js";
|
|
6
|
+
import { Photo } from "./card-photo.js";
|
|
6
7
|
import { makeCard } from "./card-view.js";
|
|
7
8
|
|
|
8
|
-
export const Card = makeCard({ View: ReactNative.View, Gradient });
|
|
9
|
+
export const Card = makeCard({ View: ReactNative.View, Gradient, Photo });
|
package/dist/card.native.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { View } from "react-native";
|
|
5
5
|
import { Gradient } from "./gradient.native.js";
|
|
6
|
+
import { Photo } from "./card-photo.native.js";
|
|
6
7
|
import { makeCard } from "./card-view.js";
|
|
7
8
|
|
|
8
|
-
export const Card = makeCard({ View, Gradient });
|
|
9
|
+
export const Card = makeCard({ View, Gradient, Photo });
|
package/dist/contract.cjs
CHANGED
|
@@ -1128,6 +1128,8 @@ const HOOKS = [
|
|
|
1128
1128
|
goTo:
|
|
1129
1129
|
"(pageIdOrSlug: string, params?: object) => void — a page's UUID (a pageRef prop) OR its SLUG (e.g. a row's page-key field); both hosts resolve either. There is NO page-name lookup: a slug is the only data-carried navigation key.",
|
|
1130
1130
|
openLink: "(link: string) => boolean",
|
|
1131
|
+
goToStartPage:
|
|
1132
|
+
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1131
1133
|
goBack: "() => void",
|
|
1132
1134
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1133
1135
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -3245,7 +3247,7 @@ const PRIMITIVES = [
|
|
|
3245
3247
|
{
|
|
3246
3248
|
name: "Card",
|
|
3247
3249
|
description:
|
|
3248
|
-
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. `style` may be an object or a React Native style array (`style={[{ flex: 1, minWidth: 0 }, align && { alignItems: align }]}`), read as the host flattens it: nested arrays and falsy entries are fine and a later entry wins. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
3250
|
+
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. `style` may be an object or a React Native style array (`style={[{ flex: 1, minWidth: 0 }, align && { alignItems: align }]}`), read as the host flattens it: nested arrays and falsy entries are fine and a later entry wins. A gradient paints on both hosts. `image` (a URL or `{ uri }`) makes it a PHOTO card: the picture fills the card edge to edge BEHIND the content, under a light gradient on the edge named by `scrim` ("bottom" default | "top" | "left" | "right" | "full" | "none") — the edge the text sits on — and cropped around `imagePosition` ("center" default | "top" | "bottom" | "left" | "right"). A photo card has no picture-shaped content to size it, so give it an `aspectRatio` or `minHeight` in `style`, and pass the children as a function — `{({ colors, onImage }) => …}` — to receive the theme colours as they read where the content sits (`colors.onSurface` for a title, `colors.onSurfaceMuted` for a supporting line): light over a picture, the theme\'s own on a card without one. `image={null}` renders an ordinary card, so a record with no photo needs no branch. A photo card is a `View` or, with `as`, a `Pressable`; put a scroller inside it, never as it. Use it whenever an item has a photograph, instead of a small `Image` with its text in a box underneath. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
3249
3251
|
rnComponent: null,
|
|
3250
3252
|
docsUrl: null,
|
|
3251
3253
|
},
|
|
@@ -5874,7 +5876,13 @@ const CONTRACT = deepFreeze({
|
|
|
5874
5876
|
// 1.177.1: fix (sc-10410) — a `refresh()` or `verify()` that succeeds
|
|
5875
5877
|
// clears `useFileSignature`'s `error`; it used to keep one failed poll's
|
|
5876
5878
|
// error for the rest of the order.
|
|
5877
|
-
|
|
5879
|
+
// 1.178.0: additive (sc-11272) — `<Card image>` paints a picture edge to
|
|
5880
|
+
// edge behind the card's content, with a `scrim` on the text's edge and
|
|
5881
|
+
// an `imagePosition` for the crop. Children may be a function taking
|
|
5882
|
+
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5883
|
+
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5884
|
+
// the signed-in user's own start page on both hosts.
|
|
5885
|
+
version: "1.179.0",
|
|
5878
5886
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5879
5887
|
hooks: HOOKS,
|
|
5880
5888
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -1128,6 +1128,8 @@ const HOOKS = [
|
|
|
1128
1128
|
goTo:
|
|
1129
1129
|
"(pageIdOrSlug: string, params?: object) => void — a page's UUID (a pageRef prop) OR its SLUG (e.g. a row's page-key field); both hosts resolve either. There is NO page-name lookup: a slug is the only data-carried navigation key.",
|
|
1130
1130
|
openLink: "(link: string) => boolean",
|
|
1131
|
+
goToStartPage:
|
|
1132
|
+
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1131
1133
|
goBack: "() => void",
|
|
1132
1134
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1133
1135
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -3245,7 +3247,7 @@ const PRIMITIVES = [
|
|
|
3245
3247
|
{
|
|
3246
3248
|
name: "Card",
|
|
3247
3249
|
description:
|
|
3248
|
-
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. `style` may be an object or a React Native style array (`style={[{ flex: 1, minWidth: 0 }, align && { alignItems: align }]}`), read as the host flattens it: nested arrays and falsy entries are fine and a later entry wins. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
3250
|
+
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. `style` may be an object or a React Native style array (`style={[{ flex: 1, minWidth: 0 }, align && { alignItems: align }]}`), read as the host flattens it: nested arrays and falsy entries are fine and a later entry wins. A gradient paints on both hosts. `image` (a URL or `{ uri }`) makes it a PHOTO card: the picture fills the card edge to edge BEHIND the content, under a light gradient on the edge named by `scrim` ("bottom" default | "top" | "left" | "right" | "full" | "none") — the edge the text sits on — and cropped around `imagePosition` ("center" default | "top" | "bottom" | "left" | "right"). A photo card has no picture-shaped content to size it, so give it an `aspectRatio` or `minHeight` in `style`, and pass the children as a function — `{({ colors, onImage }) => …}` — to receive the theme colours as they read where the content sits (`colors.onSurface` for a title, `colors.onSurfaceMuted` for a supporting line): light over a picture, the theme\'s own on a card without one. `image={null}` renders an ordinary card, so a record with no photo needs no branch. A photo card is a `View` or, with `as`, a `Pressable`; put a scroller inside it, never as it. Use it whenever an item has a photograph, instead of a small `Image` with its text in a box underneath. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
3249
3251
|
rnComponent: null,
|
|
3250
3252
|
docsUrl: null,
|
|
3251
3253
|
},
|
|
@@ -5870,7 +5872,13 @@ const CONTRACT = deepFreeze({
|
|
|
5870
5872
|
// 1.177.1: fix (sc-10410) — a `refresh()` or `verify()` that succeeds
|
|
5871
5873
|
// clears `useFileSignature`'s `error`; it used to keep one failed poll's
|
|
5872
5874
|
// error for the rest of the order.
|
|
5873
|
-
|
|
5875
|
+
// 1.178.0: additive (sc-11272) — `<Card image>` paints a picture edge to
|
|
5876
|
+
// edge behind the card's content, with a `scrim` on the text's edge and
|
|
5877
|
+
// an `imagePosition` for the crop. Children may be a function taking
|
|
5878
|
+
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5879
|
+
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5880
|
+
// the signed-in user's own start page on both hosts.
|
|
5881
|
+
version: "1.179.0",
|
|
5874
5882
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5875
5883
|
hooks: HOOKS,
|
|
5876
5884
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -72,17 +72,20 @@ export function WidgetContextProvider({ value, children }) {
|
|
|
72
72
|
* @internal — a surface painting its own fill inside a coloured section
|
|
73
73
|
* becomes the surface its content reads and its buttons are measured against
|
|
74
74
|
* (`<Card>` uses it), by the derivation a layout container applies.
|
|
75
|
-
* Outside such a section there is no backdrop and nothing changes
|
|
75
|
+
* Outside such a section there is no backdrop and nothing changes, unless
|
|
76
|
+
* `always`: a surface whose fill is fixed (a photograph) re-inks everywhere.
|
|
76
77
|
*/
|
|
77
|
-
export function BackdropScope({ basis, children }) {
|
|
78
|
+
export function BackdropScope({ basis, always = false, children }) {
|
|
78
79
|
const ctx = useContext(HostWidgetContext);
|
|
79
80
|
const theme = ctx && ctx.workspace ? ctx.workspace.theme : null;
|
|
80
81
|
const colors = theme && theme.colors;
|
|
81
|
-
const scoped = Boolean(
|
|
82
|
+
const scoped = Boolean(
|
|
83
|
+
basis && colors && (always || (colors.backdrop && colors.backdrop !== basis)),
|
|
84
|
+
);
|
|
82
85
|
const value = useMemo(
|
|
83
86
|
() =>
|
|
84
87
|
scoped
|
|
85
|
-
? { ...ctx, workspace: { ...ctx.workspace, theme: { ...theme, colors: { ...colors, ...deriveSurfaceTokens(basis), backdrop: basis } } } }
|
|
88
|
+
? { ...ctx, workspace: { ...ctx.workspace, theme: { ...theme, colors: { ...colors, ...deriveSurfaceTokens(basis), backdrop: basis, backdropBase: colors.backdropBase || colors.surface } } } }
|
|
86
89
|
: ctx,
|
|
87
90
|
[scoped, ctx, theme, colors, basis],
|
|
88
91
|
);
|
|
@@ -679,10 +682,11 @@ export function useContainerWidth() {
|
|
|
679
682
|
|
|
680
683
|
/**
|
|
681
684
|
* Returns the host-provided navigation surface:
|
|
682
|
-
* `{ goTo, goBack, push, replace, back, currentRoute }`.
|
|
685
|
+
* `{ goTo, goToStartPage, goBack, push, replace, back, currentRoute }`.
|
|
683
686
|
*
|
|
684
|
-
* `goTo(pageId, params?)` navigates to an internal app page. `
|
|
685
|
-
*
|
|
687
|
+
* `goTo(pageId, params?)` navigates to an internal app page. `goToStartPage()`
|
|
688
|
+
* opens the signed-in user's own start page, which can differ per user.
|
|
689
|
+
* `goBack()` pops the stack. `push` / `replace` / `back` are aliases that map to
|
|
686
690
|
* the platform's native navigator (react-router on web, react-navigation
|
|
687
691
|
* on native). Missing methods degrade to no-ops on the Studio canvas
|
|
688
692
|
* preview, where there is no live router.
|
package/dist/index.d.ts
CHANGED
|
@@ -765,6 +765,7 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
765
765
|
};
|
|
766
766
|
navigation: {
|
|
767
767
|
goTo(pageId: string, params?: Record<string, string>): void;
|
|
768
|
+
goToStartPage(): void;
|
|
768
769
|
goBack(): void;
|
|
769
770
|
push(pageId: string, params?: Record<string, unknown>): void;
|
|
770
771
|
replace(pageId: string, params?: Record<string, unknown>): void;
|
|
@@ -2019,12 +2020,14 @@ export function useFill(): boolean;
|
|
|
2019
2020
|
|
|
2020
2021
|
/**
|
|
2021
2022
|
* The host-provided navigation surface. `goTo(pageId, params?)` navigates
|
|
2022
|
-
* to an internal app page; `goBack()` pops the stack.
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2023
|
+
* to an internal app page; `goBack()` pops the stack. `goToStartPage()`
|
|
2024
|
+
* opens the signed-in user's own start page, which can differ per user.
|
|
2025
|
+
* Missing methods degrade to no-ops on the Studio canvas preview where no
|
|
2026
|
+
* live router is mounted. For external URLs use the `Linking` primitive.
|
|
2025
2027
|
*/
|
|
2026
2028
|
export function useNavigation(): {
|
|
2027
2029
|
goTo(pageId: string, params?: Record<string, unknown>): void;
|
|
2030
|
+
goToStartPage(): void;
|
|
2028
2031
|
goBack(): void;
|
|
2029
2032
|
push(pageId: string, params?: Record<string, unknown>): void;
|
|
2030
2033
|
replace(pageId: string, params?: Record<string, unknown>): void;
|
|
@@ -3697,6 +3700,9 @@ export function initialsFor(name: unknown): string;
|
|
|
3697
3700
|
* @example
|
|
3698
3701
|
* <Card><Text>…</Text></Card>
|
|
3699
3702
|
* <Card as={Pressable} onPress={open} radius={12} shadow="md">…</Card>
|
|
3703
|
+
* <Card image={url} scrim="bottom" style={{ aspectRatio: 1, justifyContent: "flex-end" }}>
|
|
3704
|
+
* {({ colors }) => <Text style={{ color: colors.onSurface }}>…</Text>}
|
|
3705
|
+
* </Card>
|
|
3700
3706
|
*/
|
|
3701
3707
|
export const Card: (props: {
|
|
3702
3708
|
background?: string;
|
|
@@ -3705,6 +3711,15 @@ export const Card: (props: {
|
|
|
3705
3711
|
padding?: number | { top?: number; right?: number; bottom?: number; left?: number };
|
|
3706
3712
|
shadow?: "none" | "sm" | "md" | "lg";
|
|
3707
3713
|
gradient?: { from: string; to: string; angle?: number };
|
|
3714
|
+
/**
|
|
3715
|
+
* A picture painted edge to edge behind the content. With none the card is
|
|
3716
|
+
* an ordinary card, so a record without a photo needs no branch.
|
|
3717
|
+
*/
|
|
3718
|
+
image?: string | { uri: string } | null;
|
|
3719
|
+
/** The part of the picture kept in frame when cropped. Default "center". */
|
|
3720
|
+
imagePosition?: CardImagePosition;
|
|
3721
|
+
/** The edge the readability gradient darkens. Default "bottom". */
|
|
3722
|
+
scrim?: CardScrim;
|
|
3708
3723
|
/**
|
|
3709
3724
|
* `card*` style fields, `shadow`, `margin` — a widget's per-instance style.
|
|
3710
3725
|
* With no `baseStyle` it also carries the box's layout (width, height, the
|
|
@@ -3721,10 +3736,17 @@ export const Card: (props: {
|
|
|
3721
3736
|
fillInGradient?: boolean;
|
|
3722
3737
|
containerStyle?: any;
|
|
3723
3738
|
withoutMargin?: boolean;
|
|
3724
|
-
|
|
3739
|
+
/**
|
|
3740
|
+
* A function is called with the theme colours as they read where the
|
|
3741
|
+
* content sits: light over a picture, the theme's own without one.
|
|
3742
|
+
*/
|
|
3743
|
+
children?: ReactNode | ((ink: { colors: Record<string, string>; onImage: boolean }) => ReactNode);
|
|
3725
3744
|
[prop: string]: any;
|
|
3726
3745
|
}) => any;
|
|
3727
3746
|
|
|
3747
|
+
export type CardScrim = "bottom" | "top" | "left" | "right" | "full" | "none";
|
|
3748
|
+
export type CardImagePosition = "center" | "top" | "bottom" | "left" | "right";
|
|
3749
|
+
|
|
3728
3750
|
export type ButtonVariant = "primary" | "secondary" | "outlined" | "danger";
|
|
3729
3751
|
export const BUTTON_VARIANTS: readonly ButtonVariant[];
|
|
3730
3752
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.223.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",
|
package/src/contract.js
CHANGED
|
@@ -1128,6 +1128,8 @@ const HOOKS = [
|
|
|
1128
1128
|
goTo:
|
|
1129
1129
|
"(pageIdOrSlug: string, params?: object) => void — a page's UUID (a pageRef prop) OR its SLUG (e.g. a row's page-key field); both hosts resolve either. There is NO page-name lookup: a slug is the only data-carried navigation key.",
|
|
1130
1130
|
openLink: "(link: string) => boolean",
|
|
1131
|
+
goToStartPage:
|
|
1132
|
+
"() => void — opens the signed-in user's own start page, the page the app opens on for them (it can differ per user, since page access is restricted per group). Use it for a 'back to start' or 'home' button instead of hard-coding a page. A no-op where there is no live router (the Studio canvas, a badge counter).",
|
|
1131
1133
|
goBack: "() => void",
|
|
1132
1134
|
push: "(pageIdOrSlug: string, params?: object) => void",
|
|
1133
1135
|
replace: "(pageIdOrSlug: string, params?: object) => void",
|
|
@@ -3245,7 +3247,7 @@ const PRIMITIVES = [
|
|
|
3245
3247
|
{
|
|
3246
3248
|
name: "Card",
|
|
3247
3249
|
description:
|
|
3248
|
-
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. `style` may be an object or a React Native style array (`style={[{ flex: 1, minWidth: 0 }, align && { alignItems: align }]}`), read as the host flattens it: nested arrays and falsy entries are fine and a later entry wins. A gradient paints on both hosts. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
3250
|
+
'Themed card surface. `<Card>…</Card>` — THE way to draw a card, panel or tile: with no props it takes the app\'s card look (Theme Settings > Cards: background, border, corner radius, padding, shadow, gradient) so it matches every built-in card. Override one card with `background`, `borderColor`, `radius`, `padding` (a number or `{ top, right, bottom, left }`), `shadow` ("none" | "sm" | "md" | "lg") or `gradient` (`{ from, to, angle }`), or pass `style={props.style}` when your widget declares the `cardBackground` / `cardBorderColor` / `cardRadius` / `cardPadding` / `cardGradient` / `shadow` / `margin` style fields, so an author can restyle it. A card that brings no `baseStyle` also takes the box\'s LAYOUT from `style` — width, height, the flex properties, `alignSelf`, `gap`, position — so `style={{ width: \'100%\', maxWidth: 480, alignSelf: \'center\' }}` sizes the card and still keeps the app\'s card look; put anything else about the container on `containerStyle`. `style` may be an object or a React Native style array (`style={[{ flex: 1, minWidth: 0 }, align && { alignItems: align }]}`), read as the host flattens it: nested arrays and falsy entries are fine and a later entry wins. A gradient paints on both hosts. `image` (a URL or `{ uri }`) makes it a PHOTO card: the picture fills the card edge to edge BEHIND the content, under a light gradient on the edge named by `scrim` ("bottom" default | "top" | "left" | "right" | "full" | "none") — the edge the text sits on — and cropped around `imagePosition` ("center" default | "top" | "bottom" | "left" | "right"). A photo card has no picture-shaped content to size it, so give it an `aspectRatio` or `minHeight` in `style`, and pass the children as a function — `{({ colors, onImage }) => …}` — to receive the theme colours as they read where the content sits (`colors.onSurface` for a title, `colors.onSurfaceMuted` for a supporting line): light over a picture, the theme\'s own on a card without one. `image={null}` renders an ordinary card, so a record with no photo needs no branch. A photo card is a `View` or, with `as`, a `Pressable`; put a scroller inside it, never as it. Use it whenever an item has a photograph, instead of a small `Image` with its text in a box underneath. `as` renders the card as another container (e.g. `Pressable`, `ScrollView`) and every other prop lands on it. Never hand-roll a card from `View` + border + radius.',
|
|
3249
3251
|
rnComponent: null,
|
|
3250
3252
|
docsUrl: null,
|
|
3251
3253
|
},
|
|
@@ -5870,7 +5872,13 @@ const CONTRACT = deepFreeze({
|
|
|
5870
5872
|
// 1.177.1: fix (sc-10410) — a `refresh()` or `verify()` that succeeds
|
|
5871
5873
|
// clears `useFileSignature`'s `error`; it used to keep one failed poll's
|
|
5872
5874
|
// error for the rest of the order.
|
|
5873
|
-
|
|
5875
|
+
// 1.178.0: additive (sc-11272) — `<Card image>` paints a picture edge to
|
|
5876
|
+
// edge behind the card's content, with a `scrim` on the text's edge and
|
|
5877
|
+
// an `imagePosition` for the crop. Children may be a function taking
|
|
5878
|
+
// `{ colors, onImage }`: the theme colours as they read over the picture.
|
|
5879
|
+
// 1.179.0: additive (sc-11253) — `useNavigation().goToStartPage()` opens
|
|
5880
|
+
// the signed-in user's own start page on both hosts.
|
|
5881
|
+
version: "1.179.0",
|
|
5874
5882
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
5875
5883
|
hooks: HOOKS,
|
|
5876
5884
|
primitives: PRIMITIVES,
|