@colixsystems/widget-sdk 0.197.0 → 0.198.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 +57 -1
- package/dist/avatar-color.js +35 -0
- package/dist/contract.cjs +184 -24
- package/dist/contract.js +184 -24
- package/dist/detail-parts-view.js +9 -4
- package/dist/hooks.js +535 -28
- package/dist/index.d.ts +177 -4
- package/dist/index.js +8 -0
- package/dist/index.native.js +8 -0
- package/dist/initials.js +3 -2
- package/dist/manifest.cjs +2 -1
- package/dist/manifest.js +2 -1
- package/dist/overlay-view.js +2 -5
- package/dist/overlay.js +12 -5
- package/dist/primitives.js +2 -0
- package/dist/primitives.native.js +2 -0
- package/dist/video.js +51 -0
- package/dist/video.native.js +61 -0
- package/dist/web-modal.js +139 -0
- package/package.json +5 -1
- package/src/contract.js +184 -24
package/dist/index.d.ts
CHANGED
|
@@ -187,6 +187,12 @@ export interface WidgetDatastoreTemplateColumn {
|
|
|
187
187
|
relationType?: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY";
|
|
188
188
|
/** REQ-ACL-RELINHERIT: opt this RELATION column into row-level ACL inheritance. */
|
|
189
189
|
inheritAcl?: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* With `inheritAcl: true` only: rows inherit read access alone. Members of
|
|
192
|
+
* the parent row read every child and may add children, but change or
|
|
193
|
+
* delete only the rows they created (pair with `grantCreatorPermissions`).
|
|
194
|
+
*/
|
|
195
|
+
inheritAclReadOnly?: boolean;
|
|
190
196
|
/**
|
|
191
197
|
* ONE_TO_ONE / ONE_TO_MANY only: what deleting the referenced row does to
|
|
192
198
|
* this table's rows. `CASCADE` deletes them too; `SET_NULL` (the default)
|
|
@@ -202,6 +208,26 @@ export interface WidgetDatastoreTemplateColumn {
|
|
|
202
208
|
* the backend must resolve); publishing one is rejected.
|
|
203
209
|
*/
|
|
204
210
|
encrypted?: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Written when a create omits the column. `"@currentUser"` (USER) stamps
|
|
213
|
+
* the signed-in user and `"@now"` (DATE / DATETIME) the current date or
|
|
214
|
+
* time. Other types take a literal of their own type. FILE, RELATION and
|
|
215
|
+
* USER_GROUP columns take none, because ids belong to the installing
|
|
216
|
+
* workspace.
|
|
217
|
+
*/
|
|
218
|
+
defaultValue?: unknown;
|
|
219
|
+
/**
|
|
220
|
+
* With a `"@currentUser"` or `"@now"` default only: the server always
|
|
221
|
+
* stamps it. Any value a client sends for the column is ignored on create
|
|
222
|
+
* and on update, so an author column cannot be forged.
|
|
223
|
+
*/
|
|
224
|
+
defaultLocked?: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Write-once: a create sets the column as usual (the value sent, or the
|
|
227
|
+
* default), and any value sent for it on update is ignored. Not valid on a
|
|
228
|
+
* `MANY_TO_MANY` relation, whose links live outside the row.
|
|
229
|
+
*/
|
|
230
|
+
immutable?: boolean;
|
|
205
231
|
}
|
|
206
232
|
|
|
207
233
|
export interface WidgetDatastoreTemplateTable {
|
|
@@ -367,7 +393,7 @@ export interface WidgetManifest {
|
|
|
367
393
|
* under a per-widget namespace (`widget.<id>.<key>`), so `useI18n().t(key)`
|
|
368
394
|
* resolves the namespaced key automatically — the author never types the
|
|
369
395
|
* prefix. Non-destructive (admin edits win; absent languages are not
|
|
370
|
-
* created) and persists across uninstalls. Caps: ≤
|
|
396
|
+
* created) and persists across uninstalls. Caps: ≤200 keys, key matches
|
|
371
397
|
* /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/, value ≤1 KB.
|
|
372
398
|
*/
|
|
373
399
|
translations?: Record<string, { en: string } & Record<string, string>>;
|
|
@@ -639,6 +665,35 @@ export interface IdentificationClient {
|
|
|
639
665
|
*/
|
|
640
666
|
export interface NotificationsClient {
|
|
641
667
|
send(body: SendNotificationRequest): Promise<NotificationRow>;
|
|
668
|
+
/**
|
|
669
|
+
* sc-10268 — the signed-in user's per-record mutes; backs `useRecordMute`
|
|
670
|
+
* and `useRecordMutes`.
|
|
671
|
+
* Optional: a host without it leaves the hook reading not muted.
|
|
672
|
+
*/
|
|
673
|
+
mutes?: {
|
|
674
|
+
get(ref: { table_id: string; record_id: string }): Promise<RecordMuteWire>;
|
|
675
|
+
set(body: {
|
|
676
|
+
table_id: string;
|
|
677
|
+
record_id: string;
|
|
678
|
+
muted_until: string | null;
|
|
679
|
+
}): Promise<RecordMuteWire>;
|
|
680
|
+
clear(ref: { table_id: string; record_id: string }): Promise<void>;
|
|
681
|
+
/** Backs `useRecordMutes`: one page of the user's active mutes. */
|
|
682
|
+
list(query: {
|
|
683
|
+
table_id: string;
|
|
684
|
+
limit?: number;
|
|
685
|
+
offset?: number;
|
|
686
|
+
}): Promise<{
|
|
687
|
+
data: Array<{ record_id: string; muted_until: string | null }>;
|
|
688
|
+
meta: { total: number; limit: number; offset: number };
|
|
689
|
+
}>;
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** The mute wire shape, snake_case verbatim. */
|
|
694
|
+
export interface RecordMuteWire {
|
|
695
|
+
muted: boolean;
|
|
696
|
+
muted_until: string | null;
|
|
642
697
|
}
|
|
643
698
|
|
|
644
699
|
export interface WidgetContext<TProps = unknown> {
|
|
@@ -988,6 +1043,11 @@ export interface DirectoryQuery {
|
|
|
988
1043
|
* `useGroups()` row. Pair with a group picker to build "members of group X".
|
|
989
1044
|
*/
|
|
990
1045
|
group_id?: string;
|
|
1046
|
+
/**
|
|
1047
|
+
* Only these users (1 to 100 ids): resolve the names behind ids you already
|
|
1048
|
+
* hold — message senders, members — without loading the whole roster.
|
|
1049
|
+
*/
|
|
1050
|
+
ids?: string[];
|
|
991
1051
|
limit?: number;
|
|
992
1052
|
offset?: number;
|
|
993
1053
|
}
|
|
@@ -1145,6 +1205,45 @@ export interface SendNotificationApi {
|
|
|
1145
1205
|
*/
|
|
1146
1206
|
export function useSendNotification(): SendNotificationApi;
|
|
1147
1207
|
|
|
1208
|
+
export interface RecordMuteApi {
|
|
1209
|
+
muted: boolean;
|
|
1210
|
+
/** ISO time the mute ends; null when muted until unmuted, or not muted. */
|
|
1211
|
+
mutedUntil: string | null;
|
|
1212
|
+
loading: boolean;
|
|
1213
|
+
error: NotificationError | null;
|
|
1214
|
+
/** An ISO time or a Date; null or omitted mutes until `unmute()`. */
|
|
1215
|
+
mute(until?: string | Date | null): Promise<void>;
|
|
1216
|
+
unmute(): Promise<void>;
|
|
1217
|
+
refetch(): Promise<void>;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* sc-10268 — the signed-in user's mute of one datastore record's
|
|
1222
|
+
* notifications. While muted, `notifications.notifyRecordSubjects` on the
|
|
1223
|
+
* record sends them nothing. A record they cannot read fails with
|
|
1224
|
+
* `RECORD_NOT_FOUND`; a falsy id returns a stable not-muted result.
|
|
1225
|
+
*/
|
|
1226
|
+
export function useRecordMute(
|
|
1227
|
+
tableId: string | null | undefined,
|
|
1228
|
+
recordId: string | null | undefined,
|
|
1229
|
+
): RecordMuteApi;
|
|
1230
|
+
|
|
1231
|
+
export interface RecordMutesApi {
|
|
1232
|
+
/** True while the record's mute is active; false once a timed mute lapses. */
|
|
1233
|
+
isMuted(recordId: string | null | undefined): boolean;
|
|
1234
|
+
loading: boolean;
|
|
1235
|
+
error: NotificationError | null;
|
|
1236
|
+
refetch(): Promise<void>;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
/**
|
|
1240
|
+
* sc-10268 — which records of one table the signed-in user has muted, in one
|
|
1241
|
+
* read for a whole list. A `useRecordMute` write on the table shows at once.
|
|
1242
|
+
*/
|
|
1243
|
+
export function useRecordMutes(
|
|
1244
|
+
tableId: string | null | undefined,
|
|
1245
|
+
): RecordMutesApi;
|
|
1246
|
+
|
|
1148
1247
|
export interface WorkspaceCurrency {
|
|
1149
1248
|
/** The ISO code this workspace charges its app users in. */
|
|
1150
1249
|
currency: string;
|
|
@@ -1704,6 +1803,35 @@ export function useNavigation(): {
|
|
|
1704
1803
|
*/
|
|
1705
1804
|
export function useRouteParams(): Record<string, unknown>;
|
|
1706
1805
|
|
|
1806
|
+
/**
|
|
1807
|
+
* sc-10268 — puts `count` on the menu entry or tab of the page this widget is
|
|
1808
|
+
* on. The host sums the page's widgets and keeps the last count after the
|
|
1809
|
+
* widget leaves the screen; 0 clears this widget's share.
|
|
1810
|
+
*/
|
|
1811
|
+
/**
|
|
1812
|
+
* sc-10268 — `{ uri }` an `<Image>` can show for a picked file before it is
|
|
1813
|
+
* uploaded. Null for no file, or a host that cannot preview one.
|
|
1814
|
+
*/
|
|
1815
|
+
export function useFilePreview(file: unknown): { uri: string | null };
|
|
1816
|
+
|
|
1817
|
+
export function usePageBadge(count: number | null | undefined): {
|
|
1818
|
+
supported: boolean;
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
/**
|
|
1822
|
+
* sc-10268 — Android's back button for a widget with pages of its own.
|
|
1823
|
+
* `handler` runs on a back press while the widget's screen is in view; return
|
|
1824
|
+
* true when it went back a step, false to let the app go back. Pass null while
|
|
1825
|
+
* there is nothing to go back from. `supported` is false on the web, which has
|
|
1826
|
+
* no hardware back, so keep a visible back control too.
|
|
1827
|
+
*
|
|
1828
|
+
* @example
|
|
1829
|
+
* useHardwareBack(openThread ? () => { closeThread(); return true; } : null);
|
|
1830
|
+
*/
|
|
1831
|
+
export function useHardwareBack(handler: (() => boolean) | null | undefined): {
|
|
1832
|
+
supported: boolean;
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1707
1835
|
/**
|
|
1708
1836
|
* REQ-NAV-05 — the PAGE's resolved context. When the page declares typed
|
|
1709
1837
|
* parameters, the host resolves them ONCE before any widget renders: `params`
|
|
@@ -2068,7 +2196,7 @@ export interface CameraOptions {
|
|
|
2068
2196
|
quality?: number;
|
|
2069
2197
|
}
|
|
2070
2198
|
|
|
2071
|
-
/** A photo taken or picked through `useCamera()`, normalised across hosts. */
|
|
2199
|
+
/** A photo (or, from `pick({ videos: true })`, a video) taken or picked through `useCamera()`, normalised across hosts. */
|
|
2072
2200
|
export interface CameraAsset {
|
|
2073
2201
|
/** Displayable source — `<Image source={{ uri }} />` / `<img src>`. */
|
|
2074
2202
|
uri: string;
|
|
@@ -2079,6 +2207,8 @@ export interface CameraAsset {
|
|
|
2079
2207
|
height: number | null;
|
|
2080
2208
|
/** Bytes, when the host reports it. */
|
|
2081
2209
|
size: number | null;
|
|
2210
|
+
/** A video's length in milliseconds when the host reports it; null for a photo. */
|
|
2211
|
+
durationMs: number | null;
|
|
2082
2212
|
/**
|
|
2083
2213
|
* Ready-to-upload part — a Blob on both hosts: a `File` on web,
|
|
2084
2214
|
* expo-file-system's `File` on native. Expo's fetch rejects any other shape.
|
|
@@ -2096,8 +2226,13 @@ export interface CameraResult {
|
|
|
2096
2226
|
supported: boolean;
|
|
2097
2227
|
/** Open the camera. Resolves null if the user dismisses it. */
|
|
2098
2228
|
capture(): Promise<CameraAsset | null>;
|
|
2099
|
-
/**
|
|
2100
|
-
|
|
2229
|
+
/**
|
|
2230
|
+
* Open the photo library. Resolves null if the user dismisses it.
|
|
2231
|
+
* `{ multiple: true }` resolves an array of up to 10; `{ videos: true }`
|
|
2232
|
+
* offers videos beside photos (play one with `<Video>`).
|
|
2233
|
+
*/
|
|
2234
|
+
pick(options: { multiple: true; videos?: boolean }): Promise<CameraAsset[] | null>;
|
|
2235
|
+
pick(options?: { multiple?: false; videos?: boolean }): Promise<CameraAsset | null>;
|
|
2101
2236
|
/** Clear `asset` and `error`, releasing the held asset. */
|
|
2102
2237
|
reset(): void;
|
|
2103
2238
|
}
|
|
@@ -2412,6 +2547,10 @@ export class NotificationError extends Error {
|
|
|
2412
2547
|
| "FORBIDDEN"
|
|
2413
2548
|
| "RECIPIENT_NOT_FOUND"
|
|
2414
2549
|
| "RATE_LIMITED"
|
|
2550
|
+
| "RECORD_NOT_FOUND"
|
|
2551
|
+
| "INVALID_MUTED_UNTIL"
|
|
2552
|
+
| "INVALID_RECORD_REF"
|
|
2553
|
+
| "NOTIFICATIONS_UNAVAILABLE"
|
|
2415
2554
|
| "INTERNAL"
|
|
2416
2555
|
| string;
|
|
2417
2556
|
constructor(
|
|
@@ -2904,6 +3043,35 @@ export const Gradient: (props: {
|
|
|
2904
3043
|
children?: ReactNode;
|
|
2905
3044
|
}) => any;
|
|
2906
3045
|
|
|
3046
|
+
/**
|
|
3047
|
+
* sc-10268 — plays a video file inline with the platform's own controls, the
|
|
3048
|
+
* first frame showing before playback. Web uses the browser's `<video>`;
|
|
3049
|
+
* native uses expo-video's VideoView. It fills the box `style` gives it.
|
|
3050
|
+
*
|
|
3051
|
+
* @example
|
|
3052
|
+
* <Video
|
|
3053
|
+
* source={file.url}
|
|
3054
|
+
* contentFit="cover"
|
|
3055
|
+
* style={{ width: 240, height: 180, borderRadius: theme.radii.md, overflow: "hidden" }}
|
|
3056
|
+
* onError={() => setPlayable(false)}
|
|
3057
|
+
* />
|
|
3058
|
+
*/
|
|
3059
|
+
export const Video: (props: {
|
|
3060
|
+
/** A playable URL; null renders an empty box. */
|
|
3061
|
+
source: string | null;
|
|
3062
|
+
/** Play, seek and fullscreen controls. Defaults to true. */
|
|
3063
|
+
controls?: boolean;
|
|
3064
|
+
/** Starts playing muted, as browsers allow only muted autoplay. */
|
|
3065
|
+
autoPlay?: boolean;
|
|
3066
|
+
/** Letterboxed ("contain", the default) or cropped to fill ("cover"). */
|
|
3067
|
+
contentFit?: "contain" | "cover";
|
|
3068
|
+
style?: any;
|
|
3069
|
+
accessibilityLabel?: string;
|
|
3070
|
+
testID?: string;
|
|
3071
|
+
/** The file could not be played on this host. */
|
|
3072
|
+
onError?: () => void;
|
|
3073
|
+
}) => any;
|
|
3074
|
+
|
|
2907
3075
|
/**
|
|
2908
3076
|
* sc-6607 — screen-level overlay. Renders OUTSIDE the widget's layout box on
|
|
2909
3077
|
* both hosts (web portals it to the document root, native uses the OS modal),
|
|
@@ -3054,9 +3222,14 @@ export const Badge: (props: {
|
|
|
3054
3222
|
}) => any;
|
|
3055
3223
|
|
|
3056
3224
|
/** Themed picture of a person, falling back to initials, then a glyph. */
|
|
3225
|
+
/** The stable colour `<Avatar seed>` paints for a seed, or null for none. */
|
|
3226
|
+
export function avatarColorFor(seed: string | null | undefined): string | null;
|
|
3227
|
+
|
|
3057
3228
|
export const Avatar: (props: {
|
|
3058
3229
|
/** Drives the initials and the accessibility label. */
|
|
3059
3230
|
name?: string;
|
|
3231
|
+
/** A stable id (a user id): tints the monogram with that person's colour. */
|
|
3232
|
+
seed?: string;
|
|
3060
3233
|
/** An absolute image URL. */
|
|
3061
3234
|
url?: string;
|
|
3062
3235
|
/** A size token, or a number of px (clamped to 16-160). */
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
useFilestoreFolders,
|
|
47
47
|
useFilestoreCreateFolder,
|
|
48
48
|
useFilestoreCopy,
|
|
49
|
+
useFilestoreDelete,
|
|
49
50
|
useFileSignature,
|
|
50
51
|
useFileSignatures,
|
|
51
52
|
useFileRoster,
|
|
@@ -66,6 +67,8 @@ export {
|
|
|
66
67
|
usePayments,
|
|
67
68
|
useWorkspaceCurrency,
|
|
68
69
|
useSendNotification,
|
|
70
|
+
useRecordMute,
|
|
71
|
+
useRecordMutes,
|
|
69
72
|
useTheme,
|
|
70
73
|
useWidgetStyle,
|
|
71
74
|
useI18n,
|
|
@@ -78,6 +81,9 @@ export {
|
|
|
78
81
|
useFill,
|
|
79
82
|
useNavigation,
|
|
80
83
|
useRouteParams,
|
|
84
|
+
usePageBadge,
|
|
85
|
+
useHardwareBack,
|
|
86
|
+
useFilePreview,
|
|
81
87
|
usePageContext,
|
|
82
88
|
useWidgetRoute,
|
|
83
89
|
useChildRenderer,
|
|
@@ -160,6 +166,7 @@ export {
|
|
|
160
166
|
DateTimePicker,
|
|
161
167
|
FilePicker,
|
|
162
168
|
Gradient,
|
|
169
|
+
Video,
|
|
163
170
|
} from "./primitives.js";
|
|
164
171
|
export { lintSource, bannedIdentifiers } from "./linter.js";
|
|
165
172
|
// sc-3696: the trailing six are the theme-derivation helpers both hosts resolve
|
|
@@ -215,6 +222,7 @@ export { List, ListRow, ListHeader, EmptyState } from "./list.js";
|
|
|
215
222
|
// Themed detail parts; the stock Badge/Avatar/Progress/Stat/Divider wrap these.
|
|
216
223
|
export { Badge, Avatar, Progress, Stat, Divider } from "./detail-parts.js";
|
|
217
224
|
export { initialsFor } from "./initials.js";
|
|
225
|
+
export { avatarColorFor } from "./avatar-color.js";
|
|
218
226
|
// The one card surface: the theme's `card` scope, a gradient fill, margin.
|
|
219
227
|
export { Card } from "./card.js";
|
|
220
228
|
export {
|
package/dist/index.native.js
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
useFilestoreFolders,
|
|
47
47
|
useFilestoreCreateFolder,
|
|
48
48
|
useFilestoreCopy,
|
|
49
|
+
useFilestoreDelete,
|
|
49
50
|
useFileSignature,
|
|
50
51
|
useFileSignatures,
|
|
51
52
|
useFileRoster,
|
|
@@ -66,6 +67,8 @@ export {
|
|
|
66
67
|
usePayments,
|
|
67
68
|
useWorkspaceCurrency,
|
|
68
69
|
useSendNotification,
|
|
70
|
+
useRecordMute,
|
|
71
|
+
useRecordMutes,
|
|
69
72
|
useTheme,
|
|
70
73
|
useWidgetStyle,
|
|
71
74
|
useI18n,
|
|
@@ -78,6 +81,9 @@ export {
|
|
|
78
81
|
useFill,
|
|
79
82
|
useNavigation,
|
|
80
83
|
useRouteParams,
|
|
84
|
+
usePageBadge,
|
|
85
|
+
useHardwareBack,
|
|
86
|
+
useFilePreview,
|
|
81
87
|
usePageContext,
|
|
82
88
|
useWidgetRoute,
|
|
83
89
|
useChildRenderer,
|
|
@@ -153,6 +159,7 @@ export {
|
|
|
153
159
|
DateTimePicker,
|
|
154
160
|
FilePicker,
|
|
155
161
|
Gradient,
|
|
162
|
+
Video,
|
|
156
163
|
} from "./primitives.native.js";
|
|
157
164
|
export { lintSource, bannedIdentifiers } from "./linter.js";
|
|
158
165
|
// sc-3696: the trailing six are the theme-derivation helpers both hosts resolve
|
|
@@ -208,6 +215,7 @@ export { List, ListRow, ListHeader, EmptyState } from "./list.native.js";
|
|
|
208
215
|
// Themed detail parts; the stock Badge/Avatar/Progress/Stat/Divider wrap these.
|
|
209
216
|
export { Badge, Avatar, Progress, Stat, Divider } from "./detail-parts.native.js";
|
|
210
217
|
export { initialsFor } from "./initials.js";
|
|
218
|
+
export { avatarColorFor } from "./avatar-color.js";
|
|
211
219
|
// The one card surface: the theme's `card` scope, a gradient fill, margin.
|
|
212
220
|
export { Card } from "./card.native.js";
|
|
213
221
|
export {
|
package/dist/initials.js
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
// Returns "" when the name yields no letters, so each caller keeps its own
|
|
5
5
|
// fallback (a "?", a person glyph).
|
|
6
6
|
|
|
7
|
-
/** "Ada Lovelace" → "AL", "
|
|
7
|
+
/** "Ada Lovelace" → "AL", "Börje" → "BÖ", " " → "". */
|
|
8
8
|
export function initialsFor(name) {
|
|
9
9
|
const parts =
|
|
10
10
|
typeof name === "string" ? name.trim().split(/\s+/).filter(Boolean) : [];
|
|
11
11
|
if (parts.length === 0) return "";
|
|
12
12
|
// Array.from, not slice: a multi-byte first character must not be halved.
|
|
13
13
|
const first = Array.from(parts[0])[0];
|
|
14
|
-
|
|
14
|
+
// sc-10268: one word still gets two letters, as the Studio's member lists show.
|
|
15
|
+
if (parts.length === 1) return Array.from(parts[0]).slice(0, 2).join("").toUpperCase();
|
|
15
16
|
const last = Array.from(parts[parts.length - 1])[0];
|
|
16
17
|
return (first + last).toUpperCase();
|
|
17
18
|
}
|
package/dist/manifest.cjs
CHANGED
|
@@ -46,7 +46,8 @@ const VALID_PLATFORMS = new Set(["web", "native"]);
|
|
|
46
46
|
// backend/src/core/services/l10n.service.js). The locale pattern mirrors that
|
|
47
47
|
// service's LANG_CODE_RE so a manifest can't declare a locale the dictionary
|
|
48
48
|
// would reject.
|
|
49
|
-
|
|
49
|
+
// One page of the dictionary's widget namespace (l10n MAX_KEYS_PER_PAGE).
|
|
50
|
+
const TRANSLATIONS_MAX_KEYS = 200;
|
|
50
51
|
const TRANSLATION_VALUE_MAX_BYTES = 1024;
|
|
51
52
|
const TRANSLATION_KEY_RE = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
52
53
|
const TRANSLATION_LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
|
package/dist/manifest.js
CHANGED
|
@@ -60,7 +60,8 @@ const ACTION_TIMEOUT_MAX_MS = 5 * 60 * 1000;
|
|
|
60
60
|
// backend/src/core/services/l10n.service.js). The locale pattern mirrors that
|
|
61
61
|
// service's LANG_CODE_RE so a manifest can't declare a locale the dictionary
|
|
62
62
|
// would reject.
|
|
63
|
-
|
|
63
|
+
// One page of the dictionary's widget namespace (l10n MAX_KEYS_PER_PAGE).
|
|
64
|
+
const TRANSLATIONS_MAX_KEYS = 200;
|
|
64
65
|
const TRANSLATION_VALUE_MAX_BYTES = 1024;
|
|
65
66
|
const TRANSLATION_KEY_RE = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
66
67
|
const TRANSLATION_LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})?$/;
|
package/dist/overlay-view.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// (`overflow: hidden` cards, scroll containers) clip it and where React Native
|
|
7
7
|
// has no `position: "fixed"` at all. `<Overlay>` fixes that for both hosts at
|
|
8
8
|
// once by rendering into React Native's `Modal`, which leaves the parent's
|
|
9
|
-
// layout on BOTH platforms —
|
|
9
|
+
// layout on BOTH platforms — the web binding portals it to `document.body`,
|
|
10
10
|
// native hands it to the OS — while keeping the children in the widget's own
|
|
11
11
|
// React tree, so their state, hooks and WidgetContext all survive.
|
|
12
12
|
//
|
|
@@ -74,10 +74,7 @@ export function makeOverlay(rn) {
|
|
|
74
74
|
{
|
|
75
75
|
visible: true,
|
|
76
76
|
transparent: true,
|
|
77
|
-
// Not animated
|
|
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.
|
|
77
|
+
// Not animated: a sheet must answer the tap that opened it at once.
|
|
81
78
|
animationType: "none",
|
|
82
79
|
onRequestClose: requestClose,
|
|
83
80
|
},
|
package/dist/overlay.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
// sc-6607 — `<Overlay>` (web). Binds the shared implementation in
|
|
2
|
-
// ./overlay-view.js to react-native-web's primitives.
|
|
2
|
+
// ./overlay-view.js to react-native-web's primitives, with ./web-modal.js in
|
|
3
|
+
// place of react-native-web's `Modal`, which breaks under React StrictMode.
|
|
3
4
|
//
|
|
4
5
|
// The import is `react-native-web` rather than the bare `react-native`
|
|
5
6
|
// specifier for the rolldown optional-peer-dep reason spelled out at the top of
|
|
6
|
-
// ./primitives.js.
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// ./primitives.js. The modal layer portals its subtree into a `document.body`
|
|
8
|
+
// container at `position: fixed`, which is exactly what makes the overlay
|
|
9
|
+
// escape the widget's clipping ancestors.
|
|
9
10
|
|
|
10
11
|
import * as ReactNative from "react-native-web";
|
|
11
12
|
import { makeOverlay } from "./overlay-view.js";
|
|
13
|
+
import { WebModal } from "./web-modal.js";
|
|
12
14
|
|
|
13
|
-
export const Overlay = makeOverlay(
|
|
15
|
+
export const Overlay = makeOverlay({
|
|
16
|
+
Modal: WebModal,
|
|
17
|
+
View: ReactNative.View,
|
|
18
|
+
Pressable: ReactNative.Pressable,
|
|
19
|
+
StyleSheet: ReactNative.StyleSheet,
|
|
20
|
+
});
|
package/dist/primitives.js
CHANGED
|
@@ -80,3 +80,5 @@ export { FilePicker } from "./filepicker.js";
|
|
|
80
80
|
// `linear-gradient` on a View; the native build (./gradient.native.js) delegates
|
|
81
81
|
// to expo-linear-gradient through the same angle projection.
|
|
82
82
|
export { Gradient } from "./gradient.js";
|
|
83
|
+
// sc-10268 — `<Video>` (web): the browser's <video>, inline with controls.
|
|
84
|
+
export { Video } from "./video.js";
|
|
@@ -40,3 +40,5 @@ export { FilePicker } from "./filepicker.native.js";
|
|
|
40
40
|
// expo-linear-gradient (pinned unconditionally in every export); the web build
|
|
41
41
|
// (./gradient.js) paints the equivalent CSS `linear-gradient`.
|
|
42
42
|
export { Gradient } from "./gradient.native.js";
|
|
43
|
+
// sc-10268 — `<Video>` (native): expo-video's VideoView.
|
|
44
|
+
export { Video } from "./video.native.js";
|
package/dist/video.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// sc-10268 — `<Video>` (web).
|
|
2
|
+
//
|
|
3
|
+
// Plays a video file inline: the browser's own <video> element with its own
|
|
4
|
+
// controls. The native counterpart (./video.native.js) is expo-video's
|
|
5
|
+
// VideoView over the same props, so a widget plays video on both hosts with
|
|
6
|
+
// no platform split.
|
|
7
|
+
//
|
|
8
|
+
// `preload="metadata"` plus a `#t=` fragment paints the first frame as the
|
|
9
|
+
// thumbnail; Safari draws nothing before playback without the fragment.
|
|
10
|
+
|
|
11
|
+
import React from "react";
|
|
12
|
+
import * as ReactNative from "react-native-web";
|
|
13
|
+
|
|
14
|
+
const FIRST_FRAME = "#t=0.001";
|
|
15
|
+
const FITS = { contain: "contain", cover: "cover" };
|
|
16
|
+
|
|
17
|
+
export function Video({
|
|
18
|
+
source,
|
|
19
|
+
controls = true,
|
|
20
|
+
autoPlay = false,
|
|
21
|
+
contentFit = "contain",
|
|
22
|
+
style,
|
|
23
|
+
accessibilityLabel,
|
|
24
|
+
testID,
|
|
25
|
+
onError,
|
|
26
|
+
}) {
|
|
27
|
+
const uri = typeof source === "string" && source ? source : null;
|
|
28
|
+
return React.createElement(
|
|
29
|
+
ReactNative.View,
|
|
30
|
+
{ style, testID },
|
|
31
|
+
uri
|
|
32
|
+
? React.createElement("video", {
|
|
33
|
+
src: uri.includes("#") ? uri : `${uri}${FIRST_FRAME}`,
|
|
34
|
+
controls: Boolean(controls),
|
|
35
|
+
// Browsers only autoplay muted video.
|
|
36
|
+
autoPlay: Boolean(autoPlay),
|
|
37
|
+
muted: Boolean(autoPlay),
|
|
38
|
+
playsInline: true,
|
|
39
|
+
preload: "metadata",
|
|
40
|
+
"aria-label": accessibilityLabel,
|
|
41
|
+
onError: onError ? () => onError() : undefined,
|
|
42
|
+
style: {
|
|
43
|
+
width: "100%",
|
|
44
|
+
height: "100%",
|
|
45
|
+
objectFit: FITS[contentFit] || FITS.contain,
|
|
46
|
+
display: "block",
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
: null,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// sc-10268 — `<Video>` (native).
|
|
2
|
+
//
|
|
3
|
+
// expo-video's VideoView (AVPlayer on iOS, ExoPlayer on Android), pinned in
|
|
4
|
+
// every export (BASE_DEPENDENCIES in
|
|
5
|
+
// backend/src/core/services/export-dependencies.ts), so Metro always resolves
|
|
6
|
+
// it. The web counterpart (./video.js) is the browser's <video> over the same
|
|
7
|
+
// props; keep the two in lockstep.
|
|
8
|
+
|
|
9
|
+
import React, { useEffect } from "react";
|
|
10
|
+
import { View, StyleSheet } from "react-native";
|
|
11
|
+
import { VideoView, useVideoPlayer } from "expo-video";
|
|
12
|
+
|
|
13
|
+
const FITS = { contain: "contain", cover: "cover" };
|
|
14
|
+
|
|
15
|
+
export function Video({
|
|
16
|
+
source,
|
|
17
|
+
controls = true,
|
|
18
|
+
autoPlay = false,
|
|
19
|
+
contentFit = "contain",
|
|
20
|
+
style,
|
|
21
|
+
accessibilityLabel,
|
|
22
|
+
testID,
|
|
23
|
+
onError,
|
|
24
|
+
}) {
|
|
25
|
+
const uri = typeof source === "string" && source ? source : null;
|
|
26
|
+
// A bare string keeps the source stable, so the player is not recreated
|
|
27
|
+
// every render; null is an idle player.
|
|
28
|
+
const player = useVideoPlayer(uri);
|
|
29
|
+
// An effect rather than the setup callback, so turning autoPlay on later
|
|
30
|
+
// still starts it. Muted, as the web host must be to autoplay.
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!uri || !autoPlay) return;
|
|
33
|
+
try {
|
|
34
|
+
player.muted = true;
|
|
35
|
+
player.play();
|
|
36
|
+
} catch (_) {
|
|
37
|
+
// A released player has nothing to start.
|
|
38
|
+
}
|
|
39
|
+
}, [player, uri, autoPlay]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!onError) return undefined;
|
|
42
|
+
const subscription = player.addListener("statusChange", (event) => {
|
|
43
|
+
if (event && event.status === "error") onError();
|
|
44
|
+
});
|
|
45
|
+
return () => subscription.remove();
|
|
46
|
+
}, [player, onError]);
|
|
47
|
+
return React.createElement(
|
|
48
|
+
View,
|
|
49
|
+
{ style, testID },
|
|
50
|
+
uri
|
|
51
|
+
? React.createElement(VideoView, {
|
|
52
|
+
player,
|
|
53
|
+
nativeControls: Boolean(controls),
|
|
54
|
+
contentFit: FITS[contentFit] || FITS.contain,
|
|
55
|
+
allowsFullscreen: true,
|
|
56
|
+
accessibilityLabel,
|
|
57
|
+
style: StyleSheet.absoluteFill,
|
|
58
|
+
})
|
|
59
|
+
: null,
|
|
60
|
+
);
|
|
61
|
+
}
|