@aglyn/aglyn 1.0.0-beta.277 → 1.0.0-beta.282

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.
Files changed (82) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +146 -11
  3. package/src/lib/app-utils/account-preferences.js +266 -18
  4. package/src/lib/app-utils/account-preferences.js.map +1 -1
  5. package/src/lib/app-utils/activity-presenter.js +2 -26
  6. package/src/lib/app-utils/activity-presenter.js.map +1 -1
  7. package/src/lib/app-utils/activity-type-labels.d.ts +18 -0
  8. package/src/lib/app-utils/activity-type-labels.js +46 -0
  9. package/src/lib/app-utils/activity-type-labels.js.map +1 -0
  10. package/src/lib/app-utils/artifact-provenance.d.ts +3 -1
  11. package/src/lib/app-utils/artifact-provenance.js.map +1 -1
  12. package/src/lib/app-utils/bundle-intent.d.ts +37 -0
  13. package/src/lib/app-utils/bundle-intent.js +47 -3
  14. package/src/lib/app-utils/bundle-intent.js.map +1 -1
  15. package/src/lib/app-utils/compose-reusable-components.d.ts +17 -1
  16. package/src/lib/app-utils/compose-reusable-components.js +80 -5
  17. package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
  18. package/src/lib/app-utils/docs-help-sections.generated.js +13 -1
  19. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  20. package/src/lib/app-utils/docs-help.generated.d.ts +23 -11
  21. package/src/lib/app-utils/docs-help.generated.js +64 -7
  22. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  23. package/src/lib/app-utils/docs-index.generated.js +357 -49
  24. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  25. package/src/lib/app-utils/health-report.d.ts +3 -2
  26. package/src/lib/app-utils/health-report.js +22 -2
  27. package/src/lib/app-utils/health-report.js.map +1 -1
  28. package/src/lib/app-utils/native-nav.d.ts +39 -4
  29. package/src/lib/app-utils/native-nav.js +69 -3
  30. package/src/lib/app-utils/native-nav.js.map +1 -1
  31. package/src/lib/app-utils/native-themes.d.ts +28 -17
  32. package/src/lib/app-utils/native-themes.js +123 -45
  33. package/src/lib/app-utils/native-themes.js.map +1 -1
  34. package/src/lib/app-utils/operator-alerts.js +4 -2
  35. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  36. package/src/lib/app-utils/org-permission-editor.d.ts +75 -0
  37. package/src/lib/app-utils/org-permission-editor.js +124 -0
  38. package/src/lib/app-utils/org-permission-editor.js.map +1 -0
  39. package/src/lib/app-utils/org-permissions.d.ts +36 -0
  40. package/src/lib/app-utils/org-permissions.js +78 -26
  41. package/src/lib/app-utils/org-permissions.js.map +1 -1
  42. package/src/lib/app-utils/org-role-grants.d.ts +59 -9
  43. package/src/lib/app-utils/org-role-grants.js +97 -10
  44. package/src/lib/app-utils/org-role-grants.js.map +1 -1
  45. package/src/lib/app-utils/plan-entitlements.d.ts +13 -0
  46. package/src/lib/app-utils/plan-entitlements.js +22 -0
  47. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  48. package/src/lib/app-utils/release-flags.d.ts +52 -1
  49. package/src/lib/app-utils/release-flags.js +68 -2
  50. package/src/lib/app-utils/release-flags.js.map +1 -1
  51. package/src/lib/app-utils/reusable-component-keys.d.ts +13 -0
  52. package/src/lib/app-utils/reusable-component-keys.js +12 -0
  53. package/src/lib/app-utils/reusable-component-keys.js.map +1 -1
  54. package/src/lib/app-utils/screen-analytics-aggregate.d.ts +34 -0
  55. package/src/lib/app-utils/screen-analytics-aggregate.js +37 -0
  56. package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -1
  57. package/src/lib/app-utils/site-care.d.ts +35 -0
  58. package/src/lib/app-utils/site-care.js +48 -0
  59. package/src/lib/app-utils/site-care.js.map +1 -1
  60. package/src/lib/app-utils/site-font-pairings.d.ts +59 -0
  61. package/src/lib/app-utils/site-font-pairings.js +514 -0
  62. package/src/lib/app-utils/site-font-pairings.js.map +1 -0
  63. package/src/lib/app-utils/site-starter.d.ts +136 -0
  64. package/src/lib/app-utils/site-starter.js +116 -0
  65. package/src/lib/app-utils/site-starter.js.map +1 -0
  66. package/src/lib/plugin-manager/feature-plugins.d.ts +32 -0
  67. package/src/lib/plugin-manager/feature-plugins.js +12 -0
  68. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  69. package/src/lib/plugin-manager/first-party-plugins.generated.js +27 -1
  70. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  71. package/src/lib/plugin-manager/plugin-entitlements.d.ts +37 -0
  72. package/src/lib/plugin-manager/plugin-entitlements.js +84 -2
  73. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  74. package/src/lib/plugin-manager/plugin-permissions.d.ts +7 -0
  75. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  76. package/src/lib/plugin-manager/plugin-product-catalog.d.ts +12 -0
  77. package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -1
  78. package/src/lib/plugin-manager/plugin-site-starters.d.ts +73 -0
  79. package/src/lib/plugin-manager/plugin-site-starters.js +30 -0
  80. package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -0
  81. package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +8 -1
  82. package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/aglyn",
