@colixsystems/widget-sdk 0.196.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 +61 -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/host.d.ts +15 -3
- 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/navigation.cjs +58 -25
- package/dist/navigation.js +58 -25
- 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/host.d.ts
CHANGED
|
@@ -254,14 +254,26 @@ export interface SidebarUnfoldItem {
|
|
|
254
254
|
[field: string]: unknown;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/** One section's local menu and the sidebar entry it hangs beneath. `inside`
|
|
258
|
+
* marks the section the visitor is in — the one that starts unfolded. */
|
|
259
|
+
export interface SidebarUnfoldTree {
|
|
260
|
+
under: string;
|
|
261
|
+
items: Array<SidebarUnfoldItem & { current: boolean }>;
|
|
262
|
+
standalone: boolean;
|
|
263
|
+
inside: boolean;
|
|
264
|
+
}
|
|
265
|
+
|
|
257
266
|
/** Where a page's `SIDEBAR` local menu unfolds (REQ-NAV-LOCAL, sc-7696).
|
|
258
|
-
* `under` is the global menu entry the
|
|
259
|
-
*
|
|
260
|
-
* list the page, so the page itself is the entry.
|
|
267
|
+
* `under` is the global menu entry the visitor's own section hangs beneath,
|
|
268
|
+
* or null when they are in none. `standalone` marks the case where the global
|
|
269
|
+
* menu does not list the page, so the page itself is the entry. `trees` is
|
|
270
|
+
* every section an entry carries, in menu order, so an entry can offer its
|
|
271
|
+
* menu from any page; a `standalone` tree comes last. */
|
|
261
272
|
export interface SidebarUnfold {
|
|
262
273
|
under: string | null;
|
|
263
274
|
items: Array<SidebarUnfoldItem & { current: boolean }>;
|
|
264
275
|
standalone: boolean;
|
|
276
|
+
trees: SidebarUnfoldTree[];
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
/** Resolve where the viewed page's local menu unfolds in the sidebar and the
|
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/navigation.cjs
CHANGED
|
@@ -636,21 +636,25 @@ function resolveTopBarTokens(theme) {
|
|
|
636
636
|
* inside it, so the menu is never lost — a section off the menu is exactly
|
|
637
637
|
* the case that would otherwise render blank.
|
|
638
638
|
*
|
|
639
|
+
* sc-10693: `trees` answers EVERY section, not only the visitor's own, so a
|
|
640
|
+
* section's entry can offer its menu from any page. `under` / `items` /
|
|
641
|
+
* `standalone` still name the one the visitor is inside.
|
|
642
|
+
*
|
|
639
643
|
* @param {{ entries?: unknown, active?: unknown, sections?: unknown }} input
|
|
640
644
|
* `entries` — the global menu's keys, in menu order.
|
|
641
645
|
* `active` — the key of the page being viewed.
|
|
642
646
|
* `sections` — `{ [key]: { menu, items } }` for pages whose local nav is
|
|
643
647
|
* `SIDEBAR`; `menu` identifies the saved menu, `items` carry their own key.
|
|
644
|
-
* @returns {{ under: string|null, items: Array<object>, standalone: boolean }}
|
|
645
|
-
* `under` is null when
|
|
646
|
-
* section's own page, which the host draws as the entry. Each
|
|
647
|
-
* returned with a `current` flag rather than leaving each host to
|
|
648
|
-
* recompute it.
|
|
648
|
+
* @returns {{ under: string|null, items: Array<object>, standalone: boolean, trees: Array<object> }}
|
|
649
|
+
* `under` is null when the visitor is in no section; when `standalone` it
|
|
650
|
+
* names the section's own page, which the host draws as the entry. Each
|
|
651
|
+
* item is returned with a `current` flag rather than leaving each host to
|
|
652
|
+
* recompute it. `trees` lists `{ under, items, standalone, inside }` in menu
|
|
653
|
+
* order, a `standalone` one last; `inside` marks the visitor's own.
|
|
649
654
|
*/
|
|
650
655
|
function resolveSidebarUnfold(input) {
|
|
651
656
|
const { entries, active, sections } = isPlainObject(input) ? input : {};
|
|
652
|
-
const
|
|
653
|
-
if (typeof active !== "string" || !active) return nothing;
|
|
657
|
+
const viewed = typeof active === "string" && active ? active : null;
|
|
654
658
|
const bank = isPlainObject(sections) ? sections : {};
|
|
655
659
|
const order = Array.isArray(entries) ? entries : [];
|
|
656
660
|
const sectionAt = (key) => {
|
|
@@ -663,31 +667,60 @@ function resolveSidebarUnfold(input) {
|
|
|
663
667
|
return (
|
|
664
668
|
Boolean(candidate) &&
|
|
665
669
|
candidate.items.some(
|
|
666
|
-
(item) => isPlainObject(item) && item.key ===
|
|
670
|
+
(item) => isPlainObject(item) && item.key === viewed,
|
|
667
671
|
)
|
|
668
672
|
);
|
|
669
673
|
};
|
|
670
674
|
// The page's own section wins; otherwise the section whose menu lists it,
|
|
671
675
|
// preferring one a global entry carries so the tree hangs where it always
|
|
672
676
|
// does. Both are the same section from the visitor's side.
|
|
673
|
-
const section =
|
|
674
|
-
sectionAt(
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
677
|
+
const section = viewed
|
|
678
|
+
? sectionAt(viewed) ||
|
|
679
|
+
sectionAt(order.find(lists)) ||
|
|
680
|
+
sectionAt(Object.keys(bank).find(lists))
|
|
681
|
+
: null;
|
|
682
|
+
const mark = (items) =>
|
|
683
|
+
items.map((item) => ({
|
|
684
|
+
...(isPlainObject(item) ? item : {}),
|
|
685
|
+
current:
|
|
686
|
+
viewed !== null &&
|
|
687
|
+
isPlainObject(item) &&
|
|
688
|
+
typeof item.key === "string" &&
|
|
689
|
+
item.key === viewed,
|
|
690
|
+
}));
|
|
691
|
+
// A saved menu hangs under the FIRST global entry carrying it, so two
|
|
692
|
+
// entries sharing one menu never draw it twice.
|
|
693
|
+
const claimed = [];
|
|
694
|
+
const trees = [];
|
|
695
|
+
for (const key of order) {
|
|
696
|
+
if (!isPlainObject(bank[key])) continue;
|
|
697
|
+
const { menu } = bank[key];
|
|
698
|
+
if (claimed.includes(menu)) continue;
|
|
699
|
+
claimed.push(menu);
|
|
700
|
+
// An unusable key still owns its menu: nothing may hang it elsewhere.
|
|
701
|
+
if (typeof key !== "string" || !key) continue;
|
|
702
|
+
const inside = Boolean(section) && menu === section.menu;
|
|
703
|
+
const own = inside ? section : sectionAt(key);
|
|
704
|
+
if (!own) continue;
|
|
705
|
+
trees.push({ under: key, items: mark(own.items), standalone: false, inside });
|
|
706
|
+
}
|
|
707
|
+
if (!section) return { under: null, items: [], standalone: false, trees };
|
|
708
|
+
const owned = trees.find((tree) => tree.inside);
|
|
709
|
+
if (owned) {
|
|
710
|
+
return { under: owned.under, items: owned.items, standalone: false, trees };
|
|
689
711
|
}
|
|
690
|
-
|
|
712
|
+
const alone = {
|
|
713
|
+
under: section.key,
|
|
714
|
+
items: mark(section.items),
|
|
715
|
+
standalone: true,
|
|
716
|
+
inside: true,
|
|
717
|
+
};
|
|
718
|
+
return {
|
|
719
|
+
under: alone.under,
|
|
720
|
+
items: alone.items,
|
|
721
|
+
standalone: true,
|
|
722
|
+
trees: [...trees, alone],
|
|
723
|
+
};
|
|
691
724
|
}
|
|
692
725
|
|
|
693
726
|
/**
|
package/dist/navigation.js
CHANGED
|
@@ -627,21 +627,25 @@ export function resolveTopBarTokens(theme) {
|
|
|
627
627
|
* inside it, so the menu is never lost — a section off the menu is exactly
|
|
628
628
|
* the case that would otherwise render blank.
|
|
629
629
|
*
|
|
630
|
+
* sc-10693: `trees` answers EVERY section, not only the visitor's own, so a
|
|
631
|
+
* section's entry can offer its menu from any page. `under` / `items` /
|
|
632
|
+
* `standalone` still name the one the visitor is inside.
|
|
633
|
+
*
|
|
630
634
|
* @param {{ entries?: unknown, active?: unknown, sections?: unknown }} input
|
|
631
635
|
* `entries` — the global menu's keys, in menu order.
|
|
632
636
|
* `active` — the key of the page being viewed.
|
|
633
637
|
* `sections` — `{ [key]: { menu, items } }` for pages whose local nav is
|
|
634
638
|
* `SIDEBAR`; `menu` identifies the saved menu, `items` carry their own key.
|
|
635
|
-
* @returns {{ under: string|null, items: Array<object>, standalone: boolean }}
|
|
636
|
-
* `under` is null when
|
|
637
|
-
* section's own page, which the host draws as the entry. Each
|
|
638
|
-
* returned with a `current` flag rather than leaving each host to
|
|
639
|
-
* recompute it.
|
|
639
|
+
* @returns {{ under: string|null, items: Array<object>, standalone: boolean, trees: Array<object> }}
|
|
640
|
+
* `under` is null when the visitor is in no section; when `standalone` it
|
|
641
|
+
* names the section's own page, which the host draws as the entry. Each
|
|
642
|
+
* item is returned with a `current` flag rather than leaving each host to
|
|
643
|
+
* recompute it. `trees` lists `{ under, items, standalone, inside }` in menu
|
|
644
|
+
* order, a `standalone` one last; `inside` marks the visitor's own.
|
|
640
645
|
*/
|
|
641
646
|
export function resolveSidebarUnfold(input) {
|
|
642
647
|
const { entries, active, sections } = isPlainObject(input) ? input : {};
|
|
643
|
-
const
|
|
644
|
-
if (typeof active !== "string" || !active) return nothing;
|
|
648
|
+
const viewed = typeof active === "string" && active ? active : null;
|
|
645
649
|
const bank = isPlainObject(sections) ? sections : {};
|
|
646
650
|
const order = Array.isArray(entries) ? entries : [];
|
|
647
651
|
const sectionAt = (key) => {
|
|
@@ -654,31 +658,60 @@ export function resolveSidebarUnfold(input) {
|
|
|
654
658
|
return (
|
|
655
659
|
Boolean(candidate) &&
|
|
656
660
|
candidate.items.some(
|
|
657
|
-
(item) => isPlainObject(item) && item.key ===
|
|
661
|
+
(item) => isPlainObject(item) && item.key === viewed,
|
|
658
662
|
)
|
|
659
663
|
);
|
|
660
664
|
};
|
|
661
665
|
// The page's own section wins; otherwise the section whose menu lists it,
|
|
662
666
|
// preferring one a global entry carries so the tree hangs where it always
|
|
663
667
|
// does. Both are the same section from the visitor's side.
|
|
664
|
-
const section =
|
|
665
|
-
sectionAt(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
668
|
+
const section = viewed
|
|
669
|
+
? sectionAt(viewed) ||
|
|
670
|
+
sectionAt(order.find(lists)) ||
|
|
671
|
+
sectionAt(Object.keys(bank).find(lists))
|
|
672
|
+
: null;
|
|
673
|
+
const mark = (items) =>
|
|
674
|
+
items.map((item) => ({
|
|
675
|
+
...(isPlainObject(item) ? item : {}),
|
|
676
|
+
current:
|
|
677
|
+
viewed !== null &&
|
|
678
|
+
isPlainObject(item) &&
|
|
679
|
+
typeof item.key === "string" &&
|
|
680
|
+
item.key === viewed,
|
|
681
|
+
}));
|
|
682
|
+
// A saved menu hangs under the FIRST global entry carrying it, so two
|
|
683
|
+
// entries sharing one menu never draw it twice.
|
|
684
|
+
const claimed = [];
|
|
685
|
+
const trees = [];
|
|
686
|
+
for (const key of order) {
|
|
687
|
+
if (!isPlainObject(bank[key])) continue;
|
|
688
|
+
const { menu } = bank[key];
|
|
689
|
+
if (claimed.includes(menu)) continue;
|
|
690
|
+
claimed.push(menu);
|
|
691
|
+
// An unusable key still owns its menu: nothing may hang it elsewhere.
|
|
692
|
+
if (typeof key !== "string" || !key) continue;
|
|
693
|
+
const inside = Boolean(section) && menu === section.menu;
|
|
694
|
+
const own = inside ? section : sectionAt(key);
|
|
695
|
+
if (!own) continue;
|
|
696
|
+
trees.push({ under: key, items: mark(own.items), standalone: false, inside });
|
|
697
|
+
}
|
|
698
|
+
if (!section) return { under: null, items: [], standalone: false, trees };
|
|
699
|
+
const owned = trees.find((tree) => tree.inside);
|
|
700
|
+
if (owned) {
|
|
701
|
+
return { under: owned.under, items: owned.items, standalone: false, trees };
|
|
680
702
|
}
|
|
681
|
-
|
|
703
|
+
const alone = {
|
|
704
|
+
under: section.key,
|
|
705
|
+
items: mark(section.items),
|
|
706
|
+
standalone: true,
|
|
707
|
+
inside: true,
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
under: alone.under,
|
|
711
|
+
items: alone.items,
|
|
712
|
+
standalone: true,
|
|
713
|
+
trees: [...trees, alone],
|
|
714
|
+
};
|
|
682
715
|
}
|
|
683
716
|
|
|
684
717
|
/**
|