@aglyn/aglyn 1.0.0-beta.277 → 1.0.0-beta.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +357 -49
- package/src/lib/app-utils/docs-index.generated.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/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-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
|
@@ -71,6 +71,13 @@ export declare function addonMaxForBaseline(kind: AddonKind, baseline: ReturnTyp
|
|
|
71
71
|
export declare function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number;
|
|
72
72
|
/** Whether a plan sells an add-on at all: priced, with a ceiling above zero. */
|
|
73
73
|
export declare function planSellsAddon(kind: AddonKind, plan: OrgPlan): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The first of `plans` (listed smallest first) that sells an add-on kind, or
|
|
76
|
+
* null when none does: the plan a picker names beside an add-on the chosen
|
|
77
|
+
* plan does not sell, so it shows as "needs Pro or higher" rather than
|
|
78
|
+
* vanishing (AGL-4025). POS registers need a plan with the point of sale.
|
|
79
|
+
*/
|
|
80
|
+
export declare function firstPlanSellingAddon(kind: AddonKind, plans: readonly OrgPlan[]): OrgPlan | null;
|
|
74
81
|
/** Quantities by kind, absent kinds meaning none. */
|
|
75
82
|
export type BundleAddons = Partial<Record<AddonKind, number>>;
|
|
76
83
|
/**
|
|
@@ -78,6 +85,12 @@ export type BundleAddons = Partial<Record<AddonKind, number>>;
|
|
|
78
85
|
* account owner chose (AGL-3955); the rest follow the same shape.
|
|
79
86
|
*/
|
|
80
87
|
export declare const BUNDLE_ADDON_TOKENS: Record<AddonKind, string>;
|
|
88
|
+
/**
|
|
89
|
+
* Every spelling the parser accepts, lower-cased: the written token, the
|
|
90
|
+
* stored kind, and the plurals a person types. Exported for the native apps'
|
|
91
|
+
* generated words (AGL-4011), which read a stored proposal the same way.
|
|
92
|
+
*/
|
|
93
|
+
export declare const TOKEN_KINDS: Readonly<Record<string, AddonKind>>;
|
|
81
94
|
/** One add-on a token names, with the Aglyn AI tier when it names one. */
|
|
82
95
|
interface ParsedToken {
|
|
83
96
|
kind: AddonKind;
|
|
@@ -131,6 +144,30 @@ export declare function fitBundleAddonsToPlan(addons: BundleAddons | null | unde
|
|
|
131
144
|
* `ai` otherwise.
|
|
132
145
|
*/
|
|
133
146
|
export declare function bundleAddonsParam(addons: BundleAddons | null | undefined, aiTier?: AiTierId | null): string;
|
|
147
|
+
/**
|
|
148
|
+
* The Aglyn AI tier a bundle sells (AGL-4025): the tier named, or the plan's
|
|
149
|
+
* default (`defaultAiTierForPlan`) when none is; null when the bundle has no
|
|
150
|
+
* Aglyn AI or the plan sells no tier. A picker shows this tier as chosen, the
|
|
151
|
+
* link writes it as `ai:<tier>`, and checkout charges it, so the three agree.
|
|
152
|
+
*/
|
|
153
|
+
export declare function bundleAiTier(addons: BundleAddons | null | undefined, plan: OrgPlan, tier?: AiTierId | null): AiTierId | null;
|
|
154
|
+
/**
|
|
155
|
+
* What a bundle costs at list price, in cents, before tax and any promotion
|
|
156
|
+
* code (AGL-4026): the plan at its price for the cadence (the annual monthly
|
|
157
|
+
* price × 12 yearly, the monthly price monthly), plus every add-on the plan
|
|
158
|
+
* sells at its unit price × quantity, × 12 yearly as every add-on bills.
|
|
159
|
+
*
|
|
160
|
+
* The figure Billing shows while Stripe cannot price the bundle with tax —
|
|
161
|
+
* no billing address yet, the preview still on its way, or a preview that
|
|
162
|
+
* failed — so the owner always sees what the bundle costs and sees it move
|
|
163
|
+
* as they change it. Never shown in place of Stripe's taxed total.
|
|
164
|
+
*/
|
|
165
|
+
export declare function bundleListPriceCents(bundle: {
|
|
166
|
+
plan: OrgPlan;
|
|
167
|
+
interval: 'month' | 'year';
|
|
168
|
+
addons?: BundleAddons | null;
|
|
169
|
+
aiTier?: AiTierId | null;
|
|
170
|
+
}): number;
|
|
134
171
|
/** Whether any add-on is asked for. */
|
|
135
172
|
export declare function hasBundleAddons(addons: BundleAddons | null | undefined): boolean;
|
|
136
173
|
/**
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* the parser never throws and never guesses: an unknown token is dropped and
|
|
32
32
|
* reported, so the page can say so in one line instead of failing.
|
|
33
33
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
34
|
-
import { AI_TIER_IDS, AI_TIERS, aiAddonName, aiTierAddonKey, aiTierName, defaultAiTierForPlan, EVENT_CALENDAR_ADDON_MONTHLY_USD, EXTRA_HOSTS_ADDON_MAX, PLAN_LABELS, parseAiTierAddonKey, PLAN_PRICING, planSellsAiTiers, planSellsSiteCare, POS_REGISTER_ADDON_MONTHLY_USD, POS_REGISTERS_ADDON_MAX, resolveOrgEntitlements, SITE_CARE_ADDON_MONTHLY_USD, UNLIMITED } from "./plan-entitlements.js";
|
|
34
|
+
import { AI_TIER_IDS, AI_TIERS, aiAddonName, aiTierAddonKey, aiTierName, defaultAiTierForPlan, EVENT_CALENDAR_ADDON_MONTHLY_USD, EXTRA_HOSTS_ADDON_MAX, isAiTierId, PLAN_LABELS, parseAiTierAddonKey, PLAN_PRICING, planSellsAiTiers, planSellsSiteCare, POS_REGISTER_ADDON_MONTHLY_USD, POS_REGISTERS_ADDON_MAX, resolveOrgEntitlements, SITE_CARE_ADDON_MONTHLY_USD, UNLIMITED } from "./plan-entitlements.js";
|
|
35
35
|
/**
|
|
36
36
|
* Every add-on kind Billing sells, in the order the Billing card lists them.
|
|
37
37
|
* `/api/billing/addons`, the webhook's price map and the native add-ons
|
|
@@ -130,6 +130,15 @@ export function isAddonKind(value) {
|
|
|
130
130
|
/** Whether a plan sells an add-on at all: priced, with a ceiling above zero. */ export function planSellsAddon(kind, plan) {
|
|
131
131
|
return addonUnitUsd(kind, plan) != null && addonMaxForPlan(kind, plan) > 0;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The first of `plans` (listed smallest first) that sells an add-on kind, or
|
|
135
|
+
* null when none does: the plan a picker names beside an add-on the chosen
|
|
136
|
+
* plan does not sell, so it shows as "needs Pro or higher" rather than
|
|
137
|
+
* vanishing (AGL-4025). POS registers need a plan with the point of sale.
|
|
138
|
+
*/ export function firstPlanSellingAddon(kind, plans) {
|
|
139
|
+
var _plans_find;
|
|
140
|
+
return (_plans_find = plans.find((plan)=>planSellsAddon(kind, plan))) != null ? _plans_find : null;
|
|
141
|
+
}
|
|
133
142
|
/**
|
|
134
143
|
* What a link writes for each kind. `ai` and `extraHost` are the words the
|
|
135
144
|
* account owner chose (AGL-3955); the rest follow the same shape.
|
|
@@ -145,8 +154,9 @@ export function isAddonKind(value) {
|
|
|
145
154
|
};
|
|
146
155
|
/**
|
|
147
156
|
* Every spelling the parser accepts, lower-cased: the written token, the
|
|
148
|
-
* stored kind, and the plurals a person types.
|
|
149
|
-
|
|
157
|
+
* stored kind, and the plurals a person types. Exported for the native apps'
|
|
158
|
+
* generated words (AGL-4011), which read a stored proposal the same way.
|
|
159
|
+
*/ export const TOKEN_KINDS = (()=>{
|
|
150
160
|
const map = {};
|
|
151
161
|
const add = (kind, ...words)=>{
|
|
152
162
|
for (const word of [
|
|
@@ -293,6 +303,40 @@ export function resolveAiAddonTier(tier) {
|
|
|
293
303
|
return quantity > 1 ? `${BUNDLE_ADDON_TOKENS[kind]}:${quantity}` : BUNDLE_ADDON_TOKENS[kind];
|
|
294
304
|
}).join(',');
|
|
295
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* The Aglyn AI tier a bundle sells (AGL-4025): the tier named, or the plan's
|
|
308
|
+
* default (`defaultAiTierForPlan`) when none is; null when the bundle has no
|
|
309
|
+
* Aglyn AI or the plan sells no tier. A picker shows this tier as chosen, the
|
|
310
|
+
* link writes it as `ai:<tier>`, and checkout charges it, so the three agree.
|
|
311
|
+
*/ export function bundleAiTier(addons, plan, tier) {
|
|
312
|
+
var _ref;
|
|
313
|
+
if (!(((_ref = addons == null ? void 0 : addons.aiAddon) != null ? _ref : 0) > 0) || !planSellsAiTiers(plan)) return null;
|
|
314
|
+
return isAiTierId(tier) ? tier : defaultAiTierForPlan(plan);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* What a bundle costs at list price, in cents, before tax and any promotion
|
|
318
|
+
* code (AGL-4026): the plan at its price for the cadence (the annual monthly
|
|
319
|
+
* price × 12 yearly, the monthly price monthly), plus every add-on the plan
|
|
320
|
+
* sells at its unit price × quantity, × 12 yearly as every add-on bills.
|
|
321
|
+
*
|
|
322
|
+
* The figure Billing shows while Stripe cannot price the bundle with tax —
|
|
323
|
+
* no billing address yet, the preview still on its way, or a preview that
|
|
324
|
+
* failed — so the owner always sees what the bundle costs and sees it move
|
|
325
|
+
* as they change it. Never shown in place of Stripe's taxed total.
|
|
326
|
+
*/ export function bundleListPriceCents(bundle) {
|
|
327
|
+
const pricing = PLAN_PRICING[bundle.plan];
|
|
328
|
+
const yearly = bundle.interval === 'year';
|
|
329
|
+
const base = yearly ? pricing.basePriceAnnualMonthlyUsd * 12 : pricing.basePriceMonthlyUsd;
|
|
330
|
+
let cents = Math.round(base * 100);
|
|
331
|
+
const addons = fitBundleAddonsToPlan(bundle.addons, bundle.plan).addons;
|
|
332
|
+
for (const kind of ADDON_KINDS){
|
|
333
|
+
var _addons_kind;
|
|
334
|
+
const quantity = (_addons_kind = addons[kind]) != null ? _addons_kind : 0;
|
|
335
|
+
const unit = addonUnitUsd(kind, bundle.plan, kind === 'aiAddon' ? bundle.aiTier : null);
|
|
336
|
+
if (quantity > 0 && unit != null) cents += Math.round(unit * 100) * quantity * (yearly ? 12 : 1);
|
|
337
|
+
}
|
|
338
|
+
return cents;
|
|
339
|
+
}
|
|
296
340
|
/** Whether any add-on is asked for. */ export function hasBundleAddons(addons) {
|
|
297
341
|
return ADDON_KINDS.some((kind)=>{
|
|
298
342
|
var _ref;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/bundle-intent.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 ADD-ONS A PLAN SELLS, AND THE BUNDLE A LINK ASKS FOR (AGL-3955).\n *\n * One list of add-on kinds and one parser for the add-ons a link, an upgrade\n * proposal or a staff dialog names, so the Billing card, the bundle checkout\n * link, the hand-off proposal (AGL-3956) and `/api/billing/checkout` agree on\n * what `addons=siteCare,ai,extraHost:2` means.\n *\n * The kinds are `OrgSeatAddons`' keys: what `resolvePurchasedAddons` counts,\n * what the webhook mirrors from Stripe, and what `/api/billing/addons` sells.\n * The prices and ceilings here are the ones `billing-addons.ts` (server)\n * re-exports, so a kind the link can preselect is always a kind the\n * checkout can price.\n *\n * A link is typed by a person and passed through hops nobody controls, so\n * the parser never throws and never guesses: an unknown token is dropped and\n * reported, so the page can say so in one line instead of failing.\n */\n\nimport type {\n AiTierId,\n OrgPlan,\n OrgSeatAddons,\n} from '../foundation/definitions/org-billing.types'\nimport {\n AI_TIER_IDS,\n AI_TIERS,\n aiAddonName,\n aiTierAddonKey,\n aiTierName,\n defaultAiTierForPlan,\n EVENT_CALENDAR_ADDON_MONTHLY_USD,\n EXTRA_HOSTS_ADDON_MAX,\n PLAN_LABELS,\n parseAiTierAddonKey,\n PLAN_PRICING,\n planSellsAiTiers,\n planSellsSiteCare,\n POS_REGISTER_ADDON_MONTHLY_USD,\n POS_REGISTERS_ADDON_MAX,\n resolveOrgEntitlements,\n SITE_CARE_ADDON_MONTHLY_USD,\n UNLIMITED,\n} from './plan-entitlements'\n\n/** An add-on kind: a key of `org.seatAddons`. */\nexport type AddonKind = keyof Required<OrgSeatAddons>\n\n/**\n * Every add-on kind Billing sells, in the order the Billing card lists them.\n * `/api/billing/addons`, the webhook's price map and the native add-ons\n * screen all read this list.\n */\nexport const ADDON_KINDS: readonly AddonKind[] = [\n 'managers',\n 'members',\n 'datasets',\n 'hosts',\n 'posRegisters',\n 'eventCalendar',\n 'aiAddon',\n 'siteCare',\n]\n\n/** The org-wide 0/1 kinds: one purchase covers the workspace. */\nexport const TOGGLE_ADDON_KIND_LIST: readonly AddonKind[] = [\n 'eventCalendar',\n 'aiAddon',\n 'siteCare',\n]\n\nexport function isAddonKind(value: unknown): value is AddonKind {\n return typeof value === 'string' && (ADDON_KINDS as readonly string[]).includes(value)\n}\n\n/**\n * Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of\n * this); null when the plan doesn't sell the kind. Single pricing source\n * of truth: `PLAN_PRICING`, the three flat add-on constants and `AI_TIERS`\n * (`tier` as `addonPriceId` reads it; absent, the tier a plan-named purchase\n * means, `defaultAiTierForPlan`, AGL-3965).\n */\nexport function addonUnitUsd(\n kind: AddonKind,\n plan: OrgPlan,\n tier?: AiTierId | null,\n): number | null {\n const pricing = PLAN_PRICING[plan]\n switch (kind) {\n case 'managers':\n return pricing.extraSeatMonthlyUsd\n case 'members':\n return pricing.extraCollaboratorMonthlyUsd\n case 'datasets':\n return pricing.extraDatasetMonthlyUsd\n case 'hosts':\n return pricing.extraHostMonthlyUsd\n case 'posRegisters':\n return POS_REGISTER_ADDON_MONTHLY_USD\n case 'eventCalendar':\n return EVENT_CALENDAR_ADDON_MONTHLY_USD\n case 'aiAddon': {\n const chosen = planSellsAiTiers(plan) ? (tier ?? defaultAiTierForPlan(plan)) : null\n return chosen ? AI_TIERS[chosen].monthlyUsd : null\n }\n case 'siteCare':\n return SITE_CARE_ADDON_MONTHLY_USD\n }\n}\n\n/**\n * Max purchasable quantity per kind, from a purchases-free entitlement\n * resolution (plan defaults + staff overrides only) so the ceiling does not\n * drift as the org buys: seat/dataset kinds stop at the plan's hard max,\n * hosts/registers use flat ceilings, the Event Calendar, Aglyn AI and Site\n * Care are 0/1 toggles. POS registers additionally require the `pos`\n * feature, and Site Care a plan that sells it (`planSellsSiteCare`), which is\n * why the plan travels with the baseline.\n *\n * One definition for every reader: `/api/billing/addons` asks how many the\n * org may buy on its plan, a plan change asks how many of what it bought the\n * new plan can deliver, and a bundle link asks how many it may preselect.\n */\nexport function addonMaxForBaseline(\n kind: AddonKind,\n baseline: ReturnType<typeof resolveOrgEntitlements>,\n plan: OrgPlan,\n): number {\n const bounded = (included: number, max: number) =>\n Number.isFinite(max) ? Math.max(0, max - included) : EXTRA_HOSTS_ADDON_MAX\n switch (kind) {\n case 'managers':\n return bounded(baseline.managersPerOrg, baseline.maxManagersPerOrg)\n case 'members':\n return bounded(baseline.membersPerHost, baseline.maxMembersPerHost)\n case 'datasets':\n return bounded(baseline.datasetsPerOrg, baseline.maxDatasetsPerOrg)\n case 'hosts':\n return baseline.hostLimit === UNLIMITED ? 0 : EXTRA_HOSTS_ADDON_MAX\n case 'posRegisters':\n return baseline.features.pos ? POS_REGISTERS_ADDON_MAX : 0\n case 'eventCalendar':\n return 1\n case 'aiAddon':\n // Org-wide like the Event Calendar: one purchase, of one tier, on any\n // plan that sells tiers (AGL-3965) — not Free, not Enterprise.\n return planSellsAiTiers(plan) ? 1 : 0\n case 'siteCare':\n // Flat-priced, so the price map cannot say \"not sold here\" the way it\n // does for Aglyn AI. This ceiling is the rule that keeps Free (no\n // subscription) and Enterprise (quoted per deal) from buying it.\n return planSellsSiteCare(plan) ? 1 : 0\n }\n}\n\n/** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */\nexport function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number {\n return addonMaxForBaseline(\n kind,\n resolveOrgEntitlements({ plan } as never),\n plan,\n )\n}\n\n/** Whether a plan sells an add-on at all: priced, with a ceiling above zero. */\nexport function planSellsAddon(kind: AddonKind, plan: OrgPlan): boolean {\n return addonUnitUsd(kind, plan) != null && addonMaxForPlan(kind, plan) > 0\n}\n\n/** Quantities by kind, absent kinds meaning none. */\nexport type BundleAddons = Partial<Record<AddonKind, number>>\n\n/**\n * What a link writes for each kind. `ai` and `extraHost` are the words the\n * account owner chose (AGL-3955); the rest follow the same shape.\n */\nexport const BUNDLE_ADDON_TOKENS: Record<AddonKind, string> = {\n managers: 'extraSeat',\n members: 'extraMember',\n datasets: 'extraDataset',\n hosts: 'extraHost',\n posRegisters: 'posRegister',\n eventCalendar: 'eventCalendar',\n aiAddon: 'ai',\n siteCare: 'siteCare',\n}\n\n/**\n * Every spelling the parser accepts, lower-cased: the written token, the\n * stored kind, and the plurals a person types.\n */\nconst TOKEN_KINDS: Record<string, AddonKind> = (() => {\n const map: Record<string, AddonKind> = {}\n const add = (kind: AddonKind, ...words: string[]) => {\n for (const word of [kind, BUNDLE_ADDON_TOKENS[kind], ...words]) {\n map[word.toLowerCase()] = kind\n }\n }\n add('managers', 'extraSeats', 'seats', 'managerSeats')\n add('members', 'extraMembers', 'collaborators', 'collaboratorSeats')\n add('datasets', 'extraDatasets')\n add('hosts', 'extraHosts', 'extraSite', 'extraSites', 'sites')\n add('posRegisters', 'registers', 'register')\n add('eventCalendar', 'events', 'calendar')\n add('aiAddon', 'aglynAi')\n add('siteCare', 'site-care', 'site_care', 'care')\n return map\n})()\n\n/** The most of a quantity kind a link may ask for, before the plan's ceiling. */\nconst MAX_LINK_QUANTITY = 999\n\n/** One add-on a token names, with the Aglyn AI tier when it names one. */\ninterface ParsedToken {\n kind: AddonKind\n quantity: number\n tier?: AiTierId\n}\n\n/**\n * THE `ai:<tier>` TOKENS (AGL-3965).\n *\n * Aglyn AI is a tier chosen on its own, on any paid plan: `ai:boost` adds\n * Aglyn AI Boost. The words are the tier ids, lower-cased as every token is,\n * and resolve through `parseAiTierAddonKey`, the one spelling of the\n * `ai:<tier>` key. A bare `ai` (or `ai:1`) still means \"add it\" and sells\n * the plan's default tier (`defaultAiTierForPlan`). An unknown tier is\n * dropped with the notice like any other unknown add-on, never read as\n * \"add whichever tier\": that would sell a tier nobody chose.\n */\nexport const AI_ADDON_TIER_TOKENS: Readonly<Record<string, AiTierId>> = Object.freeze(\n Object.fromEntries(AI_TIER_IDS.map((tier) => [tier.toLowerCase(), tier])),\n)\n\nexport function resolveAiAddonTier(tier: string): ParsedToken | null {\n const word = tier.trim().toLowerCase()\n if (!Object.prototype.hasOwnProperty.call(AI_ADDON_TIER_TOKENS, word)) return null\n const parsed = parseAiTierAddonKey(aiTierAddonKey(AI_ADDON_TIER_TOKENS[word]))\n return parsed ? { kind: 'aiAddon', quantity: 1, tier: parsed } : null\n}\n\nexport interface ParsedBundleAddons {\n addons: BundleAddons\n /** The tokens nothing understood, as they were written. */\n dropped: string[]\n /** The Aglyn AI tier an `ai:<tier>` token named (AGL-3965), if any. */\n aiTier?: AiTierId\n}\n\n/** One token, `kind` or `kind:quantity`, or null when nothing understands it. */\nfunction parseToken(raw: string): ParsedToken | null {\n const token = raw.trim()\n if (!token) return null\n const [name, ...rest] = token.split(':')\n const kind = TOKEN_KINDS[name.trim().toLowerCase()]\n if (!kind) return null\n const value = rest.join(':').trim()\n if (!value) return { kind, quantity: 1 }\n if (/^\\d+$/.test(value)) {\n const quantity = Math.min(MAX_LINK_QUANTITY, Number(value))\n if (quantity < 1) return null\n return {\n kind,\n quantity: (TOGGLE_ADDON_KIND_LIST as readonly string[]).includes(kind) ? 1 : quantity,\n }\n }\n return kind === 'aiAddon' ? resolveAiAddonTier(value) : null\n}\n\n/**\n * Read the add-ons a link, a proposal or a request names.\n *\n * Accepts the link form (`\"siteCare,ai,extraHost:2\"`), a list of tokens, or a\n * map of kind → quantity or flag (`{ siteCare: true, hosts: 2 }`, the shape\n * the native staff screens post from their toggles). Never throws: anything\n * it cannot read is reported in `dropped` and left out.\n */\nexport function parseBundleAddons(input: unknown): ParsedBundleAddons {\n const addons: BundleAddons = {}\n const dropped: string[] = []\n let aiTier: AiTierId | undefined\n const take = (parsed: ParsedToken | null, raw: string) => {\n if (!parsed) {\n if (raw.trim()) dropped.push(raw.trim().slice(0, 40))\n return\n }\n addons[parsed.kind] = Math.max(addons[parsed.kind] ?? 0, parsed.quantity)\n // The first tier named wins: one workspace holds one tier.\n if (parsed.tier && !aiTier) aiTier = parsed.tier\n }\n if (typeof input === 'string') {\n for (const raw of input.split(/[,\\s]+/)) take(parseToken(raw), raw)\n } else if (Array.isArray(input)) {\n for (const raw of input) {\n if (typeof raw === 'string') take(parseToken(raw), raw)\n }\n } else if (input && typeof input === 'object') {\n for (const [name, value] of Object.entries(input as Record<string, unknown>)) {\n const off =\n value === false ||\n value === 0 ||\n value === null ||\n value === undefined ||\n value === '' ||\n value === 'false' ||\n value === '0'\n if (off) continue\n const quantity =\n value === true || value === 'true'\n ? ''\n : typeof value === 'number' || typeof value === 'string'\n ? String(value)\n : ''\n take(parseToken(quantity ? `${name}:${quantity}` : name), name)\n }\n }\n return { addons, dropped, ...(aiTier ? { aiTier } : {}) }\n}\n\nexport interface FittedBundleAddons {\n addons: BundleAddons\n /** Kinds the plan does not sell, dropped. */\n notSold: AddonKind[]\n}\n\n/**\n * The add-ons a plan can actually carry: kinds it does not sell are dropped,\n * quantities are held to its ceiling. What a link preselects on one plan is\n * re-fitted when the owner picks another, so the checkout is never asked for\n * something it would refuse.\n */\nexport function fitBundleAddonsToPlan(\n addons: BundleAddons | null | undefined,\n plan: OrgPlan,\n): FittedBundleAddons {\n const fitted: BundleAddons = {}\n const notSold: AddonKind[] = []\n for (const kind of ADDON_KINDS) {\n const wanted = Math.floor(Number(addons?.[kind] ?? 0))\n if (!(wanted > 0)) continue\n if (!planSellsAddon(kind, plan)) {\n notSold.push(kind)\n continue\n }\n fitted[kind] = Math.min(wanted, addonMaxForPlan(kind, plan))\n }\n return { addons: fitted, notSold }\n}\n\n/**\n * The add-ons as the link writes them: `siteCare,ai:boost,extraHost:2`.\n * Aglyn AI is written with its tier when one is named (AGL-3965), as bare\n * `ai` otherwise.\n */\nexport function bundleAddonsParam(\n addons: BundleAddons | null | undefined,\n aiTier?: AiTierId | null,\n): string {\n return ADDON_KINDS.filter((kind) => (addons?.[kind] ?? 0) > 0)\n .map((kind) => {\n if (kind === 'aiAddon' && aiTier) return aiTierAddonKey(aiTier)\n const quantity = addons?.[kind] ?? 0\n return quantity > 1 ? `${BUNDLE_ADDON_TOKENS[kind]}:${quantity}` : BUNDLE_ADDON_TOKENS[kind]\n })\n .join(',')\n}\n\n/** Whether any add-on is asked for. */\nexport function hasBundleAddons(addons: BundleAddons | null | undefined): boolean {\n return ADDON_KINDS.some((kind) => (addons?.[kind] ?? 0) > 0)\n}\n\n/**\n * How each kind is named to a customer, singular. Aglyn AI carries its tier\n * when one is named: \"Aglyn AI Boost\" (AGL-3965).\n */\nexport function bundleAddonLabel(\n kind: AddonKind,\n brand?: string,\n aiTier?: AiTierId | null,\n): string {\n switch (kind) {\n case 'managers':\n return 'Extra manager seat'\n case 'members':\n return 'Extra collaborator seat'\n case 'datasets':\n return 'Extra dataset'\n case 'hosts':\n return 'Extra site'\n case 'posRegisters':\n return 'POS register'\n case 'eventCalendar':\n return 'Event Calendar'\n case 'aiAddon':\n return aiTier ? aiTierName(aiTier, brand) : aiAddonName(brand)\n case 'siteCare':\n return 'Site Care'\n }\n}\n\n/** \"Site Care\", \"2 × Extra site\", \"Aglyn AI Boost\". */\nexport function describeBundleAddon(\n kind: AddonKind,\n quantity: number,\n brand?: string,\n aiTier?: AiTierId | null,\n): string {\n const label = bundleAddonLabel(kind, brand, aiTier)\n return quantity > 1 ? `${quantity} × ${label}` : label\n}\n\n/** The longest coupon code a link carries. Stripe codes are far shorter. */\nexport const BUNDLE_COUPON_MAX = 64\n\n/**\n * A promotion code as a link carries it, or undefined. Only the characters\n * Stripe allows in a customer-facing code; anything else is not a code\n * somebody was handed, and is dropped rather than sent to Stripe.\n *\n * The code is only APPLIED here. Codes are minted on the staff Coupons page,\n * which audits them; nothing on this path creates one.\n */\nexport function normalizeBundleCoupon(raw: unknown): string | undefined {\n if (typeof raw !== 'string') return undefined\n const code = raw.trim()\n if (!code || code.length > BUNDLE_COUPON_MAX) return undefined\n return /^[A-Za-z0-9_-]+$/.test(code) ? code : undefined\n}\n\n/**\n * A domain name as a bundle carries it — lower-cased, no scheme, no path —\n * or undefined. Whether it is for sale and at what price is the registrar's\n * answer, re-checked at purchase; this only refuses what cannot be a name.\n */\nexport function normalizeBundleDomain(raw: unknown): string | undefined {\n if (typeof raw !== 'string') return undefined\n const name = raw\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\//, '')\n .replace(/\\/.*$/, '')\n .replace(/\\.$/, '')\n if (!name || name.length > 253) return undefined\n const labels = name.split('.')\n if (labels.length < 2) return undefined\n const ok = labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))\n return ok && /^[a-z]{2,63}$/.test(labels[labels.length - 1]) ? name : undefined\n}\n\n/** \"$15.99\". */\nexport function formatBundleCents(cents: number): string {\n return `$${(Math.max(0, Math.round(cents)) / 100).toFixed(2)}`\n}\n\n/**\n * The domain line a bundle shows (AGL-3955): \"example.com — $15.99 first\n * year, renews yearly at $15.99\". The price is the registrar's at the moment\n * the link was minted; the purchase re-checks it.\n */\nexport function bundleDomainLine(domain: {\n name: string\n firstYearCents: number\n renewalCents: number\n}): string {\n return (\n `${domain.name} — ${formatBundleCents(domain.firstYearCents)} first year, ` +\n `renews yearly at ${formatBundleCents(domain.renewalCents)}`\n )\n}\n\n/**\n * The bundle in one line, for an email, a dialog or a snackbar:\n * \"Starter with Site Care and Aglyn AI, billed yearly, code BOOTH25\".\n */\nexport function describeBundle(\n bundle: {\n plan: OrgPlan\n addons?: BundleAddons | null\n interval?: 'month' | 'year' | null\n coupon?: string | null\n /** The Aglyn AI tier, named with the AI add-on (AGL-3965). */\n aiTier?: AiTierId | null\n },\n options: { brand?: string } = {},\n): string {\n const parts = ADDON_KINDS.filter((kind) => (bundle.addons?.[kind] ?? 0) > 0).map((kind) =>\n describeBundleAddon(kind, bundle.addons?.[kind] ?? 0, options.brand, bundle.aiTier),\n )\n const addons =\n parts.length === 0\n ? ''\n : parts.length === 1\n ? ` with ${parts[0]}`\n : ` with ${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n const interval =\n bundle.interval === 'year'\n ? ', billed yearly'\n : bundle.interval === 'month'\n ? ', billed monthly'\n : ''\n const coupon = bundle.coupon ? `, code ${bundle.coupon}` : ''\n return `${PLAN_LABELS[bundle.plan] ?? bundle.plan}${addons}${interval}${coupon}`\n}\n"],"names":["AI_TIER_IDS","AI_TIERS","aiAddonName","aiTierAddonKey","aiTierName","defaultAiTierForPlan","EVENT_CALENDAR_ADDON_MONTHLY_USD","EXTRA_HOSTS_ADDON_MAX","PLAN_LABELS","parseAiTierAddonKey","PLAN_PRICING","planSellsAiTiers","planSellsSiteCare","POS_REGISTER_ADDON_MONTHLY_USD","POS_REGISTERS_ADDON_MAX","resolveOrgEntitlements","SITE_CARE_ADDON_MONTHLY_USD","UNLIMITED","ADDON_KINDS","TOGGLE_ADDON_KIND_LIST","isAddonKind","value","includes","addonUnitUsd","kind","plan","tier","pricing","extraSeatMonthlyUsd","extraCollaboratorMonthlyUsd","extraDatasetMonthlyUsd","extraHostMonthlyUsd","chosen","monthlyUsd","addonMaxForBaseline","baseline","bounded","included","max","Number","isFinite","Math","managersPerOrg","maxManagersPerOrg","membersPerHost","maxMembersPerHost","datasetsPerOrg","maxDatasetsPerOrg","hostLimit","features","pos","addonMaxForPlan","planSellsAddon","BUNDLE_ADDON_TOKENS","managers","members","datasets","hosts","posRegisters","eventCalendar","aiAddon","siteCare","TOKEN_KINDS","map","add","words","word","toLowerCase","MAX_LINK_QUANTITY","AI_ADDON_TIER_TOKENS","Object","freeze","fromEntries","resolveAiAddonTier","trim","prototype","hasOwnProperty","call","parsed","quantity","parseToken","raw","token","name","rest","split","join","test","min","parseBundleAddons","input","addons","dropped","aiTier","take","push","slice","Array","isArray","entries","off","undefined","String","fitBundleAddonsToPlan","fitted","notSold","wanted","floor","bundleAddonsParam","filter","hasBundleAddons","some","bundleAddonLabel","brand","describeBundleAddon","label","BUNDLE_COUPON_MAX","normalizeBundleCoupon","code","length","normalizeBundleDomain","replace","labels","ok","every","formatBundleCents","cents","round","toFixed","bundleDomainLine","domain","firstYearCents","renewalCents","describeBundle","bundle","options","parts","interval","coupon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/bundle-intent.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 ADD-ONS A PLAN SELLS, AND THE BUNDLE A LINK ASKS FOR (AGL-3955).\n *\n * One list of add-on kinds and one parser for the add-ons a link, an upgrade\n * proposal or a staff dialog names, so the Billing card, the bundle checkout\n * link, the hand-off proposal (AGL-3956) and `/api/billing/checkout` agree on\n * what `addons=siteCare,ai,extraHost:2` means.\n *\n * The kinds are `OrgSeatAddons`' keys: what `resolvePurchasedAddons` counts,\n * what the webhook mirrors from Stripe, and what `/api/billing/addons` sells.\n * The prices and ceilings here are the ones `billing-addons.ts` (server)\n * re-exports, so a kind the link can preselect is always a kind the\n * checkout can price.\n *\n * A link is typed by a person and passed through hops nobody controls, so\n * the parser never throws and never guesses: an unknown token is dropped and\n * reported, so the page can say so in one line instead of failing.\n */\n\nimport type {\n AiTierId,\n OrgPlan,\n OrgSeatAddons,\n} from '../foundation/definitions/org-billing.types'\nimport {\n AI_TIER_IDS,\n AI_TIERS,\n aiAddonName,\n aiTierAddonKey,\n aiTierName,\n defaultAiTierForPlan,\n EVENT_CALENDAR_ADDON_MONTHLY_USD,\n EXTRA_HOSTS_ADDON_MAX,\n isAiTierId,\n PLAN_LABELS,\n parseAiTierAddonKey,\n PLAN_PRICING,\n planSellsAiTiers,\n planSellsSiteCare,\n POS_REGISTER_ADDON_MONTHLY_USD,\n POS_REGISTERS_ADDON_MAX,\n resolveOrgEntitlements,\n SITE_CARE_ADDON_MONTHLY_USD,\n UNLIMITED,\n} from './plan-entitlements'\n\n/** An add-on kind: a key of `org.seatAddons`. */\nexport type AddonKind = keyof Required<OrgSeatAddons>\n\n/**\n * Every add-on kind Billing sells, in the order the Billing card lists them.\n * `/api/billing/addons`, the webhook's price map and the native add-ons\n * screen all read this list.\n */\nexport const ADDON_KINDS: readonly AddonKind[] = [\n 'managers',\n 'members',\n 'datasets',\n 'hosts',\n 'posRegisters',\n 'eventCalendar',\n 'aiAddon',\n 'siteCare',\n]\n\n/** The org-wide 0/1 kinds: one purchase covers the workspace. */\nexport const TOGGLE_ADDON_KIND_LIST: readonly AddonKind[] = [\n 'eventCalendar',\n 'aiAddon',\n 'siteCare',\n]\n\nexport function isAddonKind(value: unknown): value is AddonKind {\n return typeof value === 'string' && (ADDON_KINDS as readonly string[]).includes(value)\n}\n\n/**\n * Monthly USD per unit for an add-on on a plan (yearly prices are ×12 of\n * this); null when the plan doesn't sell the kind. Single pricing source\n * of truth: `PLAN_PRICING`, the three flat add-on constants and `AI_TIERS`\n * (`tier` as `addonPriceId` reads it; absent, the tier a plan-named purchase\n * means, `defaultAiTierForPlan`, AGL-3965).\n */\nexport function addonUnitUsd(\n kind: AddonKind,\n plan: OrgPlan,\n tier?: AiTierId | null,\n): number | null {\n const pricing = PLAN_PRICING[plan]\n switch (kind) {\n case 'managers':\n return pricing.extraSeatMonthlyUsd\n case 'members':\n return pricing.extraCollaboratorMonthlyUsd\n case 'datasets':\n return pricing.extraDatasetMonthlyUsd\n case 'hosts':\n return pricing.extraHostMonthlyUsd\n case 'posRegisters':\n return POS_REGISTER_ADDON_MONTHLY_USD\n case 'eventCalendar':\n return EVENT_CALENDAR_ADDON_MONTHLY_USD\n case 'aiAddon': {\n const chosen = planSellsAiTiers(plan) ? (tier ?? defaultAiTierForPlan(plan)) : null\n return chosen ? AI_TIERS[chosen].monthlyUsd : null\n }\n case 'siteCare':\n return SITE_CARE_ADDON_MONTHLY_USD\n }\n}\n\n/**\n * Max purchasable quantity per kind, from a purchases-free entitlement\n * resolution (plan defaults + staff overrides only) so the ceiling does not\n * drift as the org buys: seat/dataset kinds stop at the plan's hard max,\n * hosts/registers use flat ceilings, the Event Calendar, Aglyn AI and Site\n * Care are 0/1 toggles. POS registers additionally require the `pos`\n * feature, and Site Care a plan that sells it (`planSellsSiteCare`), which is\n * why the plan travels with the baseline.\n *\n * One definition for every reader: `/api/billing/addons` asks how many the\n * org may buy on its plan, a plan change asks how many of what it bought the\n * new plan can deliver, and a bundle link asks how many it may preselect.\n */\nexport function addonMaxForBaseline(\n kind: AddonKind,\n baseline: ReturnType<typeof resolveOrgEntitlements>,\n plan: OrgPlan,\n): number {\n const bounded = (included: number, max: number) =>\n Number.isFinite(max) ? Math.max(0, max - included) : EXTRA_HOSTS_ADDON_MAX\n switch (kind) {\n case 'managers':\n return bounded(baseline.managersPerOrg, baseline.maxManagersPerOrg)\n case 'members':\n return bounded(baseline.membersPerHost, baseline.maxMembersPerHost)\n case 'datasets':\n return bounded(baseline.datasetsPerOrg, baseline.maxDatasetsPerOrg)\n case 'hosts':\n return baseline.hostLimit === UNLIMITED ? 0 : EXTRA_HOSTS_ADDON_MAX\n case 'posRegisters':\n return baseline.features.pos ? POS_REGISTERS_ADDON_MAX : 0\n case 'eventCalendar':\n return 1\n case 'aiAddon':\n // Org-wide like the Event Calendar: one purchase, of one tier, on any\n // plan that sells tiers (AGL-3965) — not Free, not Enterprise.\n return planSellsAiTiers(plan) ? 1 : 0\n case 'siteCare':\n // Flat-priced, so the price map cannot say \"not sold here\" the way it\n // does for Aglyn AI. This ceiling is the rule that keeps Free (no\n // subscription) and Enterprise (quoted per deal) from buying it.\n return planSellsSiteCare(plan) ? 1 : 0\n }\n}\n\n/** The ceiling a PLAN would impose on an add-on kind, purchases excluded. */\nexport function addonMaxForPlan(kind: AddonKind, plan: OrgPlan): number {\n return addonMaxForBaseline(\n kind,\n resolveOrgEntitlements({ plan } as never),\n plan,\n )\n}\n\n/** Whether a plan sells an add-on at all: priced, with a ceiling above zero. */\nexport function planSellsAddon(kind: AddonKind, plan: OrgPlan): boolean {\n return addonUnitUsd(kind, plan) != null && addonMaxForPlan(kind, plan) > 0\n}\n\n/**\n * The first of `plans` (listed smallest first) that sells an add-on kind, or\n * null when none does: the plan a picker names beside an add-on the chosen\n * plan does not sell, so it shows as \"needs Pro or higher\" rather than\n * vanishing (AGL-4025). POS registers need a plan with the point of sale.\n */\nexport function firstPlanSellingAddon(\n kind: AddonKind,\n plans: readonly OrgPlan[],\n): OrgPlan | null {\n return plans.find((plan) => planSellsAddon(kind, plan)) ?? null\n}\n\n/** Quantities by kind, absent kinds meaning none. */\nexport type BundleAddons = Partial<Record<AddonKind, number>>\n\n/**\n * What a link writes for each kind. `ai` and `extraHost` are the words the\n * account owner chose (AGL-3955); the rest follow the same shape.\n */\nexport const BUNDLE_ADDON_TOKENS: Record<AddonKind, string> = {\n managers: 'extraSeat',\n members: 'extraMember',\n datasets: 'extraDataset',\n hosts: 'extraHost',\n posRegisters: 'posRegister',\n eventCalendar: 'eventCalendar',\n aiAddon: 'ai',\n siteCare: 'siteCare',\n}\n\n/**\n * Every spelling the parser accepts, lower-cased: the written token, the\n * stored kind, and the plurals a person types. Exported for the native apps'\n * generated words (AGL-4011), which read a stored proposal the same way.\n */\nexport const TOKEN_KINDS: Readonly<Record<string, AddonKind>> = (() => {\n const map: Record<string, AddonKind> = {}\n const add = (kind: AddonKind, ...words: string[]) => {\n for (const word of [kind, BUNDLE_ADDON_TOKENS[kind], ...words]) {\n map[word.toLowerCase()] = kind\n }\n }\n add('managers', 'extraSeats', 'seats', 'managerSeats')\n add('members', 'extraMembers', 'collaborators', 'collaboratorSeats')\n add('datasets', 'extraDatasets')\n add('hosts', 'extraHosts', 'extraSite', 'extraSites', 'sites')\n add('posRegisters', 'registers', 'register')\n add('eventCalendar', 'events', 'calendar')\n add('aiAddon', 'aglynAi')\n add('siteCare', 'site-care', 'site_care', 'care')\n return map\n})()\n\n/** The most of a quantity kind a link may ask for, before the plan's ceiling. */\nconst MAX_LINK_QUANTITY = 999\n\n/** One add-on a token names, with the Aglyn AI tier when it names one. */\ninterface ParsedToken {\n kind: AddonKind\n quantity: number\n tier?: AiTierId\n}\n\n/**\n * THE `ai:<tier>` TOKENS (AGL-3965).\n *\n * Aglyn AI is a tier chosen on its own, on any paid plan: `ai:boost` adds\n * Aglyn AI Boost. The words are the tier ids, lower-cased as every token is,\n * and resolve through `parseAiTierAddonKey`, the one spelling of the\n * `ai:<tier>` key. A bare `ai` (or `ai:1`) still means \"add it\" and sells\n * the plan's default tier (`defaultAiTierForPlan`). An unknown tier is\n * dropped with the notice like any other unknown add-on, never read as\n * \"add whichever tier\": that would sell a tier nobody chose.\n */\nexport const AI_ADDON_TIER_TOKENS: Readonly<Record<string, AiTierId>> = Object.freeze(\n Object.fromEntries(AI_TIER_IDS.map((tier) => [tier.toLowerCase(), tier])),\n)\n\nexport function resolveAiAddonTier(tier: string): ParsedToken | null {\n const word = tier.trim().toLowerCase()\n if (!Object.prototype.hasOwnProperty.call(AI_ADDON_TIER_TOKENS, word)) return null\n const parsed = parseAiTierAddonKey(aiTierAddonKey(AI_ADDON_TIER_TOKENS[word]))\n return parsed ? { kind: 'aiAddon', quantity: 1, tier: parsed } : null\n}\n\nexport interface ParsedBundleAddons {\n addons: BundleAddons\n /** The tokens nothing understood, as they were written. */\n dropped: string[]\n /** The Aglyn AI tier an `ai:<tier>` token named (AGL-3965), if any. */\n aiTier?: AiTierId\n}\n\n/** One token, `kind` or `kind:quantity`, or null when nothing understands it. */\nfunction parseToken(raw: string): ParsedToken | null {\n const token = raw.trim()\n if (!token) return null\n const [name, ...rest] = token.split(':')\n const kind = TOKEN_KINDS[name.trim().toLowerCase()]\n if (!kind) return null\n const value = rest.join(':').trim()\n if (!value) return { kind, quantity: 1 }\n if (/^\\d+$/.test(value)) {\n const quantity = Math.min(MAX_LINK_QUANTITY, Number(value))\n if (quantity < 1) return null\n return {\n kind,\n quantity: (TOGGLE_ADDON_KIND_LIST as readonly string[]).includes(kind) ? 1 : quantity,\n }\n }\n return kind === 'aiAddon' ? resolveAiAddonTier(value) : null\n}\n\n/**\n * Read the add-ons a link, a proposal or a request names.\n *\n * Accepts the link form (`\"siteCare,ai,extraHost:2\"`), a list of tokens, or a\n * map of kind → quantity or flag (`{ siteCare: true, hosts: 2 }`, the shape\n * the native staff screens post from their toggles). Never throws: anything\n * it cannot read is reported in `dropped` and left out.\n */\nexport function parseBundleAddons(input: unknown): ParsedBundleAddons {\n const addons: BundleAddons = {}\n const dropped: string[] = []\n let aiTier: AiTierId | undefined\n const take = (parsed: ParsedToken | null, raw: string) => {\n if (!parsed) {\n if (raw.trim()) dropped.push(raw.trim().slice(0, 40))\n return\n }\n addons[parsed.kind] = Math.max(addons[parsed.kind] ?? 0, parsed.quantity)\n // The first tier named wins: one workspace holds one tier.\n if (parsed.tier && !aiTier) aiTier = parsed.tier\n }\n if (typeof input === 'string') {\n for (const raw of input.split(/[,\\s]+/)) take(parseToken(raw), raw)\n } else if (Array.isArray(input)) {\n for (const raw of input) {\n if (typeof raw === 'string') take(parseToken(raw), raw)\n }\n } else if (input && typeof input === 'object') {\n for (const [name, value] of Object.entries(input as Record<string, unknown>)) {\n const off =\n value === false ||\n value === 0 ||\n value === null ||\n value === undefined ||\n value === '' ||\n value === 'false' ||\n value === '0'\n if (off) continue\n const quantity =\n value === true || value === 'true'\n ? ''\n : typeof value === 'number' || typeof value === 'string'\n ? String(value)\n : ''\n take(parseToken(quantity ? `${name}:${quantity}` : name), name)\n }\n }\n return { addons, dropped, ...(aiTier ? { aiTier } : {}) }\n}\n\nexport interface FittedBundleAddons {\n addons: BundleAddons\n /** Kinds the plan does not sell, dropped. */\n notSold: AddonKind[]\n}\n\n/**\n * The add-ons a plan can actually carry: kinds it does not sell are dropped,\n * quantities are held to its ceiling. What a link preselects on one plan is\n * re-fitted when the owner picks another, so the checkout is never asked for\n * something it would refuse.\n */\nexport function fitBundleAddonsToPlan(\n addons: BundleAddons | null | undefined,\n plan: OrgPlan,\n): FittedBundleAddons {\n const fitted: BundleAddons = {}\n const notSold: AddonKind[] = []\n for (const kind of ADDON_KINDS) {\n const wanted = Math.floor(Number(addons?.[kind] ?? 0))\n if (!(wanted > 0)) continue\n if (!planSellsAddon(kind, plan)) {\n notSold.push(kind)\n continue\n }\n fitted[kind] = Math.min(wanted, addonMaxForPlan(kind, plan))\n }\n return { addons: fitted, notSold }\n}\n\n/**\n * The add-ons as the link writes them: `siteCare,ai:boost,extraHost:2`.\n * Aglyn AI is written with its tier when one is named (AGL-3965), as bare\n * `ai` otherwise.\n */\nexport function bundleAddonsParam(\n addons: BundleAddons | null | undefined,\n aiTier?: AiTierId | null,\n): string {\n return ADDON_KINDS.filter((kind) => (addons?.[kind] ?? 0) > 0)\n .map((kind) => {\n if (kind === 'aiAddon' && aiTier) return aiTierAddonKey(aiTier)\n const quantity = addons?.[kind] ?? 0\n return quantity > 1 ? `${BUNDLE_ADDON_TOKENS[kind]}:${quantity}` : BUNDLE_ADDON_TOKENS[kind]\n })\n .join(',')\n}\n\n/**\n * The Aglyn AI tier a bundle sells (AGL-4025): the tier named, or the plan's\n * default (`defaultAiTierForPlan`) when none is; null when the bundle has no\n * Aglyn AI or the plan sells no tier. A picker shows this tier as chosen, the\n * link writes it as `ai:<tier>`, and checkout charges it, so the three agree.\n */\nexport function bundleAiTier(\n addons: BundleAddons | null | undefined,\n plan: OrgPlan,\n tier?: AiTierId | null,\n): AiTierId | null {\n if (!((addons?.aiAddon ?? 0) > 0) || !planSellsAiTiers(plan)) return null\n return isAiTierId(tier) ? tier : defaultAiTierForPlan(plan)\n}\n\n/**\n * What a bundle costs at list price, in cents, before tax and any promotion\n * code (AGL-4026): the plan at its price for the cadence (the annual monthly\n * price × 12 yearly, the monthly price monthly), plus every add-on the plan\n * sells at its unit price × quantity, × 12 yearly as every add-on bills.\n *\n * The figure Billing shows while Stripe cannot price the bundle with tax —\n * no billing address yet, the preview still on its way, or a preview that\n * failed — so the owner always sees what the bundle costs and sees it move\n * as they change it. Never shown in place of Stripe's taxed total.\n */\nexport function bundleListPriceCents(bundle: {\n plan: OrgPlan\n interval: 'month' | 'year'\n addons?: BundleAddons | null\n aiTier?: AiTierId | null\n}): number {\n const pricing = PLAN_PRICING[bundle.plan]\n const yearly = bundle.interval === 'year'\n const base = yearly ? pricing.basePriceAnnualMonthlyUsd * 12 : pricing.basePriceMonthlyUsd\n let cents = Math.round(base * 100)\n const addons = fitBundleAddonsToPlan(bundle.addons, bundle.plan).addons\n for (const kind of ADDON_KINDS) {\n const quantity = addons[kind] ?? 0\n const unit = addonUnitUsd(kind, bundle.plan, kind === 'aiAddon' ? bundle.aiTier : null)\n if (quantity > 0 && unit != null) cents += Math.round(unit * 100) * quantity * (yearly ? 12 : 1)\n }\n return cents\n}\n\n/** Whether any add-on is asked for. */\nexport function hasBundleAddons(addons: BundleAddons | null | undefined): boolean {\n return ADDON_KINDS.some((kind) => (addons?.[kind] ?? 0) > 0)\n}\n\n/**\n * How each kind is named to a customer, singular. Aglyn AI carries its tier\n * when one is named: \"Aglyn AI Boost\" (AGL-3965).\n */\nexport function bundleAddonLabel(\n kind: AddonKind,\n brand?: string,\n aiTier?: AiTierId | null,\n): string {\n switch (kind) {\n case 'managers':\n return 'Extra manager seat'\n case 'members':\n return 'Extra collaborator seat'\n case 'datasets':\n return 'Extra dataset'\n case 'hosts':\n return 'Extra site'\n case 'posRegisters':\n return 'POS register'\n case 'eventCalendar':\n return 'Event Calendar'\n case 'aiAddon':\n return aiTier ? aiTierName(aiTier, brand) : aiAddonName(brand)\n case 'siteCare':\n return 'Site Care'\n }\n}\n\n/** \"Site Care\", \"2 × Extra site\", \"Aglyn AI Boost\". */\nexport function describeBundleAddon(\n kind: AddonKind,\n quantity: number,\n brand?: string,\n aiTier?: AiTierId | null,\n): string {\n const label = bundleAddonLabel(kind, brand, aiTier)\n return quantity > 1 ? `${quantity} × ${label}` : label\n}\n\n/** The longest coupon code a link carries. Stripe codes are far shorter. */\nexport const BUNDLE_COUPON_MAX = 64\n\n/**\n * A promotion code as a link carries it, or undefined. Only the characters\n * Stripe allows in a customer-facing code; anything else is not a code\n * somebody was handed, and is dropped rather than sent to Stripe.\n *\n * The code is only APPLIED here. Codes are minted on the staff Coupons page,\n * which audits them; nothing on this path creates one.\n */\nexport function normalizeBundleCoupon(raw: unknown): string | undefined {\n if (typeof raw !== 'string') return undefined\n const code = raw.trim()\n if (!code || code.length > BUNDLE_COUPON_MAX) return undefined\n return /^[A-Za-z0-9_-]+$/.test(code) ? code : undefined\n}\n\n/**\n * A domain name as a bundle carries it — lower-cased, no scheme, no path —\n * or undefined. Whether it is for sale and at what price is the registrar's\n * answer, re-checked at purchase; this only refuses what cannot be a name.\n */\nexport function normalizeBundleDomain(raw: unknown): string | undefined {\n if (typeof raw !== 'string') return undefined\n const name = raw\n .trim()\n .toLowerCase()\n .replace(/^https?:\\/\\//, '')\n .replace(/\\/.*$/, '')\n .replace(/\\.$/, '')\n if (!name || name.length > 253) return undefined\n const labels = name.split('.')\n if (labels.length < 2) return undefined\n const ok = labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))\n return ok && /^[a-z]{2,63}$/.test(labels[labels.length - 1]) ? name : undefined\n}\n\n/** \"$15.99\". */\nexport function formatBundleCents(cents: number): string {\n return `$${(Math.max(0, Math.round(cents)) / 100).toFixed(2)}`\n}\n\n/**\n * The domain line a bundle shows (AGL-3955): \"example.com — $15.99 first\n * year, renews yearly at $15.99\". The price is the registrar's at the moment\n * the link was minted; the purchase re-checks it.\n */\nexport function bundleDomainLine(domain: {\n name: string\n firstYearCents: number\n renewalCents: number\n}): string {\n return (\n `${domain.name} — ${formatBundleCents(domain.firstYearCents)} first year, ` +\n `renews yearly at ${formatBundleCents(domain.renewalCents)}`\n )\n}\n\n/**\n * The bundle in one line, for an email, a dialog or a snackbar:\n * \"Starter with Site Care and Aglyn AI, billed yearly, code BOOTH25\".\n */\nexport function describeBundle(\n bundle: {\n plan: OrgPlan\n addons?: BundleAddons | null\n interval?: 'month' | 'year' | null\n coupon?: string | null\n /** The Aglyn AI tier, named with the AI add-on (AGL-3965). */\n aiTier?: AiTierId | null\n },\n options: { brand?: string } = {},\n): string {\n const parts = ADDON_KINDS.filter((kind) => (bundle.addons?.[kind] ?? 0) > 0).map((kind) =>\n describeBundleAddon(kind, bundle.addons?.[kind] ?? 0, options.brand, bundle.aiTier),\n )\n const addons =\n parts.length === 0\n ? ''\n : parts.length === 1\n ? ` with ${parts[0]}`\n : ` with ${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`\n const interval =\n bundle.interval === 'year'\n ? ', billed yearly'\n : bundle.interval === 'month'\n ? ', billed monthly'\n : ''\n const coupon = bundle.coupon ? `, code ${bundle.coupon}` : ''\n return `${PLAN_LABELS[bundle.plan] ?? bundle.plan}${addons}${interval}${coupon}`\n}\n"],"names":["AI_TIER_IDS","AI_TIERS","aiAddonName","aiTierAddonKey","aiTierName","defaultAiTierForPlan","EVENT_CALENDAR_ADDON_MONTHLY_USD","EXTRA_HOSTS_ADDON_MAX","isAiTierId","PLAN_LABELS","parseAiTierAddonKey","PLAN_PRICING","planSellsAiTiers","planSellsSiteCare","POS_REGISTER_ADDON_MONTHLY_USD","POS_REGISTERS_ADDON_MAX","resolveOrgEntitlements","SITE_CARE_ADDON_MONTHLY_USD","UNLIMITED","ADDON_KINDS","TOGGLE_ADDON_KIND_LIST","isAddonKind","value","includes","addonUnitUsd","kind","plan","tier","pricing","extraSeatMonthlyUsd","extraCollaboratorMonthlyUsd","extraDatasetMonthlyUsd","extraHostMonthlyUsd","chosen","monthlyUsd","addonMaxForBaseline","baseline","bounded","included","max","Number","isFinite","Math","managersPerOrg","maxManagersPerOrg","membersPerHost","maxMembersPerHost","datasetsPerOrg","maxDatasetsPerOrg","hostLimit","features","pos","addonMaxForPlan","planSellsAddon","firstPlanSellingAddon","plans","find","BUNDLE_ADDON_TOKENS","managers","members","datasets","hosts","posRegisters","eventCalendar","aiAddon","siteCare","TOKEN_KINDS","map","add","words","word","toLowerCase","MAX_LINK_QUANTITY","AI_ADDON_TIER_TOKENS","Object","freeze","fromEntries","resolveAiAddonTier","trim","prototype","hasOwnProperty","call","parsed","quantity","parseToken","raw","token","name","rest","split","join","test","min","parseBundleAddons","input","addons","dropped","aiTier","take","push","slice","Array","isArray","entries","off","undefined","String","fitBundleAddonsToPlan","fitted","notSold","wanted","floor","bundleAddonsParam","filter","bundleAiTier","bundleListPriceCents","bundle","yearly","interval","base","basePriceAnnualMonthlyUsd","basePriceMonthlyUsd","cents","round","unit","hasBundleAddons","some","bundleAddonLabel","brand","describeBundleAddon","label","BUNDLE_COUPON_MAX","normalizeBundleCoupon","code","length","normalizeBundleDomain","replace","labels","ok","every","formatBundleCents","toFixed","bundleDomainLine","domain","firstYearCents","renewalCents","describeBundle","options","parts","coupon"],"mappings":"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"}
|
|
@@ -309,6 +309,20 @@ export interface InstanceLeafBinding {
|
|
|
309
309
|
* Pure: no canvas, no DOM, inputs never mutated.
|
|
310
310
|
*/
|
|
311
311
|
export declare function resolveInstanceLeafBinding(graftedId: NodeId | null | undefined, instanceId: NodeId | null | undefined, definition: Pick<ReusableComponentTree, 'nodes' | 'props'> | null | undefined, contentProp?: string, rootId?: NodeId | null): InstanceLeafBinding | null;
|
|
312
|
+
/**
|
|
313
|
+
* The formatted text an instance renders for a declared prop, or `''` when
|
|
314
|
+
* it renders the prop plain — what the canvas editor opens with.
|
|
315
|
+
*/
|
|
316
|
+
export declare function getInstanceEffectivePropHtml(instanceProps: unknown, declared: ReusableComponentProp[] | undefined | null, propName: string): string;
|
|
317
|
+
/**
|
|
318
|
+
* The definition nodes whose text IS one declared prop: their `children` is
|
|
319
|
+
* exactly its `{{prop.<name>}}` token. These are the elements formatted text
|
|
320
|
+
* for that prop is drawn into, so a caller offering formatting asks each of
|
|
321
|
+
* them whether it can draw it.
|
|
322
|
+
*/
|
|
323
|
+
export declare function listPropFedTextNodeIds(nodes: Readonly<Record<string, {
|
|
324
|
+
props?: unknown;
|
|
325
|
+
} | undefined>> | null | undefined, propName: string): string[];
|
|
312
326
|
/**
|
|
313
327
|
* The text an instance currently renders for a declared prop: its override
|
|
314
328
|
* when set, else the prop's `defaultValue`, else `''`. `''` counts as unset,
|
|
@@ -386,7 +400,9 @@ export declare function resolveComponentPropTokens<N extends AglynNodeSchema = A
|
|
|
386
400
|
* substitution would see a token instead of the value and hide whatever the
|
|
387
401
|
* definition merely MENTIONED.
|
|
388
402
|
*/
|
|
389
|
-
export declare function applyDeclaredProps<N extends AglynNodeSchema = AglynNodeSchema>(nodes: NormalizedNodes<N>, declared: ReusableComponentProp[] | undefined | null, values: Readonly<Record<string, unknown>> | null | undefined, rootId: NodeId
|
|
403
|
+
export declare function applyDeclaredProps<N extends AglynNodeSchema = AglynNodeSchema>(nodes: NormalizedNodes<N>, declared: ReusableComponentProp[] | undefined | null, values: Readonly<Record<string, unknown>> | null | undefined, rootId: NodeId,
|
|
404
|
+
/** The page's formatted text, keyed like `values` — see {@link REUSABLE_INSTANCE_PROP_HTML_KEY}. */
|
|
405
|
+
formatted?: Readonly<Record<string, unknown>> | null): NormalizedNodes<N>;
|
|
390
406
|
/**
|
|
391
407
|
* Whether a node's props ask for it to be dropped (AGL-1314) — see
|
|
392
408
|
* {@link NODE_HIDE_IF_PROP} and {@link NODE_HIDE_UNLESS_PROP}.
|
|
@@ -18,7 +18,7 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
18
18
|
*/ // By path: the naming rule alone, without the icon catalog's loader beside it.
|
|
19
19
|
import { iconPathPropName } from "@aglyn/shared-data-mdi/utils/icon-path-prop-name";
|
|
20
20
|
import { reusablePropValueClass } from "../foundation/definitions/property-kinds.js";
|
|
21
|
-
import { COMPONENT_NODE_ID_PREFIX, COMPONENT_PROP_TOKEN_PREFIX, INSTANCE_CARRIED_NODE_FIELDS, INSTANCE_CARRIED_PROPS, NODE_HIDE_IF_PROP, NODE_HIDE_UNLESS_PROP, REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY, STYLE_OVERRIDES_ROOT_KEY } from "./reusable-component-keys.js";
|
|
21
|
+
import { COMPONENT_NODE_ID_PREFIX, COMPONENT_PROP_TOKEN_PREFIX, INSTANCE_CARRIED_NODE_FIELDS, INSTANCE_CARRIED_PROPS, NODE_HIDE_IF_PROP, NODE_HIDE_UNLESS_PROP, REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_HTML_KEY, REUSABLE_INSTANCE_PROP_VALUES_KEY, STYLE_OVERRIDES_ROOT_KEY } from "./reusable-component-keys.js";
|
|
22
22
|
import { REPEAT_DIRECTIVE_PROPS, REPEAT_SELF_PROP, repeatKey, withoutRepeatDirective } from "./expand-repeatables.js";
|
|
23
23
|
import { FORM_COMPONENT_ID, FORM_ID_PROP } from "./forms.js";
|
|
24
24
|
/** The Form's after-submit reveal target: a node id (`form.tsx`). */ const FORM_REVEAL_NODE_PROP = 'revealNodeId';
|
|
@@ -465,6 +465,52 @@ function instancePrefix(instanceId) {
|
|
|
465
465
|
const values = instanceProps == null ? void 0 : instanceProps[REUSABLE_INSTANCE_PROP_VALUES_KEY];
|
|
466
466
|
return values && typeof values === 'object' && !Array.isArray(values) ? values : undefined;
|
|
467
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* An instance's formatted text, keyed by declared prop name — see
|
|
470
|
+
* {@link REUSABLE_INSTANCE_PROP_HTML_KEY}. Only non-empty strings count.
|
|
471
|
+
*/ function instancePropHtml(instanceProps) {
|
|
472
|
+
const stored = instanceProps == null ? void 0 : instanceProps[REUSABLE_INSTANCE_PROP_HTML_KEY];
|
|
473
|
+
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
const markup = {};
|
|
477
|
+
for (const [name, value] of Object.entries(stored)){
|
|
478
|
+
if (typeof value === 'string' && value) markup[name] = value;
|
|
479
|
+
}
|
|
480
|
+
return Object.keys(markup).length ? markup : undefined;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The markup a prop's formatted text renders as, or `''` when it renders
|
|
484
|
+
* plain.
|
|
485
|
+
*
|
|
486
|
+
* Formatting counts only beside a value of its own: the plain reading in
|
|
487
|
+
* `propValues` is the half the Attributes field and every walker read, and
|
|
488
|
+
* markup with no value under it describes text the instance no longer sets.
|
|
489
|
+
* An undeclared name never substitutes, so its markup never draws either.
|
|
490
|
+
*/ function formattedPropMarkup(name, declared, values, formatted) {
|
|
491
|
+
const markup = formatted == null ? void 0 : formatted[name];
|
|
492
|
+
if (typeof markup !== 'string' || !markup) return '';
|
|
493
|
+
const value = values == null ? void 0 : values[name];
|
|
494
|
+
if (typeof value !== 'string' || !value) return '';
|
|
495
|
+
return (declared != null ? declared : []).some((prop)=>(prop == null ? void 0 : prop.name) === name) ? markup : '';
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* The formatted text an instance renders for a declared prop, or `''` when
|
|
499
|
+
* it renders the prop plain — what the canvas editor opens with.
|
|
500
|
+
*/ export function getInstanceEffectivePropHtml(instanceProps, declared, propName) {
|
|
501
|
+
return formattedPropMarkup(propName, declared, instancePropValues(instanceProps), instancePropHtml(instanceProps));
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The definition nodes whose text IS one declared prop: their `children` is
|
|
505
|
+
* exactly its `{{prop.<name>}}` token. These are the elements formatted text
|
|
506
|
+
* for that prop is drawn into, so a caller offering formatting asks each of
|
|
507
|
+
* them whether it can draw it.
|
|
508
|
+
*/ export function listPropFedTextNodeIds(nodes, propName) {
|
|
509
|
+
return Object.entries(nodes != null ? nodes : {}).filter(([, node])=>{
|
|
510
|
+
var _node_props;
|
|
511
|
+
return matchComponentPropToken(node == null ? void 0 : (_node_props = node.props) == null ? void 0 : _node_props['children']) === propName;
|
|
512
|
+
}).map(([id])=>id);
|
|
513
|
+
}
|
|
468
514
|
/**
|
|
469
515
|
* The text an instance currently renders for a declared prop: its override
|
|
470
516
|
* when set, else the prop's `defaultValue`, else `''`. `''` counts as unset,
|
|
@@ -726,9 +772,37 @@ function instancePrefix(instanceId) {
|
|
|
726
772
|
* they read `{{prop.*}}` like everything else, so pruning ahead of
|
|
727
773
|
* substitution would see a token instead of the value and hide whatever the
|
|
728
774
|
* definition merely MENTIONED.
|
|
729
|
-
*/ export function applyDeclaredProps(nodes, declared, values, rootId) {
|
|
775
|
+
*/ export function applyDeclaredProps(nodes, declared, values, rootId, /** The page's formatted text, keyed like `values` — see {@link REUSABLE_INSTANCE_PROP_HTML_KEY}. */ formatted) {
|
|
730
776
|
const resolved = resolveReusablePropValues(declared, values);
|
|
731
|
-
return pruneHiddenNodes(resolveComponentPropTokens(nodes, declared, resolved.tokens, resolved.iconPaths, resolved.values), rootId);
|
|
777
|
+
return pruneHiddenNodes(withFormattedPropText(resolveComponentPropTokens(nodes, declared, resolved.tokens, resolved.iconPaths, resolved.values), nodes, declared, values, formatted), rootId);
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Hands each element whose text IS a formatted prop that prop's markup, as
|
|
781
|
+
* the element's own `html`.
|
|
782
|
+
*
|
|
783
|
+
* `children` already holds the plain reading the substitution wrote, so the
|
|
784
|
+
* element ends up exactly as a formatted text node is stored: markup for the
|
|
785
|
+
* renderer, which draws `html` in preference and sanitizes it as it draws,
|
|
786
|
+
* and plain text for everything that reads words. Read off `source` — the
|
|
787
|
+
* nodes BEFORE substitution — because that is where the token still says
|
|
788
|
+
* which prop fills the element.
|
|
789
|
+
*/ function withFormattedPropText(substituted, source, declared, values, formatted) {
|
|
790
|
+
if (!formatted) return substituted;
|
|
791
|
+
let next;
|
|
792
|
+
for (const [id, node] of Object.entries(source)){
|
|
793
|
+
var _node_props;
|
|
794
|
+
const name = matchComponentPropToken(node == null ? void 0 : (_node_props = node.props) == null ? void 0 : _node_props['children']);
|
|
795
|
+
if (!name || !substituted[id]) continue;
|
|
796
|
+
const markup = formattedPropMarkup(name, declared, values, formatted);
|
|
797
|
+
if (!markup) continue;
|
|
798
|
+
next = next != null ? next : _extends({}, substituted);
|
|
799
|
+
next[id] = _extends({}, substituted[id], {
|
|
800
|
+
props: _extends({}, substituted[id].props, {
|
|
801
|
+
html: markup
|
|
802
|
+
})
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
return next != null ? next : substituted;
|
|
732
806
|
}
|
|
733
807
|
/**
|
|
734
808
|
* Whether a node's props ask for it to be dropped (AGL-1314) — see
|
|
@@ -1068,7 +1142,7 @@ function hasVisibilityDirective(props) {
|
|
|
1068
1142
|
// prop that is not text is finished by that prop's kind instead — see
|
|
1069
1143
|
// `resolveComponentPropTokens` — and the parts this page hides are
|
|
1070
1144
|
// pruned from the result (`applyDeclaredProps`).
|
|
1071
|
-
Object.assign(next, applyDeclaredProps(grafted, definition.props, instancePropValues(instanceNode.props), prefixId(definition.rootId)));
|
|
1145
|
+
Object.assign(next, applyDeclaredProps(grafted, definition.props, instancePropValues(instanceNode.props), prefixId(definition.rootId), instancePropHtml(instanceNode.props)));
|
|
1072
1146
|
/*
|
|
1073
1147
|
* A definition whose ROOT names the definition itself is grafted
|
|
1074
1148
|
* UNWRAPPED — its root's children attach to the instance, and the
|
|
@@ -1401,7 +1475,8 @@ function hasVisibilityDirective(props) {
|
|
|
1401
1475
|
// this page hid (AGL-1314) are absent from the copy too — a detached hero
|
|
1402
1476
|
// that grew its mockup back is the same class of bug as one that rendered
|
|
1403
1477
|
// `{{prop.headline}}`.
|
|
1404
|
-
return Object.assign(next, applyDeclaredProps(copied, definition.props, instancePropValues(instanceNode.props), instanceId
|
|
1478
|
+
return Object.assign(next, applyDeclaredProps(copied, definition.props, instancePropValues(instanceNode.props), instanceId, // Formatting is part of what the instance renders, so the copy keeps it.
|
|
1479
|
+
instancePropHtml(instanceNode.props)));
|
|
1405
1480
|
}
|
|
1406
1481
|
/**
|
|
1407
1482
|
* The definition a subtree becomes when it is promoted (AGL-2908): the
|