@aglyn/aglyn 1.0.0-beta.172 → 1.0.0-beta.174

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.
Files changed (39) hide show
  1. package/package.json +10 -10
  2. package/src/lib/app-utils/campaign-membership.d.ts +10 -8
  3. package/src/lib/app-utils/campaign-membership.js +11 -9
  4. package/src/lib/app-utils/campaign-membership.js.map +1 -1
  5. package/src/lib/app-utils/compose-layout-props.d.ts +6 -2
  6. package/src/lib/app-utils/compose-layout-props.js +8 -3
  7. package/src/lib/app-utils/compose-layout-props.js.map +1 -1
  8. package/src/lib/app-utils/compose-reusable-components.d.ts +85 -1
  9. package/src/lib/app-utils/compose-reusable-components.js +137 -11
  10. package/src/lib/app-utils/compose-reusable-components.js.map +1 -1
  11. package/src/lib/app-utils/crm.d.ts +3 -3
  12. package/src/lib/app-utils/crm.js.map +1 -1
  13. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  14. package/src/lib/app-utils/docs-help.generated.js +1 -0
  15. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  16. package/src/lib/app-utils/docs-index.generated.js +11 -4
  17. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  18. package/src/lib/app-utils/element-color-scheme.d.ts +43 -0
  19. package/src/lib/app-utils/element-color-scheme.js +41 -0
  20. package/src/lib/app-utils/element-color-scheme.js.map +1 -0
  21. package/src/lib/app-utils/layout-style-overrides.d.ts +106 -0
  22. package/src/lib/app-utils/layout-style-overrides.js +158 -0
  23. package/src/lib/app-utils/layout-style-overrides.js.map +1 -0
  24. package/src/lib/app-utils/server.d.ts +2 -0
  25. package/src/lib/app-utils/server.js +4 -0
  26. package/src/lib/app-utils/server.js.map +1 -1
  27. package/src/lib/canvas-manager/canvas-manager.d.ts +5 -0
  28. package/src/lib/canvas-manager/canvas-manager.js +7 -0
  29. package/src/lib/canvas-manager/canvas-manager.js.map +1 -1
  30. package/src/lib/foundation/definitions/platform.types.d.ts +9 -0
  31. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  32. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +6 -1
  33. package/src/lib/plugin-manager/first-party-plugins.generated.js +17 -11
  34. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  35. package/src/lib/plugin-manager/plugin-host-collections.d.ts +52 -1
  36. package/src/lib/plugin-manager/plugin-host-collections.js +23 -6
  37. package/src/lib/plugin-manager/plugin-host-collections.js.map +1 -1
  38. package/src/lib/types/nodes.d.ts +12 -0
  39. package/src/lib/types/nodes.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/aglyn",
