@colixsystems/widget-sdk 0.197.0 → 0.199.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/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: ≤100 keys, key matches
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,11 +665,46 @@ 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> {
645
700
  props: TProps;
646
- widget: { id: string; instanceId: string; version: string };
701
+ widget: {
702
+ id: string;
703
+ instanceId: string;
704
+ version: string;
705
+ /** Bundled file name → absolute URL; `{}` when the package bundles nothing. */
706
+ assets: Record<string, string>;
707
+ };
647
708
  /**
648
709
  * REQ-LAY-08 — optional host layout hint backing `useFill()`. `true` when
649
710
  * the host sized this widget to fill its layout slot's available height (a
@@ -988,6 +1049,11 @@ export interface DirectoryQuery {
988
1049
  * `useGroups()` row. Pair with a group picker to build "members of group X".
989
1050
  */
990
1051
  group_id?: string;
1052
+ /**
1053
+ * Only these users (1 to 100 ids): resolve the names behind ids you already
1054
+ * hold — message senders, members — without loading the whole roster.
1055
+ */
1056
+ ids?: string[];
991
1057
  limit?: number;
992
1058
  offset?: number;
993
1059
  }
@@ -1145,6 +1211,45 @@ export interface SendNotificationApi {
1145
1211
  */
1146
1212
  export function useSendNotification(): SendNotificationApi;
1147
1213
 
1214
+ export interface RecordMuteApi {
1215
+ muted: boolean;
1216
+ /** ISO time the mute ends; null when muted until unmuted, or not muted. */
1217
+ mutedUntil: string | null;
1218
+ loading: boolean;
1219
+ error: NotificationError | null;
1220
+ /** An ISO time or a Date; null or omitted mutes until `unmute()`. */
1221
+ mute(until?: string | Date | null): Promise<void>;
1222
+ unmute(): Promise<void>;
1223
+ refetch(): Promise<void>;
1224
+ }
1225
+
1226
+ /**
1227
+ * sc-10268 — the signed-in user's mute of one datastore record's
1228
+ * notifications. While muted, `notifications.notifyRecordSubjects` on the
1229
+ * record sends them nothing. A record they cannot read fails with
1230
+ * `RECORD_NOT_FOUND`; a falsy id returns a stable not-muted result.
1231
+ */
1232
+ export function useRecordMute(
1233
+ tableId: string | null | undefined,
1234
+ recordId: string | null | undefined,
1235
+ ): RecordMuteApi;
1236
+
1237
+ export interface RecordMutesApi {
1238
+ /** True while the record's mute is active; false once a timed mute lapses. */
1239
+ isMuted(recordId: string | null | undefined): boolean;
1240
+ loading: boolean;
1241
+ error: NotificationError | null;
1242
+ refetch(): Promise<void>;
1243
+ }
1244
+
1245
+ /**
1246
+ * sc-10268 — which records of one table the signed-in user has muted, in one
1247
+ * read for a whole list. A `useRecordMute` write on the table shows at once.
1248
+ */
1249
+ export function useRecordMutes(
1250
+ tableId: string | null | undefined,
1251
+ ): RecordMutesApi;
1252
+
1148
1253
  export interface WorkspaceCurrency {
1149
1254
  /** The ISO code this workspace charges its app users in. */
1150
1255
  currency: string;
@@ -1515,6 +1620,18 @@ export function useAsset(assetId: string | null | undefined): {
1515
1620
  refetch(): Promise<void>;
1516
1621
  };
1517
1622
 
1623
+ /**
1624
+ * URL of an image or sound file the widget package ships in its `assets/`
1625
+ * directory, or `null` when the package has no file with that name.
1626
+ *
1627
+ * ```jsx
1628
+ * const logo = useBundledAsset("logo.png");
1629
+ * const chime = useAudioPlayer(useBundledAsset("chime.mp3"));
1630
+ * return logo ? <Image source={{ uri: logo }} /> : null;
1631
+ * ```
1632
+ */
1633
+ export function useBundledAsset(fileName: string | null | undefined): string | null;
1634
+
1518
1635
  /**
1519
1636
  * List every tenant asset carrying a given tag. Reads `ctx.assets.list({
1520
1637
  * tag, type, limit })` and unwraps the `{ data, meta }` envelope.
@@ -1704,6 +1821,35 @@ export function useNavigation(): {
1704
1821
  */
1705
1822
  export function useRouteParams(): Record<string, unknown>;
1706
1823
 
1824
+ /**
1825
+ * sc-10268 — puts `count` on the menu entry or tab of the page this widget is
1826
+ * on. The host sums the page's widgets and keeps the last count after the
1827
+ * widget leaves the screen; 0 clears this widget's share.
1828
+ */
1829
+ /**
1830
+ * sc-10268 — `{ uri }` an `<Image>` can show for a picked file before it is
1831
+ * uploaded. Null for no file, or a host that cannot preview one.
1832
+ */
1833
+ export function useFilePreview(file: unknown): { uri: string | null };
1834
+
1835
+ export function usePageBadge(count: number | null | undefined): {
1836
+ supported: boolean;
1837
+ };
1838
+
1839
+ /**
1840
+ * sc-10268 — Android's back button for a widget with pages of its own.
1841
+ * `handler` runs on a back press while the widget's screen is in view; return
1842
+ * true when it went back a step, false to let the app go back. Pass null while
1843
+ * there is nothing to go back from. `supported` is false on the web, which has
1844
+ * no hardware back, so keep a visible back control too.
1845
+ *
1846
+ * @example
1847
+ * useHardwareBack(openThread ? () => { closeThread(); return true; } : null);
1848
+ */
1849
+ export function useHardwareBack(handler: (() => boolean) | null | undefined): {
1850
+ supported: boolean;
1851
+ };
1852
+
1707
1853
  /**
1708
1854
  * REQ-NAV-05 — the PAGE's resolved context. When the page declares typed
1709
1855
  * parameters, the host resolves them ONCE before any widget renders: `params`
@@ -2068,7 +2214,7 @@ export interface CameraOptions {
2068
2214
  quality?: number;
2069
2215
  }
2070
2216
 
2071
- /** A photo taken or picked through `useCamera()`, normalised across hosts. */
2217
+ /** A photo (or, from `pick({ videos: true })`, a video) taken or picked through `useCamera()`, normalised across hosts. */
2072
2218
  export interface CameraAsset {
2073
2219
  /** Displayable source — `<Image source={{ uri }} />` / `<img src>`. */
2074
2220
  uri: string;
@@ -2079,6 +2225,8 @@ export interface CameraAsset {
2079
2225
  height: number | null;
2080
2226
  /** Bytes, when the host reports it. */
2081
2227
  size: number | null;
2228
+ /** A video's length in milliseconds when the host reports it; null for a photo. */
2229
+ durationMs: number | null;
2082
2230
  /**
2083
2231
  * Ready-to-upload part — a Blob on both hosts: a `File` on web,
2084
2232
  * expo-file-system's `File` on native. Expo's fetch rejects any other shape.
@@ -2096,8 +2244,13 @@ export interface CameraResult {
2096
2244
  supported: boolean;
2097
2245
  /** Open the camera. Resolves null if the user dismisses it. */
2098
2246
  capture(): Promise<CameraAsset | null>;
2099
- /** Open the photo library. Resolves null if the user dismisses it. */
2100
- pick(): Promise<CameraAsset | null>;
2247
+ /**
2248
+ * Open the photo library. Resolves null if the user dismisses it.
2249
+ * `{ multiple: true }` resolves an array of up to 10; `{ videos: true }`
2250
+ * offers videos beside photos (play one with `<Video>`).
2251
+ */
2252
+ pick(options: { multiple: true; videos?: boolean }): Promise<CameraAsset[] | null>;
2253
+ pick(options?: { multiple?: false; videos?: boolean }): Promise<CameraAsset | null>;
2101
2254
  /** Clear `asset` and `error`, releasing the held asset. */
2102
2255
  reset(): void;
2103
2256
  }
@@ -2412,6 +2565,10 @@ export class NotificationError extends Error {
2412
2565
  | "FORBIDDEN"
2413
2566
  | "RECIPIENT_NOT_FOUND"
2414
2567
  | "RATE_LIMITED"
2568
+ | "RECORD_NOT_FOUND"
2569
+ | "INVALID_MUTED_UNTIL"
2570
+ | "INVALID_RECORD_REF"
2571
+ | "NOTIFICATIONS_UNAVAILABLE"
2415
2572
  | "INTERNAL"
2416
2573
  | string;
2417
2574
  constructor(
@@ -2904,6 +3061,35 @@ export const Gradient: (props: {
2904
3061
  children?: ReactNode;
2905
3062
  }) => any;
2906
3063
 
3064
+ /**
3065
+ * sc-10268 — plays a video file inline with the platform's own controls, the
3066
+ * first frame showing before playback. Web uses the browser's `<video>`;
3067
+ * native uses expo-video's VideoView. It fills the box `style` gives it.
3068
+ *
3069
+ * @example
3070
+ * <Video
3071
+ * source={file.url}
3072
+ * contentFit="cover"
3073
+ * style={{ width: 240, height: 180, borderRadius: theme.radii.md, overflow: "hidden" }}
3074
+ * onError={() => setPlayable(false)}
3075
+ * />
3076
+ */
3077
+ export const Video: (props: {
3078
+ /** A playable URL; null renders an empty box. */
3079
+ source: string | null;
3080
+ /** Play, seek and fullscreen controls. Defaults to true. */
3081
+ controls?: boolean;
3082
+ /** Starts playing muted, as browsers allow only muted autoplay. */
3083
+ autoPlay?: boolean;
3084
+ /** Letterboxed ("contain", the default) or cropped to fill ("cover"). */
3085
+ contentFit?: "contain" | "cover";
3086
+ style?: any;
3087
+ accessibilityLabel?: string;
3088
+ testID?: string;
3089
+ /** The file could not be played on this host. */
3090
+ onError?: () => void;
3091
+ }) => any;
3092
+
2907
3093
  /**
2908
3094
  * sc-6607 — screen-level overlay. Renders OUTSIDE the widget's layout box on
2909
3095
  * both hosts (web portals it to the document root, native uses the OS modal),
@@ -3054,9 +3240,14 @@ export const Badge: (props: {
3054
3240
  }) => any;
3055
3241
 
3056
3242
  /** Themed picture of a person, falling back to initials, then a glyph. */
3243
+ /** The stable colour `<Avatar seed>` paints for a seed, or null for none. */
3244
+ export function avatarColorFor(seed: string | null | undefined): string | null;
3245
+
3057
3246
  export const Avatar: (props: {
3058
3247
  /** Drives the initials and the accessibility label. */
3059
3248
  name?: string;
3249
+ /** A stable id (a user id): tints the monogram with that person's colour. */
3250
+ seed?: string;
3060
3251
  /** An absolute image URL. */
3061
3252
  url?: string;
3062
3253
  /** A size token, or a number of px (clamped to 16-160). */
@@ -3532,6 +3723,12 @@ export interface LintFinding {
3532
3723
  }
3533
3724
  export interface LintOptions {
3534
3725
  manifest?: { requestedScopes?: string[]; supportedPlatforms?: string[] };
3726
+ /**
3727
+ * File names in the package's `assets/` directory. When given, a
3728
+ * `useBundledAsset("name")` literal naming a file outside the list is a
3729
+ * `bundled-asset-missing` error; omit it to skip that rule.
3730
+ */
3731
+ assetFiles?: string[];
3535
3732
  }
3536
3733
  export function lintSource(
3537
3734
  source: string,
@@ -3541,6 +3738,40 @@ export function lintSource(
3541
3738
  findings: LintFinding[];
3542
3739
  };
3543
3740
 
3741
+ // Bundled assets — the files a package ships in its `assets/` directory.
3742
+ export type BundledAssetKind = "image" | "audio";
3743
+ export type BundledAssetErrorCode =
3744
+ | "ASSET_NAME_INVALID"
3745
+ | "ASSET_TYPE_NOT_ALLOWED"
3746
+ | "ASSET_EMPTY"
3747
+ | "ASSET_CONTENT_MISMATCH"
3748
+ | "ASSET_NAME_DUPLICATE"
3749
+ | "ASSET_TOO_MANY";
3750
+ export interface BundledAssetError {
3751
+ code: BundledAssetErrorCode;
3752
+ /** `null` for an error about the directory as a whole (`ASSET_TOO_MANY`). */
3753
+ name: string | null;
3754
+ message: string;
3755
+ }
3756
+ export interface BundledAsset {
3757
+ name: string;
3758
+ kind: BundledAssetKind;
3759
+ mimeType: string;
3760
+ sizeBytes: number;
3761
+ }
3762
+ /** The package directory bundled files live in (`"assets"`); from the `./bundled-assets` entry. */
3763
+ export const BUNDLED_ASSET_DIRECTORY: string;
3764
+ /** True when `name` may be used as a file name inside `assets/`. */
3765
+ export function isValidBundledAssetName(name: unknown): boolean;
3766
+ /** Identifies a bundled file from its leading bytes; `null` when it is not an allowed type. */
3767
+ export function sniffBundledAssetType(
3768
+ bytes: Uint8Array,
3769
+ ): { kind: BundledAssetKind; mimeType: string; extensions: string[] } | null;
3770
+ /** Validates the files of an `assets/` directory; `assets` is sorted by name. */
3771
+ export function validateBundledAssets(
3772
+ files: Array<{ name: string; bytes: Uint8Array }>,
3773
+ ): { ok: boolean; errors: BundledAssetError[]; assets: BundledAsset[] };
3774
+
3544
3775
  // --------------------------------------------------------------- CONTRACT
3545
3776
  //
3546
3777
  // Single-source-of-truth contract artefact. See docs/design/ai-widget-contract.md.
@@ -3595,6 +3826,17 @@ export interface AiWidgetContract {
3595
3826
  readonly manifestSchema: Readonly<Record<string, ContractManifestField>>;
3596
3827
  readonly manifestCategories: ReadonlyArray<string>;
3597
3828
  readonly manifestPlatforms: ReadonlyArray<string>;
3829
+ /** Largest package a widget may ship, bundled files included. */
3830
+ readonly packageMaxBytes: number;
3831
+ /** What a package may ship in its `assets/` directory. */
3832
+ readonly bundledAssets: {
3833
+ readonly directory: string;
3834
+ readonly maxFiles: number;
3835
+ readonly maxNameLength: number;
3836
+ readonly types: Readonly<
3837
+ Record<BundledAssetKind, { readonly extensions: ReadonlyArray<string> }>
3838
+ >;
3839
+ };
3598
3840
  readonly themeTokens: ThemeTokens;
3599
3841
  /** sc-7800 — the brand colour tokens a `type: "color"` field may bind to. */
3600
3842
  readonly themeColorTokens: ReadonlyArray<{ path: string; label: string }>;
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ export {
39
39
  useAgent,
40
40
  useAsset,
41
41
  useAssetsByTag,
42
+ useBundledAsset,
42
43
  useFilestoreFiles,
43
44
  useFilestoreFile,
44
45
  useFilestoreUpload,
@@ -46,6 +47,7 @@ export {
46
47
  useFilestoreFolders,
47
48
  useFilestoreCreateFolder,
48
49
  useFilestoreCopy,
50
+ useFilestoreDelete,
49
51
  useFileSignature,
50
52
  useFileSignatures,
51
53
  useFileRoster,
@@ -66,6 +68,8 @@ export {
66
68
  usePayments,
67
69
  useWorkspaceCurrency,
68
70
  useSendNotification,
71
+ useRecordMute,
72
+ useRecordMutes,
69
73
  useTheme,
70
74
  useWidgetStyle,
71
75
  useI18n,
@@ -78,6 +82,9 @@ export {
78
82
  useFill,
79
83
  useNavigation,
80
84
  useRouteParams,
85
+ usePageBadge,
86
+ useHardwareBack,
87
+ useFilePreview,
81
88
  usePageContext,
82
89
  useWidgetRoute,
83
90
  useChildRenderer,
@@ -160,8 +167,14 @@ export {
160
167
  DateTimePicker,
161
168
  FilePicker,
162
169
  Gradient,
170
+ Video,
163
171
  } from "./primitives.js";
164
172
  export { lintSource, bannedIdentifiers } from "./linter.js";
173
+ export {
174
+ validateBundledAssets,
175
+ sniffBundledAssetType,
176
+ isValidBundledAssetName,
177
+ } from "./bundled-assets.js";
165
178
  // sc-3696: the trailing six are the theme-derivation helpers both hosts resolve
166
179
  // `useTheme()` with (the Player through widgetTheme.js, the export through its
167
180
  // generated theme module), plus the gradient angle projection `<Gradient>`
@@ -215,6 +228,7 @@ export { List, ListRow, ListHeader, EmptyState } from "./list.js";
215
228
  // Themed detail parts; the stock Badge/Avatar/Progress/Stat/Divider wrap these.
216
229
  export { Badge, Avatar, Progress, Stat, Divider } from "./detail-parts.js";
217
230
  export { initialsFor } from "./initials.js";
231
+ export { avatarColorFor } from "./avatar-color.js";
218
232
  // The one card surface: the theme's `card` scope, a gradient fill, margin.
219
233
  export { Card } from "./card.js";
220
234
  export {
@@ -39,6 +39,7 @@ export {
39
39
  useAgent,
40
40
  useAsset,
41
41
  useAssetsByTag,
42
+ useBundledAsset,
42
43
  useFilestoreFiles,
43
44
  useFilestoreFile,
44
45
  useFilestoreUpload,
@@ -46,6 +47,7 @@ export {
46
47
  useFilestoreFolders,
47
48
  useFilestoreCreateFolder,
48
49
  useFilestoreCopy,
50
+ useFilestoreDelete,
49
51
  useFileSignature,
50
52
  useFileSignatures,
51
53
  useFileRoster,
@@ -66,6 +68,8 @@ export {
66
68
  usePayments,
67
69
  useWorkspaceCurrency,
68
70
  useSendNotification,
71
+ useRecordMute,
72
+ useRecordMutes,
69
73
  useTheme,
70
74
  useWidgetStyle,
71
75
  useI18n,
@@ -78,6 +82,9 @@ export {
78
82
  useFill,
79
83
  useNavigation,
80
84
  useRouteParams,
85
+ usePageBadge,
86
+ useHardwareBack,
87
+ useFilePreview,
81
88
  usePageContext,
82
89
  useWidgetRoute,
83
90
  useChildRenderer,
@@ -153,8 +160,14 @@ export {
153
160
  DateTimePicker,
154
161
  FilePicker,
155
162
  Gradient,
163
+ Video,
156
164
  } from "./primitives.native.js";
157
165
  export { lintSource, bannedIdentifiers } from "./linter.js";
166
+ export {
167
+ validateBundledAssets,
168
+ sniffBundledAssetType,
169
+ isValidBundledAssetName,
170
+ } from "./bundled-assets.js";
158
171
  // sc-3696: the trailing six are the theme-derivation helpers both hosts resolve
159
172
  // `useTheme()` with (the Player through widgetTheme.js, the export through its
160
173
  // generated theme module), plus the gradient angle projection `<Gradient>`
@@ -208,6 +221,7 @@ export { List, ListRow, ListHeader, EmptyState } from "./list.native.js";
208
221
  // Themed detail parts; the stock Badge/Avatar/Progress/Stat/Divider wrap these.
209
222
  export { Badge, Avatar, Progress, Stat, Divider } from "./detail-parts.native.js";
210
223
  export { initialsFor } from "./initials.js";
224
+ export { avatarColorFor } from "./avatar-color.js";
211
225
  // The one card surface: the theme's `card` scope, a gradient fill, margin.
212
226
  export { Card } from "./card.native.js";
213
227
  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", "Ada" → "A", " " → "". */
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
- if (parts.length === 1) return first.toUpperCase();
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/linter.cjs CHANGED
@@ -1082,6 +1082,33 @@ function _jsxOpenTagEnd(source, from) {
1082
1082
  return source.length;
1083
1083
  }
1084
1084
 
1085
+ // sc-10721 — a useBundledAsset literal must name a file the package ships.
1086
+ const BUNDLED_ASSET_CALL_RE =
1087
+ /\buseBundledAsset\s*\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|`([^`]*)`)\s*[,)]/g;
1088
+
1089
+ function _bundledAssetRules(source, assetFiles) {
1090
+ if (!Array.isArray(assetFiles)) return [];
1091
+ const findings = [];
1092
+ // Comments blanked, strings kept: the file name IS a string literal.
1093
+ const code = _stripNonCode(source, { keepStrings: true });
1094
+ const sourceLines = source.split(/\r?\n/);
1095
+ for (const match of code.matchAll(BUNDLED_ASSET_CALL_RE)) {
1096
+ const name = match[1] || match[2] || match[3] || "";
1097
+ if (name.includes("${") || assetFiles.includes(name)) continue;
1098
+ const line = code.slice(0, match.index).split(/\r?\n/).length;
1099
+ findings.push({
1100
+ rule: "bundled-asset-missing",
1101
+ severity: "error",
1102
+ label:
1103
+ `useBundledAsset("${name}") names a file that is not in the package's ` +
1104
+ "assets/ directory. Add the file there or correct the name.",
1105
+ line,
1106
+ snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
1107
+ });
1108
+ }
1109
+ return findings;
1110
+ }
1111
+
1085
1112
  // sc-4649 / sc-4686 — see linter.js for the rationale comments. The two
1086
1113
  // files must stay in lockstep (a test asserts behaviour-equivalence).
1087
1114
  const REQUEST_PAYMENT_CALL = "requestPayment(";
@@ -2015,6 +2042,7 @@ function lintSource(source, options) {
2015
2042
  findings.push(..._translationApiRules(source));
2016
2043
  findings.push(..._handBuiltPageUrlRules(source));
2017
2044
  findings.push(..._lucideIconRules(source));
2045
+ findings.push(..._bundledAssetRules(source, options && options.assetFiles));
2018
2046
  findings.push(..._selfContainedReferenceRules(source));
2019
2047
  findings.push(..._imagePercentHeightRules(source));
2020
2048
  // sc-7274 — soft warning: a percentage flexBasis sizes the main axis, so in a
package/dist/linter.js CHANGED
@@ -1081,6 +1081,33 @@ function _jsxOpenTagEnd(source, from) {
1081
1081
  return source.length;
1082
1082
  }
1083
1083
 
1084
+ // sc-10721 — a useBundledAsset literal must name a file the package ships.
1085
+ const BUNDLED_ASSET_CALL_RE =
1086
+ /\buseBundledAsset\s*\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|`([^`]*)`)\s*[,)]/g;
1087
+
1088
+ function _bundledAssetRules(source, assetFiles) {
1089
+ if (!Array.isArray(assetFiles)) return [];
1090
+ const findings = [];
1091
+ // Comments blanked, strings kept: the file name IS a string literal.
1092
+ const code = _stripNonCode(source, { keepStrings: true });
1093
+ const sourceLines = source.split(/\r?\n/);
1094
+ for (const match of code.matchAll(BUNDLED_ASSET_CALL_RE)) {
1095
+ const name = match[1] || match[2] || match[3] || "";
1096
+ if (name.includes("${") || assetFiles.includes(name)) continue;
1097
+ const line = code.slice(0, match.index).split(/\r?\n/).length;
1098
+ findings.push({
1099
+ rule: "bundled-asset-missing",
1100
+ severity: "error",
1101
+ label:
1102
+ `useBundledAsset("${name}") names a file that is not in the package's ` +
1103
+ "assets/ directory. Add the file there or correct the name.",
1104
+ line,
1105
+ snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
1106
+ });
1107
+ }
1108
+ return findings;
1109
+ }
1110
+
1084
1111
  // sc-4649 / sc-4686 — payment-currency.
1085
1112
  //
1086
1113
  // A widget must not name a currency AT ALL. Each workspace charges its app users
@@ -2080,6 +2107,7 @@ export function lintSource(source, options) {
2080
2107
  findings.push(..._translationApiRules(source));
2081
2108
  findings.push(..._handBuiltPageUrlRules(source));
2082
2109
  findings.push(..._lucideIconRules(source));
2110
+ findings.push(..._bundledAssetRules(source, options && options.assetFiles));
2083
2111
  // sc-2353 / sc-5897 — widget source must be self-contained: a referenced
2084
2112
  // `React` global or SDK export with no import throws at render.
2085
2113
  findings.push(..._selfContainedReferenceRules(source));
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
- const TRANSLATIONS_MAX_KEYS = 100;
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})?$/;
@@ -328,6 +329,7 @@ function validateManifest(m) {
328
329
  }
329
330
 
330
331
  module.exports = {
332
+ TRANSLATIONS_MAX_KEYS,
331
333
  validateManifest,
332
334
  validateManifestTranslations,
333
335
  canonicalCategory,
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
- const TRANSLATIONS_MAX_KEYS = 100;
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})?$/;
@@ -374,6 +375,7 @@ function validateManifest(m) {
374
375
  }
375
376
 
376
377
  export {
378
+ TRANSLATIONS_MAX_KEYS,
377
379
  validateManifest,
378
380
  validateManifestTranslations,
379
381
  canonicalCategory,
@@ -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 — react-native-web portals it to `document.body`,
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 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.
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. 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.
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(ReactNative);
15
+ export const Overlay = makeOverlay({
16
+ Modal: WebModal,
17
+ View: ReactNative.View,
18
+ Pressable: ReactNative.Pressable,
19
+ StyleSheet: ReactNative.StyleSheet,
20
+ });
@@ -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";