@aglyn/aglyn 1.0.0-beta.269 → 1.0.0-beta.270

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 (59) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-preferences.d.ts +248 -0
  3. package/src/lib/app-utils/account-preferences.js +434 -0
  4. package/src/lib/app-utils/account-preferences.js.map +1 -0
  5. package/src/lib/app-utils/country-options.d.ts +27 -0
  6. package/src/lib/app-utils/country-options.js +96 -0
  7. package/src/lib/app-utils/country-options.js.map +1 -0
  8. package/src/lib/app-utils/docs-help.generated.d.ts +6 -0
  9. package/src/lib/app-utils/docs-help.generated.js +13 -0
  10. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  11. package/src/lib/app-utils/docs-index.generated.js +16 -9
  12. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  13. package/src/lib/app-utils/mobile-push.d.ts +8 -0
  14. package/src/lib/app-utils/mobile-push.js +4 -1
  15. package/src/lib/app-utils/mobile-push.js.map +1 -1
  16. package/src/lib/app-utils/native-app-association.d.ts +3 -2
  17. package/src/lib/app-utils/native-app-association.js +4 -2
  18. package/src/lib/app-utils/native-app-association.js.map +1 -1
  19. package/src/lib/app-utils/native-nav.d.ts +265 -0
  20. package/src/lib/app-utils/native-nav.js +741 -0
  21. package/src/lib/app-utils/native-nav.js.map +1 -0
  22. package/src/lib/app-utils/native-themes.d.ts +249 -0
  23. package/src/lib/app-utils/native-themes.js +732 -0
  24. package/src/lib/app-utils/native-themes.js.map +1 -0
  25. package/src/lib/app-utils/notifications.d.ts +1 -1
  26. package/src/lib/app-utils/notifications.js +10 -0
  27. package/src/lib/app-utils/notifications.js.map +1 -1
  28. package/src/lib/app-utils/one-box-search.d.ts +108 -0
  29. package/src/lib/app-utils/one-box-search.js +167 -0
  30. package/src/lib/app-utils/one-box-search.js.map +1 -0
  31. package/src/lib/app-utils/org-permissions.js +10 -61
  32. package/src/lib/app-utils/org-permissions.js.map +1 -1
  33. package/src/lib/app-utils/org-role-grants.d.ts +95 -0
  34. package/src/lib/app-utils/org-role-grants.js +193 -0
  35. package/src/lib/app-utils/org-role-grants.js.map +1 -0
  36. package/src/lib/app-utils/plan-entitlements.d.ts +41 -1
  37. package/src/lib/app-utils/plan-entitlements.js +65 -3
  38. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  39. package/src/lib/app-utils/tenant-dns-targets.d.ts +62 -0
  40. package/src/lib/app-utils/tenant-dns-targets.js +80 -0
  41. package/src/lib/app-utils/tenant-dns-targets.js.map +1 -0
  42. package/src/lib/foundation/definitions/org-billing.types.d.ts +14 -0
  43. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  44. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  45. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  46. package/src/lib/plugin-manager/feature-plugins.d.ts +38 -0
  47. package/src/lib/plugin-manager/feature-plugins.js +20 -0
  48. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  49. package/src/lib/plugin-manager/first-party-plugins.generated.js +22 -1
  50. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  51. package/src/lib/plugin-manager/plugin-domain-events.d.ts +4 -3
  52. package/src/lib/plugin-manager/plugin-domain-events.js.map +1 -1
  53. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +25 -0
  54. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
  55. package/src/lib/plugin-manager/plugin-search-sources.d.ts +85 -0
  56. package/src/lib/plugin-manager/plugin-search-sources.js +91 -0
  57. package/src/lib/plugin-manager/plugin-search-sources.js.map +1 -0
  58. package/src/lib/plugin-manager/plugin-services.js +6 -0
  59. package/src/lib/plugin-manager/plugin-services.js.map +1 -1