3
- "version": "1.0.0-beta.172",
3
+ "version": "1.0.0-beta.174",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -33,15 +33,15 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "dependencies": {
36
- "@aglyn/shared-data-enums": "1.0.0-beta.172",
37
- "@aglyn/shared-data-mdi": "1.0.0-beta.172",
38
- "@aglyn/shared-data-types": "1.0.0-beta.172",
39
- "@aglyn/shared-util-email": "1.0.0-beta.172",
40
- "@aglyn/shared-util-http": "1.0.0-beta.172",
41
- "@aglyn/shared-util-logger": "1.0.0-beta.172",
42
- "@aglyn/shared-util-timestamp": "1.0.0-beta.172",
43
- "@aglyn/shared-util-tools": "1.0.0-beta.172",
44
- "@aglyn/shared-util-vendor": "1.0.0-beta.172",
36
+ "@aglyn/shared-data-enums": "1.0.0-beta.174",
37
+ "@aglyn/shared-data-mdi": "1.0.0-beta.174",
38
+ "@aglyn/shared-data-types": "1.0.0-beta.174",
39
+ "@aglyn/shared-util-email": "1.0.0-beta.174",
40
+ "@aglyn/shared-util-http": "1.0.0-beta.174",
41
+ "@aglyn/shared-util-logger": "1.0.0-beta.174",
42
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.174",
43
+ "@aglyn/shared-util-tools": "1.0.0-beta.174",
44
+ "@aglyn/shared-util-vendor": "1.0.0-beta.174",
45
45
  "@data-driven-forms/react-form-renderer": "^4.2.0",
46
46
  "@msgpack/msgpack": "^3.1.3",
47
47
  "@types/unist": "^3.0.3",
@@ -46,8 +46,9 @@ export declare function normalizeCampaignIds(raw: unknown): string[];
46
46
  * The campaigns a HOST resource — a form, a screen — belongs to.
47
47
  *
48
48
  * Host resources carry the field at the top of the document: they belong to
49
- * one site, which is the same site the campaign belongs to, so there is no
50
- * second holder to keep the edge away from.
49
+ * one site, and whoever may read the resource may read which of the org's
50
+ * campaigns it is filed under, so there is no second holder to keep the edge
51
+ * away from.
51
52
  */
52
53
  export declare function readCampaignIds(resource: Record<string, unknown> | null | undefined): string[];
53
54
  /**
@@ -100,12 +101,13 @@ export declare function campaignMembershipUnchanged(stored: readonly string[], s
100
101
  * detach fails the build rather than shipping a campaign whose removal leaves
101
102
  * that collection pointing at nothing.
102
103
  *
103
- * A lead (`hosts/{hostId}/leads/{personKey}`, AGL-3254) carries the field at
104
- * the top of its document like a form does: a lead is one site's own record,
105
- * captured there and worked there, so there is no second holder to keep the
106
- * edge away from. It joins a campaign by hand — the New lead drawer, the CSV
107
- * import, the list's bulk bar — or by being enrolled in a sequence that is
108
- * in one, and it hands the membership to the contact it converts into.
104
+ * `leads` is here for the site rows the lead migration (AGL-3276) has not
105
+ * reached. A live lead is an org row (`orgs/{orgId}/leads/{personKey}`) that
106
+ * carries the field at the top of its document like a form does, and the
107
+ * deletion walks that collection by name beside this list. A lead joins a
108
+ * campaign by hand — the New lead drawer, the CSV import, the list's bulk
109
+ * bar — or by being enrolled in a sequence that is in one, and it hands the
110
+ * membership to the contact it converts into.
109
111
  *
110
112
  * Contacts are deliberately NOT here. They live on the org
111
113
  * (`orgs/{orgId}/contacts`), not the host, and carry the field inside a
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * ## The edge is a field on the RESOURCE, not a list on the campaign
21
21
  *
22
- * A campaign container lives at `hosts/{hostId}/emailCampaigns/{campaignId}`
22
+ * A campaign container lives at `orgs/{orgId}/emailCampaigns/{campaignId}`
23
23
  * and names nothing outside itself. A send joins one by carrying
24
24
  * `emailCampaignId`; every other member joins the same way, through
25
25
  * {@link CAMPAIGN_MEMBERSHIP_FIELD} on its own document.
@@ -98,8 +98,9 @@
98
98
  * The campaigns a HOST resource — a form, a screen — belongs to.
99
99
  *
100
100
  * Host resources carry the field at the top of the document: they belong to
101
- * one site, which is the same site the campaign belongs to, so there is no
102
- * second holder to keep the edge away from.
101
+ * one site, and whoever may read the resource may read which of the org's
102
+ * campaigns it is filed under, so there is no second holder to keep the edge
103
+ * away from.
103
104
  */ export function readCampaignIds(resource) {
104
105
  return normalizeCampaignIds((resource != null ? resource : {})[CAMPAIGN_MEMBERSHIP_FIELD]);
105
106
  }
@@ -164,12 +165,13 @@
164
165
  * detach fails the build rather than shipping a campaign whose removal leaves
165
166
  * that collection pointing at nothing.
166
167
  *
167
- * A lead (`hosts/{hostId}/leads/{personKey}`, AGL-3254) carries the field at
168
- * the top of its document like a form does: a lead is one site's own record,
169
- * captured there and worked there, so there is no second holder to keep the
170
- * edge away from. It joins a campaign by hand — the New lead drawer, the CSV
171
- * import, the list's bulk bar — or by being enrolled in a sequence that is
172
- * in one, and it hands the membership to the contact it converts into.
168
+ * `leads` is here for the site rows the lead migration (AGL-3276) has not
169
+ * reached. A live lead is an org row (`orgs/{orgId}/leads/{personKey}`) that
170
+ * carries the field at the top of its document like a form does, and the
171
+ * deletion walks that collection by name beside this list. A lead joins a
172
+ * campaign by hand — the New lead drawer, the CSV import, the list's bulk
173
+ * bar — or by being enrolled in a sequence that is in one, and it hands the
174
+ * membership to the contact it converts into.
173
175
  *
174
176
  * Contacts are deliberately NOT here. They live on the org
175
177
  * (`orgs/{orgId}/contacts`), not the host, and carry the field inside a
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/campaign-membership.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT ELSE IS IN A CAMPAIGN: the edge between a campaign and the forms,\n * screens and people a push is coordinated across.\n *\n * ## The edge is a field on the RESOURCE, not a list on the campaign\n *\n * A campaign container lives at `hosts/{hostId}/emailCampaigns/{campaignId}`\n * and names nothing outside itself. A send joins one by carrying\n * `emailCampaignId`; every other member joins the same way, through\n * {@link CAMPAIGN_MEMBERSHIP_FIELD} on its own document.\n *\n * Three properties decide it, and all three point the same way:\n *\n * - **Reading is free where it is asked.** A form's own page reads the form\n * document to draw anything at all, so the campaign comes with it. A\n * membership collection under the campaign would make \"which campaign is\n * this in\" a query issued on mount by every resource page, for a fact one\n * field already carries.\n * - **The reduction is already gated this way.** `campaign-manage.ts` clears\n * `emailCampaignId` from every send before it removes a container, so no\n * send is left naming one that is gone. Extending that pass to the\n * membership field is the SAME mechanism; a membership collection would be\n * a second, different deletion story beside it.\n * - **Deleting a MEMBER can leave nothing behind.** The campaign holds no\n * list, so a deleted form takes its own edge with it. This matters most on\n * a contact: an erasure removes the document, and a membership row stored\n * anywhere else would survive it holding the id of a person who asked to\n * be forgotten.\n *\n * ## Plural, because a landing page outlives one campaign\n *\n * The field is an ARRAY. A signup form built for the spring push is the same\n * form the summer push places, and a single-valued field would make the\n * second assignment silently erase the first — a campaign losing a member\n * with nothing on screen to say so. The automation step that has always\n * written this edge (`assignCampaign`) already used `arrayUnion`, so the\n * product has treated it as many-to-many from the start.\n *\n * ## Ids, never names\n *\n * A campaign's name is editable, and every reader here resolves the name from\n * the campaign document at read time. Renaming a campaign therefore moves no\n * membership: the stored value is the container's document id, which nothing\n * rewrites.\n */\n\nimport { contactFacetPath, readContactFacet } from './contacts'\n\n/**\n * The field naming the campaigns a resource belongs to.\n *\n * Not `emailCampaignId`, which is the SEND's field: on a send document the\n * word `campaignId` already means the send's own id — it is what `cid`\n * carries into every unsubscribe footer — so that collection needed a\n * different word. A form, a screen and a contact have no second meaning for\n * it, and the plural says the resource may be in more than one.\n */\nexport const CAMPAIGN_MEMBERSHIP_FIELD = 'campaignIds'\n\n/**\n * How many campaigns one resource may name.\n *\n * A ceiling on the FIELD, not on the product: an array Firestore has to index\n * on every write is not the place to discover that a script has been adding\n * an id a day for a year. Twenty is far past what a person assigns by hand\n * and far short of anything that costs a write.\n */\nexport const CAMPAIGN_MEMBERSHIP_CAP = 20\n\n/**\n * A stored membership array as a clean list of ids.\n *\n * Deduped, trimmed, non-strings dropped, capped. Every reader goes through\n * this because the field is written by three consoles and one automation\n * step, and a surface that trusted the raw value would render `undefined` as\n * a chip the first time one of them wrote a blank.\n */\nexport function normalizeCampaignIds(raw: unknown): string[] {\n if (!Array.isArray(raw)) return []\n const seen: string[] = []\n for (const entry of raw) {\n if (typeof entry !== 'string') continue\n const id = entry.trim()\n if (!id || seen.includes(id)) continue\n seen.push(id)\n if (seen.length >= CAMPAIGN_MEMBERSHIP_CAP) break\n }\n return seen\n}\n\n/**\n * The campaigns a HOST resource — a form, a screen — belongs to.\n *\n * Host resources carry the field at the top of the document: they belong to\n * one site, which is the same site the campaign belongs to, so there is no\n * second holder to keep the edge away from.\n */\nexport function readCampaignIds(\n resource: Record<string, unknown> | null | undefined,\n): string[] {\n return normalizeCampaignIds((resource ?? {})[CAMPAIGN_MEMBERSHIP_FIELD])\n}\n\n/**\n * The dotted path to a contact's membership, inside ONE holder's facet.\n *\n * A contact document is shared by every site in the org — one human who\n * touched two sites is one row — and almost nothing on it is legitimately\n * shared. Which campaigns a merchant has filed a person under is that\n * merchant's business record on exactly the footing their notes and tags are,\n * so it lives at `facets.{groupId}.campaignIds` and not at the top of the\n * document, where every other site in an agency's account could read it.\n */\nexport function contactCampaignFieldPath(groupId: string): string {\n return contactFacetPath(groupId, CAMPAIGN_MEMBERSHIP_FIELD)\n}\n\n/** The campaigns ONE holder has filed this contact under. */\nexport function readContactCampaignIds(\n contact: Record<string, unknown> | null | undefined,\n groupId: string,\n): string[] {\n return normalizeCampaignIds(readContactFacet(contact, groupId).campaignIds)\n}\n\n/**\n * The membership a save should write, given what is on screen.\n *\n * Pure, and shared by the three consoles that offer the picker, because the\n * decision each of them makes is identical and is not obvious: an EMPTY\n * selection has to be stored as an empty array rather than as a removed\n * field.\n *\n * Removing the field would read the same to every surface here — the readers\n * above answer `[]` either way — and would be wrong for the one reader that\n * is not a surface. `campaign-manage.ts` finds a campaign's members with\n * `array-contains`, which matches on the automatic single-field index; that\n * index has an entry only while the array does, so the two shapes are the\n * same to a reader and different to a writer only in cost. Storing `[]` keeps\n * one shape for \"belongs to no campaign\" across every writer, which is the\n * property the send collection's own detach comment argues for one field\n * over.\n *\n * @returns the value to store under {@link CAMPAIGN_MEMBERSHIP_FIELD}.\n */\nexport function campaignMembershipValue(selected: readonly string[]): string[] {\n return normalizeCampaignIds(selected)\n}\n\n/**\n * Whether a document's stored membership already says what a save would.\n *\n * Order-insensitive: a picker hands back its options' order and the stored\n * array is in the order it was written, so comparing them literally would\n * report a change on every open and leave Save permanently enabled.\n */\nexport function campaignMembershipUnchanged(\n stored: readonly string[],\n selected: readonly string[],\n): boolean {\n if (stored.length !== selected.length) return false\n const sortedStored = [...stored].sort()\n const sortedSelected = [...selected].sort()\n return sortedStored.every((id, index) => id === sortedSelected[index])\n}\n\n/**\n * The host subcollections whose documents may name a campaign.\n *\n * The list a campaign's deletion walks, and the list this feature's coverage\n * spec reads — so a collection that grows the picker without growing the\n * detach fails the build rather than shipping a campaign whose removal leaves\n * that collection pointing at nothing.\n *\n * A lead (`hosts/{hostId}/leads/{personKey}`, AGL-3254) carries the field at\n * the top of its document like a form does: a lead is one site's own record,\n * captured there and worked there, so there is no second holder to keep the\n * edge away from. It joins a campaign by hand — the New lead drawer, the CSV\n * import, the list's bulk bar — or by being enrolled in a sequence that is\n * in one, and it hands the membership to the contact it converts into.\n *\n * Contacts are deliberately NOT here. They live on the org\n * (`orgs/{orgId}/contacts`), not the host, and carry the field inside a\n * per-holder facet — so they are detached by their own pass, against a field\n * path that names the group. Nor are a plugin's own members — a sequence and\n * its enrollments live under the org, in collections the core does not name\n * — so a plugin registers a detacher for those\n * (`plugin-manager/plugin-membership-detach.ts`) and the deletion runs it.\n */\nexport const CAMPAIGN_MEMBER_HOST_COLLECTIONS = ['forms', 'screens', 'leads'] as const\n\nexport type CampaignMemberHostCollection =\n (typeof CAMPAIGN_MEMBER_HOST_COLLECTIONS)[number]\n"],"names":["contactFacetPath","readContactFacet","CAMPAIGN_MEMBERSHIP_FIELD","CAMPAIGN_MEMBERSHIP_CAP","normalizeCampaignIds","raw","Array","isArray","seen","entry","id","trim","includes","push","length","readCampaignIds","resource","contactCampaignFieldPath","groupId","readContactCampaignIds","contact","campaignIds","campaignMembershipValue","selected","campaignMembershipUnchanged","stored","sortedStored","sort","sortedSelected","every","index","CAMPAIGN_MEMBER_HOST_COLLECTIONS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CC,GAED,SAASA,gBAAgB,EAAEC,gBAAgB,QAAQ,gBAAY;AAE/D;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B,cAAa;AAEtD;;;;;;;CAOC,GACD,OAAO,MAAMC,0BAA0B,GAAE;AAEzC;;;;;;;CAOC,GACD,OAAO,SAASC,qBAAqBC,GAAY;IAC/C,IAAI,CAACC,MAAMC,OAAO,CAACF,MAAM,OAAO,EAAE;IAClC,MAAMG,OAAiB,EAAE;IACzB,KAAK,MAAMC,SAASJ,IAAK;QACvB,IAAI,OAAOI,UAAU,UAAU;QAC/B,MAAMC,KAAKD,MAAME,IAAI;QACrB,IAAI,CAACD,MAAMF,KAAKI,QAAQ,CAACF,KAAK;QAC9BF,KAAKK,IAAI,CAACH;QACV,IAAIF,KAAKM,MAAM,IAAIX,yBAAyB;IAC9C;IACA,OAAOK;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASO,gBACdC,QAAoD;IAEpD,OAAOZ,qBAAqB,CAACY,mBAAAA,WAAY,CAAC,EAAE,CAACd,0BAA0B;AACzE;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASe,yBAAyBC,OAAe;IACtD,OAAOlB,iBAAiBkB,SAAShB;AACnC;AAEA,2DAA2D,GAC3D,OAAO,SAASiB,uBACdC,OAAmD,EACnDF,OAAe;IAEf,OAAOd,qBAAqBH,iBAAiBmB,SAASF,SAASG,WAAW;AAC5E;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,wBAAwBC,QAA2B;IACjE,OAAOnB,qBAAqBmB;AAC9B;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,4BACdC,MAAyB,EACzBF,QAA2B;IAE3B,IAAIE,OAAOX,MAAM,KAAKS,SAAST,MAAM,EAAE,OAAO;IAC9C,MAAMY,eAAe;WAAID;KAAO,CAACE,IAAI;IACrC,MAAMC,iBAAiB;WAAIL;KAAS,CAACI,IAAI;IACzC,OAAOD,aAAaG,KAAK,CAAC,CAACnB,IAAIoB,QAAUpB,OAAOkB,cAAc,CAACE,MAAM;AACvE;AAEA;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,MAAMC,mCAAmC;IAAC;IAAS;IAAW;CAAQ,CAAS"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/campaign-membership.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * WHAT ELSE IS IN A CAMPAIGN: the edge between a campaign and the forms,\n * screens and people a push is coordinated across.\n *\n * ## The edge is a field on the RESOURCE, not a list on the campaign\n *\n * A campaign container lives at `orgs/{orgId}/emailCampaigns/{campaignId}`\n * and names nothing outside itself. A send joins one by carrying\n * `emailCampaignId`; every other member joins the same way, through\n * {@link CAMPAIGN_MEMBERSHIP_FIELD} on its own document.\n *\n * Three properties decide it, and all three point the same way:\n *\n * - **Reading is free where it is asked.** A form's own page reads the form\n * document to draw anything at all, so the campaign comes with it. A\n * membership collection under the campaign would make \"which campaign is\n * this in\" a query issued on mount by every resource page, for a fact one\n * field already carries.\n * - **The reduction is already gated this way.** `campaign-manage.ts` clears\n * `emailCampaignId` from every send before it removes a container, so no\n * send is left naming one that is gone. Extending that pass to the\n * membership field is the SAME mechanism; a membership collection would be\n * a second, different deletion story beside it.\n * - **Deleting a MEMBER can leave nothing behind.** The campaign holds no\n * list, so a deleted form takes its own edge with it. This matters most on\n * a contact: an erasure removes the document, and a membership row stored\n * anywhere else would survive it holding the id of a person who asked to\n * be forgotten.\n *\n * ## Plural, because a landing page outlives one campaign\n *\n * The field is an ARRAY. A signup form built for the spring push is the same\n * form the summer push places, and a single-valued field would make the\n * second assignment silently erase the first — a campaign losing a member\n * with nothing on screen to say so. The automation step that has always\n * written this edge (`assignCampaign`) already used `arrayUnion`, so the\n * product has treated it as many-to-many from the start.\n *\n * ## Ids, never names\n *\n * A campaign's name is editable, and every reader here resolves the name from\n * the campaign document at read time. Renaming a campaign therefore moves no\n * membership: the stored value is the container's document id, which nothing\n * rewrites.\n */\n\nimport { contactFacetPath, readContactFacet } from './contacts'\n\n/**\n * The field naming the campaigns a resource belongs to.\n *\n * Not `emailCampaignId`, which is the SEND's field: on a send document the\n * word `campaignId` already means the send's own id — it is what `cid`\n * carries into every unsubscribe footer — so that collection needed a\n * different word. A form, a screen and a contact have no second meaning for\n * it, and the plural says the resource may be in more than one.\n */\nexport const CAMPAIGN_MEMBERSHIP_FIELD = 'campaignIds'\n\n/**\n * How many campaigns one resource may name.\n *\n * A ceiling on the FIELD, not on the product: an array Firestore has to index\n * on every write is not the place to discover that a script has been adding\n * an id a day for a year. Twenty is far past what a person assigns by hand\n * and far short of anything that costs a write.\n */\nexport const CAMPAIGN_MEMBERSHIP_CAP = 20\n\n/**\n * A stored membership array as a clean list of ids.\n *\n * Deduped, trimmed, non-strings dropped, capped. Every reader goes through\n * this because the field is written by three consoles and one automation\n * step, and a surface that trusted the raw value would render `undefined` as\n * a chip the first time one of them wrote a blank.\n */\nexport function normalizeCampaignIds(raw: unknown): string[] {\n if (!Array.isArray(raw)) return []\n const seen: string[] = []\n for (const entry of raw) {\n if (typeof entry !== 'string') continue\n const id = entry.trim()\n if (!id || seen.includes(id)) continue\n seen.push(id)\n if (seen.length >= CAMPAIGN_MEMBERSHIP_CAP) break\n }\n return seen\n}\n\n/**\n * The campaigns a HOST resource — a form, a screen — belongs to.\n *\n * Host resources carry the field at the top of the document: they belong to\n * one site, and whoever may read the resource may read which of the org's\n * campaigns it is filed under, so there is no second holder to keep the edge\n * away from.\n */\nexport function readCampaignIds(\n resource: Record<string, unknown> | null | undefined,\n): string[] {\n return normalizeCampaignIds((resource ?? {})[CAMPAIGN_MEMBERSHIP_FIELD])\n}\n\n/**\n * The dotted path to a contact's membership, inside ONE holder's facet.\n *\n * A contact document is shared by every site in the org — one human who\n * touched two sites is one row — and almost nothing on it is legitimately\n * shared. Which campaigns a merchant has filed a person under is that\n * merchant's business record on exactly the footing their notes and tags are,\n * so it lives at `facets.{groupId}.campaignIds` and not at the top of the\n * document, where every other site in an agency's account could read it.\n */\nexport function contactCampaignFieldPath(groupId: string): string {\n return contactFacetPath(groupId, CAMPAIGN_MEMBERSHIP_FIELD)\n}\n\n/** The campaigns ONE holder has filed this contact under. */\nexport function readContactCampaignIds(\n contact: Record<string, unknown> | null | undefined,\n groupId: string,\n): string[] {\n return normalizeCampaignIds(readContactFacet(contact, groupId).campaignIds)\n}\n\n/**\n * The membership a save should write, given what is on screen.\n *\n * Pure, and shared by the three consoles that offer the picker, because the\n * decision each of them makes is identical and is not obvious: an EMPTY\n * selection has to be stored as an empty array rather than as a removed\n * field.\n *\n * Removing the field would read the same to every surface here — the readers\n * above answer `[]` either way — and would be wrong for the one reader that\n * is not a surface. `campaign-manage.ts` finds a campaign's members with\n * `array-contains`, which matches on the automatic single-field index; that\n * index has an entry only while the array does, so the two shapes are the\n * same to a reader and different to a writer only in cost. Storing `[]` keeps\n * one shape for \"belongs to no campaign\" across every writer, which is the\n * property the send collection's own detach comment argues for one field\n * over.\n *\n * @returns the value to store under {@link CAMPAIGN_MEMBERSHIP_FIELD}.\n */\nexport function campaignMembershipValue(selected: readonly string[]): string[] {\n return normalizeCampaignIds(selected)\n}\n\n/**\n * Whether a document's stored membership already says what a save would.\n *\n * Order-insensitive: a picker hands back its options' order and the stored\n * array is in the order it was written, so comparing them literally would\n * report a change on every open and leave Save permanently enabled.\n */\nexport function campaignMembershipUnchanged(\n stored: readonly string[],\n selected: readonly string[],\n): boolean {\n if (stored.length !== selected.length) return false\n const sortedStored = [...stored].sort()\n const sortedSelected = [...selected].sort()\n return sortedStored.every((id, index) => id === sortedSelected[index])\n}\n\n/**\n * The host subcollections whose documents may name a campaign.\n *\n * The list a campaign's deletion walks, and the list this feature's coverage\n * spec reads — so a collection that grows the picker without growing the\n * detach fails the build rather than shipping a campaign whose removal leaves\n * that collection pointing at nothing.\n *\n * `leads` is here for the site rows the lead migration (AGL-3276) has not\n * reached. A live lead is an org row (`orgs/{orgId}/leads/{personKey}`) that\n * carries the field at the top of its document like a form does, and the\n * deletion walks that collection by name beside this list. A lead joins a\n * campaign by hand — the New lead drawer, the CSV import, the list's bulk\n * bar — or by being enrolled in a sequence that is in one, and it hands the\n * membership to the contact it converts into.\n *\n * Contacts are deliberately NOT here. They live on the org\n * (`orgs/{orgId}/contacts`), not the host, and carry the field inside a\n * per-holder facet — so they are detached by their own pass, against a field\n * path that names the group. Nor are a plugin's own members — a sequence and\n * its enrollments live under the org, in collections the core does not name\n * — so a plugin registers a detacher for those\n * (`plugin-manager/plugin-membership-detach.ts`) and the deletion runs it.\n */\nexport const CAMPAIGN_MEMBER_HOST_COLLECTIONS = ['forms', 'screens', 'leads'] as const\n\nexport type CampaignMemberHostCollection =\n (typeof CAMPAIGN_MEMBER_HOST_COLLECTIONS)[number]\n"],"names":["contactFacetPath","readContactFacet","CAMPAIGN_MEMBERSHIP_FIELD","CAMPAIGN_MEMBERSHIP_CAP","normalizeCampaignIds","raw","Array","isArray","seen","entry","id","trim","includes","push","length","readCampaignIds","resource","contactCampaignFieldPath","groupId","readContactCampaignIds","contact","campaignIds","campaignMembershipValue","selected","campaignMembershipUnchanged","stored","sortedStored","sort","sortedSelected","every","index","CAMPAIGN_MEMBER_HOST_COLLECTIONS"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CC,GAED,SAASA,gBAAgB,EAAEC,gBAAgB,QAAQ,gBAAY;AAE/D;;;;;;;;CAQC,GACD,OAAO,MAAMC,4BAA4B,cAAa;AAEtD;;;;;;;CAOC,GACD,OAAO,MAAMC,0BAA0B,GAAE;AAEzC;;;;;;;CAOC,GACD,OAAO,SAASC,qBAAqBC,GAAY;IAC/C,IAAI,CAACC,MAAMC,OAAO,CAACF,MAAM,OAAO,EAAE;IAClC,MAAMG,OAAiB,EAAE;IACzB,KAAK,MAAMC,SAASJ,IAAK;QACvB,IAAI,OAAOI,UAAU,UAAU;QAC/B,MAAMC,KAAKD,MAAME,IAAI;QACrB,IAAI,CAACD,MAAMF,KAAKI,QAAQ,CAACF,KAAK;QAC9BF,KAAKK,IAAI,CAACH;QACV,IAAIF,KAAKM,MAAM,IAAIX,yBAAyB;IAC9C;IACA,OAAOK;AACT;AAEA;;;;;;;CAOC,GACD,OAAO,SAASO,gBACdC,QAAoD;IAEpD,OAAOZ,qBAAqB,CAACY,mBAAAA,WAAY,CAAC,EAAE,CAACd,0BAA0B;AACzE;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASe,yBAAyBC,OAAe;IACtD,OAAOlB,iBAAiBkB,SAAShB;AACnC;AAEA,2DAA2D,GAC3D,OAAO,SAASiB,uBACdC,OAAmD,EACnDF,OAAe;IAEf,OAAOd,qBAAqBH,iBAAiBmB,SAASF,SAASG,WAAW;AAC5E;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC,wBAAwBC,QAA2B;IACjE,OAAOnB,qBAAqBmB;AAC9B;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,4BACdC,MAAyB,EACzBF,QAA2B;IAE3B,IAAIE,OAAOX,MAAM,KAAKS,SAAST,MAAM,EAAE,OAAO;IAC9C,MAAMY,eAAe;WAAID;KAAO,CAACE,IAAI;IACrC,MAAMC,iBAAiB;WAAIL;KAAS,CAACI,IAAI;IACzC,OAAOD,aAAaG,KAAK,CAAC,CAACnB,IAAIoB,QAAUpB,OAAOkB,cAAc,CAACE,MAAM;AACvE;AAEA;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,OAAO,MAAMC,mCAAmC;IAAC;IAAS;IAAW;CAAQ,CAAS"}
@@ -61,11 +61,15 @@ export declare function applyLayoutProps<N extends AglynNodeSchema = AglynNodeSc
61
61
  /**
62
62
  * A screen grafted through its whole layout chain, innermost first, with each
63
63
  * layout's properties applied from the screen's values for that layout
64
- * (`layoutPropValues`, keyed by layout id).
64
+ * (`layoutPropValues`, keyed by layout id), and then the screen's per-page
65
+ * restyling of that layout's elements (`layoutStyleOverrides`, AGL-3286) —
66
+ * after the properties, so a page's style wins over whatever a property
67
+ * bound, and before the graft, so an element the layout hides for this page
68
+ * is simply not there to be styled.
65
69
  *
66
70
  * The one composition the published page, the besigner's Preview and its
67
71
  * layout chrome all run, so none of them can disagree about which value a
68
72
  * layout renders with.
69
73
  */
70
- export declare function composeLayoutChainWithProps<N extends AglynNodeSchema = AglynNodeSchema>(chain: ReadonlyArray<LayoutChainEntry<N> | null | undefined>, screenNodes: NormalizedNodes<N>, layoutPropValues?: unknown): NormalizedNodes<N>;
74
+ export declare function composeLayoutChainWithProps<N extends AglynNodeSchema = AglynNodeSchema>(chain: ReadonlyArray<LayoutChainEntry<N> | null | undefined>, screenNodes: NormalizedNodes<N>, layoutPropValues?: unknown, layoutStyleOverrides?: unknown): NormalizedNodes<N>;
71
75
  export default composeLayoutChainWithProps;
@@ -16,6 +16,7 @@
16
16
  */ import { NODE_ROOT_ID } from "../canvas-manager/canvas-manager.js";
17
17
  import { composeLayoutChainAndScreenNodes } from "./compose-layout-nodes.js";
18
18
  import { applyDeclaredProps } from "./compose-reusable-components.js";
19
+ import { applyLayoutStyleOverrides, layoutStyleOverridesFor } from "./layout-style-overrides.js";
19
20
  /**
20
21
  * Screen-version key holding the screen's values for the layouts it renders
21
22
  * inside, beside the `layoutId` binding. Persisted in screen version documents
@@ -48,13 +49,17 @@ import { applyDeclaredProps } from "./compose-reusable-components.js";
48
49
  /**
49
50
  * A screen grafted through its whole layout chain, innermost first, with each
50
51
  * layout's properties applied from the screen's values for that layout
51
- * (`layoutPropValues`, keyed by layout id).
52
+ * (`layoutPropValues`, keyed by layout id), and then the screen's per-page
53
+ * restyling of that layout's elements (`layoutStyleOverrides`, AGL-3286) —
54
+ * after the properties, so a page's style wins over whatever a property
55
+ * bound, and before the graft, so an element the layout hides for this page
56
+ * is simply not there to be styled.
52
57
  *
53
58
  * The one composition the published page, the besigner's Preview and its
54
59
  * layout chrome all run, so none of them can disagree about which value a
55
60
  * layout renders with.
56
- */ export function composeLayoutChainWithProps(chain, screenNodes, layoutPropValues) {
57
- return composeLayoutChainAndScreenNodes(chain.map((entry)=>applyLayoutProps(entry == null ? void 0 : entry.nodes, entry == null ? void 0 : entry.props, layoutPropValuesFor(layoutPropValues, entry == null ? void 0 : entry.layoutId))), screenNodes);
61
+ */ export function composeLayoutChainWithProps(chain, screenNodes, layoutPropValues, layoutStyleOverrides) {
62
+ return composeLayoutChainAndScreenNodes(chain.map((entry)=>applyLayoutStyleOverrides(applyLayoutProps(entry == null ? void 0 : entry.nodes, entry == null ? void 0 : entry.props, layoutPropValuesFor(layoutPropValues, entry == null ? void 0 : entry.layoutId)), layoutStyleOverridesFor(layoutStyleOverrides, entry == null ? void 0 : entry.layoutId))), screenNodes);
58
63
  }
59
64
  export default composeLayoutChainWithProps;
60
65
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/compose-layout-props.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 {\n AglynNodeSchema,\n NodeId,\n ReusableComponentProp,\n} from '../foundation'\nimport { NODE_ROOT_ID } from '../canvas-manager/canvas-manager'\nimport { composeLayoutChainAndScreenNodes } from './compose-layout-nodes'\nimport { applyDeclaredProps } from './compose-reusable-components'\n\n/**\n * Layout properties (AGL-2893): a shared layout declares properties exactly as\n * a reusable component does, binds its own nodes to them with `{{prop.*}}`,\n * and each screen rendering inside it sets the values.\n *\n * The values are applied to each layout's OWN nodes before the screen is\n * grafted into its slot, so a `{{prop.*}}` the screen's own copy happens to\n * contain is never touched, and a component the layout places receives the\n * values it is handed before it is grafted.\n */\n\ntype NormalizedNodes<N extends AglynNodeSchema> = Record<NodeId, N>\n\n/**\n * Screen-version key holding the screen's values for the layouts it renders\n * inside, beside the `layoutId` binding. Persisted in screen version documents\n * — never rename.\n */\nexport const SCREEN_LAYOUT_PROP_VALUES_KEY = 'layoutPropValues'\n\n/** One layout of a screen's chain, as composition reads it. */\nexport interface LayoutChainEntry<N extends AglynNodeSchema = AglynNodeSchema> {\n /** The layout document's id, which a screen's values are keyed by. */\n layoutId?: string | null\n /** The published version's nodes. */\n nodes?: NormalizedNodes<N> | null\n /** The published version's declared properties. */\n props?: ReusableComponentProp[] | null\n}\n\n/**\n * A screen's values for one layout of its chain, or `undefined` where it set\n * none. Anything that is not a map of values is read as none.\n */\nexport function layoutPropValuesFor(\n layoutPropValues: unknown,\n layoutId: string | null | undefined,\n): Record<string, unknown> | undefined {\n if (!layoutId || !layoutPropValues || typeof layoutPropValues !== 'object') {\n return undefined\n }\n const values = (layoutPropValues as Record<string, unknown>)[layoutId]\n return values && typeof values === 'object' && !Array.isArray(values)\n ? (values as Record<string, unknown>)\n : undefined\n}\n\n/**\n * A layout's nodes with its declared properties applied for one screen: each\n * `{{prop.*}}` takes the screen's value or the property's default, a field\n * bound to one property receives the value its kind holds, and the parts the\n * screen hides (`hideIf` / `hideUnless`) are pruned — the sequence a reusable\n * component's graft runs, over the layout's root.\n *\n * A layout with no root is returned as it is, for the reason\n * `composeLayoutAndScreenNodes` gives: a broken layout degrades to the bare\n * screen rather than taking a page down.\n */\nexport function applyLayoutProps<N extends AglynNodeSchema = AglynNodeSchema>(\n nodes: NormalizedNodes<N> | null | undefined,\n props: ReusableComponentProp[] | null | undefined,\n values: Readonly<Record<string, unknown>> | null | undefined,\n): NormalizedNodes<N> | undefined {\n if (!nodes || !nodes[NODE_ROOT_ID]) return nodes ?? undefined\n return applyDeclaredProps(nodes, props, values, NODE_ROOT_ID)\n}\n\n/**\n * A screen grafted through its whole layout chain, innermost first, with each\n * layout's properties applied from the screen's values for that layout\n * (`layoutPropValues`, keyed by layout id).\n *\n * The one composition the published page, the besigner's Preview and its\n * layout chrome all run, so none of them can disagree about which value a\n * layout renders with.\n */\nexport function composeLayoutChainWithProps<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n chain: ReadonlyArray<LayoutChainEntry<N> | null | undefined>,\n screenNodes: NormalizedNodes<N>,\n layoutPropValues?: unknown,\n): NormalizedNodes<N> {\n return composeLayoutChainAndScreenNodes(\n chain.map((entry) =>\n applyLayoutProps(\n entry?.nodes,\n entry?.props,\n layoutPropValuesFor(layoutPropValues, entry?.layoutId),\n ),\n ),\n screenNodes,\n )\n}\n\nexport default composeLayoutChainWithProps\n"],"names":["NODE_ROOT_ID","composeLayoutChainAndScreenNodes","applyDeclaredProps","SCREEN_LAYOUT_PROP_VALUES_KEY","layoutPropValuesFor","layoutPropValues","layoutId","undefined","values","Array","isArray","applyLayoutProps","nodes","props","composeLayoutChainWithProps","chain","screenNodes","map","entry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAOD,SAASA,YAAY,QAAQ,sCAAkC;AAC/D,SAASC,gCAAgC,QAAQ,4BAAwB;AACzE,SAASC,kBAAkB,QAAQ,mCAA+B;AAelE;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,mBAAkB;AAY/D;;;CAGC,GACD,OAAO,SAASC,oBACdC,gBAAyB,EACzBC,QAAmC;IAEnC,IAAI,CAACA,YAAY,CAACD,oBAAoB,OAAOA,qBAAqB,UAAU;QAC1E,OAAOE;IACT;IACA,MAAMC,SAAS,AAACH,gBAA4C,CAACC,SAAS;IACtE,OAAOE,UAAU,OAAOA,WAAW,YAAY,CAACC,MAAMC,OAAO,CAACF,UACzDA,SACDD;AACN;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASI,iBACdC,KAA4C,EAC5CC,KAAiD,EACjDL,MAA4D;IAE5D,IAAI,CAACI,SAAS,CAACA,KAAK,CAACZ,aAAa,EAAE,OAAOY,gBAAAA,QAASL;IACpD,OAAOL,mBAAmBU,OAAOC,OAAOL,QAAQR;AAClD;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASc,4BAGdC,KAA4D,EAC5DC,WAA+B,EAC/BX,gBAA0B;IAE1B,OAAOJ,iCACLc,MAAME,GAAG,CAAC,CAACC,QACTP,iBACEO,yBAAAA,MAAON,KAAK,EACZM,yBAAAA,MAAOL,KAAK,EACZT,oBAAoBC,kBAAkBa,yBAAAA,MAAOZ,QAAQ,KAGzDU;AAEJ;AAEA,eAAeF,4BAA2B"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/compose-layout-props.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 {\n AglynNodeSchema,\n NodeId,\n ReusableComponentProp,\n} from '../foundation'\nimport { NODE_ROOT_ID } from '../canvas-manager/canvas-manager'\nimport { composeLayoutChainAndScreenNodes } from './compose-layout-nodes'\nimport { applyDeclaredProps } from './compose-reusable-components'\nimport {\n applyLayoutStyleOverrides,\n layoutStyleOverridesFor,\n} from './layout-style-overrides'\n\n/**\n * Layout properties (AGL-2893): a shared layout declares properties exactly as\n * a reusable component does, binds its own nodes to them with `{{prop.*}}`,\n * and each screen rendering inside it sets the values.\n *\n * The values are applied to each layout's OWN nodes before the screen is\n * grafted into its slot, so a `{{prop.*}}` the screen's own copy happens to\n * contain is never touched, and a component the layout places receives the\n * values it is handed before it is grafted.\n */\n\ntype NormalizedNodes<N extends AglynNodeSchema> = Record<NodeId, N>\n\n/**\n * Screen-version key holding the screen's values for the layouts it renders\n * inside, beside the `layoutId` binding. Persisted in screen version documents\n * — never rename.\n */\nexport const SCREEN_LAYOUT_PROP_VALUES_KEY = 'layoutPropValues'\n\n/** One layout of a screen's chain, as composition reads it. */\nexport interface LayoutChainEntry<N extends AglynNodeSchema = AglynNodeSchema> {\n /** The layout document's id, which a screen's values are keyed by. */\n layoutId?: string | null\n /** The published version's nodes. */\n nodes?: NormalizedNodes<N> | null\n /** The published version's declared properties. */\n props?: ReusableComponentProp[] | null\n}\n\n/**\n * A screen's values for one layout of its chain, or `undefined` where it set\n * none. Anything that is not a map of values is read as none.\n */\nexport function layoutPropValuesFor(\n layoutPropValues: unknown,\n layoutId: string | null | undefined,\n): Record<string, unknown> | undefined {\n if (!layoutId || !layoutPropValues || typeof layoutPropValues !== 'object') {\n return undefined\n }\n const values = (layoutPropValues as Record<string, unknown>)[layoutId]\n return values && typeof values === 'object' && !Array.isArray(values)\n ? (values as Record<string, unknown>)\n : undefined\n}\n\n/**\n * A layout's nodes with its declared properties applied for one screen: each\n * `{{prop.*}}` takes the screen's value or the property's default, a field\n * bound to one property receives the value its kind holds, and the parts the\n * screen hides (`hideIf` / `hideUnless`) are pruned — the sequence a reusable\n * component's graft runs, over the layout's root.\n *\n * A layout with no root is returned as it is, for the reason\n * `composeLayoutAndScreenNodes` gives: a broken layout degrades to the bare\n * screen rather than taking a page down.\n */\nexport function applyLayoutProps<N extends AglynNodeSchema = AglynNodeSchema>(\n nodes: NormalizedNodes<N> | null | undefined,\n props: ReusableComponentProp[] | null | undefined,\n values: Readonly<Record<string, unknown>> | null | undefined,\n): NormalizedNodes<N> | undefined {\n if (!nodes || !nodes[NODE_ROOT_ID]) return nodes ?? undefined\n return applyDeclaredProps(nodes, props, values, NODE_ROOT_ID)\n}\n\n/**\n * A screen grafted through its whole layout chain, innermost first, with each\n * layout's properties applied from the screen's values for that layout\n * (`layoutPropValues`, keyed by layout id), and then the screen's per-page\n * restyling of that layout's elements (`layoutStyleOverrides`, AGL-3286) —\n * after the properties, so a page's style wins over whatever a property\n * bound, and before the graft, so an element the layout hides for this page\n * is simply not there to be styled.\n *\n * The one composition the published page, the besigner's Preview and its\n * layout chrome all run, so none of them can disagree about which value a\n * layout renders with.\n */\nexport function composeLayoutChainWithProps<\n N extends AglynNodeSchema = AglynNodeSchema,\n>(\n chain: ReadonlyArray<LayoutChainEntry<N> | null | undefined>,\n screenNodes: NormalizedNodes<N>,\n layoutPropValues?: unknown,\n layoutStyleOverrides?: unknown,\n): NormalizedNodes<N> {\n return composeLayoutChainAndScreenNodes(\n chain.map((entry) =>\n applyLayoutStyleOverrides(\n applyLayoutProps(\n entry?.nodes,\n entry?.props,\n layoutPropValuesFor(layoutPropValues, entry?.layoutId),\n ),\n layoutStyleOverridesFor(layoutStyleOverrides, entry?.layoutId),\n ),\n ),\n screenNodes,\n )\n}\n\nexport default composeLayoutChainWithProps\n"],"names":["NODE_ROOT_ID","composeLayoutChainAndScreenNodes","applyDeclaredProps","applyLayoutStyleOverrides","layoutStyleOverridesFor","SCREEN_LAYOUT_PROP_VALUES_KEY","layoutPropValuesFor","layoutPropValues","layoutId","undefined","values","Array","isArray","applyLayoutProps","nodes","props","composeLayoutChainWithProps","chain","screenNodes","layoutStyleOverrides","map","entry"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAOD,SAASA,YAAY,QAAQ,sCAAkC;AAC/D,SAASC,gCAAgC,QAAQ,4BAAwB;AACzE,SAASC,kBAAkB,QAAQ,mCAA+B;AAClE,SACEC,yBAAyB,EACzBC,uBAAuB,QAClB,8BAA0B;AAejC;;;;CAIC,GACD,OAAO,MAAMC,gCAAgC,mBAAkB;AAY/D;;;CAGC,GACD,OAAO,SAASC,oBACdC,gBAAyB,EACzBC,QAAmC;IAEnC,IAAI,CAACA,YAAY,CAACD,oBAAoB,OAAOA,qBAAqB,UAAU;QAC1E,OAAOE;IACT;IACA,MAAMC,SAAS,AAACH,gBAA4C,CAACC,SAAS;IACtE,OAAOE,UAAU,OAAOA,WAAW,YAAY,CAACC,MAAMC,OAAO,CAACF,UACzDA,SACDD;AACN;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASI,iBACdC,KAA4C,EAC5CC,KAAiD,EACjDL,MAA4D;IAE5D,IAAI,CAACI,SAAS,CAACA,KAAK,CAACd,aAAa,EAAE,OAAOc,gBAAAA,QAASL;IACpD,OAAOP,mBAAmBY,OAAOC,OAAOL,QAAQV;AAClD;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASgB,4BAGdC,KAA4D,EAC5DC,WAA+B,EAC/BX,gBAA0B,EAC1BY,oBAA8B;IAE9B,OAAOlB,iCACLgB,MAAMG,GAAG,CAAC,CAACC,QACTlB,0BACEU,iBACEQ,yBAAAA,MAAOP,KAAK,EACZO,yBAAAA,MAAON,KAAK,EACZT,oBAAoBC,kBAAkBc,yBAAAA,MAAOb,QAAQ,IAEvDJ,wBAAwBe,sBAAsBE,yBAAAA,MAAOb,QAAQ,KAGjEU;AAEJ;AAEA,eAAeF,4BAA2B"}
@@ -17,11 +17,53 @@
17
17
  import type { AglynNodeSchema, NodeId, ReusableComponentIcon, ReusableComponentProp } from '../foundation';
18
18
  export * from './reusable-component-keys';
19
19
  export * from './reusable-prop-values';
20
+ /**
21
+ * Whether a node is a placed form that names a form entity — the second kind
22
+ * of node that stands for a tree stored on another document (see
23
+ * {@link PlacementKind}). A `form` node with no `formId` is an ordinary form
24
+ * whose fields are its own children, and has no override layer.
25
+ *
26
+ * The ref test matches the graft's own resolution rule (a non-empty string),
27
+ * so the editor and the renderer agree about which forms are placements.
28
+ */
29
+ export declare function isPlacedFormNode(node: {
30
+ componentId?: string;
31
+ props?: unknown;
32
+ } | undefined | null): boolean;
33
+ /**
34
+ * Whether a node is a placement that GRAFTS a definition — a reusable
35
+ * component instance or a placed form — and therefore carries the two
36
+ * per-placement override layers (`styleOverrides`, `attrOverrides`, AGL-3285).
37
+ *
38
+ * One predicate for both kinds on purpose: the graft already expands them in
39
+ * one pass, and an override layer that one kind honoured and the other
40
+ * silently ignored would be the canvas/Preview/tenant disagreement this file
41
+ * exists to prevent.
42
+ */
43
+ export declare function isDefinitionPlacement(node: {
44
+ componentId?: string;
45
+ props?: unknown;
46
+ } | undefined | null): boolean;
47
+ /**
48
+ * The tree a placement grafts, from whichever map its kind reads — the
49
+ * component definitions for an instance, the published form designs for a
50
+ * placed form — or `undefined` when it is not a placement or its ref resolves
51
+ * to nothing (AGL-3285). The editor panels use it to offer the same targets
52
+ * the graft will consult.
53
+ */
54
+ export declare function placementDefinitionFor<N extends AglynNodeSchema = AglynNodeSchema>(node: {
55
+ componentId?: string;
56
+ props?: unknown;
57
+ } | undefined | null, sources: {
58
+ definitions?: Record<string, ReusableComponentTree<N> | undefined>;
59
+ formDesigns?: Record<string, Pick<ReusableComponentTree<N>, 'rootId' | 'nodes'> | undefined>;
60
+ }): Pick<ReusableComponentTree<N>, 'rootId' | 'nodes'> | undefined;
20
61
  /**
21
62
  * Every per-instance style override an instance node carries, keyed the way
22
63
  * it is stored (AGL-1332): {@link STYLE_OVERRIDES_ROOT_KEY} for the
23
64
  * component root, and a DEFINITION-internal node id for each overridden
24
- * leaf. Empty for a node that is not an instance or carries none.
65
+ * leaf. Empty for a node that is not a placement ({@link
66
+ * isDefinitionPlacement}: an instance or a placed form) or carries none.
25
67
  *
26
68
  * Keyed on the definition's own id, never the grafted `cmp__…` id: the
27
69
  * graft id is DERIVED from the instance id, so keying on it would break the
@@ -39,6 +81,7 @@ export * from './reusable-prop-values';
39
81
  */
40
82
  export declare function getInstanceStyleOverrides(node: {
41
83
  componentId?: string;
84
+ props?: unknown;
42
85
  styleOverrides?: unknown;
43
86
  } | undefined | null): Record<string, Record<string, unknown>>;
44
87
  /**
@@ -48,8 +91,48 @@ export declare function getInstanceStyleOverrides(node: {
48
91
  */
49
92
  export declare function getInstanceRootStyleOverride(node: {
50
93
  componentId?: string;
94
+ props?: unknown;
51
95
  styleOverrides?: unknown;
52
96
  } | undefined | null): Record<string, unknown> | undefined;
97
+ /**
98
+ * Props a placed form's attribute overrides must never address, on any node
99
+ * of its design (AGL-3285).
100
+ *
101
+ * A form is edited once and every page placing it follows — that is the
102
+ * whole point of the form entity. Its submission list, its dataset columns,
103
+ * its consent tick and its routing are all keyed to what the ENTITY declares,
104
+ * so a page that renamed a field, retyped it, made it optional, rewrote its
105
+ * choices or re-pointed where the form sends would file answers the form's
106
+ * own schema does not describe, from one page only, with nothing on the
107
+ * Forms page saying so. Per page, a form may LOOK different; it may not
108
+ * collect something different.
109
+ *
110
+ * - The form node's identity and submission config: `formId`, `formName`,
111
+ * `datasetId`, `datasetName`, and the after-submit outcome
112
+ * (`afterSubmit`, `redirectScreenId`, `redirectUrl`, `revealNodeId`).
113
+ * - Each field's contract: `fieldName` (the submission key),
114
+ * `datasetFieldId`, `fieldType`, `options` (a ticked option's TEXT is the
115
+ * posted value, and the consent check reads it) and `required`.
116
+ * - Element attributes a browser submission reads directly: `name`, `type`
117
+ * (a submit button made a plain button no longer submits), `value`,
118
+ * `action`, `method`, and `disabled` (a disabled input is not posted).
119
+ * - The hide directives: a hidden field is pruned from the page, so its
120
+ * value is never sent.
121
+ *
122
+ * Label, placeholder, help text, the submit label and success message, and
123
+ * everything about styling stay overridable.
124
+ */
125
+ export declare const PLACED_FORM_REFUSED_ATTR_PROPS: ReadonlySet<string>;
126
+ /**
127
+ * Whether `prop` may never be an attribute override on this placement — `sx`
128
+ * on every placement, plus {@link PLACED_FORM_REFUSED_ATTR_PROPS} on a placed
129
+ * form. The render layer and the editor's writer both ask this, so a refused
130
+ * prop can neither be offered nor take effect.
131
+ */
132
+ export declare function isAttrOverrideRefused(placement: {
133
+ componentId?: string;
134
+ props?: unknown;
135
+ } | undefined | null, prop: string): boolean;
53
136
  /**
54
137
  * Every per-instance ATTRIBUTE override an instance node carries, keyed the
55
138
  * way {@link getInstanceStyleOverrides} keys styles — `root` for the
@@ -62,6 +145,7 @@ export declare function getInstanceRootStyleOverride(node: {
62
145
  */
63
146
  export declare function getInstanceAttrOverrides(node: {
64
147
  componentId?: string;
148
+ props?: unknown;
65
149
  attrOverrides?: unknown;
66
150
  } | undefined | null): Record<string, Record<string, unknown>>;
67
151
  /** One styleable target inside an instance — see {@link listInstanceStyleTargets}. */
@@ -20,6 +20,7 @@ import { iconPathPropName } from "@aglyn/shared-data-mdi/utils/icon-path-prop-na
20
20
  import { reusablePropValueClass } from "../foundation/definitions/property-kinds.js";
21
21
  import { COMPONENT_NODE_ID_PREFIX, COMPONENT_PROP_TOKEN_PREFIX, INSTANCE_CARRIED_NODE_FIELDS, INSTANCE_CARRIED_PROPS, NODE_HIDE_IF_PROP, NODE_HIDE_UNLESS_PROP, REUSABLE_INSTANCE_COMPONENT_ID, REUSABLE_INSTANCE_PROP_VALUES_KEY, STYLE_OVERRIDES_ROOT_KEY } from "./reusable-component-keys.js";
22
22
  import { REPEAT_DIRECTIVE_PROPS, REPEAT_SELF_PROP, repeatKey, withoutRepeatDirective } from "./expand-repeatables.js";
23
+ import { FORM_COMPONENT_ID, FORM_ID_PROP } from "./forms.js";
23
24
  import { mergeNodeSx } from "./merge-node-sx.js";
24
25
  import { hasUrlScheme, isSafeNodeUrl, NODE_URL_PROPS } from "./node-url-policy.js";
25
26
  import { readInstanceIconValue, readYesNoValue, resolveReusablePropValues } from "./reusable-prop-values.js";
@@ -36,11 +37,60 @@ const isStyleRecord = (value)=>typeof value === 'object' && value !== null && !A
36
37
  ...new Set(names)
37
38
  ].join(' ') : undefined;
