@aglyn/aglyn 1.0.0-beta.277 → 1.0.0-beta.283
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -11
- package/src/lib/app-utils/account-preferences.d.ts +146 -11
- package/src/lib/app-utils/account-preferences.js +266 -18
- package/src/lib/app-utils/account-preferences.js.map +1 -1
- package/src/lib/app-utils/activity-presenter.js +2 -26
- package/src/lib/app-utils/activity-presenter.js.map +1 -1
- package/src/lib/app-utils/activity-type-labels.d.ts +18 -0
- package/src/lib/app-utils/activity-type-labels.js +46 -0
- package/src/lib/app-utils/activity-type-labels.js.map +1 -0
- package/src/lib/app-utils/artifact-provenance.d.ts +3 -1
- package/src/lib/app-utils/artifact-provenance.js.map +1 -1
- package/src/lib/app-utils/bundle-intent.d.ts +37 -0
- package/src/lib/app-utils/bundle-intent.js +47 -3
- package/src/lib/app-utils/bundle-intent.js.map +1 -1
- package/src/lib/app-utils/compose-reusable-components.d.ts +17 -1
- package/src/lib/app-utils/compose-reusable-components.js +80 -5
- package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +13 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +23 -11
- package/src/lib/app-utils/docs-help.generated.js +64 -7
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +359 -51
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/error-beacon.d.ts +19 -0
- package/src/lib/app-utils/error-beacon.js +28 -0
- package/src/lib/app-utils/error-beacon.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +3 -2
- package/src/lib/app-utils/health-report.js +22 -2
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/native-nav.d.ts +39 -4
- package/src/lib/app-utils/native-nav.js +69 -3
- package/src/lib/app-utils/native-nav.js.map +1 -1
- package/src/lib/app-utils/native-themes.d.ts +28 -17
- package/src/lib/app-utils/native-themes.js +123 -45
- package/src/lib/app-utils/native-themes.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.js +4 -2
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/org-permission-editor.d.ts +75 -0
- package/src/lib/app-utils/org-permission-editor.js +124 -0
- package/src/lib/app-utils/org-permission-editor.js.map +1 -0
- package/src/lib/app-utils/org-permissions.d.ts +36 -0
- package/src/lib/app-utils/org-permissions.js +78 -26
- package/src/lib/app-utils/org-permissions.js.map +1 -1
- package/src/lib/app-utils/org-role-grants.d.ts +59 -9
- package/src/lib/app-utils/org-role-grants.js +97 -10
- package/src/lib/app-utils/org-role-grants.js.map +1 -1
- package/src/lib/app-utils/plan-entitlements.d.ts +13 -0
- package/src/lib/app-utils/plan-entitlements.js +22 -0
- package/src/lib/app-utils/plan-entitlements.js.map +1 -1
- package/src/lib/app-utils/release-flags.d.ts +52 -1
- package/src/lib/app-utils/release-flags.js +68 -2
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/reusable-component-keys.d.ts +13 -0
- package/src/lib/app-utils/reusable-component-keys.js +12 -0
- package/src/lib/app-utils/reusable-component-keys.js.map +1 -1
- package/src/lib/app-utils/screen-analytics-aggregate.d.ts +34 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js +37 -0
- package/src/lib/app-utils/screen-analytics-aggregate.js.map +1 -1
- package/src/lib/app-utils/site-care.d.ts +35 -0
- package/src/lib/app-utils/site-care.js +48 -0
- package/src/lib/app-utils/site-care.js.map +1 -1
- package/src/lib/app-utils/site-font-pairings.d.ts +59 -0
- package/src/lib/app-utils/site-font-pairings.js +514 -0
- package/src/lib/app-utils/site-font-pairings.js.map +1 -0
- package/src/lib/app-utils/site-starter.d.ts +136 -0
- package/src/lib/app-utils/site-starter.js +116 -0
- package/src/lib/app-utils/site-starter.js.map +1 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker-server.d.ts +95 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker-server.js +333 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker-server.js.map +1 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker.d.ts +188 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker.js +193 -0
- package/src/lib/app-utils/stock-photos/stock-photo-picker.js.map +1 -0
- package/src/lib/plugin-manager/feature-plugins.d.ts +32 -0
- package/src/lib/plugin-manager/feature-plugins.js +12 -0
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +27 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +37 -0
- package/src/lib/plugin-manager/plugin-entitlements.js +84 -2
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +15 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -1
- package/src/lib/plugin-manager/plugin-permissions.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-product-catalog.d.ts +12 -0
- package/src/lib/plugin-manager/plugin-product-catalog.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-starters.d.ts +73 -0
- package/src/lib/plugin-manager/plugin-site-starters.js +30 -0
- package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -0
- package/src/lib/plugin-manager/plugin-sitemap-sections.d.ts +8 -1
- package/src/lib/plugin-manager/plugin-sitemap-sections.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +12 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
|
@@ -1 +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 * | `pinnedHosts` | {@link PinnedHost}s, in the order they were pinned | 5 a workspace, 100 in all |\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// ── Site pins ───────────────────────────────────────────────────────────────\n\n/** `users/{uid}` field holding the sites the person pinned, each with its workspace (AGL-4005). */\nexport const PINNED_HOSTS_FIELD = 'pinnedHosts'\n\n/** The most sites a person may pin in one workspace: five, as for workspaces. */\nexport const PINNED_HOSTS_PER_WORKSPACE = 5\n\n/**\n * The most site pins the account keeps across every workspace, the bound the\n * Firestore rules hold. Nobody meets it by pinning: it takes twenty\n * workspaces with five each. Past it, the oldest pins of OTHER workspaces\n * give way (see {@link pinHost}).\n */\nexport const PINNED_HOSTS_MAX = 100\n\n/**\n * One pinned site and the workspace it belongs to. The workspace rides with\n * the pin so a surface that holds only one workspace's sites (the site\n * switcher's window) can tell its own pins apart without reading every site.\n */\nexport interface PinnedHost {\n orgId: string\n hostId: string\n}\n\nfunction pinnedHostList(value: unknown): PinnedHost[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const pins: PinnedHost[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const { orgId, hostId } = entry as Record<string, unknown>\n if (typeof orgId !== 'string' || !orgId) continue\n if (typeof hostId !== 'string' || !hostId || seen.has(hostId)) continue\n seen.add(hostId)\n pins.push({ orgId, hostId })\n }\n return pins\n}\n\n/**\n * Every site pin the person stored, in the order pinned, without repeats.\n * Anything that is not a `{ orgId, hostId }` reads as no pin, so a stray\n * value can never take the sites list down.\n */\nexport function readPinnedHosts(profile: unknown): PinnedHost[] {\n return pinnedHostList(field(profile, PINNED_HOSTS_FIELD))\n}\n\n/** One workspace's pinned site ids, in the order pinned, at most five. */\nexport function workspacePinnedHosts(stored: readonly PinnedHost[], orgId: string): string[] {\n return pinnedHostList(stored)\n .filter((pin) => pin.orgId === orgId)\n .map((pin) => pin.hostId)\n .slice(0, PINNED_HOSTS_PER_WORKSPACE)\n}\n\nexport interface PinHostResult {\n /** Every stored pin after the request: unchanged unless `outcome` is `pinned`. */\n pinnedHosts: PinnedHost[]\n outcome: PinOrgOutcome\n}\n\n/**\n * Pins a site at the end of its workspace's pins, or says that workspace\n * already has five. As with workspaces, a full list is never made room in\n * quietly: the caller asks \"Pinned is full. Replace one?\" and calls\n * {@link replacePinnedHost} with the answer. Only the account-wide bound\n * ({@link PINNED_HOSTS_MAX}) makes room, and only from other workspaces.\n */\nexport function pinHost(stored: readonly PinnedHost[], pin: PinnedHost): PinHostResult {\n const current = pinnedHostList(stored)\n if (!pin.orgId || !pin.hostId) return { pinnedHosts: current, outcome: 'alreadyPinned' }\n const inWorkspace = workspacePinnedHosts(current, pin.orgId)\n if (inWorkspace.includes(pin.hostId)) return { pinnedHosts: current, outcome: 'alreadyPinned' }\n if (inWorkspace.length >= PINNED_HOSTS_PER_WORKSPACE) {\n return { pinnedHosts: current, outcome: 'full' }\n }\n // A site pinned under the workspace it has since moved out of moves with it.\n const next = [\n ...current.filter((entry) => entry.hostId !== pin.hostId),\n { orgId: pin.orgId, hostId: pin.hostId },\n ]\n while (next.length > PINNED_HOSTS_MAX) {\n const oldest = next.findIndex((entry) => entry.orgId !== pin.orgId)\n if (oldest === -1) break\n next.splice(oldest, 1)\n }\n return { pinnedHosts: next, outcome: 'pinned' }\n}\n\n/** The stored pins without one site. */\nexport function unpinHost(stored: readonly PinnedHost[], hostId: string): PinnedHost[] {\n return pinnedHostList(stored).filter((entry) => entry.hostId !== hostId)\n}\n\n/**\n * The stored pins with `pin` in the place `replacing` held in the same\n * workspace: the answer to \"Pinned is full. Replace one?\". Unchanged when\n * `replacing` is not one of that workspace's pins or `pin` already is.\n */\nexport function replacePinnedHost(\n stored: readonly PinnedHost[],\n replacing: string,\n pin: PinnedHost,\n): PinnedHost[] {\n const current = pinnedHostList(stored)\n const inWorkspace = workspacePinnedHosts(current, pin.orgId)\n if (!pin.hostId || inWorkspace.includes(pin.hostId) || !inWorkspace.includes(replacing)) {\n return current\n }\n return current\n .filter((entry) => entry.hostId !== pin.hostId)\n .map((entry) =>\n entry.hostId === replacing ? { orgId: pin.orgId, hostId: pin.hostId } : entry,\n )\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","PINNED_HOSTS_FIELD","PINNED_HOSTS_PER_WORKSPACE","PINNED_HOSTS_MAX","pinnedHostList","seen","pins","hostId","add","push","readPinnedHosts","workspacePinnedHosts","pin","pinHost","pinnedHosts","inWorkspace","oldest","findIndex","splice","unpinHost","replacePinnedHost","readRecentOrgs","recordRecentOrg","recent","railWorkspaces","orgIds","members","belongs","pinnedSet","rest","allTile","total","accountItemKey","item","kind","stringMap","out","key","Object","entries","keys","itemRef","raw","text","screen","params","label","at","Number","isFinite","isStarColor","readStarred","ref","color","starredAtMs","toggleStarred","starred","nowMs","some","setStarColor","readRecents","openedAtMs","recordRecent","recents","readWorkspaceColors","resolveWorkspaceColor","picked","brandColor","logoColor","source","tintsApp","oneOf","allowed","fallback","readAppTheme"],"mappings":"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"}
|
|
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 * | `pinnedHosts` | {@link PinnedHost}s, in the order they were pinned | 5 a workspace, 100 in all |\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 * | `tabBars` | {@link TabBarPins}, the phone's tab bar per site and per workspace | 4 areas each, 100 in all |\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/** `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 /**\n * `plain` keeps the sidebar on the theme's surface; `tinted` lays the THEME's accent over it at 5%, on a theme other\n * than Aglyn only (Aglyn's sidebar is always neutral). A workspace's color never tints it: that is its rail tile's.\n */\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 // Plain: a sidebar keeps the theme's neutral ground unless the person picks Tinted (AGL-3990).\n sidebarStyle: 'plain',\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// ── Site pins ───────────────────────────────────────────────────────────────\n\n/** `users/{uid}` field holding the sites the person pinned, each with its workspace (AGL-4005). */\nexport const PINNED_HOSTS_FIELD = 'pinnedHosts'\n\n/** The most sites a person may pin in one workspace: five, as for workspaces. */\nexport const PINNED_HOSTS_PER_WORKSPACE = 5\n\n/**\n * The most site pins the account keeps across every workspace, the bound the\n * Firestore rules hold. Nobody meets it by pinning: it takes twenty\n * workspaces with five each. Past it, the oldest pins of OTHER workspaces\n * give way (see {@link pinHost}).\n */\nexport const PINNED_HOSTS_MAX = 100\n\n/**\n * One pinned site and the workspace it belongs to. The workspace rides with\n * the pin so a surface that holds only one workspace's sites (the site\n * switcher's window) can tell its own pins apart without reading every site.\n */\nexport interface PinnedHost {\n orgId: string\n hostId: string\n}\n\nfunction pinnedHostList(value: unknown): PinnedHost[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const pins: PinnedHost[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const { orgId, hostId } = entry as Record<string, unknown>\n if (typeof orgId !== 'string' || !orgId) continue\n if (typeof hostId !== 'string' || !hostId || seen.has(hostId)) continue\n seen.add(hostId)\n pins.push({ orgId, hostId })\n }\n return pins\n}\n\n/**\n * Every site pin the person stored, in the order pinned, without repeats.\n * Anything that is not a `{ orgId, hostId }` reads as no pin, so a stray\n * value can never take the sites list down.\n */\nexport function readPinnedHosts(profile: unknown): PinnedHost[] {\n return pinnedHostList(field(profile, PINNED_HOSTS_FIELD))\n}\n\n/** One workspace's pinned site ids, in the order pinned, at most five. */\nexport function workspacePinnedHosts(stored: readonly PinnedHost[], orgId: string): string[] {\n return pinnedHostList(stored)\n .filter((pin) => pin.orgId === orgId)\n .map((pin) => pin.hostId)\n .slice(0, PINNED_HOSTS_PER_WORKSPACE)\n}\n\nexport interface PinHostResult {\n /** Every stored pin after the request: unchanged unless `outcome` is `pinned`. */\n pinnedHosts: PinnedHost[]\n outcome: PinOrgOutcome\n}\n\n/**\n * Pins a site at the end of its workspace's pins, or says that workspace\n * already has five. As with workspaces, a full list is never made room in\n * quietly: the caller asks \"Pinned is full. Replace one?\" and calls\n * {@link replacePinnedHost} with the answer. Only the account-wide bound\n * ({@link PINNED_HOSTS_MAX}) makes room, and only from other workspaces.\n */\nexport function pinHost(stored: readonly PinnedHost[], pin: PinnedHost): PinHostResult {\n const current = pinnedHostList(stored)\n if (!pin.orgId || !pin.hostId) return { pinnedHosts: current, outcome: 'alreadyPinned' }\n const inWorkspace = workspacePinnedHosts(current, pin.orgId)\n if (inWorkspace.includes(pin.hostId)) return { pinnedHosts: current, outcome: 'alreadyPinned' }\n if (inWorkspace.length >= PINNED_HOSTS_PER_WORKSPACE) {\n return { pinnedHosts: current, outcome: 'full' }\n }\n // A site pinned under the workspace it has since moved out of moves with it.\n const next = [\n ...current.filter((entry) => entry.hostId !== pin.hostId),\n { orgId: pin.orgId, hostId: pin.hostId },\n ]\n while (next.length > PINNED_HOSTS_MAX) {\n const oldest = next.findIndex((entry) => entry.orgId !== pin.orgId)\n if (oldest === -1) break\n next.splice(oldest, 1)\n }\n return { pinnedHosts: next, outcome: 'pinned' }\n}\n\n/** The stored pins without one site. */\nexport function unpinHost(stored: readonly PinnedHost[], hostId: string): PinnedHost[] {\n return pinnedHostList(stored).filter((entry) => entry.hostId !== hostId)\n}\n\n/**\n * The stored pins with `pin` in the place `replacing` held in the same\n * workspace: the answer to \"Pinned is full. Replace one?\". Unchanged when\n * `replacing` is not one of that workspace's pins or `pin` already is.\n */\nexport function replacePinnedHost(\n stored: readonly PinnedHost[],\n replacing: string,\n pin: PinnedHost,\n): PinnedHost[] {\n const current = pinnedHostList(stored)\n const inWorkspace = workspacePinnedHosts(current, pin.orgId)\n if (!pin.hostId || inWorkspace.includes(pin.hostId) || !inWorkspace.includes(replacing)) {\n return current\n }\n return current\n .filter((entry) => entry.hostId !== pin.hostId)\n .map((entry) =>\n entry.hostId === replacing ? { orgId: pin.orgId, hostId: pin.hostId } : entry,\n )\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 /**\n * Workspaces listed on the rail beyond the pins and the recents. Always\n * empty: every account reaches the rest through the All tile\n * (AGL-3996). The field stays so the apps that read it need no change.\n */\n rest: string[]\n /** Whether the last tile is \"All workspaces\", opening the searchable switcher: whenever there is a workspace. */\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, the same for every account however few\n * workspaces it has (Figma Rail 12:338, \"Many workspaces\"): up to five\n * pinned, a divider, up to three recent under \"Recent\", a divider, then the\n * \"All workspaces\" tile with the total, which opens the switcher where pins\n * are edited. A small account that saw every tile in one unlabeled column\n * had no way into the switcher and no Recent label or divider. `orgIds` is every membership, in the switcher's order;\n * 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 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: members.length > 0,\n total: members.length,\n }\n}\n\n/**\n * The one letter a workspace tile without a logo shows (Figma Rail/Workspace\n * tile, Type=Initial): the first letter or digit of its name, upper-cased;\n * \"?\" for a name with neither. One letter, not two: the tile's color, picked\n * per workspace, tells two workspaces with the same letter apart.\n */\nexport function workspaceInitial(name: string): string {\n const first = /[\\p{L}\\p{N}]/u.exec(name ?? '')\n return first ? first[0].toUpperCase() : '?'\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\n/**\n * The colors a workspace's tile takes when nothing else gives it one\n * ({@link workspaceStableColor}): the console's blue for text on a light\n * ground (primary.dark), deep tones of the brand palette's\n * violet and pink, and a rose, orange, gold, green and teal beside them,\n * each dark enough that the tile's white initial reads at 4.5:1 or more. The\n * slate is left out: it is the tile of a workspace with no id at all.\n */\nexport const WORKSPACE_STABLE_COLORS: readonly string[] = [\n '#0077AD',\n '#6A4FD8',\n '#A62BBF',\n '#C2185B',\n '#B4501A',\n '#8A6A00',\n '#2E7D5B',\n '#00796B',\n]\n\n/**\n * The color a workspace with no picked, brand or logo color shows on its\n * tile: one of {@link WORKSPACE_STABLE_COLORS}, chosen by its id, so the same\n * workspace has the same color on every device and in every app, and two\n * workspaces with the same initial usually differ. The id is hashed with\n * 32-bit FNV-1a over its UTF-16 code units, which each platform computes the\n * same way. Null for an empty id.\n */\nexport function workspaceStableColor(orgId: unknown): string | null {\n if (typeof orgId !== 'string' || orgId === '') return null\n let hash = 0x811c9dc5\n for (let i = 0; i < orgId.length; i++) {\n hash ^= orgId.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193) >>> 0\n }\n return WORKSPACE_STABLE_COLORS[hash % WORKSPACE_STABLE_COLORS.length] ?? null\n}\n\nexport type WorkspaceColorSource = 'picked' | 'brand' | 'logo' | 'stable' | '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 *\n * Given the workspace's id, a workspace with none of the three still gets a\n * color: its stable one ({@link workspaceStableColor}). That one colors only\n * its tile (the rail, the pickers, the menus, the sidebar's header), never\n * the app.\n */\nexport function resolveWorkspaceColor(\n picked: unknown,\n brandColor: unknown,\n logoColor: unknown,\n useWorkspaceBrand: boolean,\n orgId?: unknown,\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 const stable = workspaceStableColor(orgId)\n if (stable) return { color: stable, source: 'stable', tintsApp: false }\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\n// ── The phone's tab bar (AGL-4006) ──────────────────────────────────────────\n\n/** `users/{uid}` field holding the areas the person keeps in the phone's tab bar, per site and per workspace. */\nexport const TAB_BARS_FIELD = 'tabBars'\n\n/**\n * The most areas one tab bar holds beside Home: iOS and\n * Material take five destinations at most, and nothing ever overflows into a\n * More tab. With four, Create leaves the bar for the top bar.\n */\nexport const TAB_BAR_AREAS_MAX = 4\n\n/** The most sites and workspaces the account keeps a tab bar for, the bound the Firestore rules hold. */\nexport const TAB_BARS_MAX = 100\n\n/** The tab bar's own slots beside the areas: Home, always first, and Create new. */\nexport const TAB_BAR_HOME = 'home'\nexport const TAB_BAR_CREATE = 'create'\n\n/** One site's or workspace's tab bar. */\nexport interface TabBarPins {\n /** `site:<hostId>` or `org:<orgId>` ({@link tabBarScope}). */\n scope: string\n /** Nav manifest row ids, in the person's order, at most {@link TAB_BAR_AREAS_MAX}. */\n areas: string[]\n /**\n * The person pinned, unpinned or reordered here by hand. Until then the\n * first areas they open take the free spots and stay (the default the\n * orchestrator settled on 2026-10-20: nothing guessed, nothing rotating);\n * after it, only the person changes the bar.\n */\n chosen: boolean\n}\n\nexport type TabPinOutcome = 'pinned' | 'alreadyPinned' | 'full'\n\nexport interface TabPinResult {\n /** Every stored tab bar after the request: unchanged unless `outcome` is `pinned`. */\n tabBars: TabBarPins[]\n outcome: TabPinOutcome\n}\n\n/** What a phone's tab bar shows, in order, and where Create new goes. */\nexport interface TabBarLayout {\n /** {@link TAB_BAR_HOME}, the areas, and {@link TAB_BAR_CREATE} in the centre unless it moved to the top bar. */\n slots: string[]\n /** Four areas fill the bar, so Create new sits in the top bar beside the AI button. */\n createInTopBar: boolean\n}\n\n/** The key of one site's tab bar, or the whole workspace's. */\nexport function tabBarScope(orgId: string, hostId?: string | null): string {\n return hostId ? `site:${hostId}` : `org:${orgId}`\n}\n\nfunction tabAreaList(value: unknown): string[] {\n return idList(value)\n .filter((id) => id !== TAB_BAR_HOME && id !== TAB_BAR_CREATE)\n .slice(0, TAB_BAR_AREAS_MAX)\n}\n\nfunction tabBarList(value: unknown): TabBarPins[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n const out: TabBarPins[] = []\n for (const entry of value) {\n if (!entry || typeof entry !== 'object') continue\n const raw = entry as Record<string, unknown>\n const scope = raw['scope']\n if (typeof scope !== 'string' || !/^(site|org):./.test(scope) || seen.has(scope)) continue\n seen.add(scope)\n out.push({ scope, areas: tabAreaList(raw['areas']), chosen: raw['chosen'] === true })\n }\n return out.slice(-TAB_BARS_MAX)\n}\n\n/**\n * Every tab bar the person keeps, each well formed, without repeats. Anything\n * that is not a `{ scope, areas, chosen }` reads as no tab bar, so a stray\n * value can never take the phone's shell down.\n */\nexport function readTabBars(profile: unknown): TabBarPins[] {\n return tabBarList(field(profile, TAB_BARS_FIELD))\n}\n\n/** One scope's areas, in the person's order. */\nexport function tabBarAreas(tabBars: readonly TabBarPins[], scope: string): string[] {\n return tabBarList(tabBars).find((bar) => bar.scope === scope)?.areas ?? []\n}\n\n/**\n * Every tab bar with one scope's replaced. A scope seen for the first time\n * goes last; past {@link TAB_BARS_MAX} the oldest OTHER scopes give way.\n */\nfunction withTabBar(tabBars: readonly TabBarPins[], next: TabBarPins): TabBarPins[] {\n const current = tabBarList(tabBars)\n const at = current.findIndex((bar) => bar.scope === next.scope)\n const out = at === -1 ? [...current, next] : current.map((bar, index) => (index === at ? next : bar))\n while (out.length > TAB_BARS_MAX) {\n const oldest = out.findIndex((bar) => bar.scope !== next.scope)\n if (oldest === -1) break\n out.splice(oldest, 1)\n }\n return out\n}\n\n/**\n * Pins an area at the end of a scope's tab bar (long-press a tile, \"Add to tab\n * bar\"), or says the bar is full: a full bar is never made room in quietly.\n */\nexport function pinTabArea(\n tabBars: readonly TabBarPins[],\n scope: string,\n area: string,\n): TabPinResult {\n const current = tabBarList(tabBars)\n const areas = tabBarAreas(current, scope)\n if (!area || area === TAB_BAR_HOME || area === TAB_BAR_CREATE || areas.includes(area)) {\n return { tabBars: current, outcome: 'alreadyPinned' }\n }\n if (areas.length >= TAB_BAR_AREAS_MAX) return { tabBars: current, outcome: 'full' }\n return {\n tabBars: withTabBar(current, { scope, areas: [...areas, area], chosen: true }),\n outcome: 'pinned',\n }\n}\n\n/** A scope's tab bar without one area; the person chose, so opened areas no longer fill the spot. */\nexport function unpinTabArea(\n tabBars: readonly TabBarPins[],\n scope: string,\n area: string,\n): TabBarPins[] {\n const areas = tabBarAreas(tabBars, scope)\n return withTabBar(tabBars, { scope, areas: areas.filter((id) => id !== area), chosen: true })\n}\n\n/** A scope's tab bar with one area moved left (`-1`) or right (`1`) a place; unchanged at an end. */\nexport function moveTabArea(\n tabBars: readonly TabBarPins[],\n scope: string,\n area: string,\n offset: number,\n): TabBarPins[] {\n const areas = tabBarAreas(tabBars, scope)\n const from = areas.indexOf(area)\n const to = from + (offset < 0 ? -1 : 1)\n if (from === -1 || to < 0 || to >= areas.length) return tabBarList(tabBars)\n const next = [...areas]\n next[from] = next[to]\n next[to] = area\n return withTabBar(tabBars, { scope, areas: next, chosen: true })\n}\n\n/** A scope's tab bar as the Tab bar sheet left it: these areas, in this order, at most four. */\nexport function setTabAreas(\n tabBars: readonly TabBarPins[],\n scope: string,\n areas: readonly string[],\n): TabBarPins[] {\n return withTabBar(tabBars, { scope, areas: tabAreaList(areas), chosen: true })\n}\n\n/**\n * The default before anyone pins: an area the person opens takes a free spot\n * and stays there, until the bar is full or the person changes it by hand.\n * Unchanged once the scope is `chosen`, the area is in the bar already, or the\n * bar is full.\n */\nexport function autoPinTabArea(\n tabBars: readonly TabBarPins[],\n scope: string,\n area: string,\n): TabBarPins[] {\n const current = tabBarList(tabBars)\n const bar = current.find((entry) => entry.scope === scope)\n const areas = bar?.areas ?? []\n if (!area || area === TAB_BAR_HOME || area === TAB_BAR_CREATE) return current\n if (bar?.chosen || areas.includes(area) || areas.length >= TAB_BAR_AREAS_MAX) return current\n return withTabBar(current, { scope, areas: [...areas, area], chosen: false })\n}\n\n/**\n * The tab bar to draw: Home first, then the pinned areas the scope can open\n * (`available`, the Home tiles' ids in this build), with Create new in the\n * centre slot. Four areas fill the bar, and Create moves to the top bar.\n */\nexport function tabBarLayout(areas: readonly string[], available: readonly string[]): TabBarLayout {\n const open = new Set(available)\n const shown = tabAreaList(areas).filter((id) => open.has(id))\n if (shown.length >= TAB_BAR_AREAS_MAX) {\n return { slots: [TAB_BAR_HOME, ...shown], createInTopBar: true }\n }\n const slots = [TAB_BAR_HOME, ...shown]\n slots.splice(Math.floor((shown.length + 2) / 2), 0, TAB_BAR_CREATE)\n return { slots, createInTopBar: false }\n}\n\n// ── Who is signed in ────────────────────────────────────────────────────────\n\n/**\n * `users/{uid}` field holding the person's own picture (AGL-1961). Lowercase\n * `url`: the roster's `photoURL` spelling reads as nothing here.\n */\nexport const ACCOUNT_PHOTO_FIELD = 'photoUrl'\n\nfunction trimmed(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/**\n * The person's own picture, the same answer in the console and both apps\n * (AGL-4010): `users/{uid}.photoUrl` first (the profile they edit, filled from\n * the identity provider on every sign-in), else the sign-in record's\n * `photoURL`, else null, so the caller draws initials. A blank value is never\n * a picture, so a photo in either place wins over an empty one in the other.\n * A library pick is stored as a `/api/media/cdn/...` path; resolving it to an\n * address is the caller's (`mediaPreviewURL` on native).\n */\nexport function readAccountPhoto(profile: unknown, authPhotoUrl: unknown): string | null {\n return trimmed(field(profile, ACCOUNT_PHOTO_FIELD)) || trimmed(authPhotoUrl) || null\n}\n\n/**\n * The person's own name, the same answer in the console and both apps\n * (AGL-2486, AGL-4010): the profile's first and last name, else the sign-in\n * record's display name (empty for an SSO account), else the email address,\n * else empty.\n */\nexport function readAccountName(\n profile: unknown,\n authDisplayName: unknown,\n email: unknown,\n): string {\n const fromProfile = [field(profile, 'firstName'), field(profile, 'lastName')]\n .map(trimmed)\n .filter(Boolean)\n .join(' ')\n return fromProfile || trimmed(authDisplayName) || trimmed(email)\n}\n"],"names":["PINNED_ORGS_FIELD","PINNED_ORGS_MAX","RECENT_ORGS_FIELD","RECENT_ORGS_MAX","RAIL_RECENT_ORGS","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","PINNED_HOSTS_FIELD","PINNED_HOSTS_PER_WORKSPACE","PINNED_HOSTS_MAX","pinnedHostList","seen","pins","hostId","add","push","readPinnedHosts","workspacePinnedHosts","pin","pinHost","pinnedHosts","inWorkspace","oldest","findIndex","splice","unpinHost","replacePinnedHost","readRecentOrgs","recordRecentOrg","recent","railWorkspaces","orgIds","members","belongs","pinnedSet","rest","allTile","total","workspaceInitial","first","exec","toUpperCase","accountItemKey","item","kind","stringMap","out","key","Object","entries","keys","itemRef","raw","text","screen","params","label","at","Number","isFinite","isStarColor","readStarred","ref","color","starredAtMs","toggleStarred","starred","nowMs","some","setStarColor","readRecents","openedAtMs","recordRecent","recents","readWorkspaceColors","WORKSPACE_STABLE_COLORS","workspaceStableColor","hash","i","charCodeAt","Math","imul","resolveWorkspaceColor","picked","brandColor","logoColor","source","tintsApp","stable","oneOf","allowed","fallback","readAppTheme","TAB_BARS_FIELD","TAB_BAR_AREAS_MAX","TAB_BARS_MAX","TAB_BAR_HOME","TAB_BAR_CREATE","tabBarScope","tabAreaList","tabBarList","scope","areas","chosen","readTabBars","tabBarAreas","tabBars","find","bar","withTabBar","index","pinTabArea","area","unpinTabArea","moveTabArea","setTabAreas","autoPinTabArea","tabBarLayout","available","open","shown","slots","createInTopBar","floor","ACCOUNT_PHOTO_FIELD","trimmed","trim","readAccountPhoto","authPhotoUrl","readAccountName","authDisplayName","email","fromProfile","Boolean","join"],"mappings":"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"}
|
|
@@ -29,32 +29,8 @@ import { PLATFORM_BRAND_NAME } from "./platform-brand.js";
|
|
|
29
29
|
import { hostEventLabel } from "./host-events.js";
|
|
30
30
|
import { duplicateActivityActionLabel } from "./duplicate-resource.js";
|
|
31
31
|
import { pluginActivityActionLabel, pluginActivityGroupForAction, pluginActivityTargetLabel, pluginActivityTargetNoun } from "../plugin-manager/plugin-activity-actions.js";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
screen: 'Page',
|
|
35
|
-
layout: 'Layout',
|
|
36
|
-
theme: 'Theme',
|
|
37
|
-
media: 'Media',
|
|
38
|
-
content: 'Content',
|
|
39
|
-
variable: 'Variable',
|
|
40
|
-
function: 'Function',
|
|
41
|
-
workflow: 'Workflow',
|
|
42
|
-
member: 'Member',
|
|
43
|
-
component: 'Component',
|
|
44
|
-
template: 'Template',
|
|
45
|
-
org: 'Organization',
|
|
46
|
-
invite: 'Invitation',
|
|
47
|
-
subscription: 'Subscription',
|
|
48
|
-
// A custom role's permission flipped (AGL-2929).
|
|
49
|
-
role: 'Role',
|
|
50
|
-
// CRM records (AGL-2622): a person added, a company or a deal created or
|
|
51
|
-
// deleted, a lead converted — logged into the feed of the site whose
|
|
52
|
-
// console did it, and linked to the record the way a screen is.
|
|
53
|
-
contact: 'Contact',
|
|
54
|
-
lead: 'Lead',
|
|
55
|
-
company: 'Company',
|
|
56
|
-
deal: 'Deal'
|
|
57
|
-
};
|
|
32
|
+
import { ACTIVITY_TYPE_LABELS } from "./activity-type-labels.js";
|
|
33
|
+
/** Human, singular noun for a target type. Falls back to the raw type. */ const TYPE_LABELS = ACTIVITY_TYPE_LABELS;
|
|
58
34
|
/**
|
|
59
35
|
* A plugin's record, where its owner publishes an address for the kind
|
|
60
36
|
* (`plugin-record-routes`): its own page, or the list it lives on when the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/activity-presenter.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// Activity-feed presenter (AGL-810). The `hosts/{id}/activity` and\n// `orgs/{id}/activity` logs store a free-text `action` plus a `{ type, id,\n// name }` target, written from ~40 scattered call sites. The three renderers\n// (host card, host table, org card) used to each format entries their own\n// way — one dropped the target entirely, one printed the raw Firestore doc\n// id when no name was recorded. This module is the single place that turns a\n// stored entry into something a human reads, and into a deep link to the\n// thing that changed. It is deliberately light (the string route table and\n// the record-route registry) so plugin libs and the console app can both\n// import it via `@aglyn/aglyn/app-utils/activity-presenter`.\n\nimport { Route, buildRoute } from './console-routes'\nimport {\n pluginRecordHref,\n pluginRecordListHref,\n type PluginRecordRouteContext,\n} from '../plugin-manager/plugin-record-routes'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport { hostEventLabel } from './host-events'\nimport { duplicateActivityActionLabel } from './duplicate-resource'\nimport {\n pluginActivityActionLabel,\n pluginActivityGroupForAction,\n pluginActivityTargetLabel,\n pluginActivityTargetNoun,\n} from '../plugin-manager/plugin-activity-actions'\n\n/** The stored `target` sub-object, read defensively (any field may be absent). */\nexport interface ActivityTargetLike {\n type?: string\n id?: string\n name?: string\n /** Present on screen saves so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/** A stored activity document, read defensively. */\nexport interface ActivityEntryLike {\n action?: string\n target?: ActivityTargetLike | null\n // Some early org entries carried the type/target id at the top level rather\n // than nested under `target` — tolerate both so no entry renders blank.\n type?: string\n targetId?: string\n /**\n * The acting uid: a person's, the literal `'api'` for a key, a\n * `system:` id for a platform process, or null when no person acted.\n */\n actorId?: string | null\n /** The address the actor had when the entry was written; null for a key. */\n actorEmail?: string | null\n /**\n * The address `actorId` holds TODAY, looked up on read by a route with the\n * Admin SDK, for an entry that recorded a uid and no address.\n */\n actorEmailNow?: string | null\n /**\n * For a STAFF reader only: the staff member behind an entry the workspace\n * did not perform (a staff cancellation), resolved by the route.\n */\n staffActorEmail?: string | null\n /** The name of the API key that wrote the entry, when a key did (AGL-2632). */\n apiKeyName?: string | null\n}\n\n/**\n * Who a host activity entry is attributed to — the writer's half of the\n * contract whose reader is {@link activityActorLabel}.\n *\n * A person is their verified uid and the address they had at the time. An\n * integration is the literal uid `'api'`, no address, and the name the\n * organization gave its key — carried because a feed that says \"Someone\"\n * about every write a key made is an audit trail with a hole in it: three\n * Zapier zaps and a nightly import all read as the same nobody.\n */\nexport interface HostActivityActor {\n uid: string\n email?: string | null\n /** The API key's name, for an entry an integration wrote. */\n apiKeyName?: string | null\n}\n\n/**\n * What the presenter needs to build a link. `host` is the `[host]` URL\n * segment — the site SLUG the console routes by (AGL-622), NOT the host doc\n * id — so pass `useParams().host`, not the doc id. When it is present the\n * entry is treated as host-scoped, otherwise org-scoped — which\n * disambiguates `member`, the one type both logs use.\n */\nexport interface ActivityLinkContext {\n orgSlug?: string\n host?: string\n}\n\n/** Human, singular noun for a target type. Falls back to the raw type. */\nconst TYPE_LABELS: Record<string, string> = {\n host: 'Site',\n screen: 'Page',\n layout: 'Layout',\n theme: 'Theme',\n media: 'Media',\n content: 'Content',\n variable: 'Variable',\n function: 'Function',\n workflow: 'Workflow',\n member: 'Member',\n component: 'Component',\n template: 'Template',\n org: 'Organization',\n invite: 'Invitation',\n subscription: 'Subscription',\n // A custom role's permission flipped (AGL-2929).\n role: 'Role',\n // CRM records (AGL-2622): a person added, a company or a deal created or\n // deleted, a lead converted — logged into the feed of the site whose\n // console did it, and linked to the record the way a screen is.\n contact: 'Contact',\n lead: 'Lead',\n company: 'Company',\n deal: 'Deal',\n}\n\n/**\n * A plugin's record, where its owner publishes an address for the kind\n * (`plugin-record-routes`): its own page, or the list it lives on when the\n * entry named no id — a deleted record still has a list to land on.\n * `undefined` for a kind no plugin publishes here.\n */\nfunction pluginRecordActivityHref(\n type: string | undefined,\n id: string | undefined,\n context: PluginRecordRouteContext,\n): string | undefined {\n if (!type) return undefined\n return (id ? pluginRecordHref(type, context, id) : pluginRecordListHref(type, context)) ?? undefined\n}\n\n/**\n * Human noun for a target type, e.g. `'screen'` → `'Screen'`. A plugin's\n * own resource is stored as `pluginId:noun` (AGL-2978), and a reader sees\n * the noun: `acme-mail:mailbox` → `Mailbox`.\n */\nexport function activityTypeLabel(type: string | undefined): string {\n if (!type) return 'Item'\n const known = TYPE_LABELS[type] ?? pluginActivityTargetLabel(type)\n if (known) return known\n const noun = pluginActivityTargetNoun(type) ?? type\n return noun.charAt(0).toUpperCase() + noun.slice(1)\n}\n\n/**\n * A label for the target column/link. Prefers the recorded name; when none\n * was written (older entries, id-only saves) it degrades to the humanized\n * type — NEVER the raw doc id, which is meaningless to a reader.\n */\nexport function activityTargetLabel(\n target: ActivityTargetLike | null | undefined,\n): string {\n if (target?.name) return target.name\n return activityTypeLabel(target?.type)\n}\n\n/**\n * What the action column reads (AGL-2929). Most actions are the sentence\n * their writer stored; a plugin's rows store a code (`ai.job.output`) so\n * that three surfaces can recognize them without parsing prose, and the\n * code is translated here — once, from the plugin-declared catalog\n * (AGL-2940) — rather than shown to a reader as a dotted path.\n */\nexport function activityActionLabel(action: string | undefined): string {\n const stored = action?.trim() ?? ''\n return (\n pluginActivityActionLabel(stored) ??\n duplicateActivityActionLabel(stored) ??\n stored\n )\n}\n\n/**\n * The filter group an entry's code belongs to — the chip that keeps it —\n * or `undefined` for a prose action no plugin declared.\n */\nexport function activityEntryGroupId(entry: ActivityEntryLike): string | undefined {\n return pluginActivityGroupForAction(entry.action?.trim())?.id\n}\n\n/**\n * The primary line for a feed entry: the action, suffixed with the target\n * name when one is known, so \"Saved the screen\" becomes \"Saved the screen —\n * Home\". A missing action degrades to the target label rather than blank.\n */\nexport function activityPrimaryText(entry: ActivityEntryLike): string {\n const action = activityActionLabel(entry.action)\n const name = entry.target?.name?.trim()\n if (action && name) return `${action} — ${name}`\n if (action) return action\n return activityTargetLabel(entry.target)\n}\n\n/** How an integration's key is named wherever an actor is: `API key Zapier`. */\nexport function apiKeyActorLabel(name: string): string {\n return `API key ${name}`\n}\n\n/**\n * The \"who\" line for an entry — always someone a reader can narrow down.\n *\n * In order: the address recorded when the entry was written; the key that\n * wrote it, by name; the address the recorded uid holds now, when the writer\n * held a uid and no address (the billing webhook, before it carried the\n * address stamped at the console act); else the uid itself, for an account\n * that no longer resolves.\n *\n * An entry with no uid at all is not an anonymous one. Every writer that\n * leaves `actorId` null is a platform process, so the feed names it: a\n * workflow run by its workflow, anything else — the billing webhook,\n * inbound CRM mail, a background AI job, a staff cancellation — as the\n * platform; a staff reader is also told which staff member. Never \"Someone\", which read as a person nobody could identify\n * (AGL-3369).\n */\nexport function activityActorLabel(entry: ActivityEntryLike): string {\n if (entry.actorEmail) return entry.actorEmail\n if (entry.apiKeyName) return apiKeyActorLabel(entry.apiKeyName)\n const actorId = entry.actorId?.trim()\n if (actorId === 'api') return 'API key'\n if (actorId && !actorId.startsWith('system:')) {\n return entry.actorEmailNow || `Account ${actorId}`\n }\n const target = entry.target ?? undefined\n if ((target?.type ?? entry.type) === 'workflow') {\n const name = target?.name?.trim()\n return name ? `Workflow ${name}` : 'Workflow'\n }\n if (entry.staffActorEmail) {\n return `${PLATFORM_BRAND_NAME} staff (${entry.staffActorEmail})`\n }\n return PLATFORM_BRAND_NAME\n}\n\n/**\n * Who set a run off (AGL-3376), as a run row stores it: the actor the event's\n * door named, carried through the engine. `webhook` is an inbound webhook's\n * caller, named by the hook.\n */\nexport interface RunTriggeredByLike {\n kind?: string\n uid?: string\n email?: string\n apiKeyName?: string\n name?: string\n}\n\n/**\n * The run history's Who: whoever set the run off, never the automation\n * itself — every row in a run history is that automation, so naming it\n * again would answer nothing. A run written before its trigger was recorded\n * says so rather than guessing from the event's name.\n */\nexport function runTriggeredByLabel(entry: {\n triggeredBy?: RunTriggeredByLike | null\n}): string {\n const by = entry.triggeredBy\n switch (by?.kind) {\n case 'member':\n return by.email || (by.uid ? `Account ${by.uid}` : 'A member')\n case 'apiKey':\n return by.apiKeyName ? apiKeyActorLabel(by.apiKeyName) : 'API key'\n case 'visitor':\n return by.email ? `Site visitor (${by.email})` : 'Site visitor'\n case 'webhook':\n return by.name ? `Inbound webhook ${by.name}` : 'Inbound webhook'\n case 'platform':\n return PLATFORM_BRAND_NAME\n default:\n return 'Not recorded'\n }\n}\n\n/**\n * A deep link to the resource an entry points at, or `undefined` when it\n * cannot be built (missing context, or a type with nowhere sensible to go).\n * `buildRoute` emits `<orgSlug?>` placeholders for missing params, so every\n * branch guards its inputs and returns `undefined` rather than a dead link.\n */\nexport function activityHref(\n entry: ActivityEntryLike,\n ctx: ActivityLinkContext,\n): string | undefined {\n const { orgSlug, host } = ctx\n if (!orgSlug) return undefined\n const target = entry.target ?? undefined\n const type = target?.type ?? entry.type\n const id = target?.id ?? entry.targetId\n const versionId = target?.versionId\n\n // Host-scoped entries: every host route needs both orgSlug and host.\n if (host) {\n switch (type) {\n case 'host':\n return buildRoute(Route.HOST_DASHBOARD, { orgSlug, host })\n case 'screen':\n // The screen detail page is version-addressed; without a versionId\n // there is no \"current\" pointer to route through, so land on the list.\n return id && versionId\n ? buildRoute(Route.SCREEN_DETAILS, {\n orgSlug,\n host,\n screenId: id,\n versionId,\n })\n : buildRoute(Route.HOST_SCREENS, { orgSlug, host })\n case 'component':\n return id\n ? buildRoute(Route.COMPONENT_DETAILS, {\n orgSlug,\n host,\n componentId: id,\n })\n : buildRoute(Route.HOST_COMPONENTS, { orgSlug, host })\n case 'template':\n return id\n ? buildRoute(Route.TEMPLATE_DETAILS, {\n orgSlug,\n host,\n templateId: id,\n })\n : buildRoute(Route.HOST_TEMPLATES, { orgSlug, host })\n case 'layout':\n return id\n ? buildRoute(Route.LAYOUT_DETAILS, { orgSlug, host, layoutId: id })\n : buildRoute(Route.HOST_LAYOUTS, { orgSlug, host })\n case 'theme':\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n case 'media':\n return buildRoute(Route.HOST_MEDIA, { orgSlug, host })\n case 'content':\n return buildRoute(Route.HOST_CONTENT, { orgSlug, host })\n case 'variable':\n return buildRoute(Route.HOST_DATA, { orgSlug, host })\n case 'function':\n return buildRoute(Route.HOST_LOGIC, { orgSlug, host })\n case 'workflow':\n return buildRoute(Route.HOST_AUTOMATION, { orgSlug, host })\n case 'member':\n // Site team is managed on the Setup page — there is no per-member\n // host route.\n return buildRoute(Route.HOST_SETUP, { orgSlug, host })\n default:\n // A plugin's record — a contact, a deal, an order — at the address\n // its owner publishes, which the route table does not hold.\n return pluginRecordActivityHref(type, id, { orgSlug, host })\n }\n }\n\n // Org-scoped entries.\n switch (type) {\n case 'org':\n return buildRoute(Route.ORG_SETTINGS, { orgSlug })\n case 'member':\n return id\n ? buildRoute(Route.MANAGE_TEAM_MEMBER, { orgSlug, uid: id })\n : buildRoute(Route.MANAGE_TEAM, { orgSlug })\n case 'invite':\n return buildRoute(Route.MANAGE_TEAM, { orgSlug })\n default:\n // A plugin's record acted on at the ORGANIZATION level (AGL-2634) — a\n // deal moved from the org board, a merge, a bulk bar's action — at the\n // organization address its owner publishes; a bulk line names no\n // record and lands on the list.\n return pluginRecordActivityHref(type, id, { orgSlug, host: null })\n }\n}\n\n/**\n * How an automation run ended (AGL-2171).\n *\n * `skipped` is a first-class outcome, not an absence. An unmet condition\n * used to be a bare `continue` that wrote nothing, so \"why didn't my\n * automation fire?\" — the most common support question about automations —\n * had no answer anywhere in the product, while `/product/workflows`\n * advertises an amber `Skipped` row that answers it.\n */\nexport type ActionRunResult = 'succeeded' | 'failed' | 'skipped'\n\n/** A stored run entry: an activity document with the run fields on it. */\nexport interface ActionRunEntryLike extends ActivityEntryLike {\n result?: string\n trigger?: string\n summary?: string\n}\n\n/**\n * `formSubmission` → `Form submitted`.\n *\n * The console rendered the raw camelCase identifier in the trigger select\n * and in every run row. The words are each event's declared label\n * (`host-events.ts`) — one map, so the picker an author chose from and the\n * run row they read afterwards cannot name the same event differently.\n */\nexport function actionTriggerLabel(event: string | undefined): string {\n return hostEventLabel(event)\n}\n\n/**\n * The run's outcome, tolerating rows written before it was recorded.\n *\n * A legacy entry carries only the prose `action` string, so it is read\n * back: `with errors:` means it failed and `Action ran on` means it\n * succeeded. Anything else is not a run at all — the activity collection\n * also holds publishes, media saves and member changes, and calling one of\n * those `succeeded` would put it in the run history under a verdict that\n * means nothing for it.\n */\nexport function actionRunResult(\n entry: ActionRunEntryLike,\n): ActionRunResult | undefined {\n const stored = String(entry.result ?? '').trim()\n if (stored === 'succeeded' || stored === 'failed' || stored === 'skipped') {\n return stored\n }\n const action = String(entry.action ?? '')\n if (!action.startsWith('Action ran on')) return undefined\n return action.includes('with errors:') ? 'failed' : 'succeeded'\n}\n\n/**\n * The `What happened` column: `Sent email · saved to Leads · webhook 200`.\n *\n * Falls back to the legacy prose, minus its `Action ran on <event>` prefix\n * — that fact is the Trigger column now, and repeating it in the summary\n * would make every historic row say the same thing twice.\n */\nexport function actionRunSummary(entry: ActionRunEntryLike): string {\n const summary = String(entry.summary ?? '').trim()\n if (summary) return summary\n const action = String(entry.action ?? '').trim()\n const errors = action.match(/with errors:\\s*(.+)$/)\n if (errors) return errors[1]\n // A pre-AGL-2171 success recorded nothing but the fact that it ran.\n if (action.startsWith('Action ran on')) return 'Ran'\n return action\n}\n"],"names":["Route","buildRoute","pluginRecordHref","pluginRecordListHref","PLATFORM_BRAND_NAME","hostEventLabel","duplicateActivityActionLabel","pluginActivityActionLabel","pluginActivityGroupForAction","pluginActivityTargetLabel","pluginActivityTargetNoun","TYPE_LABELS","host","screen","layout","theme","media","content","variable","function","workflow","member","component","template","org","invite","subscription","role","contact","lead","company","deal","pluginRecordActivityHref","type","id","context","undefined","activityTypeLabel","known","noun","charAt","toUpperCase","slice","activityTargetLabel","target","name","activityActionLabel","action","stored","trim","activityEntryGroupId","entry","activityPrimaryText","apiKeyActorLabel","activityActorLabel","actorEmail","apiKeyName","actorId","startsWith","actorEmailNow","staffActorEmail","runTriggeredByLabel","by","triggeredBy","kind","email","uid","activityHref","ctx","orgSlug","targetId","versionId","HOST_DASHBOARD","SCREEN_DETAILS","screenId","HOST_SCREENS","COMPONENT_DETAILS","componentId","HOST_COMPONENTS","TEMPLATE_DETAILS","templateId","HOST_TEMPLATES","LAYOUT_DETAILS","layoutId","HOST_LAYOUTS","HOST_SETUP_THEME","HOST_MEDIA","HOST_CONTENT","HOST_DATA","HOST_LOGIC","HOST_AUTOMATION","HOST_SETUP","ORG_SETTINGS","MANAGE_TEAM_MEMBER","MANAGE_TEAM","actionTriggerLabel","event","actionRunResult","String","result","includes","actionRunSummary","summary","errors","match"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/activity-presenter.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// Activity-feed presenter (AGL-810). The `hosts/{id}/activity` and\n// `orgs/{id}/activity` logs store a free-text `action` plus a `{ type, id,\n// name }` target, written from ~40 scattered call sites. The three renderers\n// (host card, host table, org card) used to each format entries their own\n// way — one dropped the target entirely, one printed the raw Firestore doc\n// id when no name was recorded. This module is the single place that turns a\n// stored entry into something a human reads, and into a deep link to the\n// thing that changed. It is deliberately light (the string route table and\n// the record-route registry) so plugin libs and the console app can both\n// import it via `@aglyn/aglyn/app-utils/activity-presenter`.\n\nimport { Route, buildRoute } from './console-routes'\nimport {\n pluginRecordHref,\n pluginRecordListHref,\n type PluginRecordRouteContext,\n} from '../plugin-manager/plugin-record-routes'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport { hostEventLabel } from './host-events'\nimport { duplicateActivityActionLabel } from './duplicate-resource'\nimport {\n pluginActivityActionLabel,\n pluginActivityGroupForAction,\n pluginActivityTargetLabel,\n pluginActivityTargetNoun,\n} from '../plugin-manager/plugin-activity-actions'\nimport { ACTIVITY_TYPE_LABELS } from './activity-type-labels'\n\n/** The stored `target` sub-object, read defensively (any field may be absent). */\nexport interface ActivityTargetLike {\n type?: string\n id?: string\n name?: string\n /** Present on screen saves so the deep link can hit the exact version. */\n versionId?: string\n}\n\n/** A stored activity document, read defensively. */\nexport interface ActivityEntryLike {\n action?: string\n target?: ActivityTargetLike | null\n // Some early org entries carried the type/target id at the top level rather\n // than nested under `target` — tolerate both so no entry renders blank.\n type?: string\n targetId?: string\n /**\n * The acting uid: a person's, the literal `'api'` for a key, a\n * `system:` id for a platform process, or null when no person acted.\n */\n actorId?: string | null\n /** The address the actor had when the entry was written; null for a key. */\n actorEmail?: string | null\n /**\n * The address `actorId` holds TODAY, looked up on read by a route with the\n * Admin SDK, for an entry that recorded a uid and no address.\n */\n actorEmailNow?: string | null\n /**\n * For a STAFF reader only: the staff member behind an entry the workspace\n * did not perform (a staff cancellation), resolved by the route.\n */\n staffActorEmail?: string | null\n /** The name of the API key that wrote the entry, when a key did (AGL-2632). */\n apiKeyName?: string | null\n}\n\n/**\n * Who a host activity entry is attributed to — the writer's half of the\n * contract whose reader is {@link activityActorLabel}.\n *\n * A person is their verified uid and the address they had at the time. An\n * integration is the literal uid `'api'`, no address, and the name the\n * organization gave its key — carried because a feed that says \"Someone\"\n * about every write a key made is an audit trail with a hole in it: three\n * Zapier zaps and a nightly import all read as the same nobody.\n */\nexport interface HostActivityActor {\n uid: string\n email?: string | null\n /** The API key's name, for an entry an integration wrote. */\n apiKeyName?: string | null\n}\n\n/**\n * What the presenter needs to build a link. `host` is the `[host]` URL\n * segment — the site SLUG the console routes by (AGL-622), NOT the host doc\n * id — so pass `useParams().host`, not the doc id. When it is present the\n * entry is treated as host-scoped, otherwise org-scoped — which\n * disambiguates `member`, the one type both logs use.\n */\nexport interface ActivityLinkContext {\n orgSlug?: string\n host?: string\n}\n\n/** Human, singular noun for a target type. Falls back to the raw type. */\nconst TYPE_LABELS = ACTIVITY_TYPE_LABELS\n\n/**\n * A plugin's record, where its owner publishes an address for the kind\n * (`plugin-record-routes`): its own page, or the list it lives on when the\n * entry named no id — a deleted record still has a list to land on.\n * `undefined` for a kind no plugin publishes here.\n */\nfunction pluginRecordActivityHref(\n type: string | undefined,\n id: string | undefined,\n context: PluginRecordRouteContext,\n): string | undefined {\n if (!type) return undefined\n return (id ? pluginRecordHref(type, context, id) : pluginRecordListHref(type, context)) ?? undefined\n}\n\n/**\n * Human noun for a target type, e.g. `'screen'` → `'Screen'`. A plugin's\n * own resource is stored as `pluginId:noun` (AGL-2978), and a reader sees\n * the noun: `acme-mail:mailbox` → `Mailbox`.\n */\nexport function activityTypeLabel(type: string | undefined): string {\n if (!type) return 'Item'\n const known = TYPE_LABELS[type] ?? pluginActivityTargetLabel(type)\n if (known) return known\n const noun = pluginActivityTargetNoun(type) ?? type\n return noun.charAt(0).toUpperCase() + noun.slice(1)\n}\n\n/**\n * A label for the target column/link. Prefers the recorded name; when none\n * was written (older entries, id-only saves) it degrades to the humanized\n * type — NEVER the raw doc id, which is meaningless to a reader.\n */\nexport function activityTargetLabel(\n target: ActivityTargetLike | null | undefined,\n): string {\n if (target?.name) return target.name\n return activityTypeLabel(target?.type)\n}\n\n/**\n * What the action column reads (AGL-2929). Most actions are the sentence\n * their writer stored; a plugin's rows store a code (`ai.job.output`) so\n * that three surfaces can recognize them without parsing prose, and the\n * code is translated here — once, from the plugin-declared catalog\n * (AGL-2940) — rather than shown to a reader as a dotted path.\n */\nexport function activityActionLabel(action: string | undefined): string {\n const stored = action?.trim() ?? ''\n return (\n pluginActivityActionLabel(stored) ??\n duplicateActivityActionLabel(stored) ??\n stored\n )\n}\n\n/**\n * The filter group an entry's code belongs to — the chip that keeps it —\n * or `undefined` for a prose action no plugin declared.\n */\nexport function activityEntryGroupId(entry: ActivityEntryLike): string | undefined {\n return pluginActivityGroupForAction(entry.action?.trim())?.id\n}\n\n/**\n * The primary line for a feed entry: the action, suffixed with the target\n * name when one is known, so \"Saved the screen\" becomes \"Saved the screen —\n * Home\". A missing action degrades to the target label rather than blank.\n */\nexport function activityPrimaryText(entry: ActivityEntryLike): string {\n const action = activityActionLabel(entry.action)\n const name = entry.target?.name?.trim()\n if (action && name) return `${action} — ${name}`\n if (action) return action\n return activityTargetLabel(entry.target)\n}\n\n/** How an integration's key is named wherever an actor is: `API key Zapier`. */\nexport function apiKeyActorLabel(name: string): string {\n return `API key ${name}`\n}\n\n/**\n * The \"who\" line for an entry — always someone a reader can narrow down.\n *\n * In order: the address recorded when the entry was written; the key that\n * wrote it, by name; the address the recorded uid holds now, when the writer\n * held a uid and no address (the billing webhook, before it carried the\n * address stamped at the console act); else the uid itself, for an account\n * that no longer resolves.\n *\n * An entry with no uid at all is not an anonymous one. Every writer that\n * leaves `actorId` null is a platform process, so the feed names it: a\n * workflow run by its workflow, anything else — the billing webhook,\n * inbound CRM mail, a background AI job, a staff cancellation — as the\n * platform; a staff reader is also told which staff member. Never \"Someone\", which read as a person nobody could identify\n * (AGL-3369).\n */\nexport function activityActorLabel(entry: ActivityEntryLike): string {\n if (entry.actorEmail) return entry.actorEmail\n if (entry.apiKeyName) return apiKeyActorLabel(entry.apiKeyName)\n const actorId = entry.actorId?.trim()\n if (actorId === 'api') return 'API key'\n if (actorId && !actorId.startsWith('system:')) {\n return entry.actorEmailNow || `Account ${actorId}`\n }\n const target = entry.target ?? undefined\n if ((target?.type ?? entry.type) === 'workflow') {\n const name = target?.name?.trim()\n return name ? `Workflow ${name}` : 'Workflow'\n }\n if (entry.staffActorEmail) {\n return `${PLATFORM_BRAND_NAME} staff (${entry.staffActorEmail})`\n }\n return PLATFORM_BRAND_NAME\n}\n\n/**\n * Who set a run off (AGL-3376), as a run row stores it: the actor the event's\n * door named, carried through the engine. `webhook` is an inbound webhook's\n * caller, named by the hook.\n */\nexport interface RunTriggeredByLike {\n kind?: string\n uid?: string\n email?: string\n apiKeyName?: string\n name?: string\n}\n\n/**\n * The run history's Who: whoever set the run off, never the automation\n * itself — every row in a run history is that automation, so naming it\n * again would answer nothing. A run written before its trigger was recorded\n * says so rather than guessing from the event's name.\n */\nexport function runTriggeredByLabel(entry: {\n triggeredBy?: RunTriggeredByLike | null\n}): string {\n const by = entry.triggeredBy\n switch (by?.kind) {\n case 'member':\n return by.email || (by.uid ? `Account ${by.uid}` : 'A member')\n case 'apiKey':\n return by.apiKeyName ? apiKeyActorLabel(by.apiKeyName) : 'API key'\n case 'visitor':\n return by.email ? `Site visitor (${by.email})` : 'Site visitor'\n case 'webhook':\n return by.name ? `Inbound webhook ${by.name}` : 'Inbound webhook'\n case 'platform':\n return PLATFORM_BRAND_NAME\n default:\n return 'Not recorded'\n }\n}\n\n/**\n * A deep link to the resource an entry points at, or `undefined` when it\n * cannot be built (missing context, or a type with nowhere sensible to go).\n * `buildRoute` emits `<orgSlug?>` placeholders for missing params, so every\n * branch guards its inputs and returns `undefined` rather than a dead link.\n */\nexport function activityHref(\n entry: ActivityEntryLike,\n ctx: ActivityLinkContext,\n): string | undefined {\n const { orgSlug, host } = ctx\n if (!orgSlug) return undefined\n const target = entry.target ?? undefined\n const type = target?.type ?? entry.type\n const id = target?.id ?? entry.targetId\n const versionId = target?.versionId\n\n // Host-scoped entries: every host route needs both orgSlug and host.\n if (host) {\n switch (type) {\n case 'host':\n return buildRoute(Route.HOST_DASHBOARD, { orgSlug, host })\n case 'screen':\n // The screen detail page is version-addressed; without a versionId\n // there is no \"current\" pointer to route through, so land on the list.\n return id && versionId\n ? buildRoute(Route.SCREEN_DETAILS, {\n orgSlug,\n host,\n screenId: id,\n versionId,\n })\n : buildRoute(Route.HOST_SCREENS, { orgSlug, host })\n case 'component':\n return id\n ? buildRoute(Route.COMPONENT_DETAILS, {\n orgSlug,\n host,\n componentId: id,\n })\n : buildRoute(Route.HOST_COMPONENTS, { orgSlug, host })\n case 'template':\n return id\n ? buildRoute(Route.TEMPLATE_DETAILS, {\n orgSlug,\n host,\n templateId: id,\n })\n : buildRoute(Route.HOST_TEMPLATES, { orgSlug, host })\n case 'layout':\n return id\n ? buildRoute(Route.LAYOUT_DETAILS, { orgSlug, host, layoutId: id })\n : buildRoute(Route.HOST_LAYOUTS, { orgSlug, host })\n case 'theme':\n return buildRoute(Route.HOST_SETUP_THEME, { orgSlug, host })\n case 'media':\n return buildRoute(Route.HOST_MEDIA, { orgSlug, host })\n case 'content':\n return buildRoute(Route.HOST_CONTENT, { orgSlug, host })\n case 'variable':\n return buildRoute(Route.HOST_DATA, { orgSlug, host })\n case 'function':\n return buildRoute(Route.HOST_LOGIC, { orgSlug, host })\n case 'workflow':\n return buildRoute(Route.HOST_AUTOMATION, { orgSlug, host })\n case 'member':\n // Site team is managed on the Setup page — there is no per-member\n // host route.\n return buildRoute(Route.HOST_SETUP, { orgSlug, host })\n default:\n // A plugin's record — a contact, a deal, an order — at the address\n // its owner publishes, which the route table does not hold.\n return pluginRecordActivityHref(type, id, { orgSlug, host })\n }\n }\n\n // Org-scoped entries.\n switch (type) {\n case 'org':\n return buildRoute(Route.ORG_SETTINGS, { orgSlug })\n case 'member':\n return id\n ? buildRoute(Route.MANAGE_TEAM_MEMBER, { orgSlug, uid: id })\n : buildRoute(Route.MANAGE_TEAM, { orgSlug })\n case 'invite':\n return buildRoute(Route.MANAGE_TEAM, { orgSlug })\n default:\n // A plugin's record acted on at the ORGANIZATION level (AGL-2634) — a\n // deal moved from the org board, a merge, a bulk bar's action — at the\n // organization address its owner publishes; a bulk line names no\n // record and lands on the list.\n return pluginRecordActivityHref(type, id, { orgSlug, host: null })\n }\n}\n\n/**\n * How an automation run ended (AGL-2171).\n *\n * `skipped` is a first-class outcome, not an absence. An unmet condition\n * used to be a bare `continue` that wrote nothing, so \"why didn't my\n * automation fire?\" — the most common support question about automations —\n * had no answer anywhere in the product, while `/product/workflows`\n * advertises an amber `Skipped` row that answers it.\n */\nexport type ActionRunResult = 'succeeded' | 'failed' | 'skipped'\n\n/** A stored run entry: an activity document with the run fields on it. */\nexport interface ActionRunEntryLike extends ActivityEntryLike {\n result?: string\n trigger?: string\n summary?: string\n}\n\n/**\n * `formSubmission` → `Form submitted`.\n *\n * The console rendered the raw camelCase identifier in the trigger select\n * and in every run row. The words are each event's declared label\n * (`host-events.ts`) — one map, so the picker an author chose from and the\n * run row they read afterwards cannot name the same event differently.\n */\nexport function actionTriggerLabel(event: string | undefined): string {\n return hostEventLabel(event)\n}\n\n/**\n * The run's outcome, tolerating rows written before it was recorded.\n *\n * A legacy entry carries only the prose `action` string, so it is read\n * back: `with errors:` means it failed and `Action ran on` means it\n * succeeded. Anything else is not a run at all — the activity collection\n * also holds publishes, media saves and member changes, and calling one of\n * those `succeeded` would put it in the run history under a verdict that\n * means nothing for it.\n */\nexport function actionRunResult(\n entry: ActionRunEntryLike,\n): ActionRunResult | undefined {\n const stored = String(entry.result ?? '').trim()\n if (stored === 'succeeded' || stored === 'failed' || stored === 'skipped') {\n return stored\n }\n const action = String(entry.action ?? '')\n if (!action.startsWith('Action ran on')) return undefined\n return action.includes('with errors:') ? 'failed' : 'succeeded'\n}\n\n/**\n * The `What happened` column: `Sent email · saved to Leads · webhook 200`.\n *\n * Falls back to the legacy prose, minus its `Action ran on <event>` prefix\n * — that fact is the Trigger column now, and repeating it in the summary\n * would make every historic row say the same thing twice.\n */\nexport function actionRunSummary(entry: ActionRunEntryLike): string {\n const summary = String(entry.summary ?? '').trim()\n if (summary) return summary\n const action = String(entry.action ?? '').trim()\n const errors = action.match(/with errors:\\s*(.+)$/)\n if (errors) return errors[1]\n // A pre-AGL-2171 success recorded nothing but the fact that it ran.\n if (action.startsWith('Action ran on')) return 'Ran'\n return action\n}\n"],"names":["Route","buildRoute","pluginRecordHref","pluginRecordListHref","PLATFORM_BRAND_NAME","hostEventLabel","duplicateActivityActionLabel","pluginActivityActionLabel","pluginActivityGroupForAction","pluginActivityTargetLabel","pluginActivityTargetNoun","ACTIVITY_TYPE_LABELS","TYPE_LABELS","pluginRecordActivityHref","type","id","context","undefined","activityTypeLabel","known","noun","charAt","toUpperCase","slice","activityTargetLabel","target","name","activityActionLabel","action","stored","trim","activityEntryGroupId","entry","activityPrimaryText","apiKeyActorLabel","activityActorLabel","actorEmail","apiKeyName","actorId","startsWith","actorEmailNow","staffActorEmail","runTriggeredByLabel","by","triggeredBy","kind","email","uid","activityHref","ctx","orgSlug","host","targetId","versionId","HOST_DASHBOARD","SCREEN_DETAILS","screenId","HOST_SCREENS","COMPONENT_DETAILS","componentId","HOST_COMPONENTS","TEMPLATE_DETAILS","templateId","HOST_TEMPLATES","LAYOUT_DETAILS","layoutId","HOST_LAYOUTS","HOST_SETUP_THEME","HOST_MEDIA","HOST_CONTENT","HOST_DATA","HOST_LOGIC","HOST_AUTOMATION","HOST_SETUP","ORG_SETTINGS","MANAGE_TEAM_MEMBER","MANAGE_TEAM","actionTriggerLabel","event","actionRunResult","String","result","includes","actionRunSummary","summary","errors","match"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
/** Human, singular noun for a target type. Falls back to the raw type. */
|
|
18
|
+
export declare const ACTIVITY_TYPE_LABELS: Readonly<Record<string, string>>;
|
|
@@ -0,0 +1,46 @@
|
|
|
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 nouns an activity row names its target by, apart from the presenter so the native apps read the same words from
|
|
18
|
+
* the contracts (AGL-4011). No imports.
|
|
19
|
+
*/ /** Human, singular noun for a target type. Falls back to the raw type. */ export const ACTIVITY_TYPE_LABELS = {
|
|
20
|
+
host: 'Site',
|
|
21
|
+
screen: 'Page',
|
|
22
|
+
layout: 'Layout',
|
|
23
|
+
theme: 'Theme',
|
|
24
|
+
media: 'Media',
|
|
25
|
+
content: 'Content',
|
|
26
|
+
variable: 'Variable',
|
|
27
|
+
function: 'Function',
|
|
28
|
+
workflow: 'Workflow',
|
|
29
|
+
member: 'Member',
|
|
30
|
+
component: 'Component',
|
|
31
|
+
template: 'Template',
|
|
32
|
+
org: 'Organization',
|
|
33
|
+
invite: 'Invitation',
|
|
34
|
+
subscription: 'Subscription',
|
|
35
|
+
// A custom role's permission flipped (AGL-2929).
|
|
36
|
+
role: 'Role',
|
|
37
|
+
// CRM records (AGL-2622): a person added, a company or a deal created or
|
|
38
|
+
// deleted, a lead converted — logged into the feed of the site whose
|
|
39
|
+
// console did it, and linked to the record the way a screen is.
|
|
40
|
+
contact: 'Contact',
|
|
41
|
+
lead: 'Lead',
|
|
42
|
+
company: 'Company',
|
|
43
|
+
deal: 'Deal'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=activity-type-labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/activity-type-labels.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 nouns an activity row names its target by, apart from the presenter so the native apps read the same words from\n * the contracts (AGL-4011). No imports.\n */\n\n/** Human, singular noun for a target type. Falls back to the raw type. */\nexport const ACTIVITY_TYPE_LABELS: Readonly<Record<string, string>> = {\n host: 'Site',\n screen: 'Page',\n layout: 'Layout',\n theme: 'Theme',\n media: 'Media',\n content: 'Content',\n variable: 'Variable',\n function: 'Function',\n workflow: 'Workflow',\n member: 'Member',\n component: 'Component',\n template: 'Template',\n org: 'Organization',\n invite: 'Invitation',\n subscription: 'Subscription',\n // A custom role's permission flipped (AGL-2929).\n role: 'Role',\n // CRM records (AGL-2622): a person added, a company or a deal created or\n // deleted, a lead converted — logged into the feed of the site whose\n // console did it, and linked to the record the way a screen is.\n contact: 'Contact',\n lead: 'Lead',\n company: 'Company',\n deal: 'Deal',\n}\n"],"names":["ACTIVITY_TYPE_LABELS","host","screen","layout","theme","media","content","variable","function","workflow","member","component","template","org","invite","subscription","role","contact","lead","company","deal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;CAGC,GAED,wEAAwE,GACxE,OAAO,MAAMA,uBAAyD;IACpEC,MAAM;IACNC,QAAQ;IACRC,QAAQ;IACRC,OAAO;IACPC,OAAO;IACPC,SAAS;IACTC,UAAU;IACVC,UAAU;IACVC,UAAU;IACVC,QAAQ;IACRC,WAAW;IACXC,UAAU;IACVC,KAAK;IACLC,QAAQ;IACRC,cAAc;IACd,iDAAiD;IACjDC,MAAM;IACN,yEAAyE;IACzE,qEAAqE;IACrE,gEAAgE;IAChEC,SAAS;IACTC,MAAM;IACNC,SAAS;IACTC,MAAM;AACR,EAAC"}
|
|
@@ -36,7 +36,9 @@ export type InstallableArtifactType = 'component' | 'template' | 'plugin' | 'lay
|
|
|
36
36
|
* Folding the two together would give one install button two behaviors and
|
|
37
37
|
* would make the catalog key an absent-field discriminator.
|
|
38
38
|
*/
|
|
39
|
-
| 'emailStarter' | 'theme'
|
|
39
|
+
| 'emailStarter' | 'theme'
|
|
40
|
+
/** A whole site picked before it exists (AGL-4049): installing one creates the site. */
|
|
41
|
+
| 'siteStarter';
|
|
40
42
|
/**
|
|
41
43
|
* Install provenance (AGL-1015): where an installed artifact came from, at
|
|
42
44
|
* what version, and what it looked like when it arrived.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-provenance.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 kinds of content a workspace can INSTALL rather than author (AGL-654).\n *\n * Installed content is a platform concept with more readers than writers:\n * whatever installs a component, a theme or an email template stamps where it\n * came from (below), and the theme editor, a template's page, the site's theme\n * resolution and the AI plugin all read that stamp without knowing who wrote\n * it. So the contract is core's and names no installer (AGL-3080). The\n * marketplace, which is today's only installer, re-exports the union under the\n * name its publish routes use, `MarketplaceArtifactType`.\n */\nexport type InstallableArtifactType =\n | 'component'\n | 'template'\n | 'plugin'\n | 'layout'\n | 'datasetSchema'\n | 'emailTemplate'\n /**\n * A campaign email design, installed as a new `kind: 'email'` screen.\n *\n * Distinct from `emailTemplate`, which is a design FOR one entry in the\n * fixed transactional catalog and can only ever land on that same key. A\n * starter belongs to no key: it is a blank-page replacement for the composer,\n * so it installs as a new document and the tenant may install ten of them.\n * Folding the two together would give one install button two behaviors and\n * would make the catalog key an absent-field discriminator.\n */\n | 'emailStarter'\n | 'theme'\n\n/**\n * Install provenance (AGL-1015): where an installed artifact came from, at\n * what version, and what it looked like when it arrived.\n *\n * Every install route used to stamp its own shape — plugins wrote a pin,\n * dataset schemas a `source`, email templates an `installedFrom`, components a\n * `marketplace` — and none of them kept the content as installed. Without the\n * original there is no way to tell the publisher's change from the user's, so\n * an update can only overwrite. This module defines the one shape all install\n * routes write and the one reader everything else asks.\n */\n\n/**\n * Immutable, content-addressed base snapshots — the content exactly as\n * installed, keyed by its own hash and stored outside the user's edit path.\n *\n * Top-level rather than per-host on purpose: the same listing version installed\n * onto twenty sites is one document, and re-installing it rewrites nothing.\n */\nexport const ARTIFACT_BASE_COLLECTION = 'marketplaceArtifactBases'\n\n/**\n * Firestore's per-document ceiling is 1 MiB. Snapshots are stored whole, so\n * anything near it is refused rather than truncated — a partial base is worse\n * than no base, because a diff would report the missing half as user edits.\n */\nexport const ARTIFACT_BASE_MAX_BYTES = 800 * 1024\n\n/** The provenance stamp written on every installed artifact (AGL-1015). */\nexport interface InstalledFrom {\n listingId: string\n /** The listing version installed, as a string; null when unversioned. */\n version: string | null\n /** Content hash of the base snapshot — the key into {@link ARTIFACT_BASE_COLLECTION}. */\n sha256: string | null\n artifactType: InstallableArtifactType\n installedAt: unknown\n /** The publishing org (`listing.profileId`, org-owned since AGL-652). */\n publisherOrgId: string | null\n /**\n * What review said about THE VERSION INSTALLED, recorded at install time.\n *\n * Optional because six of the seven artifact types are auto-listed and have\n * no verdict to record. Where it is written it is a fact about one version\n * and is never recomputed from the listing afterwards: a listing that earns\n * `reviewStatus: 'verified'` next month did not retroactively review the\n * bytes installed today, and a surface that resolved assurance live would\n * say it did. Stamping it is what keeps \"unreviewed\" from decaying into a\n * silence a reader mistakes for approval.\n */\n assurance?: 'approved' | 'unreviewed' | 'rejected'\n}\n\n/**\n * How much is actually known about where an artifact came from.\n *\n * `inferred` exists because the marketplace shipped before this stamp did:\n * pre-AGL-1015 installs carry a listing id somewhere but no hash and no base.\n * Calling those `recorded` would let an update think it has an origin to merge\n * against; calling them `unknown` would throw away a listing id we really do\n * have. They are their own state, and the UI says \"installed before update\n * tracking\" rather than implying either.\n */\nexport type ProvenanceState = 'recorded' | 'inferred' | 'unknown'\n\nexport interface ResolvedProvenance {\n state: ProvenanceState\n listingId: string | null\n version: string | null\n sha256: string | null\n artifactType: InstallableArtifactType | null\n publisherOrgId: string | null\n /**\n * A base snapshot exists, so this artifact can be diffed and safely updated.\n * False for everything installed before the stamp — and saying so plainly is\n * the point: a merge built on a fabricated origin destroys work silently.\n */\n updatable: boolean\n}\n\nconst UNKNOWN_PROVENANCE: ResolvedProvenance = {\n state: 'unknown',\n listingId: null,\n version: null,\n sha256: null,\n artifactType: null,\n publisherOrgId: null,\n updatable: false,\n}\n\nfunction asVersion(value: unknown): string | null {\n return value == null || value === '' ? null : String(value)\n}\n\n/**\n * Reads provenance off an installed document, tolerating every shape the\n * install routes wrote before AGL-1015.\n *\n * The legacy fields are read rather than migrated: a batch backfill could only\n * ever recover the listing id and version, which is exactly what this recovers\n * lazily, and it cannot invent a base snapshot. Installs re-stamp themselves\n * properly the next time they are updated.\n */\nexport function resolveProvenance(\n doc:\n | {\n installedFrom?: Partial<InstalledFrom> | null\n /** Component installs (`hosts/{h}/components`). */\n marketplace?: { listingId?: string; profileId?: string; version?: unknown } | null\n /** Template, layout and dataset-schema installs. */\n source?: { type?: string; listingId?: string; version?: unknown } | null\n /** Plugin pins are their own provenance: the pin IS the version. */\n pluginId?: string\n listingId?: string\n profileId?: string\n version?: unknown\n sha256?: string\n }\n | null\n | undefined,\n fallbackArtifactType?: InstallableArtifactType,\n): ResolvedProvenance {\n if (!doc) return UNKNOWN_PROVENANCE\n\n const stamped = doc.installedFrom\n // A full stamp is the only thing that carries a base snapshot. Email\n // templates have written a partial `installedFrom` since AGL-789, so the\n // discriminator is the hash and the artifact type, not the field's presence.\n if (stamped?.listingId && stamped.sha256 && stamped.artifactType) {\n return {\n state: 'recorded',\n listingId: stamped.listingId,\n version: asVersion(stamped.version),\n sha256: stamped.sha256,\n artifactType: stamped.artifactType as InstallableArtifactType,\n publisherOrgId: stamped.publisherOrgId ?? null,\n updatable: true,\n }\n }\n\n const legacy = stamped?.listingId\n ? { listingId: stamped.listingId, version: stamped.version, profileId: null }\n : doc.marketplace?.listingId\n ? {\n listingId: doc.marketplace.listingId,\n version: doc.marketplace.version,\n profileId: doc.marketplace.profileId ?? null,\n }\n : doc.source?.listingId\n ? {\n listingId: doc.source.listingId,\n version: doc.source.version,\n profileId: null,\n }\n : // A plugin pin is flat: `{listingId, version, sha256}` on the install\n // doc itself. It is the one artifact that never needed a snapshot —\n // the bytes are immutable and content-addressed already — so a pin\n // resolves as fully recorded.\n doc.listingId\n ? {\n listingId: doc.listingId,\n version: doc.version,\n profileId: doc.profileId ?? null,\n }\n : null\n if (!legacy) return UNKNOWN_PROVENANCE\n\n const isPin = Boolean(doc.pluginId && doc.sha256)\n return {\n state: isPin ? 'recorded' : 'inferred',\n listingId: legacy.listingId,\n version: asVersion(legacy.version),\n sha256: doc.sha256 ?? null,\n artifactType: isPin ? 'plugin' : (fallbackArtifactType ?? null),\n publisherOrgId: legacy.profileId,\n updatable: isPin,\n }\n}\n\n/**\n * Deterministic JSON with object keys in sorted order, so the same content\n * hashes to the same id no matter what order Firestore handed the fields back.\n *\n * `undefined` members are dropped exactly as `JSON.stringify` drops them, which\n * keeps `{a: 1}` and `{a: 1, b: undefined}` — the same document after a\n * Firestore round-trip — hashing alike.\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value ?? null)\n }\n if (Array.isArray(value)) {\n return `[${value.map((entry) => stableStringify(entry)).join(',')}]`\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, entryValue]) => entryValue !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)\n return `{${entries.join(',')}}`\n}\n"],"names":["ARTIFACT_BASE_COLLECTION","ARTIFACT_BASE_MAX_BYTES","UNKNOWN_PROVENANCE","state","listingId","version","sha256","artifactType","publisherOrgId","updatable","asVersion","value","String","resolveProvenance","doc","fallbackArtifactType","stamped","installedFrom","legacy","profileId","marketplace","source","isPin","Boolean","pluginId","stableStringify","JSON","stringify","Array","isArray","map","entry","join","entries","Object","filter","entryValue","undefined","sort","a","b","key"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/artifact-provenance.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 kinds of content a workspace can INSTALL rather than author (AGL-654).\n *\n * Installed content is a platform concept with more readers than writers:\n * whatever installs a component, a theme or an email template stamps where it\n * came from (below), and the theme editor, a template's page, the site's theme\n * resolution and the AI plugin all read that stamp without knowing who wrote\n * it. So the contract is core's and names no installer (AGL-3080). The\n * marketplace, which is today's only installer, re-exports the union under the\n * name its publish routes use, `MarketplaceArtifactType`.\n */\nexport type InstallableArtifactType =\n | 'component'\n | 'template'\n | 'plugin'\n | 'layout'\n | 'datasetSchema'\n | 'emailTemplate'\n /**\n * A campaign email design, installed as a new `kind: 'email'` screen.\n *\n * Distinct from `emailTemplate`, which is a design FOR one entry in the\n * fixed transactional catalog and can only ever land on that same key. A\n * starter belongs to no key: it is a blank-page replacement for the composer,\n * so it installs as a new document and the tenant may install ten of them.\n * Folding the two together would give one install button two behaviors and\n * would make the catalog key an absent-field discriminator.\n */\n | 'emailStarter'\n | 'theme'\n /** A whole site picked before it exists (AGL-4049): installing one creates the site. */\n | 'siteStarter'\n\n/**\n * Install provenance (AGL-1015): where an installed artifact came from, at\n * what version, and what it looked like when it arrived.\n *\n * Every install route used to stamp its own shape — plugins wrote a pin,\n * dataset schemas a `source`, email templates an `installedFrom`, components a\n * `marketplace` — and none of them kept the content as installed. Without the\n * original there is no way to tell the publisher's change from the user's, so\n * an update can only overwrite. This module defines the one shape all install\n * routes write and the one reader everything else asks.\n */\n\n/**\n * Immutable, content-addressed base snapshots — the content exactly as\n * installed, keyed by its own hash and stored outside the user's edit path.\n *\n * Top-level rather than per-host on purpose: the same listing version installed\n * onto twenty sites is one document, and re-installing it rewrites nothing.\n */\nexport const ARTIFACT_BASE_COLLECTION = 'marketplaceArtifactBases'\n\n/**\n * Firestore's per-document ceiling is 1 MiB. Snapshots are stored whole, so\n * anything near it is refused rather than truncated — a partial base is worse\n * than no base, because a diff would report the missing half as user edits.\n */\nexport const ARTIFACT_BASE_MAX_BYTES = 800 * 1024\n\n/** The provenance stamp written on every installed artifact (AGL-1015). */\nexport interface InstalledFrom {\n listingId: string\n /** The listing version installed, as a string; null when unversioned. */\n version: string | null\n /** Content hash of the base snapshot — the key into {@link ARTIFACT_BASE_COLLECTION}. */\n sha256: string | null\n artifactType: InstallableArtifactType\n installedAt: unknown\n /** The publishing org (`listing.profileId`, org-owned since AGL-652). */\n publisherOrgId: string | null\n /**\n * What review said about THE VERSION INSTALLED, recorded at install time.\n *\n * Optional because six of the seven artifact types are auto-listed and have\n * no verdict to record. Where it is written it is a fact about one version\n * and is never recomputed from the listing afterwards: a listing that earns\n * `reviewStatus: 'verified'` next month did not retroactively review the\n * bytes installed today, and a surface that resolved assurance live would\n * say it did. Stamping it is what keeps \"unreviewed\" from decaying into a\n * silence a reader mistakes for approval.\n */\n assurance?: 'approved' | 'unreviewed' | 'rejected'\n}\n\n/**\n * How much is actually known about where an artifact came from.\n *\n * `inferred` exists because the marketplace shipped before this stamp did:\n * pre-AGL-1015 installs carry a listing id somewhere but no hash and no base.\n * Calling those `recorded` would let an update think it has an origin to merge\n * against; calling them `unknown` would throw away a listing id we really do\n * have. They are their own state, and the UI says \"installed before update\n * tracking\" rather than implying either.\n */\nexport type ProvenanceState = 'recorded' | 'inferred' | 'unknown'\n\nexport interface ResolvedProvenance {\n state: ProvenanceState\n listingId: string | null\n version: string | null\n sha256: string | null\n artifactType: InstallableArtifactType | null\n publisherOrgId: string | null\n /**\n * A base snapshot exists, so this artifact can be diffed and safely updated.\n * False for everything installed before the stamp — and saying so plainly is\n * the point: a merge built on a fabricated origin destroys work silently.\n */\n updatable: boolean\n}\n\nconst UNKNOWN_PROVENANCE: ResolvedProvenance = {\n state: 'unknown',\n listingId: null,\n version: null,\n sha256: null,\n artifactType: null,\n publisherOrgId: null,\n updatable: false,\n}\n\nfunction asVersion(value: unknown): string | null {\n return value == null || value === '' ? null : String(value)\n}\n\n/**\n * Reads provenance off an installed document, tolerating every shape the\n * install routes wrote before AGL-1015.\n *\n * The legacy fields are read rather than migrated: a batch backfill could only\n * ever recover the listing id and version, which is exactly what this recovers\n * lazily, and it cannot invent a base snapshot. Installs re-stamp themselves\n * properly the next time they are updated.\n */\nexport function resolveProvenance(\n doc:\n | {\n installedFrom?: Partial<InstalledFrom> | null\n /** Component installs (`hosts/{h}/components`). */\n marketplace?: { listingId?: string; profileId?: string; version?: unknown } | null\n /** Template, layout and dataset-schema installs. */\n source?: { type?: string; listingId?: string; version?: unknown } | null\n /** Plugin pins are their own provenance: the pin IS the version. */\n pluginId?: string\n listingId?: string\n profileId?: string\n version?: unknown\n sha256?: string\n }\n | null\n | undefined,\n fallbackArtifactType?: InstallableArtifactType,\n): ResolvedProvenance {\n if (!doc) return UNKNOWN_PROVENANCE\n\n const stamped = doc.installedFrom\n // A full stamp is the only thing that carries a base snapshot. Email\n // templates have written a partial `installedFrom` since AGL-789, so the\n // discriminator is the hash and the artifact type, not the field's presence.\n if (stamped?.listingId && stamped.sha256 && stamped.artifactType) {\n return {\n state: 'recorded',\n listingId: stamped.listingId,\n version: asVersion(stamped.version),\n sha256: stamped.sha256,\n artifactType: stamped.artifactType as InstallableArtifactType,\n publisherOrgId: stamped.publisherOrgId ?? null,\n updatable: true,\n }\n }\n\n const legacy = stamped?.listingId\n ? { listingId: stamped.listingId, version: stamped.version, profileId: null }\n : doc.marketplace?.listingId\n ? {\n listingId: doc.marketplace.listingId,\n version: doc.marketplace.version,\n profileId: doc.marketplace.profileId ?? null,\n }\n : doc.source?.listingId\n ? {\n listingId: doc.source.listingId,\n version: doc.source.version,\n profileId: null,\n }\n : // A plugin pin is flat: `{listingId, version, sha256}` on the install\n // doc itself. It is the one artifact that never needed a snapshot —\n // the bytes are immutable and content-addressed already — so a pin\n // resolves as fully recorded.\n doc.listingId\n ? {\n listingId: doc.listingId,\n version: doc.version,\n profileId: doc.profileId ?? null,\n }\n : null\n if (!legacy) return UNKNOWN_PROVENANCE\n\n const isPin = Boolean(doc.pluginId && doc.sha256)\n return {\n state: isPin ? 'recorded' : 'inferred',\n listingId: legacy.listingId,\n version: asVersion(legacy.version),\n sha256: doc.sha256 ?? null,\n artifactType: isPin ? 'plugin' : (fallbackArtifactType ?? null),\n publisherOrgId: legacy.profileId,\n updatable: isPin,\n }\n}\n\n/**\n * Deterministic JSON with object keys in sorted order, so the same content\n * hashes to the same id no matter what order Firestore handed the fields back.\n *\n * `undefined` members are dropped exactly as `JSON.stringify` drops them, which\n * keeps `{a: 1}` and `{a: 1, b: undefined}` — the same document after a\n * Firestore round-trip — hashing alike.\n */\nexport function stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') {\n return JSON.stringify(value ?? null)\n }\n if (Array.isArray(value)) {\n return `[${value.map((entry) => stableStringify(entry)).join(',')}]`\n }\n const entries = Object.entries(value as Record<string, unknown>)\n .filter(([, entryValue]) => entryValue !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)\n return `{${entries.join(',')}}`\n}\n"],"names":["ARTIFACT_BASE_COLLECTION","ARTIFACT_BASE_MAX_BYTES","UNKNOWN_PROVENANCE","state","listingId","version","sha256","artifactType","publisherOrgId","updatable","asVersion","value","String","resolveProvenance","doc","fallbackArtifactType","stamped","installedFrom","legacy","profileId","marketplace","source","isPin","Boolean","pluginId","stableStringify","JSON","stringify","Array","isArray","map","entry","join","entries","Object","filter","entryValue","undefined","sort","a","b","key"],"mappings":"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"}
|