@@ -0,0 +1,434 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * THE PREFERENCES A PERSON KEEPS ON THEIR ACCOUNT (AGL-3943).
19
+ *
20
+ * One documented shape on `users/{uid}`, read and written the same way by
21
+ * the console and both native apps (docs/mobile/native-navigation.md):
22
+ *
23
+ * | field | shape | cap |
24
+ * | -- | -- | -- |
25
+ * | `pinnedOrgs` | workspace ids, in the order the person keeps them | 5 |
26
+ * | `recentOrgs` | workspace ids, most recently opened first | 8 |
27
+ * | `starred` | {@link StarredItem}s, in the order they were starred | 50 |
28
+ * | `recents` | {@link RecentItem}s, most recently opened first | 30 |
29
+ * | `workspaceColors` | workspace id → `#RRGGBB` the person picked | 100 |
30
+ * | `appTheme` | {@link AppThemePrefs}, the apps' look, synced | — |
31
+ *
32
+ * The caps are held twice: here, by every writer, and by the Firestore rules,
33
+ * which refuse a write that grows a list past its cap. A list that is ALREADY
34
+ * past it (pins made before the cap, Zach 2026-10-18) may only shrink, so a
35
+ * person with seven pins can still unpin one; the console asks them once
36
+ * which five to keep, and until they answer the first five are the pins.
37
+ *
38
+ * A per-device override of the theme ("this device only") is the device's
39
+ * own, kept in local storage on each platform, and never written here.
40
+ *
41
+ * NO IMPORTS, on purpose: the native apps read these constants, types and
42
+ * the answers of these functions through the generated contracts, so it must
43
+ * stay a plain module.
44
+ */ /** `users/{uid}` field holding the ids of the workspaces the person pinned. */ export const PINNED_ORGS_FIELD = 'pinnedOrgs';
45
+ /** The most workspaces a person may pin, everywhere, the console included. */ export const PINNED_ORGS_MAX = 5;
46
+ /** `users/{uid}` field holding the workspaces opened most recently, newest first. */ export const RECENT_ORGS_FIELD = 'recentOrgs';
47
+ /** How many recently opened workspaces are kept. */ export const RECENT_ORGS_MAX = 8;
48
+ /** How many recent workspaces the rail shows below the pins. */ export const RAIL_RECENT_ORGS = 3;
49
+ /** An account with this many workspaces or fewer sees them all on the rail. */ export const RAIL_ALL_WORKSPACES_AT_MOST = 8;
50
+ /** `users/{uid}` field holding the person's starred screens and records. */ export const STARRED_FIELD = 'starred';
51
+ /** The most items a person may star. */ export const STARRED_MAX = 50;
52
+ /** `users/{uid}` field holding the screens and records opened most recently. */ export const RECENTS_FIELD = 'recents';
53
+ /** How many recently opened screens and records are kept. */ export const RECENTS_MAX = 30;
54
+ /** `users/{uid}` field holding the color the person picked for each workspace. */ export const WORKSPACE_COLORS_FIELD = 'workspaceColors';
55
+ /** The most workspaces a person may pick a color for. */ export const WORKSPACE_COLORS_MAX = 100;
56
+ /** `users/{uid}` field holding the apps' theme, synced across devices. */ export const APP_THEME_FIELD = 'appTheme';
57
+ /** The longest label a starred or recent item keeps. */ export const ACCOUNT_ITEM_LABEL_MAX = 120;
58
+ /** A picked color: `#` and six hex digits. */ export const ACCOUNT_COLOR_PATTERN = '^#[0-9A-Fa-f]{6}$';
59
+ /** Every star color, in the order the picker lists them. */ export const STAR_COLORS = [
60
+ 'blue',
61
+ 'purple',
62
+ 'pink',
63
+ 'red',
64
+ 'orange',
65
+ 'yellow',
66
+ 'green',
67
+ 'grey'
68
+ ];
69
+ /** What a person who never changed anything sees. */ export const DEFAULT_APP_THEME = {
70
+ appearance: 'system',
71
+ theme: 'aglyn',
72
+ accent: 'theme',
73
+ density: 'comfortable',
74
+ textSize: 'system',
75
+ sidebarStyle: 'tinted',
76
+ appIcon: 'aglyn',
77
+ useWorkspaceBrand: true
78
+ };
79
+ /** Every value each closed theme field accepts, in the order a picker lists them. */ export const APP_APPEARANCES = [
80
+ 'system',
81
+ 'light',
82
+ 'dark'
83
+ ];
84
+ export const APP_THEME_NAMES = [
85
+ 'aglyn',
86
+ 'quiet',
87
+ 'night',
88
+ 'ocean',
89
+ 'warm',
90
+ 'highContrast'
91
+ ];
92
+ export const APP_DENSITIES = [
93
+ 'comfortable',
94
+ 'compact'
95
+ ];
96
+ export const APP_TEXT_SIZES = [
97
+ 'system',
98
+ 'small',
99
+ 'medium',
100
+ 'large',
101
+ 'extraLarge'
102
+ ];
103
+ export const APP_SIDEBAR_STYLES = [
104
+ 'tinted',
105
+ 'plain'
106
+ ];
107
+ /** The longest app icon id. */ export const APP_ICON_ID_MAX = 40;
108
+ function field(profile, name) {
109
+ return profile && typeof profile === 'object' ? profile[name] : undefined;
110
+ }
111
+ function idList(value) {
112
+ if (!Array.isArray(value)) return [];
113
+ return [
114
+ ...new Set(value.filter((entry)=>typeof entry === 'string' && entry !== ''))
115
+ ];
116
+ }
117
+ /** Whether a value is a picked color, `#` and six hex digits. */ export function isAccountColor(value) {
118
+ return typeof value === 'string' && new RegExp(ACCOUNT_COLOR_PATTERN).test(value);
119
+ }
120
+ // ── Pins ────────────────────────────────────────────────────────────────────
121
+ /**
122
+ * Every id the person stored as pinned, in their order, without repeats.
123
+ *
124
+ * Anything that is not a list of non-empty strings reads as no pins, so a
125
+ * stray value can never take a switcher down. May hold more than
126
+ * {@link PINNED_ORGS_MAX} for an account that pinned before the cap; read
127
+ * {@link effectivePinnedOrgs} for the pins to show.
128
+ */ export function readPinnedOrgs(profile) {
129
+ return idList(field(profile, PINNED_ORGS_FIELD));
130
+ }
131
+ /** The pins in force: the first {@link PINNED_ORGS_MAX} stored, until the person picks. */ export function effectivePinnedOrgs(stored) {
132
+ return idList(stored).slice(0, PINNED_ORGS_MAX);
133
+ }
134
+ /** The account pinned more than the cap before there was one, and has to pick which to keep. */ export function pinnedOrgsOverCap(stored) {
135
+ return idList(stored).length > PINNED_ORGS_MAX;
136
+ }
137
+ /**
138
+ * Pins a workspace at the end of the list, or says the list is full. A full
139
+ * list is never made room in quietly: the caller asks "Pinned is full.
140
+ * Replace one?" and calls {@link replacePinnedOrg} with the answer.
141
+ */ export function pinOrg(pinned, orgId) {
142
+ const current = effectivePinnedOrgs(pinned);
143
+ if (!orgId) return {
144
+ pinned: current,
145
+ outcome: 'alreadyPinned'
146
+ };
147
+ if (current.includes(orgId)) return {
148
+ pinned: current,
149
+ outcome: 'alreadyPinned'
150
+ };
151
+ if (current.length >= PINNED_ORGS_MAX) return {
152
+ pinned: current,
153
+ outcome: 'full'
154
+ };
155
+ return {
156
+ pinned: [
157
+ ...current,
158
+ orgId
159
+ ],
160
+ outcome: 'pinned'
161
+ };
162
+ }
163
+ /** The pins without one workspace. */ export function unpinOrg(pinned, orgId) {
164
+ return idList(pinned).filter((id)=>id !== orgId);
165
+ }
166
+ /**
167
+ * The pins with `orgId` in the place `replacing` held — the answer to
168
+ * "Pinned is full. Replace one?". Unchanged when `replacing` is not pinned
169
+ * or `orgId` already is.
170
+ */ export function replacePinnedOrg(pinned, replacing, orgId) {
171
+ const current = effectivePinnedOrgs(pinned);
172
+ if (!orgId || current.includes(orgId) || !current.includes(replacing)) return current;
173
+ return current.map((id)=>id === replacing ? orgId : id);
174
+ }
175
+ /**
176
+ * The answer to "Keep which 5?": the stored pins the person ticked, in the
177
+ * order they were pinned, never more than the cap.
178
+ */ export function keepPinnedOrgs(stored, keep) {
179
+ const kept = new Set(keep);
180
+ return idList(stored).filter((id)=>kept.has(id)).slice(0, PINNED_ORGS_MAX);
181
+ }
182
+ /** The pins with one moved up (`-1`) or down (`1`) a place; unchanged at an end. */ export function movePinnedOrg(pinned, orgId, offset) {
183
+ const current = effectivePinnedOrgs(pinned);
184
+ const from = current.indexOf(orgId);
185
+ const to = from + (offset < 0 ? -1 : 1);
186
+ if (from === -1 || to < 0 || to >= current.length) return current;
187
+ const next = [
188
+ ...current
189
+ ];
190
+ next[from] = next[to];
191
+ next[to] = orgId;
192
+ return next;
193
+ }
194
+ // ── Recent workspaces and the rail ──────────────────────────────────────────
195
+ /** The recently opened workspaces as stored, newest first. */ export function readRecentOrgs(profile) {
196
+ return idList(field(profile, RECENT_ORGS_FIELD)).slice(0, RECENT_ORGS_MAX);
197
+ }
198
+ /** The recent workspaces after opening one: it moves to the front, the oldest falls off. */ export function recordRecentOrg(recent, orgId) {
199
+ if (!orgId) return idList(recent).slice(0, RECENT_ORGS_MAX);
200
+ return [
201
+ orgId,
202
+ ...idList(recent).filter((id)=>id !== orgId)
203
+ ].slice(0, RECENT_ORGS_MAX);
204
+ }
205
+ /**
206
+ * The rail for one account: up to five pinned, a divider, up to three recent,
207
+ * then an "All workspaces (N)" tile. An account with
208
+ * {@link RAIL_ALL_WORKSPACES_AT_MOST} workspaces or fewer sees them all and
209
+ * no All tile, since there is nothing to hide. `orgIds` is every membership,
210
+ * in the switcher's order; an id the person no longer belongs to is ignored.
211
+ */ export function railWorkspaces(orgIds, pinned, recent) {
212
+ const members = idList(orgIds);
213
+ const belongs = new Set(members);
214
+ const pins = effectivePinnedOrgs(pinned).filter((id)=>belongs.has(id));
215
+ const pinnedSet = new Set(pins);
216
+ if (members.length <= RAIL_ALL_WORKSPACES_AT_MOST) {
217
+ return {
218
+ pinned: pins,
219
+ recent: [],
220
+ rest: members.filter((id)=>!pinnedSet.has(id)),
221
+ allTile: false,
222
+ total: members.length
223
+ };
224
+ }
225
+ return {
226
+ pinned: pins,
227
+ recent: idList(recent).filter((id)=>belongs.has(id) && !pinnedSet.has(id)).slice(0, RAIL_RECENT_ORGS),
228
+ rest: [],
229
+ allTile: true,
230
+ total: members.length
231
+ };
232
+ }
233
+ // ── Starred and recent items ────────────────────────────────────────────────
234
+ /** One item's identity: the same screen or record in the same place. */ export function accountItemKey(item) {
235
+ var _item_hostId;
236
+ return `${item.kind}:${item.orgId}:${(_item_hostId = item.hostId) != null ? _item_hostId : ''}:${item.id}`;
237
+ }
238
+ function stringMap(value) {
239
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
240
+ const out = {};
241
+ for (const [key, entry] of Object.entries(value)){
242
+ if (typeof entry === 'string') out[key] = entry;
243
+ }
244
+ return Object.keys(out).length ? out : undefined;
245
+ }
246
+ function itemRef(value) {
247
+ if (!value || typeof value !== 'object') return null;
248
+ const raw = value;
249
+ const text = (key)=>typeof raw[key] === 'string' ? raw[key] : '';
250
+ const kind = text('kind');
251
+ const id = text('id');
252
+ const orgId = text('orgId');
253
+ const screen = text('screen');
254
+ if (!kind || !id || !orgId || !screen) return null;
255
+ const hostId = text('hostId');
256
+ const params = stringMap(raw['params']);
257
+ return _extends({
258
+ kind,
259
+ id,
260
+ orgId
261
+ }, hostId ? {
262
+ hostId
263
+ } : {}, {
264
+ label: text('label').slice(0, ACCOUNT_ITEM_LABEL_MAX),
265
+ screen
266
+ }, params ? {
267
+ params
268
+ } : {});
269
+ }
270
+ function at(value) {
271
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
272
+ }
273
+ function isStarColor(value) {
274
+ return STAR_COLORS.includes(value);
275
+ }
276
+ /** The starred items as stored, each well formed, without repeats, never more than the cap. */ export function readStarred(profile) {
277
+ const stored = field(profile, STARRED_FIELD);
278
+ if (!Array.isArray(stored)) return [];
279
+ const seen = new Set();
280
+ const out = [];
281
+ for (const entry of stored){
282
+ const ref = itemRef(entry);
283
+ if (!ref) continue;
284
+ const key = accountItemKey(ref);
285
+ if (seen.has(key)) continue;
286
+ seen.add(key);
287
+ const color = entry['color'];
288
+ out.push(_extends({}, ref, isStarColor(color) ? {
289
+ color
290
+ } : {}, {
291
+ starredAtMs: at(entry['starredAtMs'])
292
+ }));
293
+ }
294
+ return out.slice(0, STARRED_MAX);
295
+ }
296
+ /** Stars an item at the end of the list, or unstars it; a full list says so instead. */ export function toggleStarred(starred, item, nowMs) {
297
+ const current = readStarred({
298
+ [STARRED_FIELD]: starred
299
+ });
300
+ const ref = itemRef(item);
301
+ if (!ref) return {
302
+ starred: current,
303
+ outcome: 'full'
304
+ };
305
+ const key = accountItemKey(ref);
306
+ if (current.some((entry)=>accountItemKey(entry) === key)) {
307
+ return {
308
+ starred: current.filter((entry)=>accountItemKey(entry) !== key),
309
+ outcome: 'unstarred'
310
+ };
311
+ }
312
+ if (current.length >= STARRED_MAX) return {
313
+ starred: current,
314
+ outcome: 'full'
315
+ };
316
+ return {
317
+ starred: [
318
+ ...current,
319
+ _extends({}, ref, {
320
+ starredAtMs: nowMs
321
+ })
322
+ ],
323
+ outcome: 'starred'
324
+ };
325
+ }
326
+ /** The starred items with one item's color dot set, or cleared with `null`. */ export function setStarColor(starred, key, color) {
327
+ return readStarred({
328
+ [STARRED_FIELD]: starred
329
+ }).map((entry)=>{
330
+ if (accountItemKey(entry) !== key) return entry;
331
+ const next = _extends({}, entry);
332
+ delete next.color;
333
+ return color && isStarColor(color) ? _extends({}, next, {
334
+ color
335
+ }) : next;
336
+ });
337
+ }
338
+ /** The recent items as stored, newest first, each well formed, never more than the cap. */ export function readRecents(profile) {
339
+ const stored = field(profile, RECENTS_FIELD);
340
+ if (!Array.isArray(stored)) return [];
341
+ const seen = new Set();
342
+ const out = [];
343
+ for (const entry of stored){
344
+ const ref = itemRef(entry);
345
+ if (!ref) continue;
346
+ const key = accountItemKey(ref);
347
+ if (seen.has(key)) continue;
348
+ seen.add(key);
349
+ out.push(_extends({}, ref, {
350
+ openedAtMs: at(entry['openedAtMs'])
351
+ }));
352
+ }
353
+ return out.slice(0, RECENTS_MAX);
354
+ }
355
+ /** The recent items after opening one: it moves to the front, the oldest falls off. */ export function recordRecent(recents, item, nowMs) {
356
+ const current = readRecents({
357
+ [RECENTS_FIELD]: recents
358
+ });
359
+ const ref = itemRef(item);
360
+ if (!ref) return current;
361
+ const key = accountItemKey(ref);
362
+ return [
363
+ _extends({}, ref, {
364
+ openedAtMs: nowMs
365
+ }),
366
+ ...current.filter((entry)=>accountItemKey(entry) !== key)
367
+ ].slice(0, RECENTS_MAX);
368
+ }
369
+ // ── Workspace colors ────────────────────────────────────────────────────────
370
+ /** The colors the person picked, by workspace id, the valid ones only. */ export function readWorkspaceColors(profile) {
371
+ const stored = field(profile, WORKSPACE_COLORS_FIELD);
372
+ if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return {};
373
+ const out = {};
374
+ for (const [orgId, color] of Object.entries(stored)){
375
+ if (orgId && isAccountColor(color)) out[orgId] = color;
376
+ if (Object.keys(out).length >= WORKSPACE_COLORS_MAX) break;
377
+ }
378
+ return out;
379
+ }
380
+ /**
381
+ * A workspace's color: the one the person picked, else the workspace's brand
382
+ * color (`brandingProfile.primaryColor`), else a color taken from its logo
383
+ * (Zach, 2026-10-18). The rail tile always shows it; the app is tinted with
384
+ * it while you are in the workspace unless the person turned "Use each
385
+ * workspace's brand" off, which stops the brand and logo colors tinting the
386
+ * app (a picked color still does: the person chose it).
387
+ */ export function resolveWorkspaceColor(picked, brandColor, logoColor, useWorkspaceBrand) {
388
+ if (isAccountColor(picked)) return {
389
+ color: picked,
390
+ source: 'picked',
391
+ tintsApp: true
392
+ };
393
+ if (isAccountColor(brandColor)) {
394
+ return {
395
+ color: brandColor,
396
+ source: 'brand',
397
+ tintsApp: useWorkspaceBrand
398
+ };
399
+ }
400
+ if (isAccountColor(logoColor)) {
401
+ return {
402
+ color: logoColor,
403
+ source: 'logo',
404
+ tintsApp: useWorkspaceBrand
405
+ };
406
+ }
407
+ return {
408
+ color: null,
409
+ source: 'none',
410
+ tintsApp: false
411
+ };
412
+ }
413
+ // ── Theme ───────────────────────────────────────────────────────────────────
414
+ function oneOf(value, allowed, fallback) {
415
+ return allowed.includes(value) ? value : fallback;
416
+ }
417
+ /** The synced theme, every field filled: a missing or unknown value reads as the default. */ export function readAppTheme(profile) {
418
+ const stored = field(profile, APP_THEME_FIELD);
419
+ const raw = stored && typeof stored === 'object' && !Array.isArray(stored) ? stored : {};
420
+ const accent = raw['accent'];
421
+ const appIcon = raw['appIcon'];
422
+ return {
423
+ appearance: oneOf(raw['appearance'], APP_APPEARANCES, DEFAULT_APP_THEME.appearance),
424
+ theme: oneOf(raw['theme'], APP_THEME_NAMES, DEFAULT_APP_THEME.theme),
425
+ accent: accent === 'theme' || isAccountColor(accent) ? accent : DEFAULT_APP_THEME.accent,
426
+ density: oneOf(raw['density'], APP_DENSITIES, DEFAULT_APP_THEME.density),
427
+ textSize: oneOf(raw['textSize'], APP_TEXT_SIZES, DEFAULT_APP_THEME.textSize),
428
+ sidebarStyle: oneOf(raw['sidebarStyle'], APP_SIDEBAR_STYLES, DEFAULT_APP_THEME.sidebarStyle),
429
+ appIcon: typeof appIcon === 'string' && appIcon !== '' && appIcon.length <= APP_ICON_ID_MAX ? appIcon : DEFAULT_APP_THEME.appIcon,
430
+ useWorkspaceBrand: typeof raw['useWorkspaceBrand'] === 'boolean' ? raw['useWorkspaceBrand'] : DEFAULT_APP_THEME.useWorkspaceBrand
431
+ };
432
+ }
433
+
434
+ //# sourceMappingURL=account-preferences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/account-preferences.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE PREFERENCES A PERSON KEEPS ON THEIR ACCOUNT (AGL-3943).\n *\n * One documented shape on `users/{uid}`, read and written the same way by\n * the console and both native apps (docs/mobile/native-navigation.md):\n *\n * | field | shape | cap |\n * | -- | -- | -- |\n * | `pinnedOrgs` | workspace ids, in the order the person keeps them | 5 |\n * | `recentOrgs` | workspace ids, most recently opened first | 8 |\n * | `starred` | {@link StarredItem}s, in the order they were starred | 50 |\n * | `recents` | {@link RecentItem}s, most recently opened first | 30 |\n * | `workspaceColors` | workspace id → `#RRGGBB` the person picked | 100 |\n * | `appTheme` | {@link AppThemePrefs}, the apps' look, synced | — |\n *\n * The caps are held twice: here, by every writer, and by the Firestore rules,\n * which refuse a write that grows a list past its cap. A list that is ALREADY\n * past it (pins made before the cap, Zach 2026-10-18) may only shrink, so a\n * person with seven pins can still unpin one; the console asks them once\n * which five to keep, and until they answer the first five are the pins.\n *\n * A per-device override of the theme (\"this device only\") is the device's\n * own, kept in local storage on each platform, and never written here.\n *\n * NO IMPORTS, on purpose: the native apps read these constants, types and\n * the answers of these functions through the generated contracts, so it must\n * stay a plain module.\n */\n\n/** `users/{uid}` field holding the ids of the workspaces the person pinned. */\nexport const PINNED_ORGS_FIELD = 'pinnedOrgs'\n\n/** The most workspaces a person may pin, everywhere, the console included. */\nexport const PINNED_ORGS_MAX = 5\n\n/** `users/{uid}` field holding the workspaces opened most recently, newest first. */\nexport const RECENT_ORGS_FIELD = 'recentOrgs'\n\n/** How many recently opened workspaces are kept. */\nexport const RECENT_ORGS_MAX = 8\n\n/** How many recent workspaces the rail shows below the pins. */\nexport const RAIL_RECENT_ORGS = 3\n\n/** An account with this many workspaces or fewer sees them all on the rail. */\nexport const RAIL_ALL_WORKSPACES_AT_MOST = 8\n\n/** `users/{uid}` field holding the person's starred screens and records. */\nexport const STARRED_FIELD = 'starred'\n\n/** The most items a person may star. */\nexport const STARRED_MAX = 50\n\n/** `users/{uid}` field holding the screens and records opened most recently. */\nexport const RECENTS_FIELD = 'recents'\n\n/** How many recently opened screens and records are kept. */\nexport const RECENTS_MAX = 30\n\n/** `users/{uid}` field holding the color the person picked for each workspace. */\nexport const WORKSPACE_COLORS_FIELD = 'workspaceColors'\n\n/** The most workspaces a person may pick a color for. */\nexport const WORKSPACE_COLORS_MAX = 100\n\n/** `users/{uid}` field holding the apps' theme, synced across devices. */\nexport const APP_THEME_FIELD = 'appTheme'\n\n/** The longest label a starred or recent item keeps. */\nexport const ACCOUNT_ITEM_LABEL_MAX = 120\n\n/** A picked color: `#` and six hex digits. */\nexport const ACCOUNT_COLOR_PATTERN = '^#[0-9A-Fa-f]{6}$'\n\n/** The color dot a starred item may carry, named so each platform draws it from its theme. */\nexport type StarColor =\n | 'blue'\n | 'purple'\n | 'pink'\n | 'red'\n | 'orange'\n | 'yellow'\n | 'green'\n | 'grey'\n\n/** Every star color, in the order the picker lists them. */\nexport const STAR_COLORS: readonly StarColor[] = [\n 'blue',\n 'purple',\n 'pink',\n 'red',\n 'orange',\n 'yellow',\n 'green',\n 'grey',\n]\n\n/**\n * A screen or a record the person can come back to: what it is, where it\n * belongs, what it was called, and the native screen that opens it.\n */\nexport interface AccountItemRef {\n /**\n * `screen` for a place in the app (a nav manifest row), otherwise the\n * one-box search kind of the record (`page`, `order`, `contact`, …).\n */\n kind: string\n /** The nav row id for a `screen`, otherwise the record's document id. */\n id: string\n /** The workspace it belongs to. */\n orgId: string\n /** The site, for an item that belongs to one. */\n hostId?: string\n /** What it was called when it was starred or opened. */\n label: string\n /** The native screen that opens it. */\n screen: string\n /** That screen's params. */\n params?: Record<string, string>\n}\n\n/** One starred item. */\nexport interface StarredItem extends AccountItemRef {\n color?: StarColor\n starredAtMs: number\n}\n\n/** One recently opened item. */\nexport interface RecentItem extends AccountItemRef {\n openedAtMs: number\n}\n\nexport type AppAppearance = 'system' | 'light' | 'dark'\n\nexport type AppThemeName = 'aglyn' | 'quiet' | 'night' | 'ocean' | 'warm' | 'highContrast'\n\nexport type AppDensity = 'comfortable' | 'compact'\n\nexport type AppTextSize = 'system' | 'small' | 'medium' | 'large' | 'extraLarge'\n\nexport type AppSidebarStyle = 'tinted' | 'plain'\n\n/** The apps' look, saved on the account so it follows the person to every device. */\nexport interface AppThemePrefs {\n /** Light, dark or the system's. */\n appearance: AppAppearance\n /** The theme: rail, sidebar tint, surfaces and accent for light and dark together. */\n theme: AppThemeName\n /** `theme` for the theme's own accent, or a `#RRGGBB` the person picked. */\n accent: string\n density: AppDensity\n /** `system` follows Dynamic Type or the font scale; the rest are the Mac and Windows sizes. */\n textSize: AppTextSize\n sidebarStyle: AppSidebarStyle\n /** The app icon's id: `aglyn`, or one of the alternates the apps ship. */\n appIcon: string\n /** A workspace's own brand color tints the app while you are in it. */\n useWorkspaceBrand: boolean\n}\n\n/** What a person who never changed anything sees. */\nexport const DEFAULT_APP_THEME: AppThemePrefs = {\n appearance: 'system',\n theme: 'aglyn',\n accent: 'theme',\n density: 'comfortable',\n textSize: 'system',\n sidebarStyle: 'tinted',\n appIcon: 'aglyn',\n useWorkspaceBrand: true,\n}\n\n/** Every value each closed theme field accepts, in the order a picker lists them. */\nexport const APP_APPEARANCES: readonly AppAppearance[] = ['system', 'light', 'dark']\nexport const APP_THEME_NAMES: readonly AppThemeName[] = [\n 'aglyn',\n 'quiet',\n 'night',\n 'ocean',\n 'warm',\n 'highContrast',\n]\nexport const APP_DENSITIES: readonly AppDensity[] = ['comfortable', 'compact']\nexport const APP_TEXT_SIZES: readonly AppTextSize[] = [\n 'system',\n 'small',\n 'medium',\n 'large',\n 'extraLarge',\n]\nexport const APP_SIDEBAR_STYLES: readonly AppSidebarStyle[] = ['tinted', 'plain']\n\n/** The longest app icon id. */\nexport const APP_ICON_ID_MAX = 40\n\nfunction field(profile: unknown, name: string): unknown {\n return profile && typeof profile === 'object'\n ? (profile as Record<string, unknown>)[name]\n : undefined\n}\n\nfunction idList(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return [\n ...new Set(\n value.filter((entry): entry is string => typeof entry === 'string' && entry !== ''),\n ),\n ]\n}\n\n/** Whether a value is a picked color, `#` and six hex digits. */\nexport function isAccountColor(value: unknown): value is string {\n return typeof value === 'string' && new RegExp(ACCOUNT_COLOR_PATTERN).test(value)\n}\n\n// ── Pins ────────────────────────────────────────────────────────────────────\n\n/**\n * Every id the person stored as pinned, in their order, without repeats.\n *\n * Anything that is not a list of non-empty strings reads as no pins, so a\n * stray value can never take a switcher down. May hold more than\n * {@link PINNED_ORGS_MAX} for an account that pinned before the cap; read\n * {@link effectivePinnedOrgs} for the pins to show.\n */\nexport function readPinnedOrgs(profile: unknown): string[] {\n return idList(field(profile, PINNED_ORGS_FIELD))\n}\n\n/** The pins in force: the first {@link PINNED_ORGS_MAX} stored, until the person picks. */\nexport function effectivePinnedOrgs(stored: readonly string[]): string[] {\n return idList(stored).slice(0, PINNED_ORGS_MAX)\n}\n\n/** The account pinned more than the cap before there was one, and has to pick which to keep. */\nexport function pinnedOrgsOverCap(stored: readonly string[]): boolean {\n return idList(stored).length > PINNED_ORGS_MAX\n}\n\nexport type PinOrgOutcome = 'pinned' | 'alreadyPinned' | 'full'\n\nexport interface PinOrgResult {\n /** The pins after the request: unchanged unless `outcome` is `pinned`. */\n pinned: string[]\n outcome: PinOrgOutcome\n}\n\n/**\n * Pins a workspace at the end of the list, or says the list is full. A full\n * list is never made room in quietly: the caller asks \"Pinned is full.\n * Replace one?\" and calls {@link replacePinnedOrg} with the answer.\n */\nexport function pinOrg(pinned: readonly string[], orgId: string): PinOrgResult {\n const current = effectivePinnedOrgs(pinned)\n if (!orgId) return { pinned: current, outcome: 'alreadyPinned' }\n if (current.includes(orgId)) return { pinned: current, outcome: 'alreadyPinned' }\n if (current.length >= PINNED_ORGS_MAX) return { pinned: current, outcome: 'full' }\n return { pinned: [...current, orgId], outcome: 'pinned' }\n}\n\n/** The pins without one workspace. */\nexport function unpinOrg(pinned: readonly string[], orgId: string): string[] {\n return idList(pinned).filter((id) => id !== orgId)\n}\n\n/**\n * The pins with `orgId` in the place `replacing` held — the answer to\n * \"Pinned is full. Replace one?\". Unchanged when `replacing` is not pinned\n * or `orgId` already is.\n */\nexport function replacePinnedOrg(\n pinned: readonly string[],\n replacing: string,\n orgId: string,\n): string[] {\n const current = effectivePinnedOrgs(pinned)\n if (!orgId || current.includes(orgId) || !current.includes(replacing)) return current\n return current.map((id) => (id === replacing ? orgId : id))\n}\n\n/**\n * The answer to \"Keep which 5?\": the stored pins the person ticked, in the\n * order they were pinned, never more than the cap.\n */\nexport function keepPinnedOrgs(stored: readonly string[], keep: readonly string[]): string[] {\n const kept = new Set(keep)\n return idList(stored)\n .filter((id) => kept.has(id))\n .slice(0, PINNED_ORGS_MAX)\n}\n\n/** The pins with one moved up (`-1`) or down (`1`) a place; unchanged at an end. */\nexport function movePinnedOrg(pinned: readonly string[], orgId: string, offset: number): string[] {\n const current = effectivePinnedOrgs(pinned)\n const from = current.indexOf(orgId)\n const to = from + (offset < 0 ? -1 : 1)\n if (from === -1 || to < 0 || to >= current.length) return current\n const next = [...current]\n next[from] = next[to]\n next[to] = orgId\n return next\n}\n\n// ── Recent workspaces and the rail ──────────────────────────────────────────\n\n/** The recently opened workspaces as stored, newest first. */\nexport function readRecentOrgs(profile: unknown): string[] {\n return idList(field(profile, RECENT_ORGS_FIELD)).slice(0, RECENT_ORGS_MAX)\n}\n\n/** The recent workspaces after opening one: it moves to the front, the oldest falls off. */\nexport function recordRecentOrg(recent: readonly string[], orgId: string): string[] {\n if (!orgId) return idList(recent).slice(0, RECENT_ORGS_MAX)\n return [orgId, ...idList(recent).filter((id) => id !== orgId)].slice(0, RECENT_ORGS_MAX)\n}\n\n/** What the workspace rail shows. */\nexport interface RailWorkspaces {\n /** Pinned, at the top, in the person's order. */\n pinned: string[]\n /** Below the divider: recently opened and not pinned, at most {@link RAIL_RECENT_ORGS}. */\n recent: string[]\n /** Every other workspace, for an account small enough to list them all. */\n rest: string[]\n /** Whether the last tile is \"All workspaces\", opening the searchable switcher. */\n allTile: boolean\n /** How many workspaces the person belongs to, shown under the All tile. */\n total: number\n}\n\n/**\n * The rail for one account: up to five pinned, a divider, up to three recent,\n * then an \"All workspaces (N)\" tile. An account with\n * {@link RAIL_ALL_WORKSPACES_AT_MOST} workspaces or fewer sees them all and\n * no All tile, since there is nothing to hide. `orgIds` is every membership,\n * in the switcher's order; an id the person no longer belongs to is ignored.\n */\nexport function railWorkspaces(\n orgIds: readonly string[],\n pinned: readonly string[],\n recent: readonly string[],\n): RailWorkspaces {\n const members = idList(orgIds)\n const belongs = new Set(members)\n const pins = effectivePinnedOrgs(pinned).filter((id) => belongs.has(id))\n const pinnedSet = new Set(pins)\n if (members.length <= RAIL_ALL_WORKSPACES_AT_MOST) {\n return {\n pinned: pins,\n recent: [],\n rest: members.filter((id) => !pinnedSet.has(id)),\n allTile: false,\n total: members.length,\n }\n }\n return {\n pinned: pins,\n recent: idList(recent)\n .filter((id) => belongs.has(id) && !pinnedSet.has(id))\n .slice(0, RAIL_RECENT_ORGS),\n rest: [],\n allTile: true,\n total: members.length,\n }\n}\n\n// ── Starred and recent items ────────────────────────────────────────────────\n\n/** One item's identity: the same screen or record in the same place. */\nexport function accountItemKey(\n item: Pick<AccountItemRef, 'kind' | 'id' | 'orgId' | 'hostId'>,\n): string {\n return `${item.kind}:${item.orgId}:${item.hostId ?? ''}:${item.id}`\n}\n\nfunction stringMap(value: unknown): Record<string, string> | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined\n const out: Record<string, string> = {}\n for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {\n if (typeof entry === 'string') out[key] = entry\n }\n return Object.keys(out).length ? out : undefined\n}\n\nfunction itemRef(value: unknown): AccountItemRef | null {\n if (!value || typeof value !== 'object') return null\n const raw = value as Record<string, unknown>\n const text = (key: string) => (typeof raw[key] === 'string' ? (raw[key] as string) : '')\n const kind = text('kind')\n const id = text('id')\n const orgId = text('orgId')\n const screen = text('screen')\n if (!kind || !id || !orgId || !screen) return null\n const hostId = text('hostId')\n const params = stringMap(raw['params'])\n return {\n kind,\n id,\n orgId,\n ...(hostId ? { hostId } : {}),\n label: text('label').slice(0, ACCOUNT_ITEM_LABEL_MAX),\n screen,\n ...(params ? { params } : {}),\n }\n}\n\nfunction at(value: unknown): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\nfunction isStarColor(value: unknown): value is StarColor {\n return (STAR_COLORS as readonly unknown[]).includes(value)\n}\n\n/** The starred items as stored, each well formed, without repeats, never more than the cap. */\nexport function readStarred(profile: unknown): StarredItem[] {\n const stored = field(profile, STARRED_FIELD)\n if (!Array.isArray(stored)) return []\n const seen = new Set<string>()\n const out: StarredItem[] = []\n for (const entry of stored) {\n const ref = itemRef(entry)\n if (!ref) continue\n const key = accountItemKey(ref)\n if (seen.has(key)) continue\n seen.add(key)\n const color = (entry as Record<string, unknown>)['color']\n out.push({\n ...ref,\n ...(isStarColor(color) ? { color } : {}),\n starredAtMs: at((entry as Record<string, unknown>)['starredAtMs']),\n })\n }\n return out.slice(0, STARRED_MAX)\n}\n\nexport type StarOutcome = 'starred' | 'unstarred' | 'full'\n\nexport interface StarResult {\n /** The starred items after the request: unchanged when `outcome` is `full`. */\n starred: StarredItem[]\n outcome: StarOutcome\n}\n\n/** Stars an item at the end of the list, or unstars it; a full list says so instead. */\nexport function toggleStarred(\n starred: readonly StarredItem[],\n item: AccountItemRef,\n nowMs: number,\n): StarResult {\n const current = readStarred({ [STARRED_FIELD]: starred })\n const ref = itemRef(item)\n if (!ref) return { starred: current, outcome: 'full' }\n const key = accountItemKey(ref)\n if (current.some((entry) => accountItemKey(entry) === key)) {\n return {\n starred: current.filter((entry) => accountItemKey(entry) !== key),\n outcome: 'unstarred',\n }\n }\n if (current.length >= STARRED_MAX) return { starred: current, outcome: 'full' }\n return { starred: [...current, { ...ref, starredAtMs: nowMs }], outcome: 'starred' }\n}\n\n/** The starred items with one item's color dot set, or cleared with `null`. */\nexport function setStarColor(\n starred: readonly StarredItem[],\n key: string,\n color: StarColor | null,\n): StarredItem[] {\n return readStarred({ [STARRED_FIELD]: starred }).map((entry) => {\n if (accountItemKey(entry) !== key) return entry\n const next: StarredItem = { ...entry }\n delete next.color\n return color && isStarColor(color) ? { ...next, color } : next\n })\n}\n\n/** The recent items as stored, newest first, each well formed, never more than the cap. */\nexport function readRecents(profile: unknown): RecentItem[] {\n const stored = field(profile, RECENTS_FIELD)\n if (!Array.isArray(stored)) return []\n const seen = new Set<string>()\n const out: RecentItem[] = []\n for (const entry of stored) {\n const ref = itemRef(entry)\n if (!ref) continue\n const key = accountItemKey(ref)\n if (seen.has(key)) continue\n seen.add(key)\n out.push({ ...ref, openedAtMs: at((entry as Record<string, unknown>)['openedAtMs']) })\n }\n return out.slice(0, RECENTS_MAX)\n}\n\n/** The recent items after opening one: it moves to the front, the oldest falls off. */\nexport function recordRecent(\n recents: readonly RecentItem[],\n item: AccountItemRef,\n nowMs: number,\n): RecentItem[] {\n const current = readRecents({ [RECENTS_FIELD]: recents })\n const ref = itemRef(item)\n if (!ref) return current\n const key = accountItemKey(ref)\n return [\n { ...ref, openedAtMs: nowMs },\n ...current.filter((entry) => accountItemKey(entry) !== key),\n ].slice(0, RECENTS_MAX)\n}\n\n// ── Workspace colors ────────────────────────────────────────────────────────\n\n/** The colors the person picked, by workspace id, the valid ones only. */\nexport function readWorkspaceColors(profile: unknown): Record<string, string> {\n const stored = field(profile, WORKSPACE_COLORS_FIELD)\n if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return {}\n const out: Record<string, string> = {}\n for (const [orgId, color] of Object.entries(stored as Record<string, unknown>)) {\n if (orgId && isAccountColor(color)) out[orgId] = color\n if (Object.keys(out).length >= WORKSPACE_COLORS_MAX) break\n }\n return out\n}\n\nexport type WorkspaceColorSource = 'picked' | 'brand' | 'logo' | 'none'\n\nexport interface WorkspaceColor {\n /** `#RRGGBB`, or null when nothing gives the workspace a color. */\n color: string | null\n source: WorkspaceColorSource\n /** Whether the app is tinted with it while you are in the workspace. */\n tintsApp: boolean\n}\n\n/**\n * A workspace's color: the one the person picked, else the workspace's brand\n * color (`brandingProfile.primaryColor`), else a color taken from its logo\n * (Zach, 2026-10-18). The rail tile always shows it; the app is tinted with\n * it while you are in the workspace unless the person turned \"Use each\n * workspace's brand\" off, which stops the brand and logo colors tinting the\n * app (a picked color still does: the person chose it).\n */\nexport function resolveWorkspaceColor(\n picked: unknown,\n brandColor: unknown,\n logoColor: unknown,\n useWorkspaceBrand: boolean,\n): WorkspaceColor {\n if (isAccountColor(picked)) return { color: picked, source: 'picked', tintsApp: true }\n if (isAccountColor(brandColor)) {\n return { color: brandColor, source: 'brand', tintsApp: useWorkspaceBrand }\n }\n if (isAccountColor(logoColor)) {\n return { color: logoColor, source: 'logo', tintsApp: useWorkspaceBrand }\n }\n return { color: null, source: 'none', tintsApp: false }\n}\n\n// ── Theme ───────────────────────────────────────────────────────────────────\n\nfunction oneOf<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {\n return (allowed as readonly unknown[]).includes(value) ? (value as T) : fallback\n}\n\n/** The synced theme, every field filled: a missing or unknown value reads as the default. */\nexport function readAppTheme(profile: unknown): AppThemePrefs {\n const stored = field(profile, APP_THEME_FIELD)\n const raw =\n stored && typeof stored === 'object' && !Array.isArray(stored)\n ? (stored as Record<string, unknown>)\n : {}\n const accent = raw['accent']\n const appIcon = raw['appIcon']\n return {\n appearance: oneOf(raw['appearance'], APP_APPEARANCES, DEFAULT_APP_THEME.appearance),\n theme: oneOf(raw['theme'], APP_THEME_NAMES, DEFAULT_APP_THEME.theme),\n accent: accent === 'theme' || isAccountColor(accent) ? accent : DEFAULT_APP_THEME.accent,\n density: oneOf(raw['density'], APP_DENSITIES, DEFAULT_APP_THEME.density),\n textSize: oneOf(raw['textSize'], APP_TEXT_SIZES, DEFAULT_APP_THEME.textSize),\n sidebarStyle: oneOf(raw['sidebarStyle'], APP_SIDEBAR_STYLES, DEFAULT_APP_THEME.sidebarStyle),\n appIcon:\n typeof appIcon === 'string' && appIcon !== '' && appIcon.length <= APP_ICON_ID_MAX\n ? appIcon\n : DEFAULT_APP_THEME.appIcon,\n useWorkspaceBrand:\n typeof raw['useWorkspaceBrand'] === 'boolean'\n ? (raw['useWorkspaceBrand'] as boolean)\n : DEFAULT_APP_THEME.useWorkspaceBrand,\n }\n}\n"],"names":["PINNED_ORGS_FIELD","PINNED_ORGS_MAX","RECENT_ORGS_FIELD","RECENT_ORGS_MAX","RAIL_RECENT_ORGS","RAIL_ALL_WORKSPACES_AT_MOST","STARRED_FIELD","STARRED_MAX","RECENTS_FIELD","RECENTS_MAX","WORKSPACE_COLORS_FIELD","WORKSPACE_COLORS_MAX","APP_THEME_FIELD","ACCOUNT_ITEM_LABEL_MAX","ACCOUNT_COLOR_PATTERN","STAR_COLORS","DEFAULT_APP_THEME","appearance","theme","accent","density","textSize","sidebarStyle","appIcon","useWorkspaceBrand","APP_APPEARANCES","APP_THEME_NAMES","APP_DENSITIES","APP_TEXT_SIZES","APP_SIDEBAR_STYLES","APP_ICON_ID_MAX","field","profile","name","undefined","idList","value","Array","isArray","Set","filter","entry","isAccountColor","RegExp","test","readPinnedOrgs","effectivePinnedOrgs","stored","slice","pinnedOrgsOverCap","length","pinOrg","pinned","orgId","current","outcome","includes","unpinOrg","id","replacePinnedOrg","replacing","map","keepPinnedOrgs","keep","kept","has","movePinnedOrg","offset","from","indexOf","to","next","readRecentOrgs","recordRecentOrg","recent","railWorkspaces","orgIds","members","belongs","pins","pinnedSet","rest","allTile","total","accountItemKey","item","kind","hostId","stringMap","out","key","Object","entries","keys","itemRef","raw","text","screen","params","label","at","Number","isFinite","isStarColor","readStarred","seen","ref","add","color","push","starredAtMs","toggleStarred","starred","nowMs","some","setStarColor","readRecents","openedAtMs","recordRecent","recents","readWorkspaceColors","resolveWorkspaceColor","picked","brandColor","logoColor","source","tintsApp","oneOf","allowed","fallback","readAppTheme"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAED,6EAA6E,GAC7E,OAAO,MAAMA,oBAAoB,aAAY;AAE7C,4EAA4E,GAC5E,OAAO,MAAMC,kBAAkB,EAAC;AAEhC,mFAAmF,GACnF,OAAO,MAAMC,oBAAoB,aAAY;AAE7C,kDAAkD,GAClD,OAAO,MAAMC,kBAAkB,EAAC;AAEhC,8DAA8D,GAC9D,OAAO,MAAMC,mBAAmB,EAAC;AAEjC,6EAA6E,GAC7E,OAAO,MAAMC,8BAA8B,EAAC;AAE5C,0EAA0E,GAC1E,OAAO,MAAMC,gBAAgB,UAAS;AAEtC,sCAAsC,GACtC,OAAO,MAAMC,cAAc,GAAE;AAE7B,8EAA8E,GAC9E,OAAO,MAAMC,gBAAgB,UAAS;AAEtC,2DAA2D,GAC3D,OAAO,MAAMC,cAAc,GAAE;AAE7B,gFAAgF,GAChF,OAAO,MAAMC,yBAAyB,kBAAiB;AAEvD,uDAAuD,GACvD,OAAO,MAAMC,uBAAuB,IAAG;AAEvC,wEAAwE,GACxE,OAAO,MAAMC,kBAAkB,WAAU;AAEzC,sDAAsD,GACtD,OAAO,MAAMC,yBAAyB,IAAG;AAEzC,4CAA4C,GAC5C,OAAO,MAAMC,wBAAwB,oBAAmB;AAaxD,0DAA0D,GAC1D,OAAO,MAAMC,cAAoC;IAC/C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AAiED,mDAAmD,GACnD,OAAO,MAAMC,oBAAmC;IAC9CC,YAAY;IACZC,OAAO;IACPC,QAAQ;IACRC,SAAS;IACTC,UAAU;IACVC,cAAc;IACdC,SAAS;IACTC,mBAAmB;AACrB,EAAC;AAED,mFAAmF,GACnF,OAAO,MAAMC,kBAA4C;IAAC;IAAU;IAAS;CAAO,CAAA;AACpF,OAAO,MAAMC,kBAA2C;IACtD;IACA;IACA;IACA;IACA;IACA;CACD,CAAA;AACD,OAAO,MAAMC,gBAAuC;IAAC;IAAe;CAAU,CAAA;AAC9E,OAAO,MAAMC,iBAAyC;IACpD;IACA;IACA;IACA;IACA;CACD,CAAA;AACD,OAAO,MAAMC,qBAAiD;IAAC;IAAU;CAAQ,CAAA;AAEjF,6BAA6B,GAC7B,OAAO,MAAMC,kBAAkB,GAAE;AAEjC,SAASC,MAAMC,OAAgB,EAAEC,IAAY;IAC3C,OAAOD,WAAW,OAAOA,YAAY,WACjC,AAACA,OAAmC,CAACC,KAAK,GAC1CC;AACN;AAEA,SAASC,OAAOC,KAAc;IAC5B,IAAI,CAACC,MAAMC,OAAO,CAACF,QAAQ,OAAO,EAAE;IACpC,OAAO;WACF,IAAIG,IACLH,MAAMI,MAAM,CAAC,CAACC,QAA2B,OAAOA,UAAU,YAAYA,UAAU;KAEnF;AACH;AAEA,+DAA+D,GAC/D,OAAO,SAASC,eAAeN,KAAc;IAC3C,OAAO,OAAOA,UAAU,YAAY,IAAIO,OAAO7B,uBAAuB8B,IAAI,CAACR;AAC7E;AAEA,+EAA+E;AAE/E;;;;;;;CAOC,GACD,OAAO,SAASS,eAAeb,OAAgB;IAC7C,OAAOG,OAAOJ,MAAMC,SAAShC;AAC/B;AAEA,yFAAyF,GACzF,OAAO,SAAS8C,oBAAoBC,MAAyB;IAC3D,OAAOZ,OAAOY,QAAQC,KAAK,CAAC,GAAG/C;AACjC;AAEA,8FAA8F,GAC9F,OAAO,SAASgD,kBAAkBF,MAAyB;IACzD,OAAOZ,OAAOY,QAAQG,MAAM,GAAGjD;AACjC;AAUA;;;;CAIC,GACD,OAAO,SAASkD,OAAOC,MAAyB,EAAEC,KAAa;IAC7D,MAAMC,UAAUR,oBAAoBM;IACpC,IAAI,CAACC,OAAO,OAAO;QAAED,QAAQE;QAASC,SAAS;IAAgB;IAC/D,IAAID,QAAQE,QAAQ,CAACH,QAAQ,OAAO;QAAED,QAAQE;QAASC,SAAS;IAAgB;IAChF,IAAID,QAAQJ,MAAM,IAAIjD,iBAAiB,OAAO;QAAEmD,QAAQE;QAASC,SAAS;IAAO;IACjF,OAAO;QAAEH,QAAQ;eAAIE;YAASD;SAAM;QAAEE,SAAS;IAAS;AAC1D;AAEA,oCAAoC,GACpC,OAAO,SAASE,SAASL,MAAyB,EAAEC,KAAa;IAC/D,OAAOlB,OAAOiB,QAAQZ,MAAM,CAAC,CAACkB,KAAOA,OAAOL;AAC9C;AAEA;;;;CAIC,GACD,OAAO,SAASM,iBACdP,MAAyB,EACzBQ,SAAiB,EACjBP,KAAa;IAEb,MAAMC,UAAUR,oBAAoBM;IACpC,IAAI,CAACC,SAASC,QAAQE,QAAQ,CAACH,UAAU,CAACC,QAAQE,QAAQ,CAACI,YAAY,OAAON;IAC9E,OAAOA,QAAQO,GAAG,CAAC,CAACH,KAAQA,OAAOE,YAAYP,QAAQK;AACzD;AAEA;;;CAGC,GACD,OAAO,SAASI,eAAef,MAAyB,EAAEgB,IAAuB;IAC/E,MAAMC,OAAO,IAAIzB,IAAIwB;IACrB,OAAO5B,OAAOY,QACXP,MAAM,CAAC,CAACkB,KAAOM,KAAKC,GAAG,CAACP,KACxBV,KAAK,CAAC,GAAG/C;AACd;AAEA,kFAAkF,GAClF,OAAO,SAASiE,cAAcd,MAAyB,EAAEC,KAAa,EAAEc,MAAc;IACpF,MAAMb,UAAUR,oBAAoBM;IACpC,MAAMgB,OAAOd,QAAQe,OAAO,CAAChB;IAC7B,MAAMiB,KAAKF,OAAQD,CAAAA,SAAS,IAAI,CAAC,IAAI,CAAA;IACrC,IAAIC,SAAS,CAAC,KAAKE,KAAK,KAAKA,MAAMhB,QAAQJ,MAAM,EAAE,OAAOI;IAC1D,MAAMiB,OAAO;WAAIjB;KAAQ;IACzBiB,IAAI,CAACH,KAAK,GAAGG,IAAI,CAACD,GAAG;IACrBC,IAAI,CAACD,GAAG,GAAGjB;IACX,OAAOkB;AACT;AAEA,+EAA+E;AAE/E,4DAA4D,GAC5D,OAAO,SAASC,eAAexC,OAAgB;IAC7C,OAAOG,OAAOJ,MAAMC,SAAS9B,oBAAoB8C,KAAK,CAAC,GAAG7C;AAC5D;AAEA,0FAA0F,GAC1F,OAAO,SAASsE,gBAAgBC,MAAyB,EAAErB,KAAa;IACtE,IAAI,CAACA,OAAO,OAAOlB,OAAOuC,QAAQ1B,KAAK,CAAC,GAAG7C;IAC3C,OAAO;QAACkD;WAAUlB,OAAOuC,QAAQlC,MAAM,CAAC,CAACkB,KAAOA,OAAOL;KAAO,CAACL,KAAK,CAAC,GAAG7C;AAC1E;AAgBA;;;;;;CAMC,GACD,OAAO,SAASwE,eACdC,MAAyB,EACzBxB,MAAyB,EACzBsB,MAAyB;IAEzB,MAAMG,UAAU1C,OAAOyC;IACvB,MAAME,UAAU,IAAIvC,IAAIsC;IACxB,MAAME,OAAOjC,oBAAoBM,QAAQZ,MAAM,CAAC,CAACkB,KAAOoB,QAAQb,GAAG,CAACP;IACpE,MAAMsB,YAAY,IAAIzC,IAAIwC;IAC1B,IAAIF,QAAQ3B,MAAM,IAAI7C,6BAA6B;QACjD,OAAO;YACL+C,QAAQ2B;YACRL,QAAQ,EAAE;YACVO,MAAMJ,QAAQrC,MAAM,CAAC,CAACkB,KAAO,CAACsB,UAAUf,GAAG,CAACP;YAC5CwB,SAAS;YACTC,OAAON,QAAQ3B,MAAM;QACvB;IACF;IACA,OAAO;QACLE,QAAQ2B;QACRL,QAAQvC,OAAOuC,QACZlC,MAAM,CAAC,CAACkB,KAAOoB,QAAQb,GAAG,CAACP,OAAO,CAACsB,UAAUf,GAAG,CAACP,KACjDV,KAAK,CAAC,GAAG5C;QACZ6E,MAAM,EAAE;QACRC,SAAS;QACTC,OAAON,QAAQ3B,MAAM;IACvB;AACF;AAEA,+EAA+E;AAE/E,sEAAsE,GACtE,OAAO,SAASkC,eACdC,IAA8D;QAEzBA;IAArC,OAAO,GAAGA,KAAKC,IAAI,CAAC,CAAC,EAAED,KAAKhC,KAAK,CAAC,CAAC,GAAEgC,eAAAA,KAAKE,MAAM,YAAXF,eAAe,GAAG,CAAC,EAAEA,KAAK3B,EAAE,EAAE;AACrE;AAEA,SAAS8B,UAAUpD,KAAc;IAC/B,IAAI,CAACA,SAAS,OAAOA,UAAU,YAAYC,MAAMC,OAAO,CAACF,QAAQ,OAAOF;IACxE,MAAMuD,MAA8B,CAAC;IACrC,KAAK,MAAM,CAACC,KAAKjD,MAAM,IAAIkD,OAAOC,OAAO,CAACxD,OAAmC;QAC3E,IAAI,OAAOK,UAAU,UAAUgD,GAAG,CAACC,IAAI,GAAGjD;IAC5C;IACA,OAAOkD,OAAOE,IAAI,CAACJ,KAAKvC,MAAM,GAAGuC,MAAMvD;AACzC;AAEA,SAAS4D,QAAQ1D,KAAc;IAC7B,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU,OAAO;IAChD,MAAM2D,MAAM3D;IACZ,MAAM4D,OAAO,CAACN,MAAiB,OAAOK,GAAG,CAACL,IAAI,KAAK,WAAYK,GAAG,CAACL,IAAI,GAAc;IACrF,MAAMJ,OAAOU,KAAK;IAClB,MAAMtC,KAAKsC,KAAK;IAChB,MAAM3C,QAAQ2C,KAAK;IACnB,MAAMC,SAASD,KAAK;IACpB,IAAI,CAACV,QAAQ,CAAC5B,MAAM,CAACL,SAAS,CAAC4C,QAAQ,OAAO;IAC9C,MAAMV,SAASS,KAAK;IACpB,MAAME,SAASV,UAAUO,GAAG,CAAC,SAAS;IACtC,OAAO;QACLT;QACA5B;QACAL;OACIkC,SAAS;QAAEA;IAAO,IAAI,CAAC;QAC3BY,OAAOH,KAAK,SAAShD,KAAK,CAAC,GAAGnC;QAC9BoF;OACIC,SAAS;QAAEA;IAAO,IAAI,CAAC;AAE/B;AAEA,SAASE,GAAGhE,KAAc;IACxB,OAAO,OAAOA,UAAU,YAAYiE,OAAOC,QAAQ,CAAClE,SAASA,QAAQ;AACvE;AAEA,SAASmE,YAAYnE,KAAc;IACjC,OAAO,AAACrB,YAAmCyC,QAAQ,CAACpB;AACtD;AAEA,6FAA6F,GAC7F,OAAO,SAASoE,YAAYxE,OAAgB;IAC1C,MAAMe,SAAShB,MAAMC,SAAS1B;IAC9B,IAAI,CAAC+B,MAAMC,OAAO,CAACS,SAAS,OAAO,EAAE;IACrC,MAAM0D,OAAO,IAAIlE;IACjB,MAAMkD,MAAqB,EAAE;IAC7B,KAAK,MAAMhD,SAASM,OAAQ;QAC1B,MAAM2D,MAAMZ,QAAQrD;QACpB,IAAI,CAACiE,KAAK;QACV,MAAMhB,MAAMN,eAAesB;QAC3B,IAAID,KAAKxC,GAAG,CAACyB,MAAM;QACnBe,KAAKE,GAAG,CAACjB;QACT,MAAMkB,QAAQ,AAACnE,KAAiC,CAAC,QAAQ;QACzDgD,IAAIoB,IAAI,CAAC,aACJH,KACCH,YAAYK,SAAS;YAAEA;QAAM,IAAI,CAAC;YACtCE,aAAaV,GAAG,AAAC3D,KAAiC,CAAC,cAAc;;IAErE;IACA,OAAOgD,IAAIzC,KAAK,CAAC,GAAGzC;AACtB;AAUA,sFAAsF,GACtF,OAAO,SAASwG,cACdC,OAA+B,EAC/B3B,IAAoB,EACpB4B,KAAa;IAEb,MAAM3D,UAAUkD,YAAY;QAAE,CAAClG,cAAc,EAAE0G;IAAQ;IACvD,MAAMN,MAAMZ,QAAQT;IACpB,IAAI,CAACqB,KAAK,OAAO;QAAEM,SAAS1D;QAASC,SAAS;IAAO;IACrD,MAAMmC,MAAMN,eAAesB;IAC3B,IAAIpD,QAAQ4D,IAAI,CAAC,CAACzE,QAAU2C,eAAe3C,WAAWiD,MAAM;QAC1D,OAAO;YACLsB,SAAS1D,QAAQd,MAAM,CAAC,CAACC,QAAU2C,eAAe3C,WAAWiD;YAC7DnC,SAAS;QACX;IACF;IACA,IAAID,QAAQJ,MAAM,IAAI3C,aAAa,OAAO;QAAEyG,SAAS1D;QAASC,SAAS;IAAO;IAC9E,OAAO;QAAEyD,SAAS;eAAI1D;YAAS,aAAKoD;gBAAKI,aAAaG;;SAAQ;QAAE1D,SAAS;IAAU;AACrF;AAEA,6EAA6E,GAC7E,OAAO,SAAS4D,aACdH,OAA+B,EAC/BtB,GAAW,EACXkB,KAAuB;IAEvB,OAAOJ,YAAY;QAAE,CAAClG,cAAc,EAAE0G;IAAQ,GAAGnD,GAAG,CAAC,CAACpB;QACpD,IAAI2C,eAAe3C,WAAWiD,KAAK,OAAOjD;QAC1C,MAAM8B,OAAoB,aAAK9B;QAC/B,OAAO8B,KAAKqC,KAAK;QACjB,OAAOA,SAASL,YAAYK,SAAS,aAAKrC;YAAMqC;aAAUrC;IAC5D;AACF;AAEA,yFAAyF,GACzF,OAAO,SAAS6C,YAAYpF,OAAgB;IAC1C,MAAMe,SAAShB,MAAMC,SAASxB;IAC9B,IAAI,CAAC6B,MAAMC,OAAO,CAACS,SAAS,OAAO,EAAE;IACrC,MAAM0D,OAAO,IAAIlE;IACjB,MAAMkD,MAAoB,EAAE;IAC5B,KAAK,MAAMhD,SAASM,OAAQ;QAC1B,MAAM2D,MAAMZ,QAAQrD;QACpB,IAAI,CAACiE,KAAK;QACV,MAAMhB,MAAMN,eAAesB;QAC3B,IAAID,KAAKxC,GAAG,CAACyB,MAAM;QACnBe,KAAKE,GAAG,CAACjB;QACTD,IAAIoB,IAAI,CAAC,aAAKH;YAAKW,YAAYjB,GAAG,AAAC3D,KAAiC,CAAC,aAAa;;IACpF;IACA,OAAOgD,IAAIzC,KAAK,CAAC,GAAGvC;AACtB;AAEA,qFAAqF,GACrF,OAAO,SAAS6G,aACdC,OAA8B,EAC9BlC,IAAoB,EACpB4B,KAAa;IAEb,MAAM3D,UAAU8D,YAAY;QAAE,CAAC5G,cAAc,EAAE+G;IAAQ;IACvD,MAAMb,MAAMZ,QAAQT;IACpB,IAAI,CAACqB,KAAK,OAAOpD;IACjB,MAAMoC,MAAMN,eAAesB;IAC3B,OAAO;QACL,aAAKA;YAAKW,YAAYJ;;WACnB3D,QAAQd,MAAM,CAAC,CAACC,QAAU2C,eAAe3C,WAAWiD;KACxD,CAAC1C,KAAK,CAAC,GAAGvC;AACb;AAEA,+EAA+E;AAE/E,wEAAwE,GACxE,OAAO,SAAS+G,oBAAoBxF,OAAgB;IAClD,MAAMe,SAAShB,MAAMC,SAAStB;IAC9B,IAAI,CAACqC,UAAU,OAAOA,WAAW,YAAYV,MAAMC,OAAO,CAACS,SAAS,OAAO,CAAC;IAC5E,MAAM0C,MAA8B,CAAC;IACrC,KAAK,MAAM,CAACpC,OAAOuD,MAAM,IAAIjB,OAAOC,OAAO,CAAC7C,QAAoC;QAC9E,IAAIM,SAASX,eAAekE,QAAQnB,GAAG,CAACpC,MAAM,GAAGuD;QACjD,IAAIjB,OAAOE,IAAI,CAACJ,KAAKvC,MAAM,IAAIvC,sBAAsB;IACvD;IACA,OAAO8E;AACT;AAYA;;;;;;;CAOC,GACD,OAAO,SAASgC,sBACdC,MAAe,EACfC,UAAmB,EACnBC,SAAkB,EAClBpG,iBAA0B;IAE1B,IAAIkB,eAAegF,SAAS,OAAO;QAAEd,OAAOc;QAAQG,QAAQ;QAAUC,UAAU;IAAK;IACrF,IAAIpF,eAAeiF,aAAa;QAC9B,OAAO;YAAEf,OAAOe;YAAYE,QAAQ;YAASC,UAAUtG;QAAkB;IAC3E;IACA,IAAIkB,eAAekF,YAAY;QAC7B,OAAO;YAAEhB,OAAOgB;YAAWC,QAAQ;YAAQC,UAAUtG;QAAkB;IACzE;IACA,OAAO;QAAEoF,OAAO;QAAMiB,QAAQ;QAAQC,UAAU;IAAM;AACxD;AAEA,+EAA+E;AAE/E,SAASC,MAAwB3F,KAAc,EAAE4F,OAAqB,EAAEC,QAAW;IACjF,OAAO,AAACD,QAA+BxE,QAAQ,CAACpB,SAAUA,QAAc6F;AAC1E;AAEA,2FAA2F,GAC3F,OAAO,SAASC,aAAalG,OAAgB;IAC3C,MAAMe,SAAShB,MAAMC,SAASpB;IAC9B,MAAMmF,MACJhD,UAAU,OAAOA,WAAW,YAAY,CAACV,MAAMC,OAAO,CAACS,UAClDA,SACD,CAAC;IACP,MAAM5B,SAAS4E,GAAG,CAAC,SAAS;IAC5B,MAAMxE,UAAUwE,GAAG,CAAC,UAAU;IAC9B,OAAO;QACL9E,YAAY8G,MAAMhC,GAAG,CAAC,aAAa,EAAEtE,iBAAiBT,kBAAkBC,UAAU;QAClFC,OAAO6G,MAAMhC,GAAG,CAAC,QAAQ,EAAErE,iBAAiBV,kBAAkBE,KAAK;QACnEC,QAAQA,WAAW,WAAWuB,eAAevB,UAAUA,SAASH,kBAAkBG,MAAM;QACxFC,SAAS2G,MAAMhC,GAAG,CAAC,UAAU,EAAEpE,eAAeX,kBAAkBI,OAAO;QACvEC,UAAU0G,MAAMhC,GAAG,CAAC,WAAW,EAAEnE,gBAAgBZ,kBAAkBK,QAAQ;QAC3EC,cAAcyG,MAAMhC,GAAG,CAAC,eAAe,EAAElE,oBAAoBb,kBAAkBM,YAAY;QAC3FC,SACE,OAAOA,YAAY,YAAYA,YAAY,MAAMA,QAAQ2B,MAAM,IAAIpB,kBAC/DP,UACAP,kBAAkBO,OAAO;QAC/BC,mBACE,OAAOuE,GAAG,CAAC,oBAAoB,KAAK,YAC/BA,GAAG,CAAC,oBAAoB,GACzB/E,kBAAkBQ,iBAAiB;IAC3C;AACF"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ export interface CountryOption {
18
+ /** ISO-3166-1 alpha-2, uppercase — the shape Stripe and Stripe Tax expect. */
19
+ code: string;
20
+ /** The name a customer reads. */
21
+ label: string;
22
+ /** Lowercased haystack: matches on the name and on the code. */
23
+ searchText: string;
24
+ }
25
+ export declare const COUNTRY_OPTIONS: CountryOption[];
26
+ /** The option for an ISO code, or null when it names no region we know. */
27
+ export declare function countryOption(code: string | null | undefined): CountryOption | null;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The country picker's options, DERIVED from the runtime's own region data.
18
+ *
19
+ * `Intl.supportedValuesOf` does not cover regions, so the list is obtained by
20
+ * asking `Intl.DisplayNames` about every two-letter code and keeping the ones
21
+ * it can name — a code it does not know is echoed back unchanged, which is the
22
+ * test. Roughly 250 rows, computed once at module load.
23
+ *
24
+ * The alternative was a checked-in list of country codes, which is the same
25
+ * mistake as a checked-in list of tax ID types in a slower-moving domain: it
26
+ * is a table nobody maintains, and the version that goes stale silently omits
27
+ * the country a customer is actually in.
28
+ *
29
+ * Stripe remains the validator. Anything this list gets wrong is refused by
30
+ * Stripe with its own message, which the billing address card surfaces.
31
+ */ /**
32
+ * Two-letter codes ICU names that are not countries Stripe bills to.
33
+ *
34
+ * DEGRADE-SAFE by construction: a code missing from this deny-list shows one
35
+ * extra row that Stripe would refuse with a clear message, so letting it go
36
+ * stale costs a confusing option and never a wrong address.
37
+ */ const NON_COUNTRY_REGIONS = new Set([
38
+ 'EU',
39
+ 'EZ',
40
+ 'UN',
41
+ 'QO',
42
+ 'XA',
43
+ 'XB',
44
+ 'ZZ'
45
+ ]);
46
+ const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
47
+ /** The region namer, or `null` on a runtime without full ICU. */ function regionNamer() {
48
+ try {
49
+ return new Intl.DisplayNames([
50
+ 'en'
51
+ ], {
52
+ type: 'region'
53
+ });
54
+ } catch (unused) {
55
+ return null;
56
+ }
57
+ }
58
+ function buildCountryOptions() {
59
+ const regionNames = regionNamer();
60
+ // An empty list is honest on a runtime that cannot name regions: the card
61
+ // falls back to a plain two-letter field rather than showing a broken
62
+ // picker with nothing in it.
63
+ if (!regionNames) return [];
64
+ const options = [];
65
+ for (const first of ALPHABET){
66
+ for (const second of ALPHABET){
67
+ const code = `${first}${second}`;
68
+ if (NON_COUNTRY_REGIONS.has(code)) continue;
69
+ let label;
70
+ try {
71
+ var _regionNames_of;
72
+ label = (_regionNames_of = regionNames.of(code)) != null ? _regionNames_of : code;
73
+ } catch (unused) {
74
+ continue;
75
+ }
76
+ // ICU echoes an unknown code back unchanged — that is how a real region
77
+ // is told from one of the 676 combinations that is not.
78
+ if (!label || label === code) continue;
79
+ options.push({
80
+ code,
81
+ label,
82
+ searchText: `${label} ${code}`.toLowerCase()
83
+ });
84
+ }
85
+ }
86
+ return options.sort((a, b)=>a.label.localeCompare(b.label));
87
+ }
88
+ export const COUNTRY_OPTIONS = buildCountryOptions();
89
+ /** The option for an ISO code, or null when it names no region we know. */ export function countryOption(code) {
90
+ var _COUNTRY_OPTIONS_find;
91
+ const wanted = String(code != null ? code : '').trim().toUpperCase();
92
+ if (!wanted) return null;
93
+ return (_COUNTRY_OPTIONS_find = COUNTRY_OPTIONS.find((option)=>option.code === wanted)) != null ? _COUNTRY_OPTIONS_find : null;
94
+ }
95
+
96
+ //# sourceMappingURL=country-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/country-options.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The country picker's options, DERIVED from the runtime's own region data.\n *\n * `Intl.supportedValuesOf` does not cover regions, so the list is obtained by\n * asking `Intl.DisplayNames` about every two-letter code and keeping the ones\n * it can name — a code it does not know is echoed back unchanged, which is the\n * test. Roughly 250 rows, computed once at module load.\n *\n * The alternative was a checked-in list of country codes, which is the same\n * mistake as a checked-in list of tax ID types in a slower-moving domain: it\n * is a table nobody maintains, and the version that goes stale silently omits\n * the country a customer is actually in.\n *\n * Stripe remains the validator. Anything this list gets wrong is refused by\n * Stripe with its own message, which the billing address card surfaces.\n */\n\n/**\n * Two-letter codes ICU names that are not countries Stripe bills to.\n *\n * DEGRADE-SAFE by construction: a code missing from this deny-list shows one\n * extra row that Stripe would refuse with a clear message, so letting it go\n * stale costs a confusing option and never a wrong address.\n */\nconst NON_COUNTRY_REGIONS = new Set([\n 'EU', // European Union\n 'EZ', // Eurozone\n 'UN', // United Nations\n 'QO', // Outlying Oceania\n 'XA', // pseudo-locale\n 'XB', // pseudo-locale\n 'ZZ', // Unknown Region\n])\n\nexport interface CountryOption {\n /** ISO-3166-1 alpha-2, uppercase — the shape Stripe and Stripe Tax expect. */\n code: string\n /** The name a customer reads. */\n label: string\n /** Lowercased haystack: matches on the name and on the code. */\n searchText: string\n}\n\nconst ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'\n\n/** The region namer, or `null` on a runtime without full ICU. */\nfunction regionNamer(): Intl.DisplayNames | null {\n try {\n return new Intl.DisplayNames(['en'], { type: 'region' })\n } catch {\n return null\n }\n}\n\nfunction buildCountryOptions(): CountryOption[] {\n const regionNames = regionNamer()\n // An empty list is honest on a runtime that cannot name regions: the card\n // falls back to a plain two-letter field rather than showing a broken\n // picker with nothing in it.\n if (!regionNames) return []\n const options: CountryOption[] = []\n for (const first of ALPHABET) {\n for (const second of ALPHABET) {\n const code = `${first}${second}`\n if (NON_COUNTRY_REGIONS.has(code)) continue\n let label: string\n try {\n label = regionNames.of(code) ?? code\n } catch {\n continue\n }\n // ICU echoes an unknown code back unchanged — that is how a real region\n // is told from one of the 676 combinations that is not.\n if (!label || label === code) continue\n options.push({ code, label, searchText: `${label} ${code}`.toLowerCase() })\n }\n }\n return options.sort((a, b) => a.label.localeCompare(b.label))\n}\n\nexport const COUNTRY_OPTIONS: CountryOption[] = buildCountryOptions()\n\n/** The option for an ISO code, or null when it names no region we know. */\nexport function countryOption(code: string | null | undefined): CountryOption | null {\n const wanted = String(code ?? '').trim().toUpperCase()\n if (!wanted) return null\n return COUNTRY_OPTIONS.find((option) => option.code === wanted) ?? null\n}\n"],"names":["NON_COUNTRY_REGIONS","Set","ALPHABET","regionNamer","Intl","DisplayNames","type","buildCountryOptions","regionNames","options","first","second","code","has","label","of","push","searchText","toLowerCase","sort","a","b","localeCompare","COUNTRY_OPTIONS","countryOption","wanted","String","trim","toUpperCase","find","option"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;CAeC,GAED;;;;;;CAMC,GACD,MAAMA,sBAAsB,IAAIC,IAAI;IAClC;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAWD,MAAMC,WAAW;AAEjB,+DAA+D,GAC/D,SAASC;IACP,IAAI;QACF,OAAO,IAAIC,KAAKC,YAAY,CAAC;YAAC;SAAK,EAAE;YAAEC,MAAM;QAAS;IACxD,EAAE,eAAM;QACN,OAAO;IACT;AACF;AAEA,SAASC;IACP,MAAMC,cAAcL;IACpB,0EAA0E;IAC1E,sEAAsE;IACtE,6BAA6B;IAC7B,IAAI,CAACK,aAAa,OAAO,EAAE;IAC3B,MAAMC,UAA2B,EAAE;IACnC,KAAK,MAAMC,SAASR,SAAU;QAC5B,KAAK,MAAMS,UAAUT,SAAU;YAC7B,MAAMU,OAAO,GAAGF,QAAQC,QAAQ;YAChC,IAAIX,oBAAoBa,GAAG,CAACD,OAAO;YACnC,IAAIE;YACJ,IAAI;oBACMN;gBAARM,SAAQN,kBAAAA,YAAYO,EAAE,CAACH,iBAAfJ,kBAAwBI;YAClC,EAAE,eAAM;gBACN;YACF;YACA,wEAAwE;YACxE,wDAAwD;YACxD,IAAI,CAACE,SAASA,UAAUF,MAAM;YAC9BH,QAAQO,IAAI,CAAC;gBAAEJ;gBAAME;gBAAOG,YAAY,GAAGH,MAAM,CAAC,EAAEF,MAAM,CAACM,WAAW;YAAG;QAC3E;IACF;IACA,OAAOT,QAAQU,IAAI,CAAC,CAACC,GAAGC,IAAMD,EAAEN,KAAK,CAACQ,aAAa,CAACD,EAAEP,KAAK;AAC7D;AAEA,OAAO,MAAMS,kBAAmChB,sBAAqB;AAErE,yEAAyE,GACzE,OAAO,SAASiB,cAAcZ,IAA+B;QAGpDW;IAFP,MAAME,SAASC,OAAOd,eAAAA,OAAQ,IAAIe,IAAI,GAAGC,WAAW;IACpD,IAAI,CAACH,QAAQ,OAAO;IACpB,QAAOF,wBAAAA,gBAAgBM,IAAI,CAAC,CAACC,SAAWA,OAAOlB,IAAI,KAAKa,mBAAjDF,wBAA4D;AACrE"}