38
39
  }
40
+ /**
41
+ * Whether a node is a placed form that names a form entity — the second kind
42
+ * of node that stands for a tree stored on another document (see
43
+ * {@link PlacementKind}). A `form` node with no `formId` is an ordinary form
44
+ * whose fields are its own children, and has no override layer.
45
+ *
46
+ * The ref test matches the graft's own resolution rule (a non-empty string),
47
+ * so the editor and the renderer agree about which forms are placements.
48
+ */ export function isPlacedFormNode(node) {
49
+ var _node_props;
50
+ if ((node == null ? void 0 : node.componentId) !== FORM_COMPONENT_ID) return false;
51
+ const formId = (_node_props = node.props) == null ? void 0 : _node_props[FORM_ID_PROP];
52
+ return typeof formId === 'string' && formId !== '';
53
+ }
54
+ /**
55
+ * Whether a node is a placement that GRAFTS a definition — a reusable
56
+ * component instance or a placed form — and therefore carries the two
57
+ * per-placement override layers (`styleOverrides`, `attrOverrides`, AGL-3285).
58
+ *
59
+ * One predicate for both kinds on purpose: the graft already expands them in
60
+ * one pass, and an override layer that one kind honoured and the other
61
+ * silently ignored would be the canvas/Preview/tenant disagreement this file
62
+ * exists to prevent.
63
+ */ export function isDefinitionPlacement(node) {
64
+ return (node == null ? void 0 : node.componentId) === REUSABLE_INSTANCE_COMPONENT_ID || isPlacedFormNode(node);
65
+ }
66
+ /**
67
+ * The tree a placement grafts, from whichever map its kind reads — the
68
+ * component definitions for an instance, the published form designs for a
69
+ * placed form — or `undefined` when it is not a placement or its ref resolves
70
+ * to nothing (AGL-3285). The editor panels use it to offer the same targets
71
+ * the graft will consult.
72
+ */ export function placementDefinitionFor(node, sources) {
73
+ const props = node == null ? void 0 : node.props;
74
+ if ((node == null ? void 0 : node.componentId) === REUSABLE_INSTANCE_COMPONENT_ID) {
75
+ var _sources_definitions;
76
+ const refId = props == null ? void 0 : props['refId'];
77
+ return typeof refId === 'string' && refId ? (_sources_definitions = sources.definitions) == null ? void 0 : _sources_definitions[refId] : undefined;
78
+ }
79
+ if (isPlacedFormNode(node)) {
80
+ var _sources_formDesigns, _design_nodes;
81
+ const design = (_sources_formDesigns = sources.formDesigns) == null ? void 0 : _sources_formDesigns[props == null ? void 0 : props[FORM_ID_PROP]];
82
+ // The graft's resolution rule: a design whose root is not among its own
83
+ // nodes grafts nothing, so it offers nothing to override either.
84
+ return (design == null ? void 0 : design.rootId) && ((_design_nodes = design.nodes) == null ? void 0 : _design_nodes[design.rootId]) ? design : undefined;
85
+ }
86
+ return undefined;
87
+ }
39
88
  /**
40
89
  * Every per-instance style override an instance node carries, keyed the way
41
90
  * it is stored (AGL-1332): {@link STYLE_OVERRIDES_ROOT_KEY} for the
42
91
  * component root, and a DEFINITION-internal node id for each overridden
43
- * leaf. Empty for a node that is not an instance or carries none.
92
+ * leaf. Empty for a node that is not a placement ({@link
93
+ * isDefinitionPlacement}: an instance or a placed form) or carries none.
44
94
  *
45
95
  * Keyed on the definition's own id, never the grafted `cmp__…` id: the
46
96
  * graft id is DERIVED from the instance id, so keying on it would break the
@@ -56,7 +106,7 @@ const isStyleRecord = (value)=>typeof value === 'object' && value !== null && !A
56
106
  * {@link composeReusableComponentNodes}, so they cannot disagree about
57
107
  * what an override means.
58
108
  */ export function getInstanceStyleOverrides(node) {
59
- if ((node == null ? void 0 : node.componentId) !== REUSABLE_INSTANCE_COMPONENT_ID) return {};
109
+ if (!isDefinitionPlacement(node)) return {};
60
110
  const overrides = node == null ? void 0 : node.styleOverrides;
61
111
  if (!isStyleRecord(overrides)) return {};
62
112
  const clean = {};
@@ -117,6 +167,65 @@ const isStyleRecord = (value)=>typeof value === 'object' && value !== null && !A
117
167
  */ const ATTR_OVERRIDE_REFUSED_PROPS = new Set([
118
168
  'sx'
119
169
  ]);
170
+ /**
171
+ * Props a placed form's attribute overrides must never address, on any node
172
+ * of its design (AGL-3285).
173
+ *
174
+ * A form is edited once and every page placing it follows — that is the
175
+ * whole point of the form entity. Its submission list, its dataset columns,
176
+ * its consent tick and its routing are all keyed to what the ENTITY declares,
177
+ * so a page that renamed a field, retyped it, made it optional, rewrote its
178
+ * choices or re-pointed where the form sends would file answers the form's
179
+ * own schema does not describe, from one page only, with nothing on the
180
+ * Forms page saying so. Per page, a form may LOOK different; it may not
181
+ * collect something different.
182
+ *
183
+ * - The form node's identity and submission config: `formId`, `formName`,
184
+ * `datasetId`, `datasetName`, and the after-submit outcome
185
+ * (`afterSubmit`, `redirectScreenId`, `redirectUrl`, `revealNodeId`).
186
+ * - Each field's contract: `fieldName` (the submission key),
187
+ * `datasetFieldId`, `fieldType`, `options` (a ticked option's TEXT is the
188
+ * posted value, and the consent check reads it) and `required`.
189
+ * - Element attributes a browser submission reads directly: `name`, `type`
190
+ * (a submit button made a plain button no longer submits), `value`,
191
+ * `action`, `method`, and `disabled` (a disabled input is not posted).
192
+ * - The hide directives: a hidden field is pruned from the page, so its
193
+ * value is never sent.
194
+ *
195
+ * Label, placeholder, help text, the submit label and success message, and
196
+ * everything about styling stay overridable.
197
+ */ export const PLACED_FORM_REFUSED_ATTR_PROPS = new Set([
198
+ FORM_ID_PROP,
199
+ 'formName',
200
+ 'datasetId',
201
+ 'datasetName',
202
+ 'afterSubmit',
203
+ 'redirectScreenId',
204
+ 'redirectUrl',
205
+ 'revealNodeId',
206
+ 'fieldName',
207
+ 'datasetFieldId',
208
+ 'fieldType',
209
+ 'options',
210
+ 'required',
211
+ 'name',
212
+ 'type',
213
+ 'value',
214
+ 'action',
215
+ 'method',
216
+ 'disabled',
217
+ NODE_HIDE_IF_PROP,
218
+ NODE_HIDE_UNLESS_PROP
219
+ ]);
220
+ /**
221
+ * Whether `prop` may never be an attribute override on this placement — `sx`
222
+ * on every placement, plus {@link PLACED_FORM_REFUSED_ATTR_PROPS} on a placed
223
+ * form. The render layer and the editor's writer both ask this, so a refused
224
+ * prop can neither be offered nor take effect.
225
+ */ export function isAttrOverrideRefused(placement, prop) {
226
+ if (ATTR_OVERRIDE_REFUSED_PROPS.has(prop)) return true;
227
+ return isPlacedFormNode(placement) && PLACED_FORM_REFUSED_ATTR_PROPS.has(prop);
228
+ }
120
229
  /**
121
230
  * Every per-instance ATTRIBUTE override an instance node carries, keyed the
122
231
  * way {@link getInstanceStyleOverrides} keys styles — `root` for the
@@ -127,7 +236,7 @@ const isStyleRecord = (value)=>typeof value === 'object' && value !== null && !A
127
236
  * that reached storage before a refusal existed (or by any path that did not
128
237
  * go through the panel) still cannot take effect.
129
238
  */ export function getInstanceAttrOverrides(node) {
130
- if ((node == null ? void 0 : node.componentId) !== REUSABLE_INSTANCE_COMPONENT_ID) return {};
239
+ if (!isDefinitionPlacement(node)) return {};
131
240
  const overrides = node == null ? void 0 : node.attrOverrides;
132
241
  if (!isStyleRecord(overrides)) return {};
133
242
  const clean = {};
@@ -135,7 +244,7 @@ const isStyleRecord = (value)=>typeof value === 'object' && value !== null && !A
135
244
  if (!key || !isStyleRecord(value)) continue;
136
245
  const slice = {};
137
246
  for (const [prop, propValue] of Object.entries(value)){
138
- if (ATTR_OVERRIDE_REFUSED_PROPS.has(prop)) continue;
247
+ if (isAttrOverrideRefused(node, prop)) continue;
139
248
  slice[prop] = propValue;
140
249
  }
141
250
  if (Object.keys(slice).length > 0) clean[key] = slice;
@@ -850,6 +959,9 @@ function hasVisibilityDirective(props) {
850
959
  // Per-leaf is not a nicety: a component's text leaves usually set
851
960
  // their OWN colour, so a root-level `color` never reaches them and an
852
961
  // instance given a white background renders white-on-white (AGL-1318).
962
+ //
963
+ // A placed form carries the same two layers (AGL-3285), so one page can
964
+ // restyle a field's label without editing the form for every page.
853
965
  const styleOverrides = getInstanceStyleOverrides(instanceNode);
854
966
  // Attribute overrides (AGL-1899) ride the same graft for the same
855
967
  // reason: this is the one point where per-instance scope exists, so
@@ -909,6 +1021,7 @@ function hasVisibilityDirective(props) {
909
1021
  const rootNamesItself = Boolean(grafting) && refIdOf(definitionRoot) === grafting;
910
1022
  const graftedRoot = next[graftedRootId];
911
1023
  if (rootNamesItself && graftedRoot) {
1024
+ var _instanceNode_props;
912
1025
  const adopted = Array.isArray(graftedRoot.nodes) ? graftedRoot.nodes : [];
913
1026
  for (const childId of adopted){
914
1027
  if (next[childId]) {
@@ -918,11 +1031,24 @@ function hasVisibilityDirective(props) {
918
1031
  }
919
1032
  }
920
1033
  delete next[graftedRootId];
921
- next[instanceId] = _extends({}, instanceNode, {
1034
+ /*
1035
+ * The root's own override slices land on the instance, because the
1036
+ * instance IS the root here (AGL-3285). The grafted root carried them
1037
+ * and has just been dropped; without this a placed form's `root`
1038
+ * target would save and never render. Same precedence as the merge
1039
+ * branch below: the slice first, the placement's own `sx` over it.
1040
+ */ const rootSx = styleOverrides[STYLE_OVERRIDES_ROOT_KEY];
1041
+ const rootAttrs = attrOverrides[STYLE_OVERRIDES_ROOT_KEY];
1042
+ const unwrappedSx = rootSx ? mergeNodeSx(rootSx, instanceNode.sx) : undefined;
1043
+ next[instanceId] = _extends({}, instanceNode, unwrappedSx === undefined ? {} : {
1044
+ sx: unwrappedSx
1045
+ }, rootAttrs ? {
1046
+ props: _extends({}, (_instanceNode_props = instanceNode.props) != null ? _instanceNode_props : {}, rootAttrs)
1047
+ } : {}, {
922
1048
  nodes: adopted
923
1049
  });
924
1050
  } else if (graftedRoot) {
925
- var _graftedRoot_props, _instanceNode_props;
1051
+ var _graftedRoot_props, _instanceNode_props1;
926
1052
  /*
927
1053
  * THE INSTANCE AND THE DEFINITION'S ROOT ARE ONE ELEMENT (AGL-2521).
928
1054
  *
@@ -979,8 +1105,8 @@ function hasVisibilityDirective(props) {
979
1105
  ];
980
1106
  const carried = {};
981
1107
  for (const key of INSTANCE_CARRIED_PROPS){
982
- var _instanceNode_props1;
983
- const value = (_instanceNode_props1 = instanceNode.props) == null ? void 0 : _instanceNode_props1[key];
1108
+ var _instanceNode_props2;
1109
+ const value = (_instanceNode_props2 = instanceNode.props) == null ? void 0 : _instanceNode_props2[key];
984
1110
  if (value !== undefined) carried[key] = value;
985
1111
  }
986
1112
  /*
@@ -998,8 +1124,8 @@ function hasVisibilityDirective(props) {
998
1124
  */ const repeatsPlacement = repeatKey(instanceNode) !== '';
999
1125
  if (repeatsPlacement) {
1000
1126
  for (const key of REPEAT_DIRECTIVE_PROPS){
1001
- var _instanceNode_props2;
1002
- const value = (_instanceNode_props2 = instanceNode.props) == null ? void 0 : _instanceNode_props2[key];
1127
+ var _instanceNode_props3;
1128
+ const value = (_instanceNode_props3 = instanceNode.props) == null ? void 0 : _instanceNode_props3[key];
1003
1129
  if (value !== undefined) carried[key] = value;
1004
1130
  }
1005
1131
  carried[REPEAT_SELF_PROP] = true;
@@ -1014,7 +1140,7 @@ function hasVisibilityDirective(props) {
1014
1140
  * element answering to the placement's id, which after the merge is
1015
1141
  * this one. Keeping either list alone drops the other author's names,
1016
1142
  * so the two are joined, each name once, in `props` and on the node.
1017
- */ const propsClassName = joinClassNames((_graftedRoot_props = graftedRoot.props) == null ? void 0 : _graftedRoot_props['className'], (_instanceNode_props = instanceNode.props) == null ? void 0 : _instanceNode_props['className']);
1143
+ */ const propsClassName = joinClassNames((_graftedRoot_props = graftedRoot.props) == null ? void 0 : _graftedRoot_props['className'], (_instanceNode_props1 = instanceNode.props) == null ? void 0 : _instanceNode_props1['className']);
1018
1144
  if (propsClassName) carried['className'] = propsClassName;
1019
1145
  const nodeClassName = joinClassNames(graftedRoot.className, instanceNode.className);
1020
1146
  // The override slices and the reference are DOCUMENT state living on