@aglyn/aglyn 1.0.0-beta.260 → 1.0.0-beta.262
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/docs-help-sections.generated.js +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +2 -2
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/element-motion.d.ts +17 -1
- package/src/lib/app-utils/element-motion.js +36 -10
- package/src/lib/app-utils/element-motion.js.map +1 -1
- package/src/lib/app-utils/notifications.d.ts +10 -6
- package/src/lib/app-utils/notifications.js +102 -9
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/operator-alerts.js +19 -0
- package/src/lib/app-utils/operator-alerts.js.map +1 -1
- package/src/lib/app-utils/weekly-summary.d.ts +100 -0
- package/src/lib/app-utils/weekly-summary.js +137 -0
- package/src/lib/app-utils/weekly-summary.js.map +1 -0
- package/src/lib/plugin-manager/plugin-figures.d.ts +42 -0
- package/src/lib/plugin-manager/plugin-figures.js +51 -0
- package/src/lib/plugin-manager/plugin-figures.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +5 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -1
|
@@ -248,5 +248,56 @@ function cellFor(column, value) {
|
|
|
248
248
|
if (!Number.isFinite(current)) return null;
|
|
249
249
|
return Math.round((current - previous) / previous * 1000) / 10;
|
|
250
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* How a cell reads to a person, by its column's kind; `null` is "—". Every
|
|
253
|
+
* consumer that shows a table to a person words its cells here, so an insight
|
|
254
|
+
* answer and a weekly summary print the same figure the same way.
|
|
255
|
+
*/ export function pluginFigureCellText(table, key, value) {
|
|
256
|
+
if (value === null || value === undefined || value === '') return '—';
|
|
257
|
+
const column = table.columns.find((entry)=>entry.key === key);
|
|
258
|
+
if (typeof value === 'string' || !column) return String(value);
|
|
259
|
+
switch(column.kind){
|
|
260
|
+
case 'money':
|
|
261
|
+
var _column_currency;
|
|
262
|
+
return new Intl.NumberFormat('en-US', {
|
|
263
|
+
style: 'currency',
|
|
264
|
+
currency: (_column_currency = column.currency) != null ? _column_currency : 'USD'
|
|
265
|
+
}).format(value);
|
|
266
|
+
case 'percent':
|
|
267
|
+
return `${value.toLocaleString('en-US', {
|
|
268
|
+
maximumFractionDigits: 1
|
|
269
|
+
})}%`;
|
|
270
|
+
case 'change':
|
|
271
|
+
return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', {
|
|
272
|
+
maximumFractionDigits: 1
|
|
273
|
+
})}%`;
|
|
274
|
+
case 'duration':
|
|
275
|
+
{
|
|
276
|
+
const seconds = Math.round(value);
|
|
277
|
+
if (seconds < 60) return `${seconds}s`;
|
|
278
|
+
const minutes = Math.floor(seconds / 60);
|
|
279
|
+
if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`;
|
|
280
|
+
return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`;
|
|
281
|
+
}
|
|
282
|
+
default:
|
|
283
|
+
return value.toLocaleString('en-US', {
|
|
284
|
+
maximumFractionDigits: 2
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */ export function pluginFigureIsoWeek(at) {
|
|
289
|
+
const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()));
|
|
290
|
+
// Thursday decides the week's year.
|
|
291
|
+
const weekday = date.getUTCDay() || 7;
|
|
292
|
+
date.setUTCDate(date.getUTCDate() + 4 - weekday);
|
|
293
|
+
const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1);
|
|
294
|
+
const week = Math.ceil(((date.getTime() - yearStart) / 86400000 + 1) / 7);
|
|
295
|
+
return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`;
|
|
296
|
+
}
|
|
297
|
+
/** The section of the weekly summary a reader is listed in, or `null` for a reader that is not in it. */ export function pluginFigureSummarySection(reader) {
|
|
298
|
+
var _reader_weeklySummary_per;
|
|
299
|
+
if (!reader.weeklySummary) return null;
|
|
300
|
+
return (_reader_weeklySummary_per = reader.weeklySummary.per) != null ? _reader_weeklySummary_per : reader.scope === 'site' ? 'site' : 'workspace';
|
|
301
|
+
}
|
|
251
302
|
|
|
252
303
|
//# sourceMappingURL=plugin-figures.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-figures.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\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Figures one plugin publishes for another (AGL-2915).\n *\n * The records behind a figure belong to the plugin that models them: orders\n * to commerce, bookings to bookings, campaigns and experiments to marketing.\n * The owner knows which documents count, which are test data, what a refund\n * does to revenue and which fields could name a person. A different plugin\n * that wants the figure — a weekly report, an export, an assistant answering\n * a question about the numbers — must not read those documents itself,\n * because it would restate the owner's rules and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the owner registers a READER from its server surfaces, and a caller asks\n * for one by id. A reader answers with one compact table:\n *\n * - read-only: a reader writes nothing, schedules nothing and sends nothing;\n * - aggregate: counts, sums, rates and labels, never the records themselves;\n * - about nobody: no row names a person, and no cell carries a name, an email\n * address, a phone number or a street address — a customer is counted,\n * never listed;\n * - bounded: at most `PLUGIN_FIGURE_MAX_ROWS` rows, each labelled with where\n * in the console the figures come from.\n *\n * Built on the typed service registry (`plugin-services`): one contract, every\n * reader registered under its id as the service's key.\n *\n * ## The registry carries no authorization of the CALLER\n *\n * Like `plugin-resource-drafts`, a reader is a pre-authorized operation: it\n * reads the site or the workspace it is handed. A caller establishes, in its\n * own terms and before it asks, who is asking, that the org owns the site, that\n * the person may see that site's figures, and that the owning plugin is\n * switched on for it.\n *\n * ## A table is checked on the way out as well as on the way in\n *\n * `normalizePluginFigureTable` holds whatever a reader returned to the shape\n * above — row and cell bounds, column kinds, and personal details masked in\n * every text cell — so a consumer that shows or forwards a table relies on the\n * contract rather than on every reader having kept it.\n */\n\n/** Whether a reader's figures are about one site or about the whole workspace. */\nexport type PluginFigureScope = 'site' | 'org'\n\n/**\n * How a column's cells read:\n *\n * - `text` — a label: a page path, a form's name, a region;\n * - `count` — a whole number of things;\n * - `number` — any other figure, such as an average per day;\n * - `money` — an amount in major units of the column's `currency`;\n * - `percent` — a share or a rate, from 0 to 100;\n * - `change` — the percent change from the window before; `null` where there\n * is no earlier window to compare with, never 0 or 100;\n * - `duration` — seconds;\n * - `date` — a calendar day, `YYYY-MM-DD`.\n */\nexport type PluginFigureColumnKind =\n | 'text'\n | 'count'\n | 'number'\n | 'money'\n | 'percent'\n | 'change'\n | 'duration'\n | 'date'\n\nexport const PLUGIN_FIGURE_COLUMN_KINDS: readonly PluginFigureColumnKind[] = [\n 'text',\n 'count',\n 'number',\n 'money',\n 'percent',\n 'change',\n 'duration',\n 'date',\n]\n\nexport interface PluginFigureColumn {\n /** The row field the column reads. */\n key: string\n label: string\n kind: PluginFigureColumnKind\n /** The ISO 4217 code of a `money` column. */\n currency?: string\n}\n\nexport type PluginFigureCell = string | number | null\n\nexport type PluginFigureRow = Record<string, PluginFigureCell>\n\nexport interface PluginFigureTable {\n title: string\n /**\n * Where the figures come from: the name of the console page, and its path\n * under the site's console root for a site reader, or under the workspace's\n * for an org reader (`commerce/orders`), so a consumer can link to it.\n */\n source: { label: string; path: string | null }\n /** The days the figures cover, inclusive; `null` for a current count. */\n period: { from: string; to: string; days: number } | null\n columns: PluginFigureColumn[]\n rows: PluginFigureRow[]\n /** Rows the reader left out past its bound. */\n omitted: number\n /** Short plain sentences a reader of the figures needs, such as a window with nothing earlier to compare with. */\n notes: string[]\n}\n\nexport interface PluginFigureRequest {\n orgId: string\n /** The site a `site` reader reads; for an `org` reader, the site the figures are narrowed to, or `null`. */\n hostId: string | null\n /** The window, in days, ending at `now`: one of the reader's `windows`, or 0 for a reader with none. */\n days: number\n now: Date\n /**\n * The member asking. A reader of records a member may see only part of —\n * a dataset shared with some sites — reads what this member may see; with\n * `null`, nobody asked, and it reads only what every member of the site may.\n */\n uid: string | null\n /** The reader's own parameters, by the names its `params` declare; each a string. */\n params: Readonly<Record<string, string>>\n}\n\n/** One parameter a reader takes beyond its window. */\nexport interface PluginFigureParam {\n /** The name a request carries it under: letters, digits and underscores. */\n name: string\n /** What it selects, in one plain sentence. */\n description: string\n required: boolean\n}\n\nexport type PluginFigureRead =\n | { ok: true; table: PluginFigureTable }\n | {\n ok: false\n status: 400 | 403 | 404\n /** Customer-safe: a caller shows it as it stands. */\n error: string\n }\n\nexport interface PluginFigureReader {\n /** Stable and unique across plugins: lowercase words joined by dots (`commerce.orders`). */\n id: string\n /** What a person calls the figures: \"Orders\". */\n label: string\n /**\n * What the reader answers, in one or two plain sentences. The same bytes for\n * every site, so a list of readers can be shown to anyone who may use them.\n */\n description: string\n scope: PluginFigureScope\n /** The windows the reader covers, in days; empty for a reader of current counts. */\n windows: readonly number[]\n /**\n * The plugin whose records the figures are read from, which must run on the\n * site — or on the workspace, for an `org` reader — before anyone is offered\n * the reader. Absent, the plugin that registered it; `null` for figures the\n * platform itself keeps, such as page views.\n */\n plugin?: string | null\n /** The plan feature the figures are sold under; a workspace without it is not offered the reader. */\n feature?: string\n /** The parameters the reader takes beyond its window; absent for none. */\n params?: readonly PluginFigureParam[]\n read(request: PluginFigureRequest): Promise<PluginFigureRead>\n}\n\nexport const PLUGIN_FIGURES = definePluginServiceContract<PluginFigureReader>('core.figures', {\n multiple: true,\n})\n\n/** The most rows one table carries; the rest are counted in `omitted`. */\nexport const PLUGIN_FIGURE_MAX_ROWS = 25\n\n/** The most columns one table carries. */\nexport const PLUGIN_FIGURE_MAX_COLUMNS = 8\n\n/** The longest text cell, title or label a table carries, in characters. */\nexport const PLUGIN_FIGURE_MAX_TEXT = 120\n\n/** The most notes one table carries. */\nexport const PLUGIN_FIGURE_MAX_NOTES = 3\n\nconst READER_ID = /^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$/\nconst PARAM_NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst COLUMN_KEY = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst CONSOLE_PATH = /^[a-z0-9][a-z0-9/_-]{0,119}$/\nconst CALENDAR_DAY = /^\\d{4}-\\d{2}-\\d{2}$/\nconst CURRENCY = /^[A-Z]{3}$/\n\n/**\n * Registers a reader. The owner is the loader's marker when a register fn is\n * running, else `options.pluginId`; with neither the registration throws. A\n * reader id another plugin already registered throws naming both, and the\n * incumbent keeps serving; the same plugin registering again replaces its own.\n */\nexport function registerPluginFigureReader(\n reader: PluginFigureReader,\n options?: { pluginId?: string },\n): void {\n const key = reader.id.trim()\n if (!READER_ID.test(key)) {\n throw new Error(`a figure reader id is lowercase words joined by dots; refused \"${reader.id}\"`)\n }\n for (const param of reader.params ?? []) {\n if (!PARAM_NAME.test(param.name)) {\n throw new Error(`figure reader \"${key}\" declares a parameter named \"${param.name}\", which is not a name`)\n }\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_FIGURES).find((entry) => entry.key === key)\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `figure reader \"${key}\" is already registered by \"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_FIGURES, reader, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginFigureReader {\n /** The plugin that owns the records behind the figures. */\n pluginId: string\n reader: PluginFigureReader\n}\n\n/** The reader registered under an id, with its owner, or `null`. */\nexport function pluginFigureReader(id: string): ResolvedPluginFigureReader | null {\n const key = id.trim()\n const entry = resolvePluginServices(PLUGIN_FIGURES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, reader: entry.impl } : null\n}\n\n/** Every registered reader with its owner, ordered by id so a list of them is the same bytes on every call. */\nexport function listPluginFigureReaders(): ResolvedPluginFigureReader[] {\n return resolvePluginServices(PLUGIN_FIGURES)\n .map((entry) => ({ pluginId: entry.pluginId, reader: entry.impl }))\n .sort((a, b) => a.reader.id.localeCompare(b.reader.id))\n}\n\n/** The plugin that must run for a reader to be offered: its own `plugin`, else its owner; `null` for platform figures. */\nexport function pluginFigureReaderPlugin(resolved: ResolvedPluginFigureReader): string | null {\n return resolved.reader.plugin === undefined ? resolved.pluginId : resolved.reader.plugin\n}\n\n/**\n * The parameters a request carries that the reader declares, each trimmed to\n * a short string, or the name of the first required one that is missing.\n */\nexport function pluginFigureParams(\n reader: Pick<PluginFigureReader, 'params'>,\n raw: Readonly<Record<string, unknown>> | null | undefined,\n): { ok: true; params: Record<string, string> } | { ok: false; missing: string } {\n const params: Record<string, string> = {}\n for (const param of reader.params ?? []) {\n const value = raw?.[param.name]\n const text = typeof value === 'string' ? value.trim().slice(0, PLUGIN_FIGURE_MAX_TEXT) : ''\n if (text) params[param.name] = text\n else if (param.required) return { ok: false, missing: param.name }\n }\n return { ok: true, params }\n}\n\n// ── Personal details ────────────────────────────────────────────────────\n\nconst EMAIL = /[\\p{L}\\p{N}._%+-]+@[\\p{L}\\p{N}-]+(?:\\.[\\p{L}\\p{N}-]+)+/gu\n/** A run of digits with the separators a phone number is written with; its digits are counted apart. */\nconst PHONE_LIKE = /(?<![\\p{L}\\p{N}/_])\\+?\\(?\\d[\\d\\s().-]{6,}\\d(?![\\p{L}\\p{N}/_])/gu\n\n/** Nine to fifteen digits: a phone number's length anywhere, and longer than any calendar day or year. */\nfunction looksLikePhone(match: string): boolean {\n const digits = match.replace(/\\D/g, '').length\n return digits >= 9 && digits <= 15\n}\n\n/**\n * The text with every email address and phone number replaced by a marker.\n * A figure's label is never meant to carry either, so a match is masked\n * rather than trusted: a campaign named after a person's address says\n * nothing about the campaign that the marker does not.\n */\nexport function maskPersonalDetails(text: string): string {\n return text\n .replace(EMAIL, '[email]')\n .replace(PHONE_LIKE, (match) => (looksLikePhone(match) ? '[phone]' : match))\n}\n\n/** Whether a text carries an email address or a phone number. */\nexport function hasPersonalDetails(text: string): boolean {\n return maskPersonalDetails(text) !== text\n}\n\n// ── The table, held to the contract ─────────────────────────────────────\n\nfunction cut(value: unknown, max = PLUGIN_FIGURE_MAX_TEXT): string {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\nfunction finite(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\nfunction day(value: unknown): string | null {\n return typeof value === 'string' && CALENDAR_DAY.test(value) && !Number.isNaN(Date.parse(value))\n ? value\n : null\n}\n\nfunction cellFor(column: PluginFigureColumn, value: unknown): PluginFigureCell {\n switch (column.kind) {\n case 'text':\n return typeof value === 'string' ? maskPersonalDetails(cut(value)) : null\n case 'date':\n return day(value)\n case 'count': {\n const number = finite(value)\n return number === null ? null : Math.round(number)\n }\n default:\n return finite(value)\n }\n}\n\n/**\n * A reader's table held to the contract: bounded, typed and masked. Columns\n * with a key, a kind or a currency the contract does not know are dropped, and\n * every cell is read through its column, so a cell of the wrong type is `null`\n * rather than passed on. `null` for a value that is not a table at all.\n */\nexport function normalizePluginFigureTable(value: unknown): PluginFigureTable | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const table = value as Partial<PluginFigureTable>\n const title = cut(table.title)\n if (!title) return null\n\n const seen = new Set<string>()\n const columns: PluginFigureColumn[] = []\n for (const raw of Array.isArray(table.columns) ? table.columns : []) {\n if (columns.length === PLUGIN_FIGURE_MAX_COLUMNS) break\n const column = raw as Partial<PluginFigureColumn> | null\n const key = typeof column?.key === 'string' ? column.key : ''\n const kind = column?.kind as PluginFigureColumnKind\n if (!COLUMN_KEY.test(key) || seen.has(key) || !PLUGIN_FIGURE_COLUMN_KINDS.includes(kind)) continue\n if (kind === 'money' && !(typeof column?.currency === 'string' && CURRENCY.test(column.currency))) {\n continue\n }\n seen.add(key)\n columns.push({\n key,\n label: cut(column?.label, 60) || key,\n kind,\n ...(kind === 'money' ? { currency: column?.currency as string } : {}),\n })\n }\n if (!columns.length) return null\n\n const rawRows = Array.isArray(table.rows) ? table.rows : []\n const rows = rawRows.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((raw) => {\n const source = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}\n return Object.fromEntries(columns.map((column) => [column.key, cellFor(column, source[column.key])]))\n })\n const omitted =\n Math.max(0, rawRows.length - PLUGIN_FIGURE_MAX_ROWS) +\n (typeof table.omitted === 'number' && Number.isFinite(table.omitted) && table.omitted > 0\n ? Math.floor(table.omitted)\n : 0)\n\n const period = table.period\n const from = day(period?.from)\n const to = day(period?.to)\n const days = finite(period?.days)\n const path = typeof table.source?.path === 'string' ? table.source.path.trim() : ''\n\n return {\n title,\n source: {\n label: cut(table.source?.label, 80) || title,\n path: CONSOLE_PATH.test(path) && !path.includes('..') ? path : null,\n },\n period:\n from && to && days !== null && days >= 1 && days <= 366\n ? { from, to, days: Math.round(days) }\n : null,\n columns,\n rows,\n omitted,\n notes: (Array.isArray(table.notes) ? table.notes : [])\n .map((note) => maskPersonalDetails(cut(note, 200)))\n .filter(Boolean)\n .slice(0, PLUGIN_FIGURE_MAX_NOTES),\n }\n}\n\n// ── Windows ─────────────────────────────────────────────────────────────\n\n/** A calendar day in UTC. */\nexport function pluginFigureDay(at: Date): string {\n return at.toISOString().slice(0, 10)\n}\n\n/**\n * The window of `days` ending on the day of `now`, inclusive, and the window\n * of the same length immediately before it: what every `change` column\n * compares, so a figure from one reader compares like a figure from another.\n */\nexport function pluginFigureWindows(\n now: Date,\n days: number,\n): {\n current: { from: string; to: string; startMs: number; endMs: number }\n previous: { from: string; to: string; startMs: number; endMs: number }\n} {\n const span = Math.max(1, Math.floor(days))\n const dayMs = 86_400_000\n const endOfToday = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + dayMs\n const currentStart = endOfToday - span * dayMs\n const previousStart = currentStart - span * dayMs\n return {\n current: {\n from: pluginFigureDay(new Date(currentStart)),\n to: pluginFigureDay(new Date(endOfToday - dayMs)),\n startMs: currentStart,\n endMs: endOfToday,\n },\n previous: {\n from: pluginFigureDay(new Date(previousStart)),\n to: pluginFigureDay(new Date(currentStart - dayMs)),\n startMs: previousStart,\n endMs: currentStart,\n },\n }\n}\n\n/**\n * The percent change from `previous` to `current`, to one decimal place.\n * `null` when there is nothing earlier to compare with, so a first window\n * reads as no change figure at all rather than as a rise from zero.\n */\nexport function pluginFigureChange(current: number, previous: number | null): number | null {\n if (previous === null || !Number.isFinite(previous) || previous <= 0) return null\n if (!Number.isFinite(current)) return null\n return Math.round(((current - previous) / previous) * 1_000) / 10\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_FIGURE_COLUMN_KINDS","PLUGIN_FIGURES","multiple","PLUGIN_FIGURE_MAX_ROWS","PLUGIN_FIGURE_MAX_COLUMNS","PLUGIN_FIGURE_MAX_TEXT","PLUGIN_FIGURE_MAX_NOTES","READER_ID","PARAM_NAME","COLUMN_KEY","CONSOLE_PATH","CALENDAR_DAY","CURRENCY","registerPluginFigureReader","reader","options","key","id","trim","test","Error","param","params","name","pluginId","incumbent","find","entry","pluginFigureReader","one","impl","listPluginFigureReaders","map","sort","a","b","localeCompare","pluginFigureReaderPlugin","resolved","plugin","undefined","pluginFigureParams","raw","value","text","slice","required","ok","missing","EMAIL","PHONE_LIKE","looksLikePhone","match","digits","replace","length","maskPersonalDetails","hasPersonalDetails","cut","max","finite","Number","isFinite","day","isNaN","Date","parse","cellFor","column","kind","number","Math","round","normalizePluginFigureTable","table","Array","isArray","title","seen","Set","columns","has","includes","currency","add","push","label","rawRows","rows","source","Object","fromEntries","omitted","floor","period","from","to","days","path","notes","note","filter","Boolean","pluginFigureDay","at","toISOString","pluginFigureWindows","now","span","dayMs","endOfToday","UTC","getUTCFullYear","getUTCMonth","getUTCDate","currentStart","previousStart","current","startMs","endMs","previous","pluginFigureChange"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/plugin-figures.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\nimport { getRegisteringPluginId } from '../app-utils/registering-plugin'\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * Figures one plugin publishes for another (AGL-2915).\n *\n * The records behind a figure belong to the plugin that models them: orders\n * to commerce, bookings to bookings, campaigns and experiments to marketing.\n * The owner knows which documents count, which are test data, what a refund\n * does to revenue and which fields could name a person. A different plugin\n * that wants the figure — a weekly report, an export, an assistant answering\n * a question about the numbers — must not read those documents itself,\n * because it would restate the owner's rules and drift from them the day the\n * owner changes one. Nor may it import the owner, which the package map\n * forbids.\n *\n * So the owner registers a READER from its server surfaces, and a caller asks\n * for one by id. A reader answers with one compact table:\n *\n * - read-only: a reader writes nothing, schedules nothing and sends nothing;\n * - aggregate: counts, sums, rates and labels, never the records themselves;\n * - about nobody: no row names a person, and no cell carries a name, an email\n * address, a phone number or a street address — a customer is counted,\n * never listed;\n * - bounded: at most `PLUGIN_FIGURE_MAX_ROWS` rows, each labelled with where\n * in the console the figures come from.\n *\n * Built on the typed service registry (`plugin-services`): one contract, every\n * reader registered under its id as the service's key.\n *\n * ## The registry carries no authorization of the CALLER\n *\n * Like `plugin-resource-drafts`, a reader is a pre-authorized operation: it\n * reads the site or the workspace it is handed. A caller establishes, in its\n * own terms and before it asks, who is asking, that the org owns the site, that\n * the person may see that site's figures, and that the owning plugin is\n * switched on for it.\n *\n * ## A table is checked on the way out as well as on the way in\n *\n * `normalizePluginFigureTable` holds whatever a reader returned to the shape\n * above — row and cell bounds, column kinds, and personal details masked in\n * every text cell — so a consumer that shows or forwards a table relies on the\n * contract rather than on every reader having kept it.\n */\n\n/** Whether a reader's figures are about one site or about the whole workspace. */\nexport type PluginFigureScope = 'site' | 'org'\n\n/**\n * How a column's cells read:\n *\n * - `text` — a label: a page path, a form's name, a region;\n * - `count` — a whole number of things;\n * - `number` — any other figure, such as an average per day;\n * - `money` — an amount in major units of the column's `currency`;\n * - `percent` — a share or a rate, from 0 to 100;\n * - `change` — the percent change from the window before; `null` where there\n * is no earlier window to compare with, never 0 or 100;\n * - `duration` — seconds;\n * - `date` — a calendar day, `YYYY-MM-DD`.\n */\nexport type PluginFigureColumnKind =\n | 'text'\n | 'count'\n | 'number'\n | 'money'\n | 'percent'\n | 'change'\n | 'duration'\n | 'date'\n\nexport const PLUGIN_FIGURE_COLUMN_KINDS: readonly PluginFigureColumnKind[] = [\n 'text',\n 'count',\n 'number',\n 'money',\n 'percent',\n 'change',\n 'duration',\n 'date',\n]\n\nexport interface PluginFigureColumn {\n /** The row field the column reads. */\n key: string\n label: string\n kind: PluginFigureColumnKind\n /** The ISO 4217 code of a `money` column. */\n currency?: string\n}\n\nexport type PluginFigureCell = string | number | null\n\nexport type PluginFigureRow = Record<string, PluginFigureCell>\n\nexport interface PluginFigureTable {\n title: string\n /**\n * Where the figures come from: the name of the console page, and its path\n * under the site's console root for a site reader, or under the workspace's\n * for an org reader (`commerce/orders`), so a consumer can link to it.\n */\n source: { label: string; path: string | null }\n /** The days the figures cover, inclusive; `null` for a current count. */\n period: { from: string; to: string; days: number } | null\n columns: PluginFigureColumn[]\n rows: PluginFigureRow[]\n /** Rows the reader left out past its bound. */\n omitted: number\n /** Short plain sentences a reader of the figures needs, such as a window with nothing earlier to compare with. */\n notes: string[]\n}\n\nexport interface PluginFigureRequest {\n orgId: string\n /** The site a `site` reader reads; for an `org` reader, the site the figures are narrowed to, or `null`. */\n hostId: string | null\n /** The window, in days, ending at `now`: one of the reader's `windows`, or 0 for a reader with none. */\n days: number\n now: Date\n /**\n * The member asking. A reader of records a member may see only part of —\n * a dataset shared with some sites — reads what this member may see; with\n * `null`, nobody asked, and it reads only what every member of the site may.\n */\n uid: string | null\n /** The reader's own parameters, by the names its `params` declare; each a string. */\n params: Readonly<Record<string, string>>\n}\n\n/** One parameter a reader takes beyond its window. */\nexport interface PluginFigureParam {\n /** The name a request carries it under: letters, digits and underscores. */\n name: string\n /** What it selects, in one plain sentence. */\n description: string\n required: boolean\n}\n\nexport type PluginFigureRead =\n | { ok: true; table: PluginFigureTable }\n | {\n ok: false\n status: 400 | 403 | 404\n /** Customer-safe: a caller shows it as it stands. */\n error: string\n }\n\nexport interface PluginFigureReader {\n /** Stable and unique across plugins: lowercase words joined by dots (`commerce.orders`). */\n id: string\n /** What a person calls the figures: \"Orders\". */\n label: string\n /**\n * What the reader answers, in one or two plain sentences. The same bytes for\n * every site, so a list of readers can be shown to anyone who may use them.\n */\n description: string\n scope: PluginFigureScope\n /** The windows the reader covers, in days; empty for a reader of current counts. */\n windows: readonly number[]\n /**\n * The plugin whose records the figures are read from, which must run on the\n * site — or on the workspace, for an `org` reader — before anyone is offered\n * the reader. Absent, the plugin that registered it; `null` for figures the\n * platform itself keeps, such as page views.\n */\n plugin?: string | null\n /** The plan feature the figures are sold under; a workspace without it is not offered the reader. */\n feature?: string\n /** The parameters the reader takes beyond its window; absent for none. */\n params?: readonly PluginFigureParam[]\n read(request: PluginFigureRequest): Promise<PluginFigureRead>\n /**\n * How the reader's table reads in the weekly summary email (AGL-3867), for\n * a reader that belongs in it; absent, the summary leaves it out. The owner\n * words its own figures, because only the owner knows which of its columns\n * say what happened: the summary reads it over 7 days with no parameters\n * and lists what `lines` answers.\n */\n weeklySummary?: PluginFigureWeeklySummary\n}\n\n/** A count a weekly summary's subject line may lead with, and how it is named. */\nexport interface PluginFigureHeadline {\n count: number\n /** The noun for one, which also keys the sum across sites: \"visitor\". */\n one: string\n /** The noun for any other count: \"visitors\". */\n many: string\n}\n\n/** A reader's part in the weekly summary. */\nexport interface PluginFigureWeeklySummary {\n /**\n * Which section lists it: `site`, under each site; `workspace`, once for the\n * whole workspace. A workspace reader may say `site` to be read narrowed to\n * each site. Absent, a site reader is `site` and a workspace reader is\n * `workspace`.\n */\n per?: 'site' | 'workspace'\n /** Where its lines sit in a section: lower first. */\n order: number\n /** What the table says, one line each; `[]` when it has nothing to report this week. */\n lines(table: PluginFigureTable): string[]\n /** The count it adds to the subject line, or `null` for none this week. */\n headline?(table: PluginFigureTable): PluginFigureHeadline | null\n}\n\nexport const PLUGIN_FIGURES = definePluginServiceContract<PluginFigureReader>('core.figures', {\n multiple: true,\n})\n\n/** The most rows one table carries; the rest are counted in `omitted`. */\nexport const PLUGIN_FIGURE_MAX_ROWS = 25\n\n/** The most columns one table carries. */\nexport const PLUGIN_FIGURE_MAX_COLUMNS = 8\n\n/** The longest text cell, title or label a table carries, in characters. */\nexport const PLUGIN_FIGURE_MAX_TEXT = 120\n\n/** The most notes one table carries. */\nexport const PLUGIN_FIGURE_MAX_NOTES = 3\n\nconst READER_ID = /^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$/\nconst PARAM_NAME = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst COLUMN_KEY = /^[A-Za-z][A-Za-z0-9_]{0,39}$/\nconst CONSOLE_PATH = /^[a-z0-9][a-z0-9/_-]{0,119}$/\nconst CALENDAR_DAY = /^\\d{4}-\\d{2}-\\d{2}$/\nconst CURRENCY = /^[A-Z]{3}$/\n\n/**\n * Registers a reader. The owner is the loader's marker when a register fn is\n * running, else `options.pluginId`; with neither the registration throws. A\n * reader id another plugin already registered throws naming both, and the\n * incumbent keeps serving; the same plugin registering again replaces its own.\n */\nexport function registerPluginFigureReader(\n reader: PluginFigureReader,\n options?: { pluginId?: string },\n): void {\n const key = reader.id.trim()\n if (!READER_ID.test(key)) {\n throw new Error(`a figure reader id is lowercase words joined by dots; refused \"${reader.id}\"`)\n }\n for (const param of reader.params ?? []) {\n if (!PARAM_NAME.test(param.name)) {\n throw new Error(`figure reader \"${key}\" declares a parameter named \"${param.name}\", which is not a name`)\n }\n }\n const pluginId = (getRegisteringPluginId() ?? options?.pluginId ?? '').trim()\n const incumbent = resolvePluginServices(PLUGIN_FIGURES).find((entry) => entry.key === key)\n if (incumbent && pluginId && incumbent.pluginId !== pluginId) {\n throw new Error(\n `figure reader \"${key}\" is already registered by \"${incumbent.pluginId}\"; refused \"${pluginId}\"`,\n )\n }\n registerPluginService(PLUGIN_FIGURES, reader, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n key,\n })\n}\n\nexport interface ResolvedPluginFigureReader {\n /** The plugin that owns the records behind the figures. */\n pluginId: string\n reader: PluginFigureReader\n}\n\n/** The reader registered under an id, with its owner, or `null`. */\nexport function pluginFigureReader(id: string): ResolvedPluginFigureReader | null {\n const key = id.trim()\n const entry = resolvePluginServices(PLUGIN_FIGURES).find((one) => one.key === key)\n return entry ? { pluginId: entry.pluginId, reader: entry.impl } : null\n}\n\n/** Every registered reader with its owner, ordered by id so a list of them is the same bytes on every call. */\nexport function listPluginFigureReaders(): ResolvedPluginFigureReader[] {\n return resolvePluginServices(PLUGIN_FIGURES)\n .map((entry) => ({ pluginId: entry.pluginId, reader: entry.impl }))\n .sort((a, b) => a.reader.id.localeCompare(b.reader.id))\n}\n\n/** The plugin that must run for a reader to be offered: its own `plugin`, else its owner; `null` for platform figures. */\nexport function pluginFigureReaderPlugin(resolved: ResolvedPluginFigureReader): string | null {\n return resolved.reader.plugin === undefined ? resolved.pluginId : resolved.reader.plugin\n}\n\n/**\n * The parameters a request carries that the reader declares, each trimmed to\n * a short string, or the name of the first required one that is missing.\n */\nexport function pluginFigureParams(\n reader: Pick<PluginFigureReader, 'params'>,\n raw: Readonly<Record<string, unknown>> | null | undefined,\n): { ok: true; params: Record<string, string> } | { ok: false; missing: string } {\n const params: Record<string, string> = {}\n for (const param of reader.params ?? []) {\n const value = raw?.[param.name]\n const text = typeof value === 'string' ? value.trim().slice(0, PLUGIN_FIGURE_MAX_TEXT) : ''\n if (text) params[param.name] = text\n else if (param.required) return { ok: false, missing: param.name }\n }\n return { ok: true, params }\n}\n\n// ── Personal details ────────────────────────────────────────────────────\n\nconst EMAIL = /[\\p{L}\\p{N}._%+-]+@[\\p{L}\\p{N}-]+(?:\\.[\\p{L}\\p{N}-]+)+/gu\n/** A run of digits with the separators a phone number is written with; its digits are counted apart. */\nconst PHONE_LIKE = /(?<![\\p{L}\\p{N}/_])\\+?\\(?\\d[\\d\\s().-]{6,}\\d(?![\\p{L}\\p{N}/_])/gu\n\n/** Nine to fifteen digits: a phone number's length anywhere, and longer than any calendar day or year. */\nfunction looksLikePhone(match: string): boolean {\n const digits = match.replace(/\\D/g, '').length\n return digits >= 9 && digits <= 15\n}\n\n/**\n * The text with every email address and phone number replaced by a marker.\n * A figure's label is never meant to carry either, so a match is masked\n * rather than trusted: a campaign named after a person's address says\n * nothing about the campaign that the marker does not.\n */\nexport function maskPersonalDetails(text: string): string {\n return text\n .replace(EMAIL, '[email]')\n .replace(PHONE_LIKE, (match) => (looksLikePhone(match) ? '[phone]' : match))\n}\n\n/** Whether a text carries an email address or a phone number. */\nexport function hasPersonalDetails(text: string): boolean {\n return maskPersonalDetails(text) !== text\n}\n\n// ── The table, held to the contract ─────────────────────────────────────\n\nfunction cut(value: unknown, max = PLUGIN_FIGURE_MAX_TEXT): string {\n const text = typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : ''\n return text.length > max ? `${text.slice(0, max - 1)}…` : text\n}\n\nfunction finite(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null\n}\n\nfunction day(value: unknown): string | null {\n return typeof value === 'string' && CALENDAR_DAY.test(value) && !Number.isNaN(Date.parse(value))\n ? value\n : null\n}\n\nfunction cellFor(column: PluginFigureColumn, value: unknown): PluginFigureCell {\n switch (column.kind) {\n case 'text':\n return typeof value === 'string' ? maskPersonalDetails(cut(value)) : null\n case 'date':\n return day(value)\n case 'count': {\n const number = finite(value)\n return number === null ? null : Math.round(number)\n }\n default:\n return finite(value)\n }\n}\n\n/**\n * A reader's table held to the contract: bounded, typed and masked. Columns\n * with a key, a kind or a currency the contract does not know are dropped, and\n * every cell is read through its column, so a cell of the wrong type is `null`\n * rather than passed on. `null` for a value that is not a table at all.\n */\nexport function normalizePluginFigureTable(value: unknown): PluginFigureTable | null {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return null\n const table = value as Partial<PluginFigureTable>\n const title = cut(table.title)\n if (!title) return null\n\n const seen = new Set<string>()\n const columns: PluginFigureColumn[] = []\n for (const raw of Array.isArray(table.columns) ? table.columns : []) {\n if (columns.length === PLUGIN_FIGURE_MAX_COLUMNS) break\n const column = raw as Partial<PluginFigureColumn> | null\n const key = typeof column?.key === 'string' ? column.key : ''\n const kind = column?.kind as PluginFigureColumnKind\n if (!COLUMN_KEY.test(key) || seen.has(key) || !PLUGIN_FIGURE_COLUMN_KINDS.includes(kind)) continue\n if (kind === 'money' && !(typeof column?.currency === 'string' && CURRENCY.test(column.currency))) {\n continue\n }\n seen.add(key)\n columns.push({\n key,\n label: cut(column?.label, 60) || key,\n kind,\n ...(kind === 'money' ? { currency: column?.currency as string } : {}),\n })\n }\n if (!columns.length) return null\n\n const rawRows = Array.isArray(table.rows) ? table.rows : []\n const rows = rawRows.slice(0, PLUGIN_FIGURE_MAX_ROWS).map((raw) => {\n const source = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}\n return Object.fromEntries(columns.map((column) => [column.key, cellFor(column, source[column.key])]))\n })\n const omitted =\n Math.max(0, rawRows.length - PLUGIN_FIGURE_MAX_ROWS) +\n (typeof table.omitted === 'number' && Number.isFinite(table.omitted) && table.omitted > 0\n ? Math.floor(table.omitted)\n : 0)\n\n const period = table.period\n const from = day(period?.from)\n const to = day(period?.to)\n const days = finite(period?.days)\n const path = typeof table.source?.path === 'string' ? table.source.path.trim() : ''\n\n return {\n title,\n source: {\n label: cut(table.source?.label, 80) || title,\n path: CONSOLE_PATH.test(path) && !path.includes('..') ? path : null,\n },\n period:\n from && to && days !== null && days >= 1 && days <= 366\n ? { from, to, days: Math.round(days) }\n : null,\n columns,\n rows,\n omitted,\n notes: (Array.isArray(table.notes) ? table.notes : [])\n .map((note) => maskPersonalDetails(cut(note, 200)))\n .filter(Boolean)\n .slice(0, PLUGIN_FIGURE_MAX_NOTES),\n }\n}\n\n// ── Windows ─────────────────────────────────────────────────────────────\n\n/** A calendar day in UTC. */\nexport function pluginFigureDay(at: Date): string {\n return at.toISOString().slice(0, 10)\n}\n\n/**\n * The window of `days` ending on the day of `now`, inclusive, and the window\n * of the same length immediately before it: what every `change` column\n * compares, so a figure from one reader compares like a figure from another.\n */\nexport function pluginFigureWindows(\n now: Date,\n days: number,\n): {\n current: { from: string; to: string; startMs: number; endMs: number }\n previous: { from: string; to: string; startMs: number; endMs: number }\n} {\n const span = Math.max(1, Math.floor(days))\n const dayMs = 86_400_000\n const endOfToday = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) + dayMs\n const currentStart = endOfToday - span * dayMs\n const previousStart = currentStart - span * dayMs\n return {\n current: {\n from: pluginFigureDay(new Date(currentStart)),\n to: pluginFigureDay(new Date(endOfToday - dayMs)),\n startMs: currentStart,\n endMs: endOfToday,\n },\n previous: {\n from: pluginFigureDay(new Date(previousStart)),\n to: pluginFigureDay(new Date(currentStart - dayMs)),\n startMs: previousStart,\n endMs: currentStart,\n },\n }\n}\n\n/**\n * The percent change from `previous` to `current`, to one decimal place.\n * `null` when there is nothing earlier to compare with, so a first window\n * reads as no change figure at all rather than as a rise from zero.\n */\nexport function pluginFigureChange(current: number, previous: number | null): number | null {\n if (previous === null || !Number.isFinite(previous) || previous <= 0) return null\n if (!Number.isFinite(current)) return null\n return Math.round(((current - previous) / previous) * 1_000) / 10\n}\n\n/**\n * How a cell reads to a person, by its column's kind; `null` is \"—\". Every\n * consumer that shows a table to a person words its cells here, so an insight\n * answer and a weekly summary print the same figure the same way.\n */\nexport function pluginFigureCellText(\n table: Pick<PluginFigureTable, 'columns'>,\n key: string,\n value: PluginFigureCell | undefined,\n): string {\n if (value === null || value === undefined || value === '') return '—'\n const column = table.columns.find((entry) => entry.key === key)\n if (typeof value === 'string' || !column) return String(value)\n switch (column.kind) {\n case 'money':\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: column.currency ?? 'USD',\n }).format(value)\n case 'percent':\n return `${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'change':\n return `${value > 0 ? '+' : ''}${value.toLocaleString('en-US', { maximumFractionDigits: 1 })}%`\n case 'duration': {\n const seconds = Math.round(value)\n if (seconds < 60) return `${seconds}s`\n const minutes = Math.floor(seconds / 60)\n if (minutes < 60) return `${minutes}m ${String(seconds % 60).padStart(2, '0')}s`\n return `${Math.floor(minutes / 60)}h ${String(minutes % 60).padStart(2, '0')}m`\n }\n default:\n return value.toLocaleString('en-US', { maximumFractionDigits: 2 })\n }\n}\n\n/** The ISO 8601 week an instant falls in, in UTC (`2026-W38`). */\nexport function pluginFigureIsoWeek(at: Date): string {\n const date = new Date(Date.UTC(at.getUTCFullYear(), at.getUTCMonth(), at.getUTCDate()))\n // Thursday decides the week's year.\n const weekday = date.getUTCDay() || 7\n date.setUTCDate(date.getUTCDate() + 4 - weekday)\n const yearStart = Date.UTC(date.getUTCFullYear(), 0, 1)\n const week = Math.ceil(((date.getTime() - yearStart) / 86_400_000 + 1) / 7)\n return `${date.getUTCFullYear()}-W${String(week).padStart(2, '0')}`\n}\n\n/** The section of the weekly summary a reader is listed in, or `null` for a reader that is not in it. */\nexport function pluginFigureSummarySection(reader: PluginFigureReader): 'site' | 'workspace' | null {\n if (!reader.weeklySummary) return null\n return reader.weeklySummary.per ?? (reader.scope === 'site' ? 'site' : 'workspace')\n}\n"],"names":["getRegisteringPluginId","definePluginServiceContract","registerPluginService","resolvePluginServices","PLUGIN_FIGURE_COLUMN_KINDS","PLUGIN_FIGURES","multiple","PLUGIN_FIGURE_MAX_ROWS","PLUGIN_FIGURE_MAX_COLUMNS","PLUGIN_FIGURE_MAX_TEXT","PLUGIN_FIGURE_MAX_NOTES","READER_ID","PARAM_NAME","COLUMN_KEY","CONSOLE_PATH","CALENDAR_DAY","CURRENCY","registerPluginFigureReader","reader","options","key","id","trim","test","Error","param","params","name","pluginId","incumbent","find","entry","pluginFigureReader","one","impl","listPluginFigureReaders","map","sort","a","b","localeCompare","pluginFigureReaderPlugin","resolved","plugin","undefined","pluginFigureParams","raw","value","text","slice","required","ok","missing","EMAIL","PHONE_LIKE","looksLikePhone","match","digits","replace","length","maskPersonalDetails","hasPersonalDetails","cut","max","finite","Number","isFinite","day","isNaN","Date","parse","cellFor","column","kind","number","Math","round","normalizePluginFigureTable","table","Array","isArray","title","seen","Set","columns","has","includes","currency","add","push","label","rawRows","rows","source","Object","fromEntries","omitted","floor","period","from","to","days","path","notes","note","filter","Boolean","pluginFigureDay","at","toISOString","pluginFigureWindows","now","span","dayMs","endOfToday","UTC","getUTCFullYear","getUTCMonth","getUTCDate","currentStart","previousStart","current","startMs","endMs","previous","pluginFigureChange","pluginFigureCellText","String","Intl","NumberFormat","style","format","toLocaleString","maximumFractionDigits","seconds","minutes","padStart","pluginFigureIsoWeek","date","weekday","getUTCDay","setUTCDate","yearStart","week","ceil","getTime","pluginFigureSummarySection","weeklySummary","per","scope"],"mappings":"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"}
|
|
@@ -198,6 +198,11 @@ export interface StockPhotoPick {
|
|
|
198
198
|
* picture of last resort when the library is down or found nothing.
|
|
199
199
|
*/
|
|
200
200
|
origin: 'library' | 'starter';
|
|
201
|
+
/**
|
|
202
|
+
* What the photo shows, in words a page's `alt` takes as they stand
|
|
203
|
+
* (AGL-3856): the photo's own where the subject's words do not describe it.
|
|
204
|
+
*/
|
|
205
|
+
alt?: string;
|
|
201
206
|
}
|
|
202
207
|
export interface StockPhotoPicker {
|
|
203
208
|
/** A photo for the subject, or `null` when none can be had. Never throws. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/stock-photo-provider.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\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * STOCK PHOTOS (AGL-3660): a library of licensed photos outside this\n * platform that a server process searches and copies from.\n *\n * Core defines the contract and never names a library. A plugin registers a\n * provider from its server declarations, and a caller — the AI site build is\n * the first — asks for {@link stockPhotoProvider} and gets the first one\n * this deployment configured, or `null`. Nothing here stores a photo: a\n * caller that keeps one copies its bytes into the site's own media library\n * (`plugin-media-ingest.ts`), because a page that names a library's own\n * address would hotlink it, and most libraries' terms refuse that.\n *\n * What a provider owes its library's terms — caching searches, its rate\n * limit, the hosts it may fetch from — it keeps inside itself, so a caller\n * cannot get them wrong. What it owes the caller is {@link\n * StockPhotoProvider.credit}: the words and links the asset records about\n * where it came from, and whether the library requires them shown.\n *\n * A provider that is registered and not configured (its key unset) answers\n * as absent, so a deployment without the key behaves as one without the\n * plugin.\n */\n\n/** The shape a caller wants; a provider maps it onto its own filter. */\nexport type StockPhotoOrientation = 'horizontal' | 'vertical' | 'any'\n\nexport interface StockPhotoSearchRequest {\n /** Plain search words, at most {@link STOCK_PHOTO_QUERY_MAX_CHARS}. */\n query: string\n orientation?: StockPhotoOrientation\n /** The smallest original a hit may have, in pixels. */\n minWidth?: number\n minHeight?: number\n /** A hint that the photo should show people, where the library can filter by it. */\n people?: boolean\n}\n\n/** The longest query a caller sends; libraries refuse or truncate longer ones. */\nexport const STOCK_PHOTO_QUERY_MAX_CHARS = 100\n\n/** One photo a search found. */\nexport interface StockPhoto {\n /** The registering provider's id. */\n provider: string\n /** The library's own id for the photo, stable across searches. */\n id: string\n /** The size of the copy {@link StockPhotoProvider.download} fetches. */\n width: number\n height: number\n /** The photo's page at the library, for the credit. */\n pageUrl: string\n /** The contributor's name as the library shows it. */\n photographer: string\n /** The contributor's page at the library, when it has one. */\n photographerUrl?: string\n /** The library's tags, lowercase. */\n tags: string[]\n /**\n * The library's own description of the photo, where it writes one (a\n * Pexels hit's `alt`). A caller ranks hits by it with the tags.\n */\n alt?: string\n}\n\n/** What an asset copied from a library records about where it came from. */\nexport interface StockPhotoCredit {\n /** The library's name, as its terms ask it written. */\n providerLabel: string\n /** The license the photo is used under. */\n license: string\n licenseUrl: string\n /** Whether the license requires the credit shown wherever the photo is. */\n attributionRequired: boolean\n /** A sentence crediting the photo, for the asset's description. */\n text: string\n}\n\nexport interface StockPhotoSearchResult {\n photos: StockPhoto[]\n /** Whether the answer came from the provider's own cache. */\n cached: boolean\n}\n\nexport interface StockPhotoDownload {\n bytes: Uint8Array\n /** `image/jpeg`, `image/png` or `image/webp`. */\n contentType: string\n}\n\nexport interface StockPhotoProvider {\n /** A stable id, the prefix of every source key: `pixabay`. */\n id: string\n /** The library's name, in customer copy. */\n label: string\n /** Whether this deployment holds what the provider needs to search. */\n isConfigured(): boolean\n /**\n * The photos matching `request`, best first. `null` when the provider\n * cannot ask right now — its rate limit is spent, or the library failed —\n * which a caller treats as \"no photo\", never as an error to retry.\n */\n search(\n request: StockPhotoSearchRequest,\n options?: { signal?: AbortSignal },\n ): Promise<StockPhotoSearchResult | null>\n /**\n * The photo's bytes, fetched only from the library's own image hosts, or\n * `null` when they cannot be had within `maxBytes` and the signal.\n */\n download(\n photo: StockPhoto,\n options: { maxBytes: number; signal?: AbortSignal },\n ): Promise<StockPhotoDownload | null>\n /** How an asset copied from `photo` credits it. */\n credit(photo: StockPhoto): StockPhotoCredit\n}\n\n/**\n * Many plugins may each register a library; a caller is handed the first\n * configured one, highest priority first.\n */\nexport const STOCK_PHOTO_PROVIDERS = definePluginServiceContract<StockPhotoProvider>(\n 'core.stock-photos',\n { multiple: true },\n)\n\n/** The key an asset copied from a library is found again by: `{provider}:{id}`. */\nexport function stockPhotoSourceKey(photo: Pick<StockPhoto, 'provider' | 'id'>): string {\n return `${photo.provider}:${photo.id}`\n}\n\n/**\n * Registers a library. The owner is the loader's marker when a register fn\n * is running, else `options.pluginId`; the provider's own id tells two\n * libraries of one plugin apart.\n */\nexport function registerStockPhotoProvider(\n provider: StockPhotoProvider,\n options?: { pluginId?: string; priority?: number },\n): void {\n if (!provider?.id?.trim()) throw new Error('a stock photo provider needs an id')\n registerPluginService(STOCK_PHOTO_PROVIDERS, provider, {\n key: provider.id,\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n })\n}\n\n/**\n * The first registered library this deployment configured, or `null`.\n * Synchronous and free of I/O. A provider whose configuration check throws\n * answers as unconfigured: a stock photo is decoration, and its failure\n * leaves the caller's own fallback standing.\n */\nexport function stockPhotoProvider(): StockPhotoProvider | null {\n for (const { impl } of resolvePluginServices(STOCK_PHOTO_PROVIDERS)) {\n try {\n if (impl.isConfigured()) return impl\n } catch {\n // Unconfigured, as the note says.\n }\n }\n return null\n}\n\n/**\n * A PHOTO FOR A SUBJECT, KEPT IN THE SITE'S LIBRARY (AGL-3755).\n *\n * The providers above answer searches; choosing well among the hits is\n * work of its own — subject nouns rather than whole sentences, hits scored\n * by how their tags meet the subject and the business's world, nothing\n * placed twice in one act, the business's own world when the subject finds\n * nothing, and a starter photo when the library is down — and it lives in\n * the plugin that learned it on live site starts (the AI plugin). A plugin's\n * draft writer that wants a picture (an event's cover) asks\n * {@link stockPhotoPicker} for the deployment's one picker and gets a photo\n * the site can serve, or `null` where no picker is registered. It never\n * imports the plugin that picks.\n *\n * The picker copies a stock photo into the site's own library as the member\n * named, through `core.media-ingest`, and only once: a photo the library\n * already holds (by its source key) is reused, never stored a second time.\n */\nexport interface StockPhotoPickRequest {\n hostId: string\n /** The member the photo is stored as; their access, the site's lockdown and the plan's storage are checked as for an upload of theirs. */\n uid: string\n /** What the photo shows, in plain words: an event's title, a product's name. The picker reads the subject's nouns out of it. */\n subject: string\n /** The business in its own words (\"a sourdough bakery\"), which holds the photo to its world and is searched when the subject finds nothing. */\n business?: string\n /** The asset's description in the library; the subject where absent. */\n alt?: string\n /** The frame the photo fills; `horizontal` where absent. */\n orientation?: StockPhotoOrientation\n /**\n * The act the photo is one of (an AI job's id): no photo is placed twice\n * within it. Absent, only the site library's own reuse applies.\n */\n scope?: string\n /** Turns the choice among equally good photos; the same seed picks the same photo again. */\n seed?: string\n /** Source keys or asset srcs the act already shows. */\n avoid?: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface StockPhotoPick {\n /** What an `image` node's `src` holds: the asset's media reference, or a starter photo's path. */\n src: string\n /** An address an `<img src>` or an image-address field takes as it stands: site-relative. */\n url: string\n width: number\n height: number\n /**\n * `library`: a stock photo in the site's own media library, copied there\n * now or reused. `starter`: one of the platform's starter photos, the\n * picture of last resort when the library is down or found nothing.\n */\n origin: 'library' | 'starter'\n}\n\nexport interface StockPhotoPicker {\n /** A photo for the subject, or `null` when none can be had. Never throws. */\n pick(request: StockPhotoPickRequest): Promise<StockPhotoPick | null>\n}\n\n/** One picker per deployment: the plugin whose choosing it is registers it. */\nexport const STOCK_PHOTO_PICKER = definePluginServiceContract<StockPhotoPicker>(\n 'core.stock-photo-picker',\n { multiple: false },\n)\n\n/** Registers the deployment's picker; registering again replaces it. */\nexport function registerStockPhotoPicker(\n picker: StockPhotoPicker,\n options?: { pluginId?: string },\n): void {\n if (typeof picker?.pick !== 'function') throw new Error('a stock photo picker needs pick')\n registerPluginService(STOCK_PHOTO_PICKER, picker, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered picker, or `null` in a process that registered none. */\nexport function stockPhotoPicker(): StockPhotoPicker | null {\n return resolvePluginService(STOCK_PHOTO_PICKER) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","resolvePluginServices","STOCK_PHOTO_QUERY_MAX_CHARS","STOCK_PHOTO_PROVIDERS","multiple","stockPhotoSourceKey","photo","provider","id","registerStockPhotoProvider","options","trim","Error","key","pluginId","priority","undefined","stockPhotoProvider","impl","isConfigured","STOCK_PHOTO_PICKER","registerStockPhotoPicker","picker","pick","stockPhotoPicker"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,EACpBC,qBAAqB,QAChB,uBAAmB;AAuC1B,gFAAgF,GAChF,OAAO,MAAMC,8BAA8B,IAAG;AA+E9C;;;CAGC,GACD,OAAO,MAAMC,wBAAwBL,4BACnC,qBACA;IAAEM,UAAU;AAAK,GAClB;AAED,iFAAiF,GACjF,OAAO,SAASC,oBAAoBC,KAA0C;IAC5E,OAAO,GAAGA,MAAMC,QAAQ,CAAC,CAAC,EAAED,MAAME,EAAE,EAAE;AACxC;AAEA;;;;CAIC,GACD,OAAO,SAASC,2BACdF,QAA4B,EAC5BG,OAAkD;QAE7CH;IAAL,IAAI,EAACA,6BAAAA,eAAAA,SAAUC,EAAE,qBAAZD,aAAcI,IAAI,KAAI,MAAM,IAAIC,MAAM;IAC3Cb,sBAAsBI,uBAAuBI,UAAU;QACrDM,KAAKN,SAASC,EAAE;OACZE,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC,GACtDJ,CAAAA,2BAAAA,QAASK,QAAQ,MAAKC,YAAY;QAAED,UAAUL,QAAQK,QAAQ;IAAC,IAAI,CAAC;AAE5E;AAEA;;;;;CAKC,GACD,OAAO,SAASE;IACd,KAAK,MAAM,EAAEC,IAAI,EAAE,IAAIjB,sBAAsBE,uBAAwB;QACnE,IAAI;YACF,IAAIe,KAAKC,YAAY,IAAI,OAAOD;QAClC,EAAE,eAAM;QACN,kCAAkC;QACpC;IACF;IACA,OAAO;AACT;AAgEA,6EAA6E,GAC7E,OAAO,MAAME,qBAAqBtB,4BAChC,2BACA;IAAEM,UAAU;AAAM,GACnB;AAED,sEAAsE,GACtE,OAAO,SAASiB,yBACdC,MAAwB,EACxBZ,OAA+B;IAE/B,IAAI,QAAOY,0BAAAA,OAAQC,IAAI,MAAK,YAAY,MAAM,IAAIX,MAAM;IACxDb,sBAAsBqB,oBAAoBE,QAAQ,aAC5CZ,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASU;QACPxB;IAAP,QAAOA,wBAAAA,qBAAqBoB,+BAArBpB,wBAA4C;AACrD"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/plugin-manager/stock-photo-provider.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\nimport {\n definePluginServiceContract,\n registerPluginService,\n resolvePluginService,\n resolvePluginServices,\n} from './plugin-services'\n\n/**\n * STOCK PHOTOS (AGL-3660): a library of licensed photos outside this\n * platform that a server process searches and copies from.\n *\n * Core defines the contract and never names a library. A plugin registers a\n * provider from its server declarations, and a caller — the AI site build is\n * the first — asks for {@link stockPhotoProvider} and gets the first one\n * this deployment configured, or `null`. Nothing here stores a photo: a\n * caller that keeps one copies its bytes into the site's own media library\n * (`plugin-media-ingest.ts`), because a page that names a library's own\n * address would hotlink it, and most libraries' terms refuse that.\n *\n * What a provider owes its library's terms — caching searches, its rate\n * limit, the hosts it may fetch from — it keeps inside itself, so a caller\n * cannot get them wrong. What it owes the caller is {@link\n * StockPhotoProvider.credit}: the words and links the asset records about\n * where it came from, and whether the library requires them shown.\n *\n * A provider that is registered and not configured (its key unset) answers\n * as absent, so a deployment without the key behaves as one without the\n * plugin.\n */\n\n/** The shape a caller wants; a provider maps it onto its own filter. */\nexport type StockPhotoOrientation = 'horizontal' | 'vertical' | 'any'\n\nexport interface StockPhotoSearchRequest {\n /** Plain search words, at most {@link STOCK_PHOTO_QUERY_MAX_CHARS}. */\n query: string\n orientation?: StockPhotoOrientation\n /** The smallest original a hit may have, in pixels. */\n minWidth?: number\n minHeight?: number\n /** A hint that the photo should show people, where the library can filter by it. */\n people?: boolean\n}\n\n/** The longest query a caller sends; libraries refuse or truncate longer ones. */\nexport const STOCK_PHOTO_QUERY_MAX_CHARS = 100\n\n/** One photo a search found. */\nexport interface StockPhoto {\n /** The registering provider's id. */\n provider: string\n /** The library's own id for the photo, stable across searches. */\n id: string\n /** The size of the copy {@link StockPhotoProvider.download} fetches. */\n width: number\n height: number\n /** The photo's page at the library, for the credit. */\n pageUrl: string\n /** The contributor's name as the library shows it. */\n photographer: string\n /** The contributor's page at the library, when it has one. */\n photographerUrl?: string\n /** The library's tags, lowercase. */\n tags: string[]\n /**\n * The library's own description of the photo, where it writes one (a\n * Pexels hit's `alt`). A caller ranks hits by it with the tags.\n */\n alt?: string\n}\n\n/** What an asset copied from a library records about where it came from. */\nexport interface StockPhotoCredit {\n /** The library's name, as its terms ask it written. */\n providerLabel: string\n /** The license the photo is used under. */\n license: string\n licenseUrl: string\n /** Whether the license requires the credit shown wherever the photo is. */\n attributionRequired: boolean\n /** A sentence crediting the photo, for the asset's description. */\n text: string\n}\n\nexport interface StockPhotoSearchResult {\n photos: StockPhoto[]\n /** Whether the answer came from the provider's own cache. */\n cached: boolean\n}\n\nexport interface StockPhotoDownload {\n bytes: Uint8Array\n /** `image/jpeg`, `image/png` or `image/webp`. */\n contentType: string\n}\n\nexport interface StockPhotoProvider {\n /** A stable id, the prefix of every source key: `pixabay`. */\n id: string\n /** The library's name, in customer copy. */\n label: string\n /** Whether this deployment holds what the provider needs to search. */\n isConfigured(): boolean\n /**\n * The photos matching `request`, best first. `null` when the provider\n * cannot ask right now — its rate limit is spent, or the library failed —\n * which a caller treats as \"no photo\", never as an error to retry.\n */\n search(\n request: StockPhotoSearchRequest,\n options?: { signal?: AbortSignal },\n ): Promise<StockPhotoSearchResult | null>\n /**\n * The photo's bytes, fetched only from the library's own image hosts, or\n * `null` when they cannot be had within `maxBytes` and the signal.\n */\n download(\n photo: StockPhoto,\n options: { maxBytes: number; signal?: AbortSignal },\n ): Promise<StockPhotoDownload | null>\n /** How an asset copied from `photo` credits it. */\n credit(photo: StockPhoto): StockPhotoCredit\n}\n\n/**\n * Many plugins may each register a library; a caller is handed the first\n * configured one, highest priority first.\n */\nexport const STOCK_PHOTO_PROVIDERS = definePluginServiceContract<StockPhotoProvider>(\n 'core.stock-photos',\n { multiple: true },\n)\n\n/** The key an asset copied from a library is found again by: `{provider}:{id}`. */\nexport function stockPhotoSourceKey(photo: Pick<StockPhoto, 'provider' | 'id'>): string {\n return `${photo.provider}:${photo.id}`\n}\n\n/**\n * Registers a library. The owner is the loader's marker when a register fn\n * is running, else `options.pluginId`; the provider's own id tells two\n * libraries of one plugin apart.\n */\nexport function registerStockPhotoProvider(\n provider: StockPhotoProvider,\n options?: { pluginId?: string; priority?: number },\n): void {\n if (!provider?.id?.trim()) throw new Error('a stock photo provider needs an id')\n registerPluginService(STOCK_PHOTO_PROVIDERS, provider, {\n key: provider.id,\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n ...(options?.priority !== undefined ? { priority: options.priority } : {}),\n })\n}\n\n/**\n * The first registered library this deployment configured, or `null`.\n * Synchronous and free of I/O. A provider whose configuration check throws\n * answers as unconfigured: a stock photo is decoration, and its failure\n * leaves the caller's own fallback standing.\n */\nexport function stockPhotoProvider(): StockPhotoProvider | null {\n for (const { impl } of resolvePluginServices(STOCK_PHOTO_PROVIDERS)) {\n try {\n if (impl.isConfigured()) return impl\n } catch {\n // Unconfigured, as the note says.\n }\n }\n return null\n}\n\n/**\n * A PHOTO FOR A SUBJECT, KEPT IN THE SITE'S LIBRARY (AGL-3755).\n *\n * The providers above answer searches; choosing well among the hits is\n * work of its own — subject nouns rather than whole sentences, hits scored\n * by how their tags meet the subject and the business's world, nothing\n * placed twice in one act, the business's own world when the subject finds\n * nothing, and a starter photo when the library is down — and it lives in\n * the plugin that learned it on live site starts (the AI plugin). A plugin's\n * draft writer that wants a picture (an event's cover) asks\n * {@link stockPhotoPicker} for the deployment's one picker and gets a photo\n * the site can serve, or `null` where no picker is registered. It never\n * imports the plugin that picks.\n *\n * The picker copies a stock photo into the site's own library as the member\n * named, through `core.media-ingest`, and only once: a photo the library\n * already holds (by its source key) is reused, never stored a second time.\n */\nexport interface StockPhotoPickRequest {\n hostId: string\n /** The member the photo is stored as; their access, the site's lockdown and the plan's storage are checked as for an upload of theirs. */\n uid: string\n /** What the photo shows, in plain words: an event's title, a product's name. The picker reads the subject's nouns out of it. */\n subject: string\n /** The business in its own words (\"a sourdough bakery\"), which holds the photo to its world and is searched when the subject finds nothing. */\n business?: string\n /** The asset's description in the library; the subject where absent. */\n alt?: string\n /** The frame the photo fills; `horizontal` where absent. */\n orientation?: StockPhotoOrientation\n /**\n * The act the photo is one of (an AI job's id): no photo is placed twice\n * within it. Absent, only the site library's own reuse applies.\n */\n scope?: string\n /** Turns the choice among equally good photos; the same seed picks the same photo again. */\n seed?: string\n /** Source keys or asset srcs the act already shows. */\n avoid?: readonly string[]\n signal?: AbortSignal\n}\n\nexport interface StockPhotoPick {\n /** What an `image` node's `src` holds: the asset's media reference, or a starter photo's path. */\n src: string\n /** An address an `<img src>` or an image-address field takes as it stands: site-relative. */\n url: string\n width: number\n height: number\n /**\n * `library`: a stock photo in the site's own media library, copied there\n * now or reused. `starter`: one of the platform's starter photos, the\n * picture of last resort when the library is down or found nothing.\n */\n origin: 'library' | 'starter'\n /**\n * What the photo shows, in words a page's `alt` takes as they stand\n * (AGL-3856): the photo's own where the subject's words do not describe it.\n */\n alt?: string\n}\n\nexport interface StockPhotoPicker {\n /** A photo for the subject, or `null` when none can be had. Never throws. */\n pick(request: StockPhotoPickRequest): Promise<StockPhotoPick | null>\n}\n\n/** One picker per deployment: the plugin whose choosing it is registers it. */\nexport const STOCK_PHOTO_PICKER = definePluginServiceContract<StockPhotoPicker>(\n 'core.stock-photo-picker',\n { multiple: false },\n)\n\n/** Registers the deployment's picker; registering again replaces it. */\nexport function registerStockPhotoPicker(\n picker: StockPhotoPicker,\n options?: { pluginId?: string },\n): void {\n if (typeof picker?.pick !== 'function') throw new Error('a stock photo picker needs pick')\n registerPluginService(STOCK_PHOTO_PICKER, picker, {\n ...(options?.pluginId ? { pluginId: options.pluginId } : {}),\n })\n}\n\n/** The registered picker, or `null` in a process that registered none. */\nexport function stockPhotoPicker(): StockPhotoPicker | null {\n return resolvePluginService(STOCK_PHOTO_PICKER) ?? null\n}\n"],"names":["definePluginServiceContract","registerPluginService","resolvePluginService","resolvePluginServices","STOCK_PHOTO_QUERY_MAX_CHARS","STOCK_PHOTO_PROVIDERS","multiple","stockPhotoSourceKey","photo","provider","id","registerStockPhotoProvider","options","trim","Error","key","pluginId","priority","undefined","stockPhotoProvider","impl","isConfigured","STOCK_PHOTO_PICKER","registerStockPhotoPicker","picker","pick","stockPhotoPicker"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,EAC3BC,qBAAqB,EACrBC,oBAAoB,EACpBC,qBAAqB,QAChB,uBAAmB;AAuC1B,gFAAgF,GAChF,OAAO,MAAMC,8BAA8B,IAAG;AA+E9C;;;CAGC,GACD,OAAO,MAAMC,wBAAwBL,4BACnC,qBACA;IAAEM,UAAU;AAAK,GAClB;AAED,iFAAiF,GACjF,OAAO,SAASC,oBAAoBC,KAA0C;IAC5E,OAAO,GAAGA,MAAMC,QAAQ,CAAC,CAAC,EAAED,MAAME,EAAE,EAAE;AACxC;AAEA;;;;CAIC,GACD,OAAO,SAASC,2BACdF,QAA4B,EAC5BG,OAAkD;QAE7CH;IAAL,IAAI,EAACA,6BAAAA,eAAAA,SAAUC,EAAE,qBAAZD,aAAcI,IAAI,KAAI,MAAM,IAAIC,MAAM;IAC3Cb,sBAAsBI,uBAAuBI,UAAU;QACrDM,KAAKN,SAASC,EAAE;OACZE,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC,GACtDJ,CAAAA,2BAAAA,QAASK,QAAQ,MAAKC,YAAY;QAAED,UAAUL,QAAQK,QAAQ;IAAC,IAAI,CAAC;AAE5E;AAEA;;;;;CAKC,GACD,OAAO,SAASE;IACd,KAAK,MAAM,EAAEC,IAAI,EAAE,IAAIjB,sBAAsBE,uBAAwB;QACnE,IAAI;YACF,IAAIe,KAAKC,YAAY,IAAI,OAAOD;QAClC,EAAE,eAAM;QACN,kCAAkC;QACpC;IACF;IACA,OAAO;AACT;AAqEA,6EAA6E,GAC7E,OAAO,MAAME,qBAAqBtB,4BAChC,2BACA;IAAEM,UAAU;AAAM,GACnB;AAED,sEAAsE,GACtE,OAAO,SAASiB,yBACdC,MAAwB,EACxBZ,OAA+B;IAE/B,IAAI,QAAOY,0BAAAA,OAAQC,IAAI,MAAK,YAAY,MAAM,IAAIX,MAAM;IACxDb,sBAAsBqB,oBAAoBE,QAAQ,aAC5CZ,CAAAA,2BAAAA,QAASI,QAAQ,IAAG;QAAEA,UAAUJ,QAAQI,QAAQ;IAAC,IAAI,CAAC;AAE9D;AAEA,wEAAwE,GACxE,OAAO,SAASU;QACPxB;IAAP,QAAOA,wBAAAA,qBAAqBoB,+BAArBpB,wBAA4C;AACrD"}
|