@aglyn/aglyn 1.0.0-beta.285 → 1.0.0-beta.288
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-acquisition.d.ts +14 -0
- package/src/lib/app-utils/account-acquisition.js +19 -0
- package/src/lib/app-utils/account-acquisition.js.map +1 -1
- package/src/lib/app-utils/api-plugins.d.ts +25 -0
- package/src/lib/app-utils/api-plugins.js +13 -0
- package/src/lib/app-utils/api-plugins.js.map +1 -1
- package/src/lib/app-utils/business-profile-values.d.ts +15 -3
- package/src/lib/app-utils/business-profile-values.js +19 -3
- package/src/lib/app-utils/business-profile-values.js.map +1 -1
- package/src/lib/app-utils/business-profile.d.ts +5 -3
- package/src/lib/app-utils/business-profile.js +2 -1
- package/src/lib/app-utils/business-profile.js.map +1 -1
- package/src/lib/app-utils/contacts.d.ts +11 -0
- package/src/lib/app-utils/contacts.js +18 -0
- package/src/lib/app-utils/contacts.js.map +1 -1
- package/src/lib/app-utils/docs-deflection.d.ts +1 -1
- package/src/lib/app-utils/docs-deflection.js +17 -0
- package/src/lib/app-utils/docs-deflection.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +5 -2
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +9 -9
- package/src/lib/app-utils/docs-help.generated.js +20 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +168 -63
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/docs-retrieval.d.ts +17 -0
- package/src/lib/app-utils/docs-retrieval.js +28 -0
- package/src/lib/app-utils/docs-retrieval.js.map +1 -1
- package/src/lib/app-utils/host-naming.d.ts +2 -1
- package/src/lib/app-utils/host-naming.js +65 -2
- package/src/lib/app-utils/host-naming.js.map +1 -1
- package/src/lib/app-utils/host-permissions.d.ts +159 -3
- package/src/lib/app-utils/host-permissions.js +384 -12
- package/src/lib/app-utils/host-permissions.js.map +1 -1
- package/src/lib/app-utils/org-permission-editor.d.ts +128 -1
- package/src/lib/app-utils/org-permission-editor.js +176 -1
- package/src/lib/app-utils/org-permission-editor.js.map +1 -1
- package/src/lib/app-utils/org-permissions.d.ts +62 -1
- package/src/lib/app-utils/org-permissions.js +55 -5
- package/src/lib/app-utils/org-permissions.js.map +1 -1
- package/src/lib/app-utils/org-role-grants.d.ts +28 -0
- package/src/lib/app-utils/org-role-grants.js +180 -2
- package/src/lib/app-utils/org-role-grants.js.map +1 -1
- package/src/lib/app-utils/org-roles.js +11 -2
- package/src/lib/app-utils/org-roles.js.map +1 -1
- package/src/lib/app-utils/redirect-query.d.ts +39 -0
- package/src/lib/app-utils/redirect-query.js +57 -0
- package/src/lib/app-utils/redirect-query.js.map +1 -0
- package/src/lib/app-utils/release-flags.js +2 -2
- package/src/lib/app-utils/release-flags.js.map +1 -1
- package/src/lib/app-utils/site-starter-links.d.ts +42 -0
- package/src/lib/app-utils/site-starter-links.js +79 -0
- package/src/lib/app-utils/site-starter-links.js.map +1 -0
- package/src/lib/app-utils/site-starter-nodes.d.ts +91 -0
- package/src/lib/app-utils/site-starter-nodes.js +210 -0
- package/src/lib/app-utils/site-starter-nodes.js.map +1 -0
- package/src/lib/app-utils/site-starter-theme.d.ts +90 -0
- package/src/lib/app-utils/site-starter-theme.js +241 -0
- package/src/lib/app-utils/site-starter-theme.js.map +1 -0
- package/src/lib/app-utils/webhook-delivery.js +6 -1
- package/src/lib/app-utils/webhook-delivery.js.map +1 -1
- package/src/lib/foundation/definitions/organization.types.d.ts +12 -2
- package/src/lib/foundation/definitions/organization.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.js +39 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-closure-notices.d.ts +88 -0
- package/src/lib/plugin-manager/plugin-closure-notices.js +123 -0
- package/src/lib/plugin-manager/plugin-closure-notices.js.map +1 -0
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +15 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js +24 -1
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-customer-display-verifier.d.ts +77 -0
- package/src/lib/plugin-manager/plugin-customer-display-verifier.js +74 -0
- package/src/lib/plugin-manager/plugin-customer-display-verifier.js.map +1 -0
- package/src/lib/plugin-manager/plugin-customer-display.d.ts +135 -0
- package/src/lib/plugin-manager/plugin-customer-display.js +189 -0
- package/src/lib/plugin-manager/plugin-customer-display.js.map +1 -0
- package/src/lib/plugin-manager/plugin-entitlements.d.ts +59 -0
- package/src/lib/plugin-manager/plugin-entitlements.js +32 -3
- package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
- package/src/lib/plugin-manager/plugin-events.d.ts +45 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-permissions.d.ts +74 -0
- package/src/lib/plugin-manager/plugin-permissions.js +91 -1
- package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-starters.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -1
- package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +87 -4
- package/src/lib/plugin-manager/plugin-tax-return-sources.js +155 -35
- package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type PortableDefinitionNodes } from './node-definition-sanitizer';
|
|
18
|
+
/**
|
|
19
|
+
* What a site starter's pages and layout may hold (AGL-4049), and the one
|
|
20
|
+
* pass that makes them so.
|
|
21
|
+
*
|
|
22
|
+
* A starter is a whole site, so it has to carry what a real site is made of:
|
|
23
|
+
* the grids, cards, accordions and icons the default site is built from and
|
|
24
|
+
* the AI site build lays out, with their styles and their interactions. The
|
|
25
|
+
* portable rule every other artifact is published under keeps none of that —
|
|
26
|
+
* it is for a component dropped into someone else's page — so a starter gets
|
|
27
|
+
* its own allowlist here, a superset of the portable one, and keeps `sx` and
|
|
28
|
+
* `interactions` once they are checked. Nothing else is widened: the template,
|
|
29
|
+
* layout and component doors still publish under the portable rule.
|
|
30
|
+
*
|
|
31
|
+
* Kept safe the same way, plus two checks of its own:
|
|
32
|
+
* - only the component ids below, nothing bound to the source site's data
|
|
33
|
+
* (content collections, datasets) or to another plugin;
|
|
34
|
+
* - the portable prop hardening (no `on*`, no `dangerouslySetInnerHTML`, an
|
|
35
|
+
* `href`/`src` only to a safe address) and the size cap;
|
|
36
|
+
* - a style may load an image only from where an image `src` may, and never
|
|
37
|
+
* from a protocol-relative address;
|
|
38
|
+
* - an interaction may only scroll, show, hide, open, close, toggle a class,
|
|
39
|
+
* stick the navigation or play a video: no script, markup, redirect or
|
|
40
|
+
* tracking rides a starter onto the sites created from it.
|
|
41
|
+
*
|
|
42
|
+
* The publish door runs every page and the layout through
|
|
43
|
+
* {@link sanitizeSiteStarterNodes}; the first-party seed holds its pages to the
|
|
44
|
+
* same pass, so a seeded and a published starter store one shape. Pure.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* The component ids a starter's pages may hold: the portable allowlist, what
|
|
48
|
+
* `default-site.ts` builds, and the presentational elements the AI site build
|
|
49
|
+
* lays out (its palette's page surface, less anything bound to a site's data).
|
|
50
|
+
*/
|
|
51
|
+
export declare const SITE_STARTER_COMPONENT_IDS: readonly string[];
|
|
52
|
+
/** The node marking where a page's content grafts into its layout. */
|
|
53
|
+
export declare const SITE_STARTER_LAYOUT_SLOT_ID = "layoutSlot";
|
|
54
|
+
/** A starter's layout may hold the same, plus the slot its pages render in. */
|
|
55
|
+
export declare const SITE_STARTER_LAYOUT_COMPONENT_IDS: readonly string[];
|
|
56
|
+
/**
|
|
57
|
+
* The interaction steps a starter may carry: each acts on the page it is on
|
|
58
|
+
* and nothing else. Scripts, markup, redirects, attributes, alerts, popups and
|
|
59
|
+
* analytics are left out, as is every step a plugin declares.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SITE_STARTER_INTERACTION_STEP_TYPES: ReadonlySet<string>;
|
|
62
|
+
/** A starter's node as stored: a portable node, with its checked styles and interactions. */
|
|
63
|
+
export type SiteStarterNodes = Record<string, PortableDefinitionNodes[string] & {
|
|
64
|
+
sx?: unknown;
|
|
65
|
+
interactions?: unknown[];
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* A starter page's or layout's nodes as a starter stores them, or why they
|
|
69
|
+
* cannot be: the portable pass under {@link SITE_STARTER_COMPONENT_IDS} (or
|
|
70
|
+
* the layout list), then each kept node's checked `sx` and `interactions`
|
|
71
|
+
* put back. Callers decode stored nodes first (`decodeStoredNodes`).
|
|
72
|
+
*/
|
|
73
|
+
export declare function sanitizeSiteStarterNodes(definition: {
|
|
74
|
+
rootId: string;
|
|
75
|
+
nodes: Record<string, any>;
|
|
76
|
+
}, options?: {
|
|
77
|
+
/** A layout: the slot its pages render in is allowed too. */
|
|
78
|
+
layout?: boolean;
|
|
79
|
+
/** The properties a layout's tree binds to, as `sanitizePortableDefinition` takes them. */
|
|
80
|
+
declaredProps?: ReadonlyArray<{
|
|
81
|
+
name?: unknown;
|
|
82
|
+
type?: unknown;
|
|
83
|
+
}>;
|
|
84
|
+
}): {
|
|
85
|
+
ok: true;
|
|
86
|
+
rootId: string;
|
|
87
|
+
nodes: SiteStarterNodes;
|
|
88
|
+
} | {
|
|
89
|
+
ok: false;
|
|
90
|
+
error: string;
|
|
91
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
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 { NODE_MAX_INTERACTIONS } from "./node-interactions.js";
|
|
18
|
+
import { PORTABLE_COMPONENT_ID_ALLOWLIST, PORTABLE_DEFINITION_MAX_BYTES, sanitizePortableDefinition } from "./node-definition-sanitizer.js";
|
|
19
|
+
import { isSafeNodeUrl } from "./node-url-policy.js";
|
|
20
|
+
import { CLIENT_INTERACTION_STEP_LABELS } from "./site-interactions.js";
|
|
21
|
+
/**
|
|
22
|
+
* What a site starter's pages and layout may hold (AGL-4049), and the one
|
|
23
|
+
* pass that makes them so.
|
|
24
|
+
*
|
|
25
|
+
* A starter is a whole site, so it has to carry what a real site is made of:
|
|
26
|
+
* the grids, cards, accordions and icons the default site is built from and
|
|
27
|
+
* the AI site build lays out, with their styles and their interactions. The
|
|
28
|
+
* portable rule every other artifact is published under keeps none of that —
|
|
29
|
+
* it is for a component dropped into someone else's page — so a starter gets
|
|
30
|
+
* its own allowlist here, a superset of the portable one, and keeps `sx` and
|
|
31
|
+
* `interactions` once they are checked. Nothing else is widened: the template,
|
|
32
|
+
* layout and component doors still publish under the portable rule.
|
|
33
|
+
*
|
|
34
|
+
* Kept safe the same way, plus two checks of its own:
|
|
35
|
+
* - only the component ids below, nothing bound to the source site's data
|
|
36
|
+
* (content collections, datasets) or to another plugin;
|
|
37
|
+
* - the portable prop hardening (no `on*`, no `dangerouslySetInnerHTML`, an
|
|
38
|
+
* `href`/`src` only to a safe address) and the size cap;
|
|
39
|
+
* - a style may load an image only from where an image `src` may, and never
|
|
40
|
+
* from a protocol-relative address;
|
|
41
|
+
* - an interaction may only scroll, show, hide, open, close, toggle a class,
|
|
42
|
+
* stick the navigation or play a video: no script, markup, redirect or
|
|
43
|
+
* tracking rides a starter onto the sites created from it.
|
|
44
|
+
*
|
|
45
|
+
* The publish door runs every page and the layout through
|
|
46
|
+
* {@link sanitizeSiteStarterNodes}; the first-party seed holds its pages to the
|
|
47
|
+
* same pass, so a seeded and a published starter store one shape. Pure.
|
|
48
|
+
*/ /**
|
|
49
|
+
* The component ids a starter's pages may hold: the portable allowlist, what
|
|
50
|
+
* `default-site.ts` builds, and the presentational elements the AI site build
|
|
51
|
+
* lays out (its palette's page surface, less anything bound to a site's data).
|
|
52
|
+
*/ export const SITE_STARTER_COMPONENT_IDS = [
|
|
53
|
+
...new Set([
|
|
54
|
+
...PORTABLE_COMPONENT_ID_ALLOWLIST,
|
|
55
|
+
// The default site (`default-site.ts`).
|
|
56
|
+
'icon',
|
|
57
|
+
'muiAccordion',
|
|
58
|
+
'muiAccordionDetails',
|
|
59
|
+
'muiAccordionSummary',
|
|
60
|
+
'muiCard',
|
|
61
|
+
'muiCardContent',
|
|
62
|
+
'muiGrid',
|
|
63
|
+
'themeModeSwitcher',
|
|
64
|
+
// The AI site build's presentational elements.
|
|
65
|
+
'dataTable',
|
|
66
|
+
'muiBox',
|
|
67
|
+
'muiBreadcrumbs',
|
|
68
|
+
'muiCardActions',
|
|
69
|
+
'muiCardHeader',
|
|
70
|
+
'muiImageList',
|
|
71
|
+
'muiImageListItem',
|
|
72
|
+
'video'
|
|
73
|
+
])
|
|
74
|
+
].sort();
|
|
75
|
+
/** The node marking where a page's content grafts into its layout. */ export const SITE_STARTER_LAYOUT_SLOT_ID = 'layoutSlot';
|
|
76
|
+
/** A starter's layout may hold the same, plus the slot its pages render in. */ export const SITE_STARTER_LAYOUT_COMPONENT_IDS = [
|
|
77
|
+
...SITE_STARTER_COMPONENT_IDS,
|
|
78
|
+
SITE_STARTER_LAYOUT_SLOT_ID
|
|
79
|
+
].sort();
|
|
80
|
+
/**
|
|
81
|
+
* The interaction steps a starter may carry: each acts on the page it is on
|
|
82
|
+
* and nothing else. Scripts, markup, redirects, attributes, alerts, popups and
|
|
83
|
+
* analytics are left out, as is every step a plugin declares.
|
|
84
|
+
*/ export const SITE_STARTER_INTERACTION_STEP_TYPES = new Set([
|
|
85
|
+
'addClass',
|
|
86
|
+
'closeDrawer',
|
|
87
|
+
'closeMenu',
|
|
88
|
+
'hideElement',
|
|
89
|
+
'openDrawer',
|
|
90
|
+
'openMenu',
|
|
91
|
+
'playVideo',
|
|
92
|
+
'removeClass',
|
|
93
|
+
'scrollTo',
|
|
94
|
+
'showElement',
|
|
95
|
+
'stickyNav',
|
|
96
|
+
'toggleClass',
|
|
97
|
+
'toggleDrawer',
|
|
98
|
+
'toggleElement',
|
|
99
|
+
'toggleMenu'
|
|
100
|
+
]);
|
|
101
|
+
const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
|
|
102
|
+
/** How a refusal names a node: its component, then its id. */ const nodeName = (id, node)=>{
|
|
103
|
+
var _ref;
|
|
104
|
+
return `the ${String((_ref = node == null ? void 0 : node.componentId) != null ? _ref : 'element')} "${id}"`;
|
|
105
|
+
};
|
|
106
|
+
const stepLabel = (type)=>{
|
|
107
|
+
var _CLIENT_INTERACTION_STEP_LABELS_type;
|
|
108
|
+
return (_CLIENT_INTERACTION_STEP_LABELS_type = CLIENT_INTERACTION_STEP_LABELS[type]) != null ? _CLIENT_INTERACTION_STEP_LABELS_type : type;
|
|
109
|
+
};
|
|
110
|
+
/** Every `url(…)` address in a style value. */ const styleUrls = (value)=>[
|
|
111
|
+
...value.matchAll(/url\(\s*(['"]?)(.*?)\1\s*\)/gi)
|
|
112
|
+
].map((match)=>match[2].trim());
|
|
113
|
+
/** Why a node's styles cannot ride a starter, or null when they can. */ function styleRefusal(id, node, sx, depth = 0) {
|
|
114
|
+
if (depth > 12) return `The styles on ${nodeName(id, node)} are nested too deeply`;
|
|
115
|
+
if (typeof sx === 'string') {
|
|
116
|
+
if (/url\(/i.test(sx) && !styleUrls(sx).length) {
|
|
117
|
+
return `A style on ${nodeName(id, node)} loads an address a starter cannot read`;
|
|
118
|
+
}
|
|
119
|
+
for (const address of styleUrls(sx)){
|
|
120
|
+
if (address.startsWith('//') || !isSafeNodeUrl('src', address)) {
|
|
121
|
+
return `A style on ${nodeName(id, node)} loads "${address.slice(0, 120)}", which a starter cannot carry`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (sx === null || typeof sx === 'number' || typeof sx === 'boolean') return null;
|
|
127
|
+
if (Array.isArray(sx)) {
|
|
128
|
+
for (const entry of sx){
|
|
129
|
+
const refusal = styleRefusal(id, node, entry, depth + 1);
|
|
130
|
+
if (refusal) return refusal;
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (isRecord(sx)) {
|
|
135
|
+
for (const entry of Object.values(sx)){
|
|
136
|
+
const refusal = styleRefusal(id, node, entry, depth + 1);
|
|
137
|
+
if (refusal) return refusal;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return `The styles on ${nodeName(id, node)} hold something that is not a style`;
|
|
142
|
+
}
|
|
143
|
+
/** Why a node's interactions cannot ride a starter, or null when they can. */ function interactionsRefusal(id, node, interactions) {
|
|
144
|
+
if (!Array.isArray(interactions)) return `The interactions on ${nodeName(id, node)} are not a list`;
|
|
145
|
+
if (interactions.length > NODE_MAX_INTERACTIONS) {
|
|
146
|
+
return `${nodeName(id, node)} has more than ${NODE_MAX_INTERACTIONS} interactions`;
|
|
147
|
+
}
|
|
148
|
+
for (const interaction of interactions){
|
|
149
|
+
if (!isRecord(interaction) || !Array.isArray(interaction['steps'])) {
|
|
150
|
+
return `An interaction on ${nodeName(id, node)} is not one`;
|
|
151
|
+
}
|
|
152
|
+
for (const step of interaction['steps']){
|
|
153
|
+
const type = isRecord(step) && typeof step['type'] === 'string' ? step['type'] : '';
|
|
154
|
+
if (!SITE_STARTER_INTERACTION_STEP_TYPES.has(type)) {
|
|
155
|
+
return `An interaction on ${nodeName(id, node)} runs "${stepLabel(type || 'an unknown step')}", ` + 'which a starter cannot carry: a starter brings no scripts, markup, redirects or ' + 'tracking to the sites created from it';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/** A JSON copy, so a stored starter never shares an object with its source. */ const copied = (value)=>JSON.parse(JSON.stringify(value));
|
|
162
|
+
/**
|
|
163
|
+
* A starter page's or layout's nodes as a starter stores them, or why they
|
|
164
|
+
* cannot be: the portable pass under {@link SITE_STARTER_COMPONENT_IDS} (or
|
|
165
|
+
* the layout list), then each kept node's checked `sx` and `interactions`
|
|
166
|
+
* put back. Callers decode stored nodes first (`decodeStoredNodes`).
|
|
167
|
+
*/ export function sanitizeSiteStarterNodes(definition, options = {}) {
|
|
168
|
+
const portable = sanitizePortableDefinition(definition, _extends({
|
|
169
|
+
componentIds: options.layout ? SITE_STARTER_LAYOUT_COMPONENT_IDS : SITE_STARTER_COMPONENT_IDS
|
|
170
|
+
}, options.declaredProps && {
|
|
171
|
+
declaredProps: options.declaredProps
|
|
172
|
+
}));
|
|
173
|
+
if (portable.ok === false) return portable;
|
|
174
|
+
const nodes = {};
|
|
175
|
+
for (const [id, kept] of Object.entries(portable.nodes)){
|
|
176
|
+
var _definition_nodes_id;
|
|
177
|
+
const source = (_definition_nodes_id = definition.nodes[id]) != null ? _definition_nodes_id : {};
|
|
178
|
+
const node = _extends({}, kept);
|
|
179
|
+
if (source.sx !== undefined && source.sx !== null) {
|
|
180
|
+
const refusal = styleRefusal(id, source, source.sx);
|
|
181
|
+
if (refusal) return {
|
|
182
|
+
ok: false,
|
|
183
|
+
error: refusal
|
|
184
|
+
};
|
|
185
|
+
node['sx'] = copied(source.sx);
|
|
186
|
+
}
|
|
187
|
+
if (source.interactions !== undefined && source.interactions !== null) {
|
|
188
|
+
const refusal = interactionsRefusal(id, source, source.interactions);
|
|
189
|
+
if (refusal) return {
|
|
190
|
+
ok: false,
|
|
191
|
+
error: refusal
|
|
192
|
+
};
|
|
193
|
+
if (source.interactions.length) node['interactions'] = copied(source.interactions);
|
|
194
|
+
}
|
|
195
|
+
nodes[id] = node;
|
|
196
|
+
}
|
|
197
|
+
if (JSON.stringify(nodes).length > PORTABLE_DEFINITION_MAX_BYTES) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
error: 'Definition is too large to publish'
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
rootId: portable.rootId,
|
|
206
|
+
nodes
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
//# sourceMappingURL=site-starter-nodes.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { HostTheme } from '@aglyn/shared-data-types';
|
|
18
|
+
import type { ConsoleThemePreset } from '../plugin-manager/feature-plugins';
|
|
19
|
+
import type { SiteStarterTheme } from './site-starter';
|
|
20
|
+
import { type ThemeLibraryEntryWrite } from './theme-library';
|
|
21
|
+
/**
|
|
22
|
+
* A site starter's theme on the site it creates (AGL-4049).
|
|
23
|
+
*
|
|
24
|
+
* A starter names its look one of two ways (`SiteStarterTheme`): a built-in
|
|
25
|
+
* theme with its brand colors and font pairing over it, or a full theme of
|
|
26
|
+
* its author's own. Either way the new site's theme is stored the way a
|
|
27
|
+
* person's own pick is: the built-in picked through the theme library with
|
|
28
|
+
* the starter's colors and fonts as the override over it, or the full theme
|
|
29
|
+
* installed into the library as a marketplace theme is. So the Theme page
|
|
30
|
+
* names the base a site came from, and every edit there starts from it.
|
|
31
|
+
*
|
|
32
|
+
* Planned against an EMPTY host, because the site does not exist yet: the
|
|
33
|
+
* create writes these fields in the same `set` that makes the host, and the
|
|
34
|
+
* library entries beside it, in one transaction.
|
|
35
|
+
*
|
|
36
|
+
* Pure: no I/O. The caller passes the built-in themes it has registered.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* The palette keys a built-in starter's brand sets: its three brand colors.
|
|
40
|
+
* The status colors (error, warning, info, success) stay the base theme's,
|
|
41
|
+
* since they mean the same thing on every site, and surfaces and text are the
|
|
42
|
+
* base's design.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SITE_STARTER_BRAND_KEYS: readonly ["primary", "secondary", "tertiary"];
|
|
45
|
+
/** The built-in theme a starter's `base` names, as the themes plugin registers it. */
|
|
46
|
+
export declare const siteStarterPresetId: (base: string) => string;
|
|
47
|
+
type BuiltinStarterTheme = Extract<SiteStarterTheme, {
|
|
48
|
+
kind: 'builtin';
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* A built-in base with the starter's brand colors and font pairing over it,
|
|
52
|
+
* modeled on the AI start's look (`aiSiteTheme`) but driven by the starter's
|
|
53
|
+
* `brand` and `fonts` rather than a style. Everything the base sets beyond
|
|
54
|
+
* those stays the base's: its status colors, surfaces, shapes and component
|
|
55
|
+
* styles. A brand with no `dark` map colors both schemes with `light`; a
|
|
56
|
+
* `fonts` of `null`, or a pairing id the platform does not have, keeps the
|
|
57
|
+
* base's own fonts.
|
|
58
|
+
*/
|
|
59
|
+
export declare function siteStarterTheme(theme: BuiltinStarterTheme, base: HostTheme): HostTheme;
|
|
60
|
+
/** What a new site's host document and theme library start with. */
|
|
61
|
+
export interface SiteStarterThemePlan {
|
|
62
|
+
/** Host fields, written in the `set` that creates the host. */
|
|
63
|
+
host: Record<string, unknown>;
|
|
64
|
+
/** Library entries to create under `hosts/{id}/themes`. */
|
|
65
|
+
entries: ThemeLibraryEntryWrite[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The host fields and library entries a new site is born with for a
|
|
69
|
+
* starter's theme, planned against an empty host through the same planner
|
|
70
|
+
* the theme library runs (`planThemeLibraryAction`):
|
|
71
|
+
*
|
|
72
|
+
* - a full theme is installed, as the marketplace installs a theme, its
|
|
73
|
+
* entry named after the starter and its provenance the starter's listing;
|
|
74
|
+
* - a built-in theme is picked as a preset, with the brand and fonts as the
|
|
75
|
+
* override over it;
|
|
76
|
+
* - the `starter` base, or a base this deployment has no built-in for, is
|
|
77
|
+
* the theme every new site is born with (`DEFAULT_SITE_THEME`), with the
|
|
78
|
+
* brand and fonts as the override over it, and no library entry.
|
|
79
|
+
*/
|
|
80
|
+
export declare function planSiteStarterTheme(input: {
|
|
81
|
+
theme: SiteStarterTheme;
|
|
82
|
+
presets: readonly ConsoleThemePreset[];
|
|
83
|
+
listing: {
|
|
84
|
+
listingId: string;
|
|
85
|
+
version: string | number;
|
|
86
|
+
displayName: string;
|
|
87
|
+
};
|
|
88
|
+
newId: () => string;
|
|
89
|
+
}): SiteStarterThemePlan;
|
|
90
|
+
export {};
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { writeThemeFonts } from "@aglyn/shared-util-tools/theme-fonts";
|
|
18
|
+
import { diffOverride, overrideWriteValue } from "./artifact-overrides.js";
|
|
19
|
+
import { DEFAULT_SITE_THEME } from "./default-site.js";
|
|
20
|
+
import { siteFontPairing } from "./site-font-pairings.js";
|
|
21
|
+
import { themeArtifactContent } from "./site-theme.js";
|
|
22
|
+
import { planThemeLibraryAction, THEME_FIELD_DELETE } from "./theme-library.js";
|
|
23
|
+
/**
|
|
24
|
+
* A site starter's theme on the site it creates (AGL-4049).
|
|
25
|
+
*
|
|
26
|
+
* A starter names its look one of two ways (`SiteStarterTheme`): a built-in
|
|
27
|
+
* theme with its brand colors and font pairing over it, or a full theme of
|
|
28
|
+
* its author's own. Either way the new site's theme is stored the way a
|
|
29
|
+
* person's own pick is: the built-in picked through the theme library with
|
|
30
|
+
* the starter's colors and fonts as the override over it, or the full theme
|
|
31
|
+
* installed into the library as a marketplace theme is. So the Theme page
|
|
32
|
+
* names the base a site came from, and every edit there starts from it.
|
|
33
|
+
*
|
|
34
|
+
* Planned against an EMPTY host, because the site does not exist yet: the
|
|
35
|
+
* create writes these fields in the same `set` that makes the host, and the
|
|
36
|
+
* library entries beside it, in one transaction.
|
|
37
|
+
*
|
|
38
|
+
* Pure: no I/O. The caller passes the built-in themes it has registered.
|
|
39
|
+
*/ /**
|
|
40
|
+
* The palette keys a built-in starter's brand sets: its three brand colors.
|
|
41
|
+
* The status colors (error, warning, info, success) stay the base theme's,
|
|
42
|
+
* since they mean the same thing on every site, and surfaces and text are the
|
|
43
|
+
* base's design.
|
|
44
|
+
*/ export const SITE_STARTER_BRAND_KEYS = [
|
|
45
|
+
'primary',
|
|
46
|
+
'secondary',
|
|
47
|
+
'tertiary'
|
|
48
|
+
];
|
|
49
|
+
/** The built-in theme a starter's `base` names, as the themes plugin registers it. */ export const siteStarterPresetId = (base)=>`theme-presets.${base}`;
|
|
50
|
+
const isRecord = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
|
|
51
|
+
/**
|
|
52
|
+
* One scheme's colors with the brand over them, each brand color written the
|
|
53
|
+
* way the theme editor writes a palette color (`writeThemeColor`): a record of
|
|
54
|
+
* `main` alone, so MUI derives its light, dark and contrast text from it, and
|
|
55
|
+
* the base's tint for that color dropped, since it was a wash of the old one.
|
|
56
|
+
*/ function brandScheme(colors, brand) {
|
|
57
|
+
let next = colors;
|
|
58
|
+
for (const key of SITE_STARTER_BRAND_KEYS){
|
|
59
|
+
const hex = brand == null ? void 0 : brand[key];
|
|
60
|
+
if (typeof hex !== 'string' || !hex.trim()) continue;
|
|
61
|
+
next = _extends({}, next, {
|
|
62
|
+
[key]: {
|
|
63
|
+
main: hex.trim()
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const tint = next.tint;
|
|
67
|
+
if (tint && key in tint) {
|
|
68
|
+
const rest = _extends({}, tint);
|
|
69
|
+
delete rest[key];
|
|
70
|
+
if (Object.keys(rest).length) next.tint = rest;
|
|
71
|
+
else delete next.tint;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A built-in base with the starter's brand colors and font pairing over it,
|
|
78
|
+
* modeled on the AI start's look (`aiSiteTheme`) but driven by the starter's
|
|
79
|
+
* `brand` and `fonts` rather than a style. Everything the base sets beyond
|
|
80
|
+
* those stays the base's: its status colors, surfaces, shapes and component
|
|
81
|
+
* styles. A brand with no `dark` map colors both schemes with `light`; a
|
|
82
|
+
* `fonts` of `null`, or a pairing id the platform does not have, keeps the
|
|
83
|
+
* base's own fonts.
|
|
84
|
+
*/ export function siteStarterTheme(theme, base) {
|
|
85
|
+
var _next_fonts;
|
|
86
|
+
const brand = isRecord(theme == null ? void 0 : theme.brand) ? theme.brand : {
|
|
87
|
+
light: {}
|
|
88
|
+
};
|
|
89
|
+
const light = isRecord(brand.light) ? brand.light : {};
|
|
90
|
+
const dark = isRecord(brand.dark) ? brand.dark : light;
|
|
91
|
+
const brandFor = {
|
|
92
|
+
light,
|
|
93
|
+
dark
|
|
94
|
+
};
|
|
95
|
+
const colorSchemes = _extends({}, base.colorSchemes);
|
|
96
|
+
for (const scheme of [
|
|
97
|
+
'light',
|
|
98
|
+
'dark'
|
|
99
|
+
]){
|
|
100
|
+
var _base_colorSchemes;
|
|
101
|
+
const colors = brandScheme((_base_colorSchemes = base.colorSchemes) == null ? void 0 : _base_colorSchemes[scheme], brandFor[scheme]);
|
|
102
|
+
if (colors) colorSchemes[scheme] = colors;
|
|
103
|
+
}
|
|
104
|
+
let next = _extends({}, base);
|
|
105
|
+
if (Object.keys(colorSchemes).length) next.colorSchemes = colorSchemes;
|
|
106
|
+
const pairing = siteFontPairing(theme == null ? void 0 : theme.fonts);
|
|
107
|
+
if (!pairing) return next;
|
|
108
|
+
// The same write the AI site look makes for a pairing: the body family,
|
|
109
|
+
// and the heading family where it differs.
|
|
110
|
+
const sameFamily = pairing.heading.family === pairing.body.family;
|
|
111
|
+
next = writeThemeFonts(next, {
|
|
112
|
+
body: {
|
|
113
|
+
family: pairing.body.family,
|
|
114
|
+
category: pairing.body.category,
|
|
115
|
+
weights: [
|
|
116
|
+
...pairing.body.weights
|
|
117
|
+
],
|
|
118
|
+
source: 'google'
|
|
119
|
+
},
|
|
120
|
+
heading: sameFamily ? null : {
|
|
121
|
+
family: pairing.heading.family,
|
|
122
|
+
category: pairing.heading.category,
|
|
123
|
+
weights: [
|
|
124
|
+
...new Set([
|
|
125
|
+
pairing.heading.weight,
|
|
126
|
+
pairing.heading.sub
|
|
127
|
+
])
|
|
128
|
+
].sort((a, b)=>a - b),
|
|
129
|
+
source: 'google'
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
// A pairing whose heading is the body family loads the heading weights too.
|
|
133
|
+
if (sameFamily && ((_next_fonts = next.fonts) == null ? void 0 : _next_fonts[0])) {
|
|
134
|
+
var _next_fonts__weights;
|
|
135
|
+
next.fonts = [
|
|
136
|
+
_extends({}, next.fonts[0], {
|
|
137
|
+
weights: [
|
|
138
|
+
...new Set([
|
|
139
|
+
...(_next_fonts__weights = next.fonts[0].weights) != null ? _next_fonts__weights : [],
|
|
140
|
+
pairing.heading.weight,
|
|
141
|
+
pairing.heading.sub
|
|
142
|
+
])
|
|
143
|
+
].sort((a, b)=>a - b)
|
|
144
|
+
}),
|
|
145
|
+
...next.fonts.slice(1)
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
return next;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A plan's host fields as a new document takes them: a field the plan
|
|
152
|
+
* deletes is simply not written, since an empty host has nothing to delete,
|
|
153
|
+
* and an empty override is no field at all.
|
|
154
|
+
*/ function creatable(fields) {
|
|
155
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value])=>value !== THEME_FIELD_DELETE && value !== null));
|
|
156
|
+
}
|
|
157
|
+
/** The override a built-in starter stores over `base`, or `null` when it changes nothing. */ function starterOverride(theme, base) {
|
|
158
|
+
return overrideWriteValue(diffOverride(base, siteStarterTheme(theme, base)), null);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The host fields and library entries a new site is born with for a
|
|
162
|
+
* starter's theme, planned against an empty host through the same planner
|
|
163
|
+
* the theme library runs (`planThemeLibraryAction`):
|
|
164
|
+
*
|
|
165
|
+
* - a full theme is installed, as the marketplace installs a theme, its
|
|
166
|
+
* entry named after the starter and its provenance the starter's listing;
|
|
167
|
+
* - a built-in theme is picked as a preset, with the brand and fonts as the
|
|
168
|
+
* override over it;
|
|
169
|
+
* - the `starter` base, or a base this deployment has no built-in for, is
|
|
170
|
+
* the theme every new site is born with (`DEFAULT_SITE_THEME`), with the
|
|
171
|
+
* brand and fonts as the override over it, and no library entry.
|
|
172
|
+
*/ export function planSiteStarterTheme(input) {
|
|
173
|
+
var _presets_find;
|
|
174
|
+
// `source`, not `listing`: this is the starter's id/version/name, not the
|
|
175
|
+
// listing document the listing write-deny guard partitions (AGL-1361).
|
|
176
|
+
const { theme, presets, listing: source, newId } = input;
|
|
177
|
+
const plan = (action)=>{
|
|
178
|
+
const planned = planThemeLibraryAction({
|
|
179
|
+
host: {},
|
|
180
|
+
entries: {},
|
|
181
|
+
newId,
|
|
182
|
+
action
|
|
183
|
+
});
|
|
184
|
+
return planned.ok === false ? null : planned;
|
|
185
|
+
};
|
|
186
|
+
if ((theme == null ? void 0 : theme.kind) === 'full' && isRecord(theme.theme)) {
|
|
187
|
+
const planned = plan({
|
|
188
|
+
action: 'install',
|
|
189
|
+
listingId: source.listingId,
|
|
190
|
+
name: `${source.displayName} theme`,
|
|
191
|
+
theme: themeArtifactContent(theme.theme),
|
|
192
|
+
installedFrom: {
|
|
193
|
+
listingId: source.listingId,
|
|
194
|
+
version: source.version,
|
|
195
|
+
artifactType: 'siteStarter'
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (planned) return {
|
|
199
|
+
host: creatable(planned.host),
|
|
200
|
+
entries: planned.entries
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const builtin = (theme == null ? void 0 : theme.kind) === 'builtin' ? theme : {
|
|
204
|
+
kind: 'builtin',
|
|
205
|
+
base: 'starter',
|
|
206
|
+
brand: {
|
|
207
|
+
light: {}
|
|
208
|
+
},
|
|
209
|
+
fonts: null
|
|
210
|
+
};
|
|
211
|
+
const preset = builtin.base === 'starter' ? null : (_presets_find = presets.find((entry)=>(entry == null ? void 0 : entry.id) === siteStarterPresetId(builtin.base))) != null ? _presets_find : null;
|
|
212
|
+
if (preset) {
|
|
213
|
+
const base = themeArtifactContent(preset.theme);
|
|
214
|
+
const planned = plan({
|
|
215
|
+
action: 'select',
|
|
216
|
+
target: {
|
|
217
|
+
kind: 'preset',
|
|
218
|
+
id: preset.id,
|
|
219
|
+
name: preset.name,
|
|
220
|
+
theme: base
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
if (planned) {
|
|
224
|
+
return {
|
|
225
|
+
host: creatable(_extends({}, planned.host, {
|
|
226
|
+
themeOverride: starterOverride(builtin, base)
|
|
227
|
+
})),
|
|
228
|
+
entries: planned.entries
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
host: creatable({
|
|
234
|
+
theme: DEFAULT_SITE_THEME,
|
|
235
|
+
themeOverride: starterOverride(builtin, DEFAULT_SITE_THEME)
|
|
236
|
+
}),
|
|
237
|
+
entries: []
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//# sourceMappingURL=site-starter-theme.js.map
|