@aglyn/aglyn 1.0.0 → 1.2.0
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/compose-reusable-components.d.ts +22 -0
- package/src/lib/app-utils/compose-reusable-components.js +94 -32
- package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +15 -1
- package/src/lib/app-utils/contacts.js +15 -1
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/crm.d.ts +61 -19
- package/src/lib/app-utils/crm.js +47 -26
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/default-site.d.ts +10 -3
- package/src/lib/app-utils/default-site.js +82 -6
- package/src/lib/app-utils/default-site.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +11 -1
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +5 -5
- package/src/lib/app-utils/docs-help.generated.js +23 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +43 -15
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/host-tokens.d.ts +7 -0
- package/src/lib/app-utils/host-tokens.js +27 -4
- package/src/lib/app-utils/host-tokens.js.map +1 -1
- package/src/lib/app-utils/node-definition-sanitizer.js +5 -1
- package/src/lib/app-utils/node-definition-sanitizer.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +1 -0
- package/src/lib/app-utils/server.js +1 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/site-brand-files.d.ts +87 -0
- package/src/lib/app-utils/site-brand-files.js +157 -0
- package/src/lib/app-utils/site-brand-files.js.map +1 -0
- package/src/lib/app-utils/site-starter-nodes.js +1 -0
- package/src/lib/app-utils/site-starter-nodes.js.map +1 -1
- package/src/lib/app-utils/site-starter-pick.d.ts +2 -2
- package/src/lib/app-utils/site-starter-pick.js +2 -2
- package/src/lib/app-utils/site-starter-pick.js.map +1 -1
- package/src/lib/plugin-manager/feature-plugins.d.ts +36 -2
- package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { normalizeImageContentType } from "./image-upload-types.js";
|
|
18
|
+
import { mediaNodeSrc } from "./media-ref.js";
|
|
19
|
+
export const SITE_BRAND_SLOTS = [
|
|
20
|
+
{
|
|
21
|
+
slot: 'logo',
|
|
22
|
+
label: 'Logo',
|
|
23
|
+
noun: 'logo'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
slot: 'logoDark',
|
|
27
|
+
label: 'Logo for dark backgrounds',
|
|
28
|
+
noun: 'dark logo'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
slot: 'favicon',
|
|
32
|
+
label: 'Favicon',
|
|
33
|
+
noun: 'favicon'
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
/** The types a logo may be, with the names a file with no type is read from. */ const LOGO_FILE_TYPES = {
|
|
37
|
+
'image/png': [
|
|
38
|
+
'.png'
|
|
39
|
+
],
|
|
40
|
+
'image/jpeg': [
|
|
41
|
+
'.jpg',
|
|
42
|
+
'.jpeg'
|
|
43
|
+
],
|
|
44
|
+
'image/webp': [
|
|
45
|
+
'.webp'
|
|
46
|
+
],
|
|
47
|
+
'image/svg+xml': [
|
|
48
|
+
'.svg'
|
|
49
|
+
]
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* What a favicon may also be. An `.ico` is linked as it is, and never reaches
|
|
53
|
+
* a logo slot: the file a header draws has to be a picture any size can use.
|
|
54
|
+
*/ const FAVICON_FILE_TYPES = _extends({}, LOGO_FILE_TYPES, {
|
|
55
|
+
'image/x-icon': [
|
|
56
|
+
'.ico'
|
|
57
|
+
],
|
|
58
|
+
'image/vnd.microsoft.icon': [
|
|
59
|
+
'.ico'
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
const typesFor = (slot)=>slot === 'favicon' ? FAVICON_FILE_TYPES : LOGO_FILE_TYPES;
|
|
63
|
+
/** The file chooser's `accept` for a field. */ export function siteBrandFileAccept(slot) {
|
|
64
|
+
return Object.entries(typesFor(slot)).flatMap(([type, extensions])=>[
|
|
65
|
+
type,
|
|
66
|
+
...extensions
|
|
67
|
+
]).join(',');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The content type to upload a file under, or `null` for one the field does
|
|
71
|
+
* not take. Read off the file's name when the browser reports none, because
|
|
72
|
+
* the upload route refuses an empty type.
|
|
73
|
+
*/ export function siteBrandFileType(file, slot) {
|
|
74
|
+
const types = typesFor(slot);
|
|
75
|
+
const reported = normalizeImageContentType(file.type);
|
|
76
|
+
if (reported) return types[reported] ? reported : null;
|
|
77
|
+
const name = file.name.toLowerCase();
|
|
78
|
+
const match = Object.entries(types).find(([, extensions])=>extensions.some((extension)=>name.endsWith(extension)));
|
|
79
|
+
return match ? match[0] : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Why a field will not take this file, said on the field before anything is
|
|
83
|
+
* created, or `null` when it will. `maxBytes` is the upload route's ceiling.
|
|
84
|
+
*/ export function siteBrandFileRefusal(file, slot, maxBytes) {
|
|
85
|
+
if (!siteBrandFileType(file, slot)) {
|
|
86
|
+
return slot === 'favicon' ? 'Use a PNG, JPEG, WebP, SVG or ICO file.' : 'Use a PNG, JPEG, WebP or SVG file.';
|
|
87
|
+
}
|
|
88
|
+
if (!file.size) return 'That file is empty.';
|
|
89
|
+
if (file.size > maxBytes) return `That file is over ${Math.round(maxBytes / (1024 * 1024))} MB.`;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Uploads the files a form is holding and writes what they became, one after
|
|
94
|
+
* the other so the storage count stays right. Never rejects: each failure is
|
|
95
|
+
* reported in `missed`.
|
|
96
|
+
*
|
|
97
|
+
* Each field stores `mediaNodeSrc`, as the Site settings cards do. A favicon
|
|
98
|
+
* left blank is the logo, the same asset rather than a second copy, and with
|
|
99
|
+
* no logo and no favicon nothing is written.
|
|
100
|
+
*/ export async function saveSiteBrandFiles(files, io) {
|
|
101
|
+
var _stored_favicon;
|
|
102
|
+
const stored = {};
|
|
103
|
+
const missed = [];
|
|
104
|
+
for (const { slot } of SITE_BRAND_SLOTS){
|
|
105
|
+
const file = files[slot];
|
|
106
|
+
if (!file) continue;
|
|
107
|
+
try {
|
|
108
|
+
const src = mediaNodeSrc(await io.upload(file, slot));
|
|
109
|
+
if (src) stored[slot] = src;
|
|
110
|
+
else missed.push(slot);
|
|
111
|
+
} catch (unused) {
|
|
112
|
+
missed.push(slot);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const favicon = (_stored_favicon = stored.favicon) != null ? _stored_favicon : stored.logo;
|
|
116
|
+
const fields = _extends({}, stored.logo ? {
|
|
117
|
+
logoUrl: stored.logo
|
|
118
|
+
} : {}, stored.logoDark ? {
|
|
119
|
+
logoDarkUrl: stored.logoDark
|
|
120
|
+
} : {}, favicon ? {
|
|
121
|
+
favicon
|
|
122
|
+
} : {});
|
|
123
|
+
if (!Object.keys(fields).length) return {
|
|
124
|
+
saved: null,
|
|
125
|
+
missed
|
|
126
|
+
};
|
|
127
|
+
try {
|
|
128
|
+
await io.save(fields);
|
|
129
|
+
return {
|
|
130
|
+
saved: fields,
|
|
131
|
+
missed
|
|
132
|
+
};
|
|
133
|
+
} catch (unused) {
|
|
134
|
+
// Every file is in the library, and nothing points at any of them.
|
|
135
|
+
return {
|
|
136
|
+
saved: null,
|
|
137
|
+
missed: SITE_BRAND_SLOTS.map(({ slot })=>slot).filter((slot)=>Boolean(files[slot]))
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** The one line that says which files did not make it, or `null` when all did. */ export function siteBrandMissedNotice(missed) {
|
|
142
|
+
const nouns = SITE_BRAND_SLOTS.filter(({ slot })=>missed.includes(slot)).map(({ noun })=>noun);
|
|
143
|
+
if (!nouns.length) return null;
|
|
144
|
+
const list = nouns.length > 1 ? `${nouns.slice(0, -1).join(', ')} and ${nouns[nouns.length - 1]}` : nouns[0];
|
|
145
|
+
return `Your site is ready, but your ${list} didn’t upload. ` + `Add ${nouns.length > 1 ? 'them' : 'it'} in Site settings.`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The query that carries {@link SiteBrandSaveOutcome.missed} over a full page
|
|
149
|
+
* load: a site made on the auth host lands on the console host, and a notice
|
|
150
|
+
* shown on the way out would go with the page. Field names only.
|
|
151
|
+
*/ export const SITE_BRAND_MISSED_PARAM = 'missed';
|
|
152
|
+
/** The fields a landing's {@link SITE_BRAND_MISSED_PARAM} names. Anything else is dropped. */ export function readSiteBrandMissed(value) {
|
|
153
|
+
const named = String(value != null ? value : '').split(',');
|
|
154
|
+
return SITE_BRAND_SLOTS.map(({ slot })=>slot).filter((slot)=>named.includes(slot));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//# sourceMappingURL=site-brand-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-brand-files.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 { normalizeImageContentType } from './image-upload-types'\nimport { mediaNodeSrc } from './media-ref'\n\n/**\n * The logo, the dark logo and the favicon a new site is created with\n * (AGL-4119).\n *\n * A first-time account has no workspace until the create makes one, so nothing\n * can go into a library while the form is open. The form holds the files, and\n * they go into the new site's own library once it exists. The site is what the\n * person asked for and these are optional, so a file that does not make it\n * never costs them the site: they are told which, and where to add it.\n */\n\n/** One of the three fields, and the host field each is written to. */\nexport type SiteBrandSlot = 'logo' | 'logoDark' | 'favicon'\n\nexport interface SiteBrandSlotSpec {\n slot: SiteBrandSlot\n /** The field's label. */\n label: string\n /** How a sentence names it. */\n noun: string\n}\n\nexport const SITE_BRAND_SLOTS: readonly SiteBrandSlotSpec[] = [\n { slot: 'logo', label: 'Logo', noun: 'logo' },\n { slot: 'logoDark', label: 'Logo for dark backgrounds', noun: 'dark logo' },\n { slot: 'favicon', label: 'Favicon', noun: 'favicon' },\n]\n\n/** The types a logo may be, with the names a file with no type is read from. */\nconst LOGO_FILE_TYPES: Readonly<Record<string, readonly string[]>> = {\n 'image/png': ['.png'],\n 'image/jpeg': ['.jpg', '.jpeg'],\n 'image/webp': ['.webp'],\n 'image/svg+xml': ['.svg'],\n}\n\n/**\n * What a favicon may also be. An `.ico` is linked as it is, and never reaches\n * a logo slot: the file a header draws has to be a picture any size can use.\n */\nconst FAVICON_FILE_TYPES: Readonly<Record<string, readonly string[]>> = {\n ...LOGO_FILE_TYPES,\n 'image/x-icon': ['.ico'],\n 'image/vnd.microsoft.icon': ['.ico'],\n}\n\nconst typesFor = (slot: SiteBrandSlot) => (slot === 'favicon' ? FAVICON_FILE_TYPES : LOGO_FILE_TYPES)\n\n/** The file chooser's `accept` for a field. */\nexport function siteBrandFileAccept(slot: SiteBrandSlot): string {\n return Object.entries(typesFor(slot))\n .flatMap(([type, extensions]) => [type, ...extensions])\n .join(',')\n}\n\n/**\n * The content type to upload a file under, or `null` for one the field does\n * not take. Read off the file's name when the browser reports none, because\n * the upload route refuses an empty type.\n */\nexport function siteBrandFileType(\n file: Pick<File, 'name' | 'type'>,\n slot: SiteBrandSlot,\n): string | null {\n const types = typesFor(slot)\n const reported = normalizeImageContentType(file.type)\n if (reported) return types[reported] ? reported : null\n const name = file.name.toLowerCase()\n const match = Object.entries(types).find(([, extensions]) =>\n extensions.some((extension) => name.endsWith(extension)),\n )\n return match ? match[0] : null\n}\n\n/**\n * Why a field will not take this file, said on the field before anything is\n * created, or `null` when it will. `maxBytes` is the upload route's ceiling.\n */\nexport function siteBrandFileRefusal(\n file: Pick<File, 'name' | 'type' | 'size'>,\n slot: SiteBrandSlot,\n maxBytes: number,\n): string | null {\n if (!siteBrandFileType(file, slot)) {\n return slot === 'favicon' ? 'Use a PNG, JPEG, WebP, SVG or ICO file.' : 'Use a PNG, JPEG, WebP or SVG file.'\n }\n if (!file.size) return 'That file is empty.'\n if (file.size > maxBytes) return `That file is over ${Math.round(maxBytes / (1024 * 1024))} MB.`\n return null\n}\n\n/** The host fields a create writes; `favicon` lands on `seo.favicon`. */\nexport interface SiteBrandFields {\n logoUrl?: string\n logoDarkUrl?: string\n favicon?: string\n}\n\nexport interface SiteBrandSaveOutcome {\n /** What was written; `null` when nothing was. */\n saved: SiteBrandFields | null\n /** The chosen files that did not make it, in field order. */\n missed: SiteBrandSlot[]\n}\n\n/**\n * Uploads the files a form is holding and writes what they became, one after\n * the other so the storage count stays right. Never rejects: each failure is\n * reported in `missed`.\n *\n * Each field stores `mediaNodeSrc`, as the Site settings cards do. A favicon\n * left blank is the logo, the same asset rather than a second copy, and with\n * no logo and no favicon nothing is written.\n */\nexport async function saveSiteBrandFiles(\n files: Readonly<Partial<Record<SiteBrandSlot, File | null>>>,\n io: {\n upload: (file: File, slot: SiteBrandSlot) => Promise<{ url?: string | null; cdnPath?: string | null }>\n save: (fields: SiteBrandFields) => Promise<void>\n },\n): Promise<SiteBrandSaveOutcome> {\n const stored: Partial<Record<SiteBrandSlot, string>> = {}\n const missed: SiteBrandSlot[] = []\n for (const { slot } of SITE_BRAND_SLOTS) {\n const file = files[slot]\n if (!file) continue\n try {\n const src = mediaNodeSrc(await io.upload(file, slot))\n if (src) stored[slot] = src\n else missed.push(slot)\n } catch {\n missed.push(slot)\n }\n }\n const favicon = stored.favicon ?? stored.logo\n const fields: SiteBrandFields = {\n ...(stored.logo ? { logoUrl: stored.logo } : {}),\n ...(stored.logoDark ? { logoDarkUrl: stored.logoDark } : {}),\n ...(favicon ? { favicon } : {}),\n }\n if (!Object.keys(fields).length) return { saved: null, missed }\n try {\n await io.save(fields)\n return { saved: fields, missed }\n } catch {\n // Every file is in the library, and nothing points at any of them.\n return {\n saved: null,\n missed: SITE_BRAND_SLOTS.map(({ slot }) => slot).filter((slot) => Boolean(files[slot])),\n }\n }\n}\n\n/** The one line that says which files did not make it, or `null` when all did. */\nexport function siteBrandMissedNotice(missed: readonly SiteBrandSlot[]): string | null {\n const nouns = SITE_BRAND_SLOTS.filter(({ slot }) => missed.includes(slot)).map(({ noun }) => noun)\n if (!nouns.length) return null\n const list = nouns.length > 1 ? `${nouns.slice(0, -1).join(', ')} and ${nouns[nouns.length - 1]}` : nouns[0]\n return (\n `Your site is ready, but your ${list} didn’t upload. ` +\n `Add ${nouns.length > 1 ? 'them' : 'it'} in Site settings.`\n )\n}\n\n/**\n * The query that carries {@link SiteBrandSaveOutcome.missed} over a full page\n * load: a site made on the auth host lands on the console host, and a notice\n * shown on the way out would go with the page. Field names only.\n */\nexport const SITE_BRAND_MISSED_PARAM = 'missed'\n\n/** The fields a landing's {@link SITE_BRAND_MISSED_PARAM} names. Anything else is dropped. */\nexport function readSiteBrandMissed(value: string | null | undefined): SiteBrandSlot[] {\n const named = String(value ?? '').split(',')\n return SITE_BRAND_SLOTS.map(({ slot }) => slot).filter((slot) => named.includes(slot))\n}\n"],"names":["normalizeImageContentType","mediaNodeSrc","SITE_BRAND_SLOTS","slot","label","noun","LOGO_FILE_TYPES","FAVICON_FILE_TYPES","typesFor","siteBrandFileAccept","Object","entries","flatMap","type","extensions","join","siteBrandFileType","file","types","reported","name","toLowerCase","match","find","some","extension","endsWith","siteBrandFileRefusal","maxBytes","size","Math","round","saveSiteBrandFiles","files","io","stored","missed","src","upload","push","favicon","logo","fields","logoUrl","logoDark","logoDarkUrl","keys","length","saved","save","map","filter","Boolean","siteBrandMissedNotice","nouns","includes","list","slice","SITE_BRAND_MISSED_PARAM","readSiteBrandMissed","value","named","String","split"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-starter-nodes.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 { NODE_MAX_INTERACTIONS } from './node-interactions'\nimport {\n PORTABLE_COMPONENT_ID_ALLOWLIST,\n PORTABLE_DEFINITION_MAX_BYTES,\n sanitizePortableDefinition,\n type PortableDefinitionNodes,\n} from './node-definition-sanitizer'\nimport { isSafeNodeUrl } from './node-url-policy'\nimport { CLIENT_INTERACTION_STEP_LABELS } from './site-interactions'\n\n/**\n * What a site starter's pages and layout may hold (AGL-4049), and the one\n * pass that makes them so.\n *\n * A starter is a whole site, so it has to carry what a real site is made of:\n * the grids, cards, accordions and icons the default site is built from and\n * the AI site build lays out, with their styles and their interactions. The\n * portable rule every other artifact is published under keeps none of that —\n * it is for a component dropped into someone else's page — so a starter gets\n * its own allowlist here, a superset of the portable one, and keeps `sx` and\n * `interactions` once they are checked. Nothing else is widened: the template,\n * layout and component doors still publish under the portable rule.\n *\n * Kept safe the same way, plus two checks of its own:\n * - only the component ids below, nothing bound to the source site's data\n * (content collections, datasets) or to another plugin;\n * - the portable prop hardening (no `on*`, no `dangerouslySetInnerHTML`, an\n * `href`/`src` only to a safe address) and the size cap;\n * - a style may load an image only from where an image `src` may, and never\n * from a protocol-relative address;\n * - an interaction may only scroll, show, hide, open, close, toggle a class,\n * stick the navigation or play a video: no script, markup, redirect or\n * tracking rides a starter onto the sites created from it.\n *\n * The publish door runs every page and the layout through\n * {@link sanitizeSiteStarterNodes}; the first-party seed holds its pages to the\n * same pass, so a seeded and a published starter store one shape. Pure.\n */\n\n/**\n * The component ids a starter's pages may hold: the portable allowlist, what\n * `default-site.ts` builds, and the presentational elements the AI site build\n * lays out (its palette's page surface, less anything bound to a site's data).\n */\nexport const SITE_STARTER_COMPONENT_IDS: readonly string[] = [\n ...new Set([\n ...PORTABLE_COMPONENT_ID_ALLOWLIST,\n // The default site (`default-site.ts`).\n 'icon',\n 'muiAccordion',\n 'muiAccordionDetails',\n 'muiAccordionSummary',\n 'muiCard',\n 'muiCardContent',\n 'muiGrid',\n 'themeModeSwitcher',\n // The AI site build's presentational elements.\n 'dataTable',\n 'muiBox',\n 'muiBreadcrumbs',\n 'muiCardActions',\n 'muiCardHeader',\n 'muiImageList',\n 'muiImageListItem',\n 'video',\n ]),\n].sort()\n\n/** The node marking where a page's content grafts into its layout. */\nexport const SITE_STARTER_LAYOUT_SLOT_ID = 'layoutSlot'\n\n/** A starter's layout may hold the same, plus the slot its pages render in. */\nexport const SITE_STARTER_LAYOUT_COMPONENT_IDS: readonly string[] = [\n ...SITE_STARTER_COMPONENT_IDS,\n SITE_STARTER_LAYOUT_SLOT_ID,\n].sort()\n\n/**\n * The interaction steps a starter may carry: each acts on the page it is on\n * and nothing else. Scripts, markup, redirects, attributes, alerts, popups and\n * analytics are left out, as is every step a plugin declares.\n */\nexport const SITE_STARTER_INTERACTION_STEP_TYPES: ReadonlySet<string> = new Set([\n 'addClass',\n 'closeDrawer',\n 'closeMenu',\n 'hideElement',\n 'openDrawer',\n 'openMenu',\n 'playVideo',\n 'removeClass',\n 'scrollTo',\n 'showElement',\n 'stickyNav',\n 'toggleClass',\n 'toggleDrawer',\n 'toggleElement',\n 'toggleMenu',\n])\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\n/** How a refusal names a node: its component, then its id. */\nconst nodeName = (id: string, node: Record<string, any>) =>\n `the ${String(node?.componentId ?? 'element')} \"${id}\"`\n\nconst stepLabel = (type: string): string =>\n (CLIENT_INTERACTION_STEP_LABELS as Readonly<Record<string, string>>)[type] ?? type\n\n/** Every `url(…)` address in a style value. */\nconst styleUrls = (value: string): string[] =>\n [...value.matchAll(/url\\(\\s*(['\"]?)(.*?)\\1\\s*\\)/gi)].map((match) => match[2].trim())\n\n/** Why a node's styles cannot ride a starter, or null when they can. */\nfunction styleRefusal(id: string, node: Record<string, any>, sx: unknown, depth = 0): string | null {\n if (depth > 12) return `The styles on ${nodeName(id, node)} are nested too deeply`\n if (typeof sx === 'string') {\n if (/url\\(/i.test(sx) && !styleUrls(sx).length) {\n return `A style on ${nodeName(id, node)} loads an address a starter cannot read`\n }\n for (const address of styleUrls(sx)) {\n if (address.startsWith('//') || !isSafeNodeUrl('src', address)) {\n return `A style on ${nodeName(id, node)} loads \"${address.slice(0, 120)}\", which a starter cannot carry`\n }\n }\n return null\n }\n if (sx === null || typeof sx === 'number' || typeof sx === 'boolean') return null\n if (Array.isArray(sx)) {\n for (const entry of sx) {\n const refusal = styleRefusal(id, node, entry, depth + 1)\n if (refusal) return refusal\n }\n return null\n }\n if (isRecord(sx)) {\n for (const entry of Object.values(sx)) {\n const refusal = styleRefusal(id, node, entry, depth + 1)\n if (refusal) return refusal\n }\n return null\n }\n return `The styles on ${nodeName(id, node)} hold something that is not a style`\n}\n\n/** Why a node's interactions cannot ride a starter, or null when they can. */\nfunction interactionsRefusal(id: string, node: Record<string, any>, interactions: unknown): string | null {\n if (!Array.isArray(interactions)) return `The interactions on ${nodeName(id, node)} are not a list`\n if (interactions.length > NODE_MAX_INTERACTIONS) {\n return `${nodeName(id, node)} has more than ${NODE_MAX_INTERACTIONS} interactions`\n }\n for (const interaction of interactions) {\n if (!isRecord(interaction) || !Array.isArray(interaction['steps'])) {\n return `An interaction on ${nodeName(id, node)} is not one`\n }\n for (const step of interaction['steps'] as unknown[]) {\n const type = isRecord(step) && typeof step['type'] === 'string' ? step['type'] : ''\n if (!SITE_STARTER_INTERACTION_STEP_TYPES.has(type)) {\n return (\n `An interaction on ${nodeName(id, node)} runs \"${stepLabel(type || 'an unknown step')}\", ` +\n 'which a starter cannot carry: a starter brings no scripts, markup, redirects or ' +\n 'tracking to the sites created from it'\n )\n }\n }\n }\n return null\n}\n\n/** A starter's node as stored: a portable node, with its checked styles and interactions. */\nexport type SiteStarterNodes = Record<\n string,\n PortableDefinitionNodes[string] & { sx?: unknown; interactions?: unknown[] }\n>\n\n/** A JSON copy, so a stored starter never shares an object with its source. */\nconst copied = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T\n\n/**\n * A starter page's or layout's nodes as a starter stores them, or why they\n * cannot be: the portable pass under {@link SITE_STARTER_COMPONENT_IDS} (or\n * the layout list), then each kept node's checked `sx` and `interactions`\n * put back. Callers decode stored nodes first (`decodeStoredNodes`).\n */\nexport function sanitizeSiteStarterNodes(\n definition: { rootId: string; nodes: Record<string, any> },\n options: {\n /** A layout: the slot its pages render in is allowed too. */\n layout?: boolean\n /** The properties a layout's tree binds to, as `sanitizePortableDefinition` takes them. */\n declaredProps?: ReadonlyArray<{ name?: unknown; type?: unknown }>\n } = {},\n): { ok: true; rootId: string; nodes: SiteStarterNodes } | { ok: false; error: string } {\n const portable = sanitizePortableDefinition(definition, {\n componentIds: options.layout ? SITE_STARTER_LAYOUT_COMPONENT_IDS : SITE_STARTER_COMPONENT_IDS,\n ...(options.declaredProps && { declaredProps: options.declaredProps }),\n })\n if (portable.ok === false) return portable\n const nodes: SiteStarterNodes = {}\n for (const [id, kept] of Object.entries(portable.nodes)) {\n const source = definition.nodes[id] ?? {}\n const node: SiteStarterNodes[string] = { ...kept }\n if (source.sx !== undefined && source.sx !== null) {\n const refusal = styleRefusal(id, source, source.sx)\n if (refusal) return { ok: false, error: refusal }\n node['sx'] = copied(source.sx)\n }\n if (source.interactions !== undefined && source.interactions !== null) {\n const refusal = interactionsRefusal(id, source, source.interactions)\n if (refusal) return { ok: false, error: refusal }\n if ((source.interactions as unknown[]).length) node['interactions'] = copied(source.interactions)\n }\n nodes[id] = node\n }\n if (JSON.stringify(nodes).length > PORTABLE_DEFINITION_MAX_BYTES) {\n return { ok: false, error: 'Definition is too large to publish' }\n }\n return { ok: true, rootId: portable.rootId, nodes }\n}\n"],"names":["NODE_MAX_INTERACTIONS","PORTABLE_COMPONENT_ID_ALLOWLIST","PORTABLE_DEFINITION_MAX_BYTES","sanitizePortableDefinition","isSafeNodeUrl","CLIENT_INTERACTION_STEP_LABELS","SITE_STARTER_COMPONENT_IDS","Set","sort","SITE_STARTER_LAYOUT_SLOT_ID","SITE_STARTER_LAYOUT_COMPONENT_IDS","SITE_STARTER_INTERACTION_STEP_TYPES","isRecord","value","Array","isArray","nodeName","id","node","String","componentId","stepLabel","type","styleUrls","matchAll","map","match","trim","styleRefusal","sx","depth","test","length","address","startsWith","slice","entry","refusal","Object","values","interactionsRefusal","interactions","interaction","step","has","copied","JSON","parse","stringify","sanitizeSiteStarterNodes","definition","options","portable","componentIds","layout","declaredProps","ok","nodes","kept","entries","source","undefined","error","rootId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-starter-nodes.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 { NODE_MAX_INTERACTIONS } from './node-interactions'\nimport {\n PORTABLE_COMPONENT_ID_ALLOWLIST,\n PORTABLE_DEFINITION_MAX_BYTES,\n sanitizePortableDefinition,\n type PortableDefinitionNodes,\n} from './node-definition-sanitizer'\nimport { isSafeNodeUrl } from './node-url-policy'\nimport { CLIENT_INTERACTION_STEP_LABELS } from './site-interactions'\n\n/**\n * What a site starter's pages and layout may hold (AGL-4049), and the one\n * pass that makes them so.\n *\n * A starter is a whole site, so it has to carry what a real site is made of:\n * the grids, cards, accordions and icons the default site is built from and\n * the AI site build lays out, with their styles and their interactions. The\n * portable rule every other artifact is published under keeps none of that —\n * it is for a component dropped into someone else's page — so a starter gets\n * its own allowlist here, a superset of the portable one, and keeps `sx` and\n * `interactions` once they are checked. Nothing else is widened: the template,\n * layout and component doors still publish under the portable rule.\n *\n * Kept safe the same way, plus two checks of its own:\n * - only the component ids below, nothing bound to the source site's data\n * (content collections, datasets) or to another plugin;\n * - the portable prop hardening (no `on*`, no `dangerouslySetInnerHTML`, an\n * `href`/`src` only to a safe address) and the size cap;\n * - a style may load an image only from where an image `src` may, and never\n * from a protocol-relative address;\n * - an interaction may only scroll, show, hide, open, close, toggle a class,\n * stick the navigation or play a video: no script, markup, redirect or\n * tracking rides a starter onto the sites created from it.\n *\n * The publish door runs every page and the layout through\n * {@link sanitizeSiteStarterNodes}; the first-party seed holds its pages to the\n * same pass, so a seeded and a published starter store one shape. Pure.\n */\n\n/**\n * The component ids a starter's pages may hold: the portable allowlist, what\n * `default-site.ts` builds, and the presentational elements the AI site build\n * lays out (its palette's page surface, less anything bound to a site's data).\n */\nexport const SITE_STARTER_COMPONENT_IDS: readonly string[] = [\n ...new Set([\n ...PORTABLE_COMPONENT_ID_ALLOWLIST,\n // The default site (`default-site.ts`).\n 'icon',\n 'muiAccordion',\n 'muiAccordionDetails',\n 'muiAccordionSummary',\n 'muiCard',\n 'muiCardContent',\n 'muiGrid',\n 'muiLinkBox',\n 'themeModeSwitcher',\n // The AI site build's presentational elements.\n 'dataTable',\n 'muiBox',\n 'muiBreadcrumbs',\n 'muiCardActions',\n 'muiCardHeader',\n 'muiImageList',\n 'muiImageListItem',\n 'video',\n ]),\n].sort()\n\n/** The node marking where a page's content grafts into its layout. */\nexport const SITE_STARTER_LAYOUT_SLOT_ID = 'layoutSlot'\n\n/** A starter's layout may hold the same, plus the slot its pages render in. */\nexport const SITE_STARTER_LAYOUT_COMPONENT_IDS: readonly string[] = [\n ...SITE_STARTER_COMPONENT_IDS,\n SITE_STARTER_LAYOUT_SLOT_ID,\n].sort()\n\n/**\n * The interaction steps a starter may carry: each acts on the page it is on\n * and nothing else. Scripts, markup, redirects, attributes, alerts, popups and\n * analytics are left out, as is every step a plugin declares.\n */\nexport const SITE_STARTER_INTERACTION_STEP_TYPES: ReadonlySet<string> = new Set([\n 'addClass',\n 'closeDrawer',\n 'closeMenu',\n 'hideElement',\n 'openDrawer',\n 'openMenu',\n 'playVideo',\n 'removeClass',\n 'scrollTo',\n 'showElement',\n 'stickyNav',\n 'toggleClass',\n 'toggleDrawer',\n 'toggleElement',\n 'toggleMenu',\n])\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n !!value && typeof value === 'object' && !Array.isArray(value)\n\n/** How a refusal names a node: its component, then its id. */\nconst nodeName = (id: string, node: Record<string, any>) =>\n `the ${String(node?.componentId ?? 'element')} \"${id}\"`\n\nconst stepLabel = (type: string): string =>\n (CLIENT_INTERACTION_STEP_LABELS as Readonly<Record<string, string>>)[type] ?? type\n\n/** Every `url(…)` address in a style value. */\nconst styleUrls = (value: string): string[] =>\n [...value.matchAll(/url\\(\\s*(['\"]?)(.*?)\\1\\s*\\)/gi)].map((match) => match[2].trim())\n\n/** Why a node's styles cannot ride a starter, or null when they can. */\nfunction styleRefusal(id: string, node: Record<string, any>, sx: unknown, depth = 0): string | null {\n if (depth > 12) return `The styles on ${nodeName(id, node)} are nested too deeply`\n if (typeof sx === 'string') {\n if (/url\\(/i.test(sx) && !styleUrls(sx).length) {\n return `A style on ${nodeName(id, node)} loads an address a starter cannot read`\n }\n for (const address of styleUrls(sx)) {\n if (address.startsWith('//') || !isSafeNodeUrl('src', address)) {\n return `A style on ${nodeName(id, node)} loads \"${address.slice(0, 120)}\", which a starter cannot carry`\n }\n }\n return null\n }\n if (sx === null || typeof sx === 'number' || typeof sx === 'boolean') return null\n if (Array.isArray(sx)) {\n for (const entry of sx) {\n const refusal = styleRefusal(id, node, entry, depth + 1)\n if (refusal) return refusal\n }\n return null\n }\n if (isRecord(sx)) {\n for (const entry of Object.values(sx)) {\n const refusal = styleRefusal(id, node, entry, depth + 1)\n if (refusal) return refusal\n }\n return null\n }\n return `The styles on ${nodeName(id, node)} hold something that is not a style`\n}\n\n/** Why a node's interactions cannot ride a starter, or null when they can. */\nfunction interactionsRefusal(id: string, node: Record<string, any>, interactions: unknown): string | null {\n if (!Array.isArray(interactions)) return `The interactions on ${nodeName(id, node)} are not a list`\n if (interactions.length > NODE_MAX_INTERACTIONS) {\n return `${nodeName(id, node)} has more than ${NODE_MAX_INTERACTIONS} interactions`\n }\n for (const interaction of interactions) {\n if (!isRecord(interaction) || !Array.isArray(interaction['steps'])) {\n return `An interaction on ${nodeName(id, node)} is not one`\n }\n for (const step of interaction['steps'] as unknown[]) {\n const type = isRecord(step) && typeof step['type'] === 'string' ? step['type'] : ''\n if (!SITE_STARTER_INTERACTION_STEP_TYPES.has(type)) {\n return (\n `An interaction on ${nodeName(id, node)} runs \"${stepLabel(type || 'an unknown step')}\", ` +\n 'which a starter cannot carry: a starter brings no scripts, markup, redirects or ' +\n 'tracking to the sites created from it'\n )\n }\n }\n }\n return null\n}\n\n/** A starter's node as stored: a portable node, with its checked styles and interactions. */\nexport type SiteStarterNodes = Record<\n string,\n PortableDefinitionNodes[string] & { sx?: unknown; interactions?: unknown[] }\n>\n\n/** A JSON copy, so a stored starter never shares an object with its source. */\nconst copied = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T\n\n/**\n * A starter page's or layout's nodes as a starter stores them, or why they\n * cannot be: the portable pass under {@link SITE_STARTER_COMPONENT_IDS} (or\n * the layout list), then each kept node's checked `sx` and `interactions`\n * put back. Callers decode stored nodes first (`decodeStoredNodes`).\n */\nexport function sanitizeSiteStarterNodes(\n definition: { rootId: string; nodes: Record<string, any> },\n options: {\n /** A layout: the slot its pages render in is allowed too. */\n layout?: boolean\n /** The properties a layout's tree binds to, as `sanitizePortableDefinition` takes them. */\n declaredProps?: ReadonlyArray<{ name?: unknown; type?: unknown }>\n } = {},\n): { ok: true; rootId: string; nodes: SiteStarterNodes } | { ok: false; error: string } {\n const portable = sanitizePortableDefinition(definition, {\n componentIds: options.layout ? SITE_STARTER_LAYOUT_COMPONENT_IDS : SITE_STARTER_COMPONENT_IDS,\n ...(options.declaredProps && { declaredProps: options.declaredProps }),\n })\n if (portable.ok === false) return portable\n const nodes: SiteStarterNodes = {}\n for (const [id, kept] of Object.entries(portable.nodes)) {\n const source = definition.nodes[id] ?? {}\n const node: SiteStarterNodes[string] = { ...kept }\n if (source.sx !== undefined && source.sx !== null) {\n const refusal = styleRefusal(id, source, source.sx)\n if (refusal) return { ok: false, error: refusal }\n node['sx'] = copied(source.sx)\n }\n if (source.interactions !== undefined && source.interactions !== null) {\n const refusal = interactionsRefusal(id, source, source.interactions)\n if (refusal) return { ok: false, error: refusal }\n if ((source.interactions as unknown[]).length) node['interactions'] = copied(source.interactions)\n }\n nodes[id] = node\n }\n if (JSON.stringify(nodes).length > PORTABLE_DEFINITION_MAX_BYTES) {\n return { ok: false, error: 'Definition is too large to publish' }\n }\n return { ok: true, rootId: portable.rootId, nodes }\n}\n"],"names":["NODE_MAX_INTERACTIONS","PORTABLE_COMPONENT_ID_ALLOWLIST","PORTABLE_DEFINITION_MAX_BYTES","sanitizePortableDefinition","isSafeNodeUrl","CLIENT_INTERACTION_STEP_LABELS","SITE_STARTER_COMPONENT_IDS","Set","sort","SITE_STARTER_LAYOUT_SLOT_ID","SITE_STARTER_LAYOUT_COMPONENT_IDS","SITE_STARTER_INTERACTION_STEP_TYPES","isRecord","value","Array","isArray","nodeName","id","node","String","componentId","stepLabel","type","styleUrls","matchAll","map","match","trim","styleRefusal","sx","depth","test","length","address","startsWith","slice","entry","refusal","Object","values","interactionsRefusal","interactions","interaction","step","has","copied","JSON","parse","stringify","sanitizeSiteStarterNodes","definition","options","portable","componentIds","layout","declaredProps","ok","nodes","kept","entries","source","undefined","error","rootId"],"mappings":"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"}
|
|
@@ -45,7 +45,7 @@ export interface SiteStarterPickRefused {
|
|
|
45
45
|
* answers as it always has. A refusal of a pick names its listing: a 402 is
|
|
46
46
|
* a price to pay, and the pick stays with "Buy and continue"; anything else
|
|
47
47
|
* (the release not open to the workspace, a starter that is gone, one with
|
|
48
|
-
* more pages than the plan holds, an AI starter
|
|
49
|
-
*
|
|
48
|
+
* more pages than the plan holds, an AI starter where the AI start is not
|
|
49
|
+
* offered) clears the pick, with the reason said.
|
|
50
50
|
*/
|
|
51
51
|
export declare function readSiteStarterPickRefusal(status: number, payload: unknown, listingId: string): SiteStarterPickRefused | null;
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* answers as it always has. A refusal of a pick names its listing: a 402 is
|
|
25
25
|
* a price to pay, and the pick stays with "Buy and continue"; anything else
|
|
26
26
|
* (the release not open to the workspace, a starter that is gone, one with
|
|
27
|
-
* more pages than the plan holds, an AI starter
|
|
28
|
-
*
|
|
27
|
+
* more pages than the plan holds, an AI starter where the AI start is not
|
|
28
|
+
* offered) clears the pick, with the reason said.
|
|
29
29
|
*/ export function readSiteStarterPickRefusal(status, payload, listingId) {
|
|
30
30
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
31
31
|
const body = payload;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-starter-pick.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 type { SiteStarterSummary } from '../plugin-manager/plugin-site-starters'\n\n/**\n * A starter picked in \"Choose your starter\" (AGL-4049), as Create site\n * decides about it: whether a site can be created from it yet, and what the\n * create's refusal of it asks for. Pure, so every surface that offers the\n * picker — the console, the native apps — decides the same way.\n */\n\n/**\n * What stands between a pick and Create site: nothing picked (`none`), or a\n * price the workspace has not paid (`purchase`); `null` when the site can be\n * created from it. `owned` is what the surface knows better than the\n * search's own answer (the person just paid, or the create still asked for\n * payment); absent, the summary's `owned` stands.\n */\nexport type SiteStarterPickBlock = 'none' | 'purchase'\n\nexport function siteStarterPickBlock(\n pick: SiteStarterSummary | null | undefined,\n owned?: boolean,\n): SiteStarterPickBlock | null {\n if (!pick) return 'none'\n if (pick.priceUsd > 0 && !(owned ?? pick.owned === true)) return 'purchase'\n return null\n}\n\n/** What Create site does with a create that refused the starter it was asked for. */\nexport interface SiteStarterPickRefused {\n /** The pick stays: it can still be created from, once bought. */\n keep: boolean\n /** It is the price that stands in the way, so buying it is offered. */\n purchase: boolean\n /** The create's own words for why. */\n message: string\n}\n\n/**\n * The create's refusal of THIS pick, or `null` for a refusal about anything\n * else (the name, the address, the plan's site count), which Create site\n * answers as it always has. A refusal of a pick names its listing: a 402 is\n * a price to pay, and the pick stays with \"Buy and continue\"; anything else\n * (the release not open to the workspace, a starter that is gone, one with\n * more pages than the plan holds, an AI starter
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/site-starter-pick.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 type { SiteStarterSummary } from '../plugin-manager/plugin-site-starters'\n\n/**\n * A starter picked in \"Choose your starter\" (AGL-4049), as Create site\n * decides about it: whether a site can be created from it yet, and what the\n * create's refusal of it asks for. Pure, so every surface that offers the\n * picker — the console, the native apps — decides the same way.\n */\n\n/**\n * What stands between a pick and Create site: nothing picked (`none`), or a\n * price the workspace has not paid (`purchase`); `null` when the site can be\n * created from it. `owned` is what the surface knows better than the\n * search's own answer (the person just paid, or the create still asked for\n * payment); absent, the summary's `owned` stands.\n */\nexport type SiteStarterPickBlock = 'none' | 'purchase'\n\nexport function siteStarterPickBlock(\n pick: SiteStarterSummary | null | undefined,\n owned?: boolean,\n): SiteStarterPickBlock | null {\n if (!pick) return 'none'\n if (pick.priceUsd > 0 && !(owned ?? pick.owned === true)) return 'purchase'\n return null\n}\n\n/** What Create site does with a create that refused the starter it was asked for. */\nexport interface SiteStarterPickRefused {\n /** The pick stays: it can still be created from, once bought. */\n keep: boolean\n /** It is the price that stands in the way, so buying it is offered. */\n purchase: boolean\n /** The create's own words for why. */\n message: string\n}\n\n/**\n * The create's refusal of THIS pick, or `null` for a refusal about anything\n * else (the name, the address, the plan's site count), which Create site\n * answers as it always has. A refusal of a pick names its listing: a 402 is\n * a price to pay, and the pick stays with \"Buy and continue\"; anything else\n * (the release not open to the workspace, a starter that is gone, one with\n * more pages than the plan holds, an AI starter where the AI start is not\n * offered) clears the pick, with the reason said.\n */\nexport function readSiteStarterPickRefusal(\n status: number,\n payload: unknown,\n listingId: string,\n): SiteStarterPickRefused | null {\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return null\n const body = payload as Record<string, unknown>\n if (!listingId || body['listingId'] !== listingId) return null\n const error = body['error']\n const message = typeof error === 'string' && error.trim() ? error.trim() : 'That starter cannot be used.'\n if (status === 402) return { keep: true, purchase: true, message }\n return { keep: false, purchase: false, message }\n}\n"],"names":["siteStarterPickBlock","pick","owned","priceUsd","readSiteStarterPickRefusal","status","payload","listingId","Array","isArray","body","error","message","trim","keep","purchase"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAoBD,OAAO,SAASA,qBACdC,IAA2C,EAC3CC,KAAe;IAEf,IAAI,CAACD,MAAM,OAAO;IAClB,IAAIA,KAAKE,QAAQ,GAAG,KAAK,EAAED,gBAAAA,QAASD,KAAKC,KAAK,KAAK,OAAO,OAAO;IACjE,OAAO;AACT;AAYA;;;;;;;;CAQC,GACD,OAAO,SAASE,2BACdC,MAAc,EACdC,OAAgB,EAChBC,SAAiB;IAEjB,IAAI,CAACD,WAAW,OAAOA,YAAY,YAAYE,MAAMC,OAAO,CAACH,UAAU,OAAO;IAC9E,MAAMI,OAAOJ;IACb,IAAI,CAACC,aAAaG,IAAI,CAAC,YAAY,KAAKH,WAAW,OAAO;IAC1D,MAAMI,QAAQD,IAAI,CAAC,QAAQ;IAC3B,MAAME,UAAU,OAAOD,UAAU,YAAYA,MAAME,IAAI,KAAKF,MAAME,IAAI,KAAK;IAC3E,IAAIR,WAAW,KAAK,OAAO;QAAES,MAAM;QAAMC,UAAU;QAAMH;IAAQ;IACjE,OAAO;QAAEE,MAAM;QAAOC,UAAU;QAAOH;IAAQ;AACjD"}
|
|
@@ -20,7 +20,7 @@ import type { ReleaseFlagKey } from '../app-utils/release-flags';
|
|
|
20
20
|
import type { SeoAuditReport } from '../app-utils/seo-audit';
|
|
21
21
|
import type { SeoListingFieldKey } from '../app-utils/seo-listing-fields';
|
|
22
22
|
import type { SitePreviewStart } from '../app-utils/site-preview';
|
|
23
|
-
import type { SiteStarterFacets, SiteStarterMode } from '../app-utils/site-starter';
|
|
23
|
+
import type { SiteStarterChoice, SiteStarterFacets, SiteStarterMode } from '../app-utils/site-starter';
|
|
24
24
|
import type { AglynOrgBilling, OrgFeatureFlags } from '../foundation';
|
|
25
25
|
import type { ComponentSchema, MdiIconProps, PresetSchema } from '../types/nodes';
|
|
26
26
|
import type { Plugin, PluginId } from './plugin-manager';
|
|
@@ -1229,10 +1229,16 @@ export interface ConsolePendingSite {
|
|
|
1229
1229
|
* (AGL-4049): a `SITE_STYLES` id, or `SITE_STYLE_AUTO` for a card that lets
|
|
1230
1230
|
* the answers pick. It is recorded on the site (`starterChoice`) and
|
|
1231
1231
|
* changes nothing it is created with: the start builds the look.
|
|
1232
|
+
*
|
|
1233
|
+
* `starter` is the AI starter the questions were filled from
|
|
1234
|
+
* ({@link ConsoleHostFirstRunZoneProps.starter}, AGL-4049): the site is
|
|
1235
|
+
* created empty for the start, as ever, and records the starter it began
|
|
1236
|
+
* from, so leaving the start gives it that starter's own pages.
|
|
1232
1237
|
*/
|
|
1233
1238
|
create: (options?: {
|
|
1234
1239
|
start?: 'import';
|
|
1235
1240
|
style?: string;
|
|
1241
|
+
starter?: SiteStarterChoice;
|
|
1236
1242
|
}) => Promise<ConsoleCreatedSite | {
|
|
1237
1243
|
error: string;
|
|
1238
1244
|
address?: boolean;
|
|
@@ -1339,12 +1345,32 @@ export interface ConsoleHostFirstRunZoneProps {
|
|
|
1339
1345
|
* person where they were.
|
|
1340
1346
|
*/
|
|
1341
1347
|
chooseStarter?: () => void;
|
|
1348
|
+
/**
|
|
1349
|
+
* An AI starter the person picked in "Choose your starter" (AGL-4049), with
|
|
1350
|
+
* {@link pendingSite}: the widget's questions open filled in from it, every
|
|
1351
|
+
* answer the person's to change, and {@link ConsolePendingSite.create} is
|
|
1352
|
+
* handed its `choice` so the site records it. Absent, the questions start
|
|
1353
|
+
* empty, as ever. Nothing is created by it.
|
|
1354
|
+
*/
|
|
1355
|
+
starter?: ConsoleHostFirstRunStarter;
|
|
1342
1356
|
/**
|
|
1343
1357
|
* Present when staff build the site for a client (AGL-3958), from the
|
|
1344
1358
|
* one-screen build. See {@link ConsoleClientBuildStart}.
|
|
1345
1359
|
*/
|
|
1346
1360
|
clientBuild?: ConsoleClientBuildStart;
|
|
1347
1361
|
}
|
|
1362
|
+
/**
|
|
1363
|
+
* An AI starter as the `hostFirstRun` zone hands it over (AGL-4049): the
|
|
1364
|
+
* listing picked, its name as the picker showed it, and the answers it
|
|
1365
|
+
* carries, as its listing stores them — an AI starter is answers and a look,
|
|
1366
|
+
* never a copy of pages. Which of them a widget asks is the widget's to map;
|
|
1367
|
+
* the core does not read them.
|
|
1368
|
+
*/
|
|
1369
|
+
export interface ConsoleHostFirstRunStarter {
|
|
1370
|
+
choice: SiteStarterChoice;
|
|
1371
|
+
displayName: string;
|
|
1372
|
+
answers: Record<string, unknown>;
|
|
1373
|
+
}
|
|
1348
1374
|
/**
|
|
1349
1375
|
* A guided start for a site staff are building for a client (AGL-3958).
|
|
1350
1376
|
*
|
|
@@ -1594,8 +1620,16 @@ export interface ConsoleHostStarterPickerZoneProps {
|
|
|
1594
1620
|
* same listing once the widget has read a pick the dialog restored through
|
|
1595
1621
|
* {@link selected}, and with `owned: true` once the widget learns the
|
|
1596
1622
|
* workspace owns it.
|
|
1623
|
+
*
|
|
1624
|
+
* For an AI starter (AGL-4049), called once more with `extra.answers` when
|
|
1625
|
+
* the widget has read the answers it carries, which only a starter the
|
|
1626
|
+
* workspace may use hands over: the dialog continues to the questions with
|
|
1627
|
+
* them. A priced AI starter not yet bought is handed back unowned, with
|
|
1628
|
+
* none.
|
|
1597
1629
|
*/
|
|
1598
|
-
onPick: (pick: SiteStarterSummary
|
|
1630
|
+
onPick: (pick: SiteStarterSummary, extra?: {
|
|
1631
|
+
answers?: Record<string, unknown>;
|
|
1632
|
+
}) => void;
|
|
1599
1633
|
/** Reports whether the widget drew a picker; `false` gives the dialog its own default. */
|
|
1600
1634
|
ready: (shown: boolean) => void;
|
|
1601
1635
|
/**
|