3
- "version": "1.0.0-beta.277",
3
+ "version": "1.0.0-beta.282",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -37,16 +37,16 @@
37
37
  "./package.json": "./package.json"
38
38
  },
39
39
  "dependencies": {
40
- "@aglyn/shared-data-enums": "1.0.0-beta.277",
41
- "@aglyn/shared-data-mdi": "1.0.0-beta.277",
42
- "@aglyn/shared-data-types": "1.0.0-beta.277",
43
- "@aglyn/shared-util-email": "1.0.0-beta.277",
44
- "@aglyn/shared-util-first-touch": "1.0.0-beta.277",
45
- "@aglyn/shared-util-http": "1.0.0-beta.277",
46
- "@aglyn/shared-util-logger": "1.0.0-beta.277",
47
- "@aglyn/shared-util-timestamp": "1.0.0-beta.277",
48
- "@aglyn/shared-util-tools": "1.0.0-beta.277",
49
- "@aglyn/shared-util-vendor": "1.0.0-beta.277",
40
+ "@aglyn/shared-data-enums": "1.0.0-beta.282",
41
+ "@aglyn/shared-data-mdi": "1.0.0-beta.282",
42
+ "@aglyn/shared-data-types": "1.0.0-beta.282",
43
+ "@aglyn/shared-util-email": "1.0.0-beta.282",
44
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.282",
45
+ "@aglyn/shared-util-http": "1.0.0-beta.282",
46
+ "@aglyn/shared-util-logger": "1.0.0-beta.282",
47
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.282",
48
+ "@aglyn/shared-util-tools": "1.0.0-beta.282",
49
+ "@aglyn/shared-util-vendor": "1.0.0-beta.282",
50
50
  "@data-driven-forms/react-form-renderer": "^4.2.0",
51
51
  "@msgpack/msgpack": "^3.1.3",
52
52
  "@types/unist": "^3.0.3",
@@ -29,6 +29,7 @@
29
29
  * | `recents` | {@link RecentItem}s, most recently opened first | 30 |
30
30
  * | `workspaceColors` | workspace id → `#RRGGBB` the person picked | 100 |
31
31
  * | `appTheme` | {@link AppThemePrefs}, the apps' look, synced | — |
32
+ * | `tabBars` | {@link TabBarPins}, the phone's tab bar per site and per workspace | 4 areas each, 100 in all |
32
33
  *
33
34
  * The caps are held twice: here, by every writer, and by the Firestore rules,
34
35
  * which refuse a write that grows a list past its cap. A list that is ALREADY
@@ -53,8 +54,6 @@ export declare const RECENT_ORGS_FIELD = "recentOrgs";
53
54
  export declare const RECENT_ORGS_MAX = 8;
54
55
  /** How many recent workspaces the rail shows below the pins. */
55
56
  export declare const RAIL_RECENT_ORGS = 3;
56
- /** An account with this many workspaces or fewer sees them all on the rail. */
57
- export declare const RAIL_ALL_WORKSPACES_AT_MOST = 8;
58
57
  /** `users/{uid}` field holding the person's starred screens and records. */
59
58
  export declare const STARRED_FIELD = "starred";
60
59
  /** The most items a person may star. */
@@ -125,6 +124,10 @@ export interface AppThemePrefs {
125
124
  density: AppDensity;
126
125
  /** `system` follows Dynamic Type or the font scale; the rest are the Mac and Windows sizes. */
127
126
  textSize: AppTextSize;
127
+ /**
128
+ * `plain` keeps the sidebar on the theme's surface; `tinted` lays the THEME's accent over it at 5%, on a theme other
129
+ * than Aglyn only (Aglyn's sidebar is always neutral). A workspace's color never tints it: that is its rail tile's.
130
+ */
128
131
  sidebarStyle: AppSidebarStyle;
129
132
  /** The app icon's id: `aglyn`, or one of the alternates the apps ship. */
130
133
  appIcon: string;
@@ -242,21 +245,34 @@ export interface RailWorkspaces {
242
245
  pinned: string[];
243
246
  /** Below the divider: recently opened and not pinned, at most {@link RAIL_RECENT_ORGS}. */
244
247
  recent: string[];
245
- /** Every other workspace, for an account small enough to list them all. */
248
+ /**
249
+ * Workspaces listed on the rail beyond the pins and the recents. Always
250
+ * empty: every account reaches the rest through the All tile
251
+ * (AGL-3996). The field stays so the apps that read it need no change.
252
+ */
246
253
  rest: string[];
247
- /** Whether the last tile is "All workspaces", opening the searchable switcher. */
254
+ /** Whether the last tile is "All workspaces", opening the searchable switcher: whenever there is a workspace. */
248
255
  allTile: boolean;
249
256
  /** How many workspaces the person belongs to, shown under the All tile. */
250
257
  total: number;
251
258
  }
252
259
  /**
253
- * The rail for one account: up to five pinned, a divider, up to three recent,
254
- * then an "All workspaces (N)" tile. An account with
255
- * {@link RAIL_ALL_WORKSPACES_AT_MOST} workspaces or fewer sees them all and
256
- * no All tile, since there is nothing to hide. `orgIds` is every membership,
257
- * in the switcher's order; an id the person no longer belongs to is ignored.
260
+ * The rail for one account, the same for every account however few
261
+ * workspaces it has (Figma Rail 12:338, "Many workspaces"): up to five
262
+ * pinned, a divider, up to three recent under "Recent", a divider, then the
263
+ * "All workspaces" tile with the total, which opens the switcher where pins
264
+ * are edited. A small account that saw every tile in one unlabeled column
265
+ * had no way into the switcher and no Recent label or divider. `orgIds` is every membership, in the switcher's order;
266
+ * an id the person no longer belongs to is ignored.
258
267
  */
259
268
  export declare function railWorkspaces(orgIds: readonly string[], pinned: readonly string[], recent: readonly string[]): RailWorkspaces;
269
+ /**
270
+ * The one letter a workspace tile without a logo shows (Figma Rail/Workspace
271
+ * tile, Type=Initial): the first letter or digit of its name, upper-cased;
272
+ * "?" for a name with neither. One letter, not two: the tile's color, picked
273
+ * per workspace, tells two workspaces with the same letter apart.
274
+ */
275
+ export declare function workspaceInitial(name: string): string;
260
276
  /** One item's identity: the same screen or record in the same place. */
261
277
  export declare function accountItemKey(item: Pick<AccountItemRef, 'kind' | 'id' | 'orgId' | 'hostId'>): string;
262
278
  /** The starred items as stored, each well formed, without repeats, never more than the cap. */
@@ -277,7 +293,25 @@ export declare function readRecents(profile: unknown): RecentItem[];
277
293
  export declare function recordRecent(recents: readonly RecentItem[], item: AccountItemRef, nowMs: number): RecentItem[];
278
294
  /** The colors the person picked, by workspace id, the valid ones only. */
279
295
  export declare function readWorkspaceColors(profile: unknown): Record<string, string>;
280
- export type WorkspaceColorSource = 'picked' | 'brand' | 'logo' | 'none';
296
+ /**
297
+ * The colors a workspace's tile takes when nothing else gives it one
298
+ * ({@link workspaceStableColor}): the console's blue for text on a light
299
+ * ground (primary.dark), deep tones of the brand palette's
300
+ * violet and pink, and a rose, orange, gold, green and teal beside them,
301
+ * each dark enough that the tile's white initial reads at 4.5:1 or more. The
302
+ * slate is left out: it is the tile of a workspace with no id at all.
303
+ */
304
+ export declare const WORKSPACE_STABLE_COLORS: readonly string[];
305
+ /**
306
+ * The color a workspace with no picked, brand or logo color shows on its
307
+ * tile: one of {@link WORKSPACE_STABLE_COLORS}, chosen by its id, so the same
308
+ * workspace has the same color on every device and in every app, and two
309
+ * workspaces with the same initial usually differ. The id is hashed with
310
+ * 32-bit FNV-1a over its UTF-16 code units, which each platform computes the
311
+ * same way. Null for an empty id.
312
+ */
313
+ export declare function workspaceStableColor(orgId: unknown): string | null;
314
+ export type WorkspaceColorSource = 'picked' | 'brand' | 'logo' | 'stable' | 'none';
281
315
  export interface WorkspaceColor {
282
316
  /** `#RRGGBB`, or null when nothing gives the workspace a color. */
283
317
  color: string | null;
@@ -292,7 +326,108 @@ export interface WorkspaceColor {
292
326
  * it while you are in the workspace unless the person turned "Use each
293
327
  * workspace's brand" off, which stops the brand and logo colors tinting the
294
328
  * app (a picked color still does: the person chose it).
329
+ *
330
+ * Given the workspace's id, a workspace with none of the three still gets a
331
+ * color: its stable one ({@link workspaceStableColor}). That one colors only
332
+ * its tile (the rail, the pickers, the menus, the sidebar's header), never
333
+ * the app.
295
334
  */
296
- export declare function resolveWorkspaceColor(picked: unknown, brandColor: unknown, logoColor: unknown, useWorkspaceBrand: boolean): WorkspaceColor;
335
+ export declare function resolveWorkspaceColor(picked: unknown, brandColor: unknown, logoColor: unknown, useWorkspaceBrand: boolean, orgId?: unknown): WorkspaceColor;
297
336
  /** The synced theme, every field filled: a missing or unknown value reads as the default. */
298
337
  export declare function readAppTheme(profile: unknown): AppThemePrefs;
338
+ /** `users/{uid}` field holding the areas the person keeps in the phone's tab bar, per site and per workspace. */
339
+ export declare const TAB_BARS_FIELD = "tabBars";
340
+ /**
341
+ * The most areas one tab bar holds beside Home: iOS and
342
+ * Material take five destinations at most, and nothing ever overflows into a
343
+ * More tab. With four, Create leaves the bar for the top bar.
344
+ */
345
+ export declare const TAB_BAR_AREAS_MAX = 4;
346
+ /** The most sites and workspaces the account keeps a tab bar for, the bound the Firestore rules hold. */
347
+ export declare const TAB_BARS_MAX = 100;
348
+ /** The tab bar's own slots beside the areas: Home, always first, and Create new. */
349
+ export declare const TAB_BAR_HOME = "home";
350
+ export declare const TAB_BAR_CREATE = "create";
351
+ /** One site's or workspace's tab bar. */
352
+ export interface TabBarPins {
353
+ /** `site:<hostId>` or `org:<orgId>` ({@link tabBarScope}). */
354
+ scope: string;
355
+ /** Nav manifest row ids, in the person's order, at most {@link TAB_BAR_AREAS_MAX}. */
356
+ areas: string[];
357
+ /**
358
+ * The person pinned, unpinned or reordered here by hand. Until then the
359
+ * first areas they open take the free spots and stay (the default the
360
+ * orchestrator settled on 2026-10-20: nothing guessed, nothing rotating);
361
+ * after it, only the person changes the bar.
362
+ */
363
+ chosen: boolean;
364
+ }
365
+ export type TabPinOutcome = 'pinned' | 'alreadyPinned' | 'full';
366
+ export interface TabPinResult {
367
+ /** Every stored tab bar after the request: unchanged unless `outcome` is `pinned`. */
368
+ tabBars: TabBarPins[];
369
+ outcome: TabPinOutcome;
370
+ }
371
+ /** What a phone's tab bar shows, in order, and where Create new goes. */
372
+ export interface TabBarLayout {
373
+ /** {@link TAB_BAR_HOME}, the areas, and {@link TAB_BAR_CREATE} in the centre unless it moved to the top bar. */
374
+ slots: string[];
375
+ /** Four areas fill the bar, so Create new sits in the top bar beside the AI button. */
376
+ createInTopBar: boolean;
377
+ }
378
+ /** The key of one site's tab bar, or the whole workspace's. */
379
+ export declare function tabBarScope(orgId: string, hostId?: string | null): string;
380
+ /**
381
+ * Every tab bar the person keeps, each well formed, without repeats. Anything
382
+ * that is not a `{ scope, areas, chosen }` reads as no tab bar, so a stray
383
+ * value can never take the phone's shell down.
384
+ */
385
+ export declare function readTabBars(profile: unknown): TabBarPins[];
386
+ /** One scope's areas, in the person's order. */
387
+ export declare function tabBarAreas(tabBars: readonly TabBarPins[], scope: string): string[];
388
+ /**
389
+ * Pins an area at the end of a scope's tab bar (long-press a tile, "Add to tab
390
+ * bar"), or says the bar is full: a full bar is never made room in quietly.
391
+ */
392
+ export declare function pinTabArea(tabBars: readonly TabBarPins[], scope: string, area: string): TabPinResult;
393
+ /** A scope's tab bar without one area; the person chose, so opened areas no longer fill the spot. */
394
+ export declare function unpinTabArea(tabBars: readonly TabBarPins[], scope: string, area: string): TabBarPins[];
395
+ /** A scope's tab bar with one area moved left (`-1`) or right (`1`) a place; unchanged at an end. */
396
+ export declare function moveTabArea(tabBars: readonly TabBarPins[], scope: string, area: string, offset: number): TabBarPins[];
397
+ /** A scope's tab bar as the Tab bar sheet left it: these areas, in this order, at most four. */
398
+ export declare function setTabAreas(tabBars: readonly TabBarPins[], scope: string, areas: readonly string[]): TabBarPins[];
399
+ /**
400
+ * The default before anyone pins: an area the person opens takes a free spot
401
+ * and stays there, until the bar is full or the person changes it by hand.
402
+ * Unchanged once the scope is `chosen`, the area is in the bar already, or the
403
+ * bar is full.
404
+ */
405
+ export declare function autoPinTabArea(tabBars: readonly TabBarPins[], scope: string, area: string): TabBarPins[];
406
+ /**
407
+ * The tab bar to draw: Home first, then the pinned areas the scope can open
408
+ * (`available`, the Home tiles' ids in this build), with Create new in the
409
+ * centre slot. Four areas fill the bar, and Create moves to the top bar.
410
+ */
411
+ export declare function tabBarLayout(areas: readonly string[], available: readonly string[]): TabBarLayout;
412
+ /**
413
+ * `users/{uid}` field holding the person's own picture (AGL-1961). Lowercase
414
+ * `url`: the roster's `photoURL` spelling reads as nothing here.
415
+ */
416
+ export declare const ACCOUNT_PHOTO_FIELD = "photoUrl";
417
+ /**
418
+ * The person's own picture, the same answer in the console and both apps
419
+ * (AGL-4010): `users/{uid}.photoUrl` first (the profile they edit, filled from
420
+ * the identity provider on every sign-in), else the sign-in record's
421
+ * `photoURL`, else null, so the caller draws initials. A blank value is never
422
+ * a picture, so a photo in either place wins over an empty one in the other.
423
+ * A library pick is stored as a `/api/media/cdn/...` path; resolving it to an
424
+ * address is the caller's (`mediaPreviewURL` on native).
425
+ */
426
+ export declare function readAccountPhoto(profile: unknown, authPhotoUrl: unknown): string | null;
427
+ /**
428
+ * The person's own name, the same answer in the console and both apps
429
+ * (AGL-2486, AGL-4010): the profile's first and last name, else the sign-in
430
+ * record's display name (empty for an SSO account), else the email address,
431
+ * else empty.
432
+ */
433
+ export declare function readAccountName(profile: unknown, authDisplayName: unknown, email: unknown): string;
@@ -29,6 +29,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
29
29
  * | `recents` | {@link RecentItem}s, most recently opened first | 30 |
30
30
  * | `workspaceColors` | workspace id → `#RRGGBB` the person picked | 100 |
31
31
  * | `appTheme` | {@link AppThemePrefs}, the apps' look, synced | — |
32
+ * | `tabBars` | {@link TabBarPins}, the phone's tab bar per site and per workspace | 4 areas each, 100 in all |
32
33
  *
33
34
  * The caps are held twice: here, by every writer, and by the Firestore rules,
34
35
  * which refuse a write that grows a list past its cap. A list that is ALREADY
@@ -47,7 +48,6 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
47
48
  /** `users/{uid}` field holding the workspaces opened most recently, newest first. */ export const RECENT_ORGS_FIELD = 'recentOrgs';
48
49
  /** How many recently opened workspaces are kept. */ export const RECENT_ORGS_MAX = 8;
49
50
  /** How many recent workspaces the rail shows below the pins. */ export const RAIL_RECENT_ORGS = 3;
50
- /** An account with this many workspaces or fewer sees them all on the rail. */ export const RAIL_ALL_WORKSPACES_AT_MOST = 8;
51
51
  /** `users/{uid}` field holding the person's starred screens and records. */ export const STARRED_FIELD = 'starred';
52
52
  /** The most items a person may star. */ export const STARRED_MAX = 50;
53
53
  /** `users/{uid}` field holding the screens and records opened most recently. */ export const RECENTS_FIELD = 'recents';
@@ -73,7 +73,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
73
73
  accent: 'theme',
74
74
  density: 'comfortable',
75
75
  textSize: 'system',
76
- sidebarStyle: 'tinted',
76
+ // Plain: a sidebar keeps the theme's neutral ground unless the person picks Tinted (AGL-3990).
77
+ sidebarStyle: 'plain',
77
78
  appIcon: 'aglyn',
78
79
  useWorkspaceBrand: true
79
80
  };
@@ -299,33 +300,35 @@ function pinnedHostList(value) {
299
300
  ].slice(0, RECENT_ORGS_MAX);
300
301
  }
301
302
  /**
302
- * The rail for one account: up to five pinned, a divider, up to three recent,
303
- * then an "All workspaces (N)" tile. An account with
304
- * {@link RAIL_ALL_WORKSPACES_AT_MOST} workspaces or fewer sees them all and
305
- * no All tile, since there is nothing to hide. `orgIds` is every membership,
306
- * in the switcher's order; an id the person no longer belongs to is ignored.
303
+ * The rail for one account, the same for every account however few
304
+ * workspaces it has (Figma Rail 12:338, "Many workspaces"): up to five
305
+ * pinned, a divider, up to three recent under "Recent", a divider, then the
306
+ * "All workspaces" tile with the total, which opens the switcher where pins
307
+ * are edited. A small account that saw every tile in one unlabeled column
308
+ * had no way into the switcher and no Recent label or divider. `orgIds` is every membership, in the switcher's order;
309
+ * an id the person no longer belongs to is ignored.
307
310
  */ export function railWorkspaces(orgIds, pinned, recent) {
308
311
  const members = idList(orgIds);
309
312
  const belongs = new Set(members);
310
313
  const pins = effectivePinnedOrgs(pinned).filter((id)=>belongs.has(id));
311
314
  const pinnedSet = new Set(pins);
312
- if (members.length <= RAIL_ALL_WORKSPACES_AT_MOST) {
313
- return {
314
- pinned: pins,
315
- recent: [],
316
- rest: members.filter((id)=>!pinnedSet.has(id)),
317
- allTile: false,
318
- total: members.length
319
- };
320
- }
321
315
  return {
322
316
  pinned: pins,
323
317
  recent: idList(recent).filter((id)=>belongs.has(id) && !pinnedSet.has(id)).slice(0, RAIL_RECENT_ORGS),
324
318
  rest: [],
325
- allTile: true,
319
+ allTile: members.length > 0,
326
320
  total: members.length
327
321
  };
328
322
  }
323
+ /**
324
+ * The one letter a workspace tile without a logo shows (Figma Rail/Workspace
325
+ * tile, Type=Initial): the first letter or digit of its name, upper-cased;
326
+ * "?" for a name with neither. One letter, not two: the tile's color, picked
327
+ * per workspace, tells two workspaces with the same letter apart.
328
+ */ export function workspaceInitial(name) {
329
+ const first = RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]", "u").exec(name != null ? name : '');
330
+ return first ? first[0].toUpperCase() : '?';
331
+ }
329
332
  // ── Starred and recent items ────────────────────────────────────────────────
330
333
  /** One item's identity: the same screen or record in the same place. */ export function accountItemKey(item) {
331
334
  var _item_hostId;
@@ -473,6 +476,40 @@ function isStarColor(value) {
473
476
  }
474
477
  return out;
475
478
  }
479
+ /**
480
+ * The colors a workspace's tile takes when nothing else gives it one
481
+ * ({@link workspaceStableColor}): the console's blue for text on a light
482
+ * ground (primary.dark), deep tones of the brand palette's
483
+ * violet and pink, and a rose, orange, gold, green and teal beside them,
484
+ * each dark enough that the tile's white initial reads at 4.5:1 or more. The
485
+ * slate is left out: it is the tile of a workspace with no id at all.
486
+ */ export const WORKSPACE_STABLE_COLORS = [
487
+ '#0077AD',
488
+ '#6A4FD8',
489
+ '#A62BBF',
490
+ '#C2185B',
491
+ '#B4501A',
492
+ '#8A6A00',
493
+ '#2E7D5B',
494
+ '#00796B'
495
+ ];
496
+ /**
497
+ * The color a workspace with no picked, brand or logo color shows on its
498
+ * tile: one of {@link WORKSPACE_STABLE_COLORS}, chosen by its id, so the same
499
+ * workspace has the same color on every device and in every app, and two
500
+ * workspaces with the same initial usually differ. The id is hashed with
501
+ * 32-bit FNV-1a over its UTF-16 code units, which each platform computes the
502
+ * same way. Null for an empty id.
503
+ */ export function workspaceStableColor(orgId) {
504
+ var _WORKSPACE_STABLE_COLORS_;
505
+ if (typeof orgId !== 'string' || orgId === '') return null;
506
+ let hash = 0x811c9dc5;
507
+ for(let i = 0; i < orgId.length; i++){
508
+ hash ^= orgId.charCodeAt(i);
509
+ hash = Math.imul(hash, 0x01000193) >>> 0;
510
+ }
511
+ return (_WORKSPACE_STABLE_COLORS_ = WORKSPACE_STABLE_COLORS[hash % WORKSPACE_STABLE_COLORS.length]) != null ? _WORKSPACE_STABLE_COLORS_ : null;
512
+ }
476
513
  /**
477
514
  * A workspace's color: the one the person picked, else the workspace's brand
478
515
  * color (`brandingProfile.primaryColor`), else a color taken from its logo
@@ -480,7 +517,12 @@ function isStarColor(value) {
480
517
  * it while you are in the workspace unless the person turned "Use each
481
518
  * workspace's brand" off, which stops the brand and logo colors tinting the
482
519
  * app (a picked color still does: the person chose it).
483
- */ export function resolveWorkspaceColor(picked, brandColor, logoColor, useWorkspaceBrand) {
520
+ *
521
+ * Given the workspace's id, a workspace with none of the three still gets a
522
+ * color: its stable one ({@link workspaceStableColor}). That one colors only
523
+ * its tile (the rail, the pickers, the menus, the sidebar's header), never
524
+ * the app.
525
+ */ export function resolveWorkspaceColor(picked, brandColor, logoColor, useWorkspaceBrand, orgId) {
484
526
  if (isAccountColor(picked)) return {
485
527
  color: picked,
486
528
  source: 'picked',
@@ -500,6 +542,12 @@ function isStarColor(value) {
500
542
  tintsApp: useWorkspaceBrand
501
543
  };
502
544
  }
545
+ const stable = workspaceStableColor(orgId);
546
+ if (stable) return {
547
+ color: stable,
548
+ source: 'stable',
549
+ tintsApp: false
550
+ };
503
551
  return {
504
552
  color: null,
505
553
  source: 'none',
@@ -526,5 +574,205 @@ function oneOf(value, allowed, fallback) {
526
574
  useWorkspaceBrand: typeof raw['useWorkspaceBrand'] === 'boolean' ? raw['useWorkspaceBrand'] : DEFAULT_APP_THEME.useWorkspaceBrand
527
575
  };
528
576
  }
577
+ // ── The phone's tab bar (AGL-4006) ──────────────────────────────────────────
578
+ /** `users/{uid}` field holding the areas the person keeps in the phone's tab bar, per site and per workspace. */ export const TAB_BARS_FIELD = 'tabBars';
579
+ /**
580
+ * The most areas one tab bar holds beside Home: iOS and
581
+ * Material take five destinations at most, and nothing ever overflows into a
582
+ * More tab. With four, Create leaves the bar for the top bar.
583
+ */ export const TAB_BAR_AREAS_MAX = 4;
584
+ /** The most sites and workspaces the account keeps a tab bar for, the bound the Firestore rules hold. */ export const TAB_BARS_MAX = 100;
585
+ /** The tab bar's own slots beside the areas: Home, always first, and Create new. */ export const TAB_BAR_HOME = 'home';
586
+ export const TAB_BAR_CREATE = 'create';
587
+ /** The key of one site's tab bar, or the whole workspace's. */ export function tabBarScope(orgId, hostId) {
588
+ return hostId ? `site:${hostId}` : `org:${orgId}`;
589
+ }
590
+ function tabAreaList(value) {
591
+ return idList(value).filter((id)=>id !== TAB_BAR_HOME && id !== TAB_BAR_CREATE).slice(0, TAB_BAR_AREAS_MAX);
592
+ }
593
+ function tabBarList(value) {
594
+ if (!Array.isArray(value)) return [];
595
+ const seen = new Set();
596
+ const out = [];
597
+ for (const entry of value){
598
+ if (!entry || typeof entry !== 'object') continue;
599
+ const raw = entry;
600
+ const scope = raw['scope'];
601
+ if (typeof scope !== 'string' || !/^(site|org):./.test(scope) || seen.has(scope)) continue;
602
+ seen.add(scope);
603
+ out.push({
604
+ scope,
605
+ areas: tabAreaList(raw['areas']),
606
+ chosen: raw['chosen'] === true
607
+ });
608
+ }
609
+ return out.slice(-TAB_BARS_MAX);
610
+ }
611
+ /**
612
+ * Every tab bar the person keeps, each well formed, without repeats. Anything
613
+ * that is not a `{ scope, areas, chosen }` reads as no tab bar, so a stray
614
+ * value can never take the phone's shell down.
615
+ */ export function readTabBars(profile) {
616
+ return tabBarList(field(profile, TAB_BARS_FIELD));
617
+ }
618
+ /** One scope's areas, in the person's order. */ export function tabBarAreas(tabBars, scope) {
619
+ var _ref;
620
+ var _tabBarList_find;
621
+ return (_ref = (_tabBarList_find = tabBarList(tabBars).find((bar)=>bar.scope === scope)) == null ? void 0 : _tabBarList_find.areas) != null ? _ref : [];
622
+ }
623
+ /**
624
+ * Every tab bar with one scope's replaced. A scope seen for the first time
625
+ * goes last; past {@link TAB_BARS_MAX} the oldest OTHER scopes give way.
626
+ */ function withTabBar(tabBars, next) {
627
+ const current = tabBarList(tabBars);
628
+ const at = current.findIndex((bar)=>bar.scope === next.scope);
629
+ const out = at === -1 ? [
630
+ ...current,
631
+ next
632
+ ] : current.map((bar, index)=>index === at ? next : bar);
633
+ while(out.length > TAB_BARS_MAX){
634
+ const oldest = out.findIndex((bar)=>bar.scope !== next.scope);
635
+ if (oldest === -1) break;
636
+ out.splice(oldest, 1);
637
+ }
638
+ return out;
639
+ }
640
+ /**
641
+ * Pins an area at the end of a scope's tab bar (long-press a tile, "Add to tab
642
+ * bar"), or says the bar is full: a full bar is never made room in quietly.
643
+ */ export function pinTabArea(tabBars, scope, area) {
644
+ const current = tabBarList(tabBars);
645
+ const areas = tabBarAreas(current, scope);
646
+ if (!area || area === TAB_BAR_HOME || area === TAB_BAR_CREATE || areas.includes(area)) {
647
+ return {
648
+ tabBars: current,
649
+ outcome: 'alreadyPinned'
650
+ };
651
+ }
652
+ if (areas.length >= TAB_BAR_AREAS_MAX) return {
653
+ tabBars: current,
654
+ outcome: 'full'
655
+ };
656
+ return {
657
+ tabBars: withTabBar(current, {
658
+ scope,
659
+ areas: [
660
+ ...areas,
661
+ area
662
+ ],
663
+ chosen: true
664
+ }),
665
+ outcome: 'pinned'
666
+ };
667
+ }
668
+ /** A scope's tab bar without one area; the person chose, so opened areas no longer fill the spot. */ export function unpinTabArea(tabBars, scope, area) {
669
+ const areas = tabBarAreas(tabBars, scope);
670
+ return withTabBar(tabBars, {
671
+ scope,
672
+ areas: areas.filter((id)=>id !== area),
673
+ chosen: true
674
+ });
675
+ }
676
+ /** A scope's tab bar with one area moved left (`-1`) or right (`1`) a place; unchanged at an end. */ export function moveTabArea(tabBars, scope, area, offset) {
677
+ const areas = tabBarAreas(tabBars, scope);
678
+ const from = areas.indexOf(area);
679
+ const to = from + (offset < 0 ? -1 : 1);
680
+ if (from === -1 || to < 0 || to >= areas.length) return tabBarList(tabBars);
681
+ const next = [
682
+ ...areas
683
+ ];
684
+ next[from] = next[to];
685
+ next[to] = area;
686
+ return withTabBar(tabBars, {
687
+ scope,
688
+ areas: next,
689
+ chosen: true
690
+ });
691
+ }
692
+ /** A scope's tab bar as the Tab bar sheet left it: these areas, in this order, at most four. */ export function setTabAreas(tabBars, scope, areas) {
693
+ return withTabBar(tabBars, {
694
+ scope,
695
+ areas: tabAreaList(areas),
696
+ chosen: true
697
+ });
698
+ }
699
+ /**
700
+ * The default before anyone pins: an area the person opens takes a free spot
701
+ * and stays there, until the bar is full or the person changes it by hand.
702
+ * Unchanged once the scope is `chosen`, the area is in the bar already, or the
703
+ * bar is full.
704
+ */ export function autoPinTabArea(tabBars, scope, area) {
705
+ var _ref;
706
+ const current = tabBarList(tabBars);
707
+ const bar = current.find((entry)=>entry.scope === scope);
708
+ const areas = (_ref = bar == null ? void 0 : bar.areas) != null ? _ref : [];
709
+ if (!area || area === TAB_BAR_HOME || area === TAB_BAR_CREATE) return current;
710
+ if ((bar == null ? void 0 : bar.chosen) || areas.includes(area) || areas.length >= TAB_BAR_AREAS_MAX) return current;
711
+ return withTabBar(current, {
712
+ scope,
713
+ areas: [
714
+ ...areas,
715
+ area
716
+ ],
717
+ chosen: false
718
+ });
719
+ }
720
+ /**
721
+ * The tab bar to draw: Home first, then the pinned areas the scope can open
722
+ * (`available`, the Home tiles' ids in this build), with Create new in the
723
+ * centre slot. Four areas fill the bar, and Create moves to the top bar.
724
+ */ export function tabBarLayout(areas, available) {
725
+ const open = new Set(available);
726
+ const shown = tabAreaList(areas).filter((id)=>open.has(id));
727
+ if (shown.length >= TAB_BAR_AREAS_MAX) {
728
+ return {
729
+ slots: [
730
+ TAB_BAR_HOME,
731
+ ...shown
732
+ ],
733
+ createInTopBar: true
734
+ };
735
+ }
736
+ const slots = [
737
+ TAB_BAR_HOME,
738
+ ...shown
739
+ ];
740
+ slots.splice(Math.floor((shown.length + 2) / 2), 0, TAB_BAR_CREATE);
741
+ return {
742
+ slots,
743
+ createInTopBar: false
744
+ };
745
+ }
746
+ // ── Who is signed in ────────────────────────────────────────────────────────
747
+ /**
748
+ * `users/{uid}` field holding the person's own picture (AGL-1961). Lowercase
749
+ * `url`: the roster's `photoURL` spelling reads as nothing here.
750
+ */ export const ACCOUNT_PHOTO_FIELD = 'photoUrl';
751
+ function trimmed(value) {
752
+ return typeof value === 'string' ? value.trim() : '';
753
+ }
754
+ /**
755
+ * The person's own picture, the same answer in the console and both apps
756
+ * (AGL-4010): `users/{uid}.photoUrl` first (the profile they edit, filled from
757
+ * the identity provider on every sign-in), else the sign-in record's
758
+ * `photoURL`, else null, so the caller draws initials. A blank value is never
759
+ * a picture, so a photo in either place wins over an empty one in the other.
760
+ * A library pick is stored as a `/api/media/cdn/...` path; resolving it to an
761
+ * address is the caller's (`mediaPreviewURL` on native).
762
+ */ export function readAccountPhoto(profile, authPhotoUrl) {
763
+ return trimmed(field(profile, ACCOUNT_PHOTO_FIELD)) || trimmed(authPhotoUrl) || null;
764
+ }
765
+ /**
766
+ * The person's own name, the same answer in the console and both apps
767
+ * (AGL-2486, AGL-4010): the profile's first and last name, else the sign-in
768
+ * record's display name (empty for an SSO account), else the email address,
769
+ * else empty.
770
+ */ export function readAccountName(profile, authDisplayName, email) {
771
+ const fromProfile = [
772
+ field(profile, 'firstName'),
773
+ field(profile, 'lastName')
774
+ ].map(trimmed).filter(Boolean).join(' ');
775
+ return fromProfile || trimmed(authDisplayName) || trimmed(email);
776
+ }
529
777
 
530
778
  //# sourceMappingURL=account-preferences.js.map