@aglyn/plugins-ai 1.0.0-beta.225 → 1.0.0-beta.227

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 (161) hide show
  1. package/README.md +2 -1
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +35 -17
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +17 -2
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  13. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  14. package/src/lib/components/ai-job-events.d.ts +14 -0
  15. package/src/lib/components/ai-job-events.js +76 -0
  16. package/src/lib/components/ai-job-events.js.map +1 -0
  17. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  18. package/src/lib/components/ai-job-follow.component.js +210 -0
  19. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  20. package/src/lib/components/ai-job-links.d.ts +42 -0
  21. package/src/lib/components/ai-job-links.js +140 -0
  22. package/src/lib/components/ai-job-links.js.map +1 -0
  23. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  24. package/src/lib/components/ai-job-plan.component.js +29 -3
  25. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  26. package/src/lib/components/ai-job-requests.d.ts +24 -0
  27. package/src/lib/components/ai-job-requests.js +76 -0
  28. package/src/lib/components/ai-job-requests.js.map +1 -0
  29. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  30. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  31. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  32. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  33. package/src/lib/components/ai-jobs-store.js +218 -0
  34. package/src/lib/components/ai-jobs-store.js.map +1 -0
  35. package/src/lib/components/ai-page-brief-dialog.component.d.ts +3 -3
  36. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  37. package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
  38. package/src/lib/components/ai-site-build-page.component.js +377 -0
  39. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  40. package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
  41. package/src/lib/components/ai-site-start-card.component.js +522 -130
  42. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  43. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  44. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  45. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  46. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  47. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  48. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  49. package/src/lib/components/assist-panel.component.js +62 -6
  50. package/src/lib/components/assist-panel.component.js.map +1 -1
  51. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  52. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  53. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  54. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  55. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  56. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  57. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  58. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  59. package/src/lib/components/use-ai-job-run.js +5 -18
  60. package/src/lib/components/use-ai-job-run.js.map +1 -1
  61. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  64. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  65. package/src/lib/jobs/ai-job-plan-step.js +161 -17
  66. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
  68. package/src/lib/jobs/ai-job-site-step.js +34 -4
  69. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  70. package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
  71. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  73. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  74. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  75. package/src/lib/jobs/ai-jobs.d.ts +31 -4
  76. package/src/lib/jobs/ai-jobs.js +196 -32
  77. package/src/lib/jobs/ai-jobs.js.map +1 -1
  78. package/src/lib/model/ai-job-activity.d.ts +112 -0
  79. package/src/lib/model/ai-job-activity.js +144 -0
  80. package/src/lib/model/ai-job-activity.js.map +1 -0
  81. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  82. package/src/lib/model/ai-job-failure-copy.js +78 -0
  83. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  84. package/src/lib/model/ai-job-notice.d.ts +61 -0
  85. package/src/lib/model/ai-job-notice.js +112 -0
  86. package/src/lib/model/ai-job-notice.js.map +1 -0
  87. package/src/lib/model/ai-jobs.types.d.ts +26 -0
  88. package/src/lib/model/ai-jobs.types.js +1 -0
  89. package/src/lib/model/ai-jobs.types.js.map +1 -1
  90. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  91. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  92. package/src/lib/model/ai-site-build-progress.d.ts +45 -0
  93. package/src/lib/model/ai-site-build-progress.js +76 -0
  94. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  95. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  96. package/src/lib/model/ai-site-inventory.js +7 -2
  97. package/src/lib/model/ai-site-inventory.js.map +1 -1
  98. package/src/lib/model/ai-site-job.d.ts +93 -2
  99. package/src/lib/model/ai-site-job.js +88 -7
  100. package/src/lib/model/ai-site-job.js.map +1 -1
  101. package/src/lib/model/ai-site-start.d.ts +3 -1
  102. package/src/lib/model/ai-site-start.js +6 -4
  103. package/src/lib/model/ai-site-start.js.map +1 -1
  104. package/src/lib/model/ai-template-subjects.d.ts +4 -3
  105. package/src/lib/model/ai-template-subjects.js +5 -4
  106. package/src/lib/model/ai-template-subjects.js.map +1 -1
  107. package/src/lib/plan-entitlements.d.ts +3 -3
  108. package/src/lib/plan-entitlements.js +3 -3
  109. package/src/lib/plan-entitlements.js.map +1 -1
  110. package/src/lib/plugin.js +28 -0
  111. package/src/lib/plugin.js.map +1 -1
  112. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  113. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  114. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  115. package/src/lib/runtime/ai-doctrine-validators.js +31 -4
  116. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  117. package/src/lib/runtime/ai-gate.d.ts +3 -2
  118. package/src/lib/runtime/ai-gate.js +3 -2
  119. package/src/lib/runtime/ai-gate.js.map +1 -1
  120. package/src/lib/runtime/ai-palette.generated.js +12 -4
  121. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  122. package/src/lib/runtime/site-inventory.d.ts +16 -1
  123. package/src/lib/runtime/site-inventory.js +43 -3
  124. package/src/lib/runtime/site-inventory.js.map +1 -1
  125. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  126. package/src/lib/server/ai-admin-credits.js +213 -0
  127. package/src/lib/server/ai-admin-credits.js.map +1 -0
  128. package/src/lib/server/ai-admin-org.js +12 -5
  129. package/src/lib/server/ai-admin-org.js.map +1 -1
  130. package/src/lib/server/ai-admin-signals.js +3 -1
  131. package/src/lib/server/ai-admin-signals.js.map +1 -1
  132. package/src/lib/server/ai-admin-user.js +12 -1
  133. package/src/lib/server/ai-admin-user.js.map +1 -1
  134. package/src/lib/server/ai-allotments.js +3 -2
  135. package/src/lib/server/ai-allotments.js.map +1 -1
  136. package/src/lib/server/ai-jobs-route.js +20 -2
  137. package/src/lib/server/ai-jobs-route.js.map +1 -1
  138. package/src/lib/server/assist-chat.js +3 -2
  139. package/src/lib/server/assist-chat.js.map +1 -1
  140. package/src/lib/server/billing-credits.js +4 -3
  141. package/src/lib/server/billing-credits.js.map +1 -1
  142. package/src/lib/server/billing-overage.js +3 -2
  143. package/src/lib/server/billing-overage.js.map +1 -1
  144. package/src/lib/server.js +11 -0
  145. package/src/lib/server.js.map +1 -1
  146. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  147. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  148. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  149. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  150. package/src/lib/usage/assist-credit-returns.js +79 -0
  151. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  152. package/src/lib/usage/assist-free-taste.js +6 -3
  153. package/src/lib/usage/assist-free-taste.js.map +1 -1
  154. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  155. package/src/lib/usage/assist-job-refund.js +109 -0
  156. package/src/lib/usage/assist-job-refund.js.map +1 -0
  157. package/src/lib/usage/assist-usage.js +5 -2
  158. package/src/lib/usage/assist-usage.js.map +1 -1
  159. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  160. package/src/lib/usage/staff-org-ai.js +19 -2
  161. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -47,8 +47,9 @@ import { type AssistReservation } from '../usage/assist-usage';
47
47
  * the trusted-hop client address, so a farm rotating accounts
48
48
  * behind one address meets one budget; skipped when no address is
49
49
  * readable
50
- * 403 account age (AGL-2925): a Free workspace's caller must hold an
51
- * account older than `AI_FREE_MIN_ACCOUNT_AGE_HOURS`; paid
50
+ * 403 account age (AGL-2925), off by default (AGL-3591): when
51
+ * `AI_FREE_MIN_ACCOUNT_AGE_HOURS` is set, a Free workspace's caller
52
+ * must hold an account at least that old; paid
52
53
  * workspaces and staff never consult it, and a record that cannot
53
54
  * be read is a 503 rather than an admission
54
55
  * --- the reservation, which fails CLOSED: it is the only global, atomic
@@ -163,8 +163,9 @@ function bearerToken(request) {
163
163
  headers: rateLimitHeaders(ipRate)
164
164
  });
165
165
  }
166
- // A Free workspace's caller must have held an account for a day
167
- // (AGL-2925). Read from the pool the token was minted in — a tenant
166
+ // A Free workspace's caller must have held an account for
167
+ // `AI_FREE_MIN_ACCOUNT_AGE_HOURS` (AGL-2925) — zero, so off, unless a
168
+ // deployment sets it (AGL-3591). Read from the pool the token was minted in — a tenant
168
169
  // user's record is not in the project pool — and after the rate limits,
169
170
  // so a burst cannot turn one cached read into many.
170
171
  const tooYoung = await freeAccountAgeRefusal({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-gate.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 { DecodedIdToken } from 'firebase-admin/auth'\nimport {\n checkEntitlement,\n type LockdownFeatureKey,\n type ReleaseFlagKey,\n} from '@aglyn/aglyn/server'\nimport {\n assistFreeTasteRefusalText,\n assistHardCapRefusalText,\n assistRefusedByHardCap,\n} from '../usage/assist-credits'\nimport type { AglynOrganization } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport type { OrgFeatureFlags } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n checkAiClientIpRateLimit,\n freeAccountAgeRefusal,\n} from './ai-abuse-guards'\nimport {\n checkRateLimit,\n rateLimitHeaders,\n type RateLimitResult,\n} from '@aglyn/tenant-data-admin/server/api-http'\nimport { aiOverageReservationRefusal } from '../billing/ai-overage-gate'\nimport { recordUserAiRefusal } from '../usage/ai-usage-by-user'\nimport { authForPool } from '@aglyn/tenant-data-admin/server/auth-pools'\nimport {\n publicAssistQuota,\n reserveAssistMessage,\n type AssistReservation,\n} from '../usage/assist-usage'\nimport { aiAllotmentRefusalText } from '../model/ai-allotments'\nimport { aiUsageMeter } from '../usage/ai-usage-meter'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { featureLockdownRefusal, lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport {\n permissionRefusal,\n getOrgForUser,\n memberHasPermissionOnHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\n\n/**\n * The gate ladder every AI door climbs before it spends a token (AGL-2903).\n *\n * The rungs, in order — each one a refusal the door sends as is, and each\n * composed from the helper the existing doors already call rather than a\n * second implementation of it:\n *\n * 405 not POST\n * 401 no bearer token, or a token the verifier refused\n * 403 email unverified (an impersonation session is exempt)\n * 400 the body did not NAME the org it is metered against (AGL-1934)\n * 403 the caller is not a member of that org\n * 403 the member's role lacks the door's AI permission (AGL-2927) — only\n * when the door names one; a verified staff claim passes, as it does\n * at every other org route\n * 404 the release flag is off — a released-off feature does not exist;\n * a verified staff claim previews through it\n * 403 the org's plan lacks the entitlement — a plan-less org resolves as\n * free, which is a real answer, never a loading default\n * 423 lockdown: the scope verdict (platform/org/user) and then the\n * feature kill switch, with its per-feature staff bypass\n * 429 the per-uid rate limit, which fails SOFT: it is a per-instance\n * window that smooths bursts and is not the spend bound\n * 429 the per-address rate limit (AGL-2925), the same window keyed on\n * the trusted-hop client address, so a farm rotating accounts\n * behind one address meets one budget; skipped when no address is\n * readable\n * 403 account age (AGL-2925): a Free workspace's caller must hold an\n * account older than `AI_FREE_MIN_ACCOUNT_AGE_HOURS`; paid\n * workspaces and staff never consult it, and a record that cannot\n * be read is a 503 rather than an admission\n * --- the reservation, which fails CLOSED: it is the only global, atomic\n * bound on provider spend, so a reservation that cannot be taken\n * refuses with 503 rather than calling the provider uncapped. On a\n * Free workspace it also decides the taste's own rungs — the\n * account's daily requests, its refusal pause, its monthly\n * allowance across every workspace it owns, and the platform-wide\n * daily ceiling — inside the same transaction\n * 429 a hard AI allotment (AGL-2942) — the caller's, the caller's on the\n * named site, or the site's — decided inside the same transaction\n * once the workspace's own ceilings admitted the request, so it sits\n * inside the band and never admits what the band refused\n *\n * A door that gets a context back has a reservation in hand and owes the\n * meter a record — or `releaseAssistMessage` if the provider was never\n * reached. What differs between doors is parameters: which entitlement,\n * which flag, which lockdown key, which rate window.\n *\n * The console chat route keeps its own copy of these rungs rather than\n * calling this: its retrieval-first design (AGL-2486) answers from the\n * docs BETWEEN the rate limit and the reservation, so its ladder is not a\n * prefix of this one, and it resolves the entitlement after the rate limit\n * because a free workspace is admitted in limited mode rather than refused.\n * The copy assistant differs the same way at the entitlement and carries no\n * flag by design. Both are documented at the route.\n */\nexport interface AiGateConfig {\n /** The plan entitlement the door sells under (`checkEntitlement`). */\n feature: keyof OrgFeatureFlags\n /** The release flag that makes the door exist. */\n releaseFlag: ReleaseFlagKey\n /** The feature kill switch on the staff lockdown page. */\n lockdownFeature: LockdownFeatureKey\n /**\n * The org permission the door sells under (AGL-2927): `ai.use` for the\n * assistants, `ai.generate` for a generation job. Resolved by\n * `memberHasPermissionOnHost` on the org axis or, for a site collaborator, on\n * the host the body named (`AiGateInput.hostId`). Omitted, the rung is\n * skipped — which is the right reading for a door that has not decided\n * which key it sells under, and the wrong one for any door a customer\n * can reach, so every shipped door names one.\n */\n permission?: 'ai.use' | 'ai.generate'\n rateLimit: {\n /** Prefix of the per-uid key, so two doors do not share one window. */\n key: string\n limit: number\n windowMs: number\n }\n}\n\nexport interface AiGateInput {\n request: Request\n /** The org the parsed body named; empty when it named none. */\n orgId: string\n /**\n * The site the parsed body named, when it named one. A collaborator's\n * permission is decided on this site; an org-wide member's is the same\n * everywhere, so a door on an org-level surface may leave it empty.\n */\n hostId?: string | null\n now?: Date\n}\n\n/** Everything a door needs after the ladder admitted the request. */\nexport interface AiGateContext {\n uid: string\n decoded: DecodedIdToken\n staff: boolean\n orgId: string\n org: Partial<AglynOrganization>\n firestore: FirebaseFirestore.Firestore\n rate: RateLimitResult\n reservation: AssistReservation\n}\n\nfunction bearerToken(request: Request): string | undefined {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n}\n\n/**\n * Climb the ladder. Resolves to the `Response` to send when a rung\n * refuses, or to the resolved context when every rung admitted the request.\n */\nexport async function aiGateLadder(\n input: AiGateInput,\n config: AiGateConfig,\n): Promise<Response | AiGateContext> {\n const { request } = input\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const idToken = bearerToken(request)\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n\n const app = firebaseAdmin.app()\n let decoded: DecodedIdToken\n try {\n decoded = await app.auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's fault and a 401; anything else\n // — the verifier's own key endpoint unreachable — is ours, and it\n // propagates so the door answers 500 rather than telling every user\n // their credential is bad during an outage (AGL-1993).\n if (isRefusedIdToken(error)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n throw error\n }\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n\n const orgId = input.orgId.trim()\n if (!orgId) {\n return Response.json(\n { error: 'Open a workspace before using this feature' },\n { status: 400 },\n )\n }\n\n // Scoped to the NAMED org: membership of some other paid org is not a key\n // to this one, and the org this returns is the one every later rung and\n // the meter read.\n const resolved = await getOrgForUser(decoded.uid, orgId)\n if (!resolved || resolved.orgId !== orgId) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n const org = resolved.org ?? {}\n\n // Permission (AGL-2927), directly after membership: it is a fact about\n // the CALLER, so it is answered before anything about the workspace —\n // the plan, the lockdown state, the quota — is disclosed. Staff pass, as\n // they do at every other org route.\n if (\n config.permission &&\n !staff &&\n !(await memberHasPermissionOnHost(\n orgId,\n input.hostId,\n resolved.member,\n config.permission,\n ))\n ) {\n return permissionRefusal(config.permission)\n }\n\n // The flag closes the ROUTE, not just the UI (AGL-1653): a released-off\n // feature does not exist, so nothing below this line — not the plan, not\n // the lockdown state — is disclosed while it is off.\n if (!staff && !(await isServerReleaseFlagOnForOrg(config.releaseFlag, orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n\n if (!checkEntitlement(org, config.feature)) {\n return Response.json(\n {\n error: \"This workspace's plan does not include that feature\",\n reason: 'entitlement',\n },\n { status: 403 },\n )\n }\n\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: org as Record<string, unknown>,\n })\n if (locked) return locked\n const featureLocked = await featureLockdownRefusal({\n feature: config.lockdownFeature,\n staff,\n // The workspace-scoped pause on the same key (AGL-2927): the staff org\n // page's spend stop, which leaves the entitlement in place.\n orgId,\n })\n if (featureLocked) return featureLocked\n\n const rate = checkRateLimit(`${config.rateLimit.key}:${decoded.uid}`, {\n limit: config.rateLimit.limit,\n windowMs: config.rateLimit.windowMs,\n })\n if (!rate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(rate) },\n )\n }\n // The same window keyed on the address (AGL-2925): a farm rotating\n // fresh accounts behind one NAT meets one budget rather than one per\n // account. Answered with the same status and reason so the panel's\n // handling is unchanged.\n const ipRate = checkAiClientIpRateLimit(request.headers)\n if (ipRate && !ipRate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(ipRate) },\n )\n }\n\n // A Free workspace's caller must have held an account for a day\n // (AGL-2925). Read from the pool the token was minted in — a tenant\n // user's record is not in the project pool — and after the rate limits,\n // so a burst cannot turn one cached read into many.\n const tooYoung = await freeAccountAgeRefusal({\n uid: decoded.uid,\n org,\n staff,\n getUser: (uid) => authForPool(decoded.firebase?.tenant).getUser(uid),\n now: input.now,\n })\n if (tooYoung) return tooYoung\n\n // RESERVED, not merely checked (AGL-2057/2073): read-and-increment in one\n // transaction BEFORE a token is spent, so N concurrent requests cannot all\n // read \"under the cap\", and a caller that hangs up has been counted. The\n // org document is passed so the plan's own band binds, not only the\n // operator backstop.\n const firestore = app.firestore()\n let reservation: AssistReservation\n try {\n reservation = await reserveAssistMessage(\n firestore,\n orgId,\n true,\n input.now ?? new Date(),\n org,\n // Who is asking and on which site, for the allotments that apply.\n { uid: decoded.uid, hostId: input.hostId ?? null },\n )\n } catch (error) {\n console.error('ai reservation failed', error)\n return Response.json(\n { error: 'This feature is temporarily unavailable' },\n { status: 503 },\n )\n }\n // A refusal is the caller's as well as the workspace's (AGL-2928): the\n // per-person month counts it beside the org counter the reservation moved.\n recordUserAiRefusal(firestore, orgId, decoded.uid, reservation)\n if (!reservation.allowed) {\n const refusedBy = reservation.refusedBy\n // The usage strip's envelope (AGL-2942), on the refusal as on an answer,\n // so the strip shows why the request stopped without a read of its own.\n const meter = aiUsageMeter(reservation)\n // A hard allotment is a monthly line a manager drew inside the band: a\n // 429 like the message cap, because it resets on the calendar, with the\n // sentence naming who can raise it.\n if (refusedBy === 'allotment') {\n return Response.json(\n {\n error: aiAllotmentRefusalText(reservation.allotment?.refusal?.scope),\n reason: 'quota',\n refusedBy,\n quota: publicAssistQuota(reservation),\n meter,\n },\n { status: 429 },\n )\n }\n // AGLYN'S OWN OVERAGE GUARDS (AGL-3011), before the workspace's own\n // controls because they are the narrower answer: a refusal here already\n // knows WHICH limit stopped the request and whether the workspace can do\n // anything about it. Everything else falls through unchanged.\n const overage = aiOverageReservationRefusal(reservation)\n if (overage) {\n return Response.json(\n {\n error: overage.text,\n reason: 'quota',\n refusedBy,\n quota: publicAssistQuota(reservation),\n meter,\n },\n { status: overage.status },\n )\n }\n // The org's own wall is a 402 (AGL-2653): credits past the band are for\n // sale and this workspace switched the sale off, so the sentence names\n // the switch. A spend ceiling and a message cap keep the 429, and are\n // told apart in words because one resets on a clock and one does not.\n const hardCapped = assistRefusedByHardCap(org, refusedBy)\n return Response.json(\n {\n error: hardCapped\n ? assistHardCapRefusalText(org)\n : // The Free taste's own precautions (AGL-2925) have their own\n // sentences: each names a clock or an upgrade, never a figure.\n (assistFreeTasteRefusalText(refusedBy) ??\n (refusedBy === 'budget' || refusedBy === 'band'\n ? reservation.budgetUsd === null\n ? 'This workspace reached its AI spending limit for the month'\n : 'This workspace used its AI credits for the month'\n : 'This workspace reached its AI limit for the month')),\n reason: 'quota',\n // CREDITS, never the reservation itself — its figures are our\n // provider bill and do not leave the server.\n quota: publicAssistQuota(reservation),\n meter,\n },\n { status: hardCapped ? 402 : 429 },\n )\n }\n\n return {\n uid: decoded.uid,\n decoded,\n staff,\n orgId,\n org,\n firestore,\n rate,\n reservation,\n }\n}\n"],"names":["checkEntitlement","assistFreeTasteRefusalText","assistHardCapRefusalText","assistRefusedByHardCap","checkAiClientIpRateLimit","freeAccountAgeRefusal","checkRateLimit","rateLimitHeaders","aiOverageReservationRefusal","recordUserAiRefusal","authForPool","publicAssistQuota","reserveAssistMessage","aiAllotmentRefusalText","aiUsageMeter","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","isRefusedIdToken","featureLockdownRefusal","lockdownRefusal","permissionRefusal","getOrgForUser","memberHasPermissionOnHost","isServerReleaseFlagOnForOrg","bearerToken","request","authorization","headers","get","startsWith","slice","length","undefined","aiGateLadder","input","config","resolved","method","Response","json","error","status","idToken","app","decoded","auth","verifyIdToken","email_verified","staff","orgId","trim","uid","org","permission","hostId","member","releaseFlag","feature","reason","locked","featureLocked","lockdownFeature","rate","rateLimit","key","limit","windowMs","allowed","ipRate","tooYoung","getUser","firebase","tenant","now","firestore","reservation","Date","console","refusedBy","meter","allotment","refusal","scope","quota","overage","text","hardCapped","budgetUsd"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-gate.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 { DecodedIdToken } from 'firebase-admin/auth'\nimport {\n checkEntitlement,\n type LockdownFeatureKey,\n type ReleaseFlagKey,\n} from '@aglyn/aglyn/server'\nimport {\n assistFreeTasteRefusalText,\n assistHardCapRefusalText,\n assistRefusedByHardCap,\n} from '../usage/assist-credits'\nimport type { AglynOrganization } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport type { OrgFeatureFlags } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport {\n checkAiClientIpRateLimit,\n freeAccountAgeRefusal,\n} from './ai-abuse-guards'\nimport {\n checkRateLimit,\n rateLimitHeaders,\n type RateLimitResult,\n} from '@aglyn/tenant-data-admin/server/api-http'\nimport { aiOverageReservationRefusal } from '../billing/ai-overage-gate'\nimport { recordUserAiRefusal } from '../usage/ai-usage-by-user'\nimport { authForPool } from '@aglyn/tenant-data-admin/server/auth-pools'\nimport {\n publicAssistQuota,\n reserveAssistMessage,\n type AssistReservation,\n} from '../usage/assist-usage'\nimport { aiAllotmentRefusalText } from '../model/ai-allotments'\nimport { aiUsageMeter } from '../usage/ai-usage-meter'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { featureLockdownRefusal, lockdownRefusal } from '@aglyn/tenant-data-admin/server/lockdown'\nimport {\n permissionRefusal,\n getOrgForUser,\n memberHasPermissionOnHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin/server/release-flags'\n\n/**\n * The gate ladder every AI door climbs before it spends a token (AGL-2903).\n *\n * The rungs, in order — each one a refusal the door sends as is, and each\n * composed from the helper the existing doors already call rather than a\n * second implementation of it:\n *\n * 405 not POST\n * 401 no bearer token, or a token the verifier refused\n * 403 email unverified (an impersonation session is exempt)\n * 400 the body did not NAME the org it is metered against (AGL-1934)\n * 403 the caller is not a member of that org\n * 403 the member's role lacks the door's AI permission (AGL-2927) — only\n * when the door names one; a verified staff claim passes, as it does\n * at every other org route\n * 404 the release flag is off — a released-off feature does not exist;\n * a verified staff claim previews through it\n * 403 the org's plan lacks the entitlement — a plan-less org resolves as\n * free, which is a real answer, never a loading default\n * 423 lockdown: the scope verdict (platform/org/user) and then the\n * feature kill switch, with its per-feature staff bypass\n * 429 the per-uid rate limit, which fails SOFT: it is a per-instance\n * window that smooths bursts and is not the spend bound\n * 429 the per-address rate limit (AGL-2925), the same window keyed on\n * the trusted-hop client address, so a farm rotating accounts\n * behind one address meets one budget; skipped when no address is\n * readable\n * 403 account age (AGL-2925), off by default (AGL-3591): when\n * `AI_FREE_MIN_ACCOUNT_AGE_HOURS` is set, a Free workspace's caller\n * must hold an account at least that old; paid\n * workspaces and staff never consult it, and a record that cannot\n * be read is a 503 rather than an admission\n * --- the reservation, which fails CLOSED: it is the only global, atomic\n * bound on provider spend, so a reservation that cannot be taken\n * refuses with 503 rather than calling the provider uncapped. On a\n * Free workspace it also decides the taste's own rungs — the\n * account's daily requests, its refusal pause, its monthly\n * allowance across every workspace it owns, and the platform-wide\n * daily ceiling — inside the same transaction\n * 429 a hard AI allotment (AGL-2942) — the caller's, the caller's on the\n * named site, or the site's — decided inside the same transaction\n * once the workspace's own ceilings admitted the request, so it sits\n * inside the band and never admits what the band refused\n *\n * A door that gets a context back has a reservation in hand and owes the\n * meter a record — or `releaseAssistMessage` if the provider was never\n * reached. What differs between doors is parameters: which entitlement,\n * which flag, which lockdown key, which rate window.\n *\n * The console chat route keeps its own copy of these rungs rather than\n * calling this: its retrieval-first design (AGL-2486) answers from the\n * docs BETWEEN the rate limit and the reservation, so its ladder is not a\n * prefix of this one, and it resolves the entitlement after the rate limit\n * because a free workspace is admitted in limited mode rather than refused.\n * The copy assistant differs the same way at the entitlement and carries no\n * flag by design. Both are documented at the route.\n */\nexport interface AiGateConfig {\n /** The plan entitlement the door sells under (`checkEntitlement`). */\n feature: keyof OrgFeatureFlags\n /** The release flag that makes the door exist. */\n releaseFlag: ReleaseFlagKey\n /** The feature kill switch on the staff lockdown page. */\n lockdownFeature: LockdownFeatureKey\n /**\n * The org permission the door sells under (AGL-2927): `ai.use` for the\n * assistants, `ai.generate` for a generation job. Resolved by\n * `memberHasPermissionOnHost` on the org axis or, for a site collaborator, on\n * the host the body named (`AiGateInput.hostId`). Omitted, the rung is\n * skipped — which is the right reading for a door that has not decided\n * which key it sells under, and the wrong one for any door a customer\n * can reach, so every shipped door names one.\n */\n permission?: 'ai.use' | 'ai.generate'\n rateLimit: {\n /** Prefix of the per-uid key, so two doors do not share one window. */\n key: string\n limit: number\n windowMs: number\n }\n}\n\nexport interface AiGateInput {\n request: Request\n /** The org the parsed body named; empty when it named none. */\n orgId: string\n /**\n * The site the parsed body named, when it named one. A collaborator's\n * permission is decided on this site; an org-wide member's is the same\n * everywhere, so a door on an org-level surface may leave it empty.\n */\n hostId?: string | null\n now?: Date\n}\n\n/** Everything a door needs after the ladder admitted the request. */\nexport interface AiGateContext {\n uid: string\n decoded: DecodedIdToken\n staff: boolean\n orgId: string\n org: Partial<AglynOrganization>\n firestore: FirebaseFirestore.Firestore\n rate: RateLimitResult\n reservation: AssistReservation\n}\n\nfunction bearerToken(request: Request): string | undefined {\n const authorization = request.headers.get('authorization') ?? ''\n return authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n}\n\n/**\n * Climb the ladder. Resolves to the `Response` to send when a rung\n * refuses, or to the resolved context when every rung admitted the request.\n */\nexport async function aiGateLadder(\n input: AiGateInput,\n config: AiGateConfig,\n): Promise<Response | AiGateContext> {\n const { request } = input\n if (request.method !== 'POST') {\n return Response.json({ error: 'Method not allowed' }, { status: 405 })\n }\n const idToken = bearerToken(request)\n if (!idToken) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n\n const app = firebaseAdmin.app()\n let decoded: DecodedIdToken\n try {\n decoded = await app.auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's fault and a 401; anything else\n // — the verifier's own key endpoint unreachable — is ours, and it\n // propagates so the door answers 500 rather than telling every user\n // their credential is bad during an outage (AGL-1993).\n if (isRefusedIdToken(error)) {\n return Response.json({ error: 'Unauthenticated' }, { status: 401 })\n }\n throw error\n }\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n\n const orgId = input.orgId.trim()\n if (!orgId) {\n return Response.json(\n { error: 'Open a workspace before using this feature' },\n { status: 400 },\n )\n }\n\n // Scoped to the NAMED org: membership of some other paid org is not a key\n // to this one, and the org this returns is the one every later rung and\n // the meter read.\n const resolved = await getOrgForUser(decoded.uid, orgId)\n if (!resolved || resolved.orgId !== orgId) {\n return Response.json(\n { error: 'You are not a member of that organization' },\n { status: 403 },\n )\n }\n const org = resolved.org ?? {}\n\n // Permission (AGL-2927), directly after membership: it is a fact about\n // the CALLER, so it is answered before anything about the workspace —\n // the plan, the lockdown state, the quota — is disclosed. Staff pass, as\n // they do at every other org route.\n if (\n config.permission &&\n !staff &&\n !(await memberHasPermissionOnHost(\n orgId,\n input.hostId,\n resolved.member,\n config.permission,\n ))\n ) {\n return permissionRefusal(config.permission)\n }\n\n // The flag closes the ROUTE, not just the UI (AGL-1653): a released-off\n // feature does not exist, so nothing below this line — not the plan, not\n // the lockdown state — is disclosed while it is off.\n if (!staff && !(await isServerReleaseFlagOnForOrg(config.releaseFlag, orgId))) {\n return Response.json({ error: 'Not found' }, { status: 404 })\n }\n\n if (!checkEntitlement(org, config.feature)) {\n return Response.json(\n {\n error: \"This workspace's plan does not include that feature\",\n reason: 'entitlement',\n },\n { status: 403 },\n )\n }\n\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org: org as Record<string, unknown>,\n })\n if (locked) return locked\n const featureLocked = await featureLockdownRefusal({\n feature: config.lockdownFeature,\n staff,\n // The workspace-scoped pause on the same key (AGL-2927): the staff org\n // page's spend stop, which leaves the entitlement in place.\n orgId,\n })\n if (featureLocked) return featureLocked\n\n const rate = checkRateLimit(`${config.rateLimit.key}:${decoded.uid}`, {\n limit: config.rateLimit.limit,\n windowMs: config.rateLimit.windowMs,\n })\n if (!rate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(rate) },\n )\n }\n // The same window keyed on the address (AGL-2925): a farm rotating\n // fresh accounts behind one NAT meets one budget rather than one per\n // account. Answered with the same status and reason so the panel's\n // handling is unchanged.\n const ipRate = checkAiClientIpRateLimit(request.headers)\n if (ipRate && !ipRate.allowed) {\n return Response.json(\n { error: 'Too many requests — slow down a moment', reason: 'rate' },\n { status: 429, headers: rateLimitHeaders(ipRate) },\n )\n }\n\n // A Free workspace's caller must have held an account for\n // `AI_FREE_MIN_ACCOUNT_AGE_HOURS` (AGL-2925) — zero, so off, unless a\n // deployment sets it (AGL-3591). Read from the pool the token was minted in — a tenant\n // user's record is not in the project pool — and after the rate limits,\n // so a burst cannot turn one cached read into many.\n const tooYoung = await freeAccountAgeRefusal({\n uid: decoded.uid,\n org,\n staff,\n getUser: (uid) => authForPool(decoded.firebase?.tenant).getUser(uid),\n now: input.now,\n })\n if (tooYoung) return tooYoung\n\n // RESERVED, not merely checked (AGL-2057/2073): read-and-increment in one\n // transaction BEFORE a token is spent, so N concurrent requests cannot all\n // read \"under the cap\", and a caller that hangs up has been counted. The\n // org document is passed so the plan's own band binds, not only the\n // operator backstop.\n const firestore = app.firestore()\n let reservation: AssistReservation\n try {\n reservation = await reserveAssistMessage(\n firestore,\n orgId,\n true,\n input.now ?? new Date(),\n org,\n // Who is asking and on which site, for the allotments that apply.\n { uid: decoded.uid, hostId: input.hostId ?? null },\n )\n } catch (error) {\n console.error('ai reservation failed', error)\n return Response.json(\n { error: 'This feature is temporarily unavailable' },\n { status: 503 },\n )\n }\n // A refusal is the caller's as well as the workspace's (AGL-2928): the\n // per-person month counts it beside the org counter the reservation moved.\n recordUserAiRefusal(firestore, orgId, decoded.uid, reservation)\n if (!reservation.allowed) {\n const refusedBy = reservation.refusedBy\n // The usage strip's envelope (AGL-2942), on the refusal as on an answer,\n // so the strip shows why the request stopped without a read of its own.\n const meter = aiUsageMeter(reservation)\n // A hard allotment is a monthly line a manager drew inside the band: a\n // 429 like the message cap, because it resets on the calendar, with the\n // sentence naming who can raise it.\n if (refusedBy === 'allotment') {\n return Response.json(\n {\n error: aiAllotmentRefusalText(reservation.allotment?.refusal?.scope),\n reason: 'quota',\n refusedBy,\n quota: publicAssistQuota(reservation),\n meter,\n },\n { status: 429 },\n )\n }\n // AGLYN'S OWN OVERAGE GUARDS (AGL-3011), before the workspace's own\n // controls because they are the narrower answer: a refusal here already\n // knows WHICH limit stopped the request and whether the workspace can do\n // anything about it. Everything else falls through unchanged.\n const overage = aiOverageReservationRefusal(reservation)\n if (overage) {\n return Response.json(\n {\n error: overage.text,\n reason: 'quota',\n refusedBy,\n quota: publicAssistQuota(reservation),\n meter,\n },\n { status: overage.status },\n )\n }\n // The org's own wall is a 402 (AGL-2653): credits past the band are for\n // sale and this workspace switched the sale off, so the sentence names\n // the switch. A spend ceiling and a message cap keep the 429, and are\n // told apart in words because one resets on a clock and one does not.\n const hardCapped = assistRefusedByHardCap(org, refusedBy)\n return Response.json(\n {\n error: hardCapped\n ? assistHardCapRefusalText(org)\n : // The Free taste's own precautions (AGL-2925) have their own\n // sentences: each names a clock or an upgrade, never a figure.\n (assistFreeTasteRefusalText(refusedBy) ??\n (refusedBy === 'budget' || refusedBy === 'band'\n ? reservation.budgetUsd === null\n ? 'This workspace reached its AI spending limit for the month'\n : 'This workspace used its AI credits for the month'\n : 'This workspace reached its AI limit for the month')),\n reason: 'quota',\n // CREDITS, never the reservation itself — its figures are our\n // provider bill and do not leave the server.\n quota: publicAssistQuota(reservation),\n meter,\n },\n { status: hardCapped ? 402 : 429 },\n )\n }\n\n return {\n uid: decoded.uid,\n decoded,\n staff,\n orgId,\n org,\n firestore,\n rate,\n reservation,\n }\n}\n"],"names":["checkEntitlement","assistFreeTasteRefusalText","assistHardCapRefusalText","assistRefusedByHardCap","checkAiClientIpRateLimit","freeAccountAgeRefusal","checkRateLimit","rateLimitHeaders","aiOverageReservationRefusal","recordUserAiRefusal","authForPool","publicAssistQuota","reserveAssistMessage","aiAllotmentRefusalText","aiUsageMeter","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","isRefusedIdToken","featureLockdownRefusal","lockdownRefusal","permissionRefusal","getOrgForUser","memberHasPermissionOnHost","isServerReleaseFlagOnForOrg","bearerToken","request","authorization","headers","get","startsWith","slice","length","undefined","aiGateLadder","input","config","resolved","method","Response","json","error","status","idToken","app","decoded","auth","verifyIdToken","email_verified","staff","orgId","trim","uid","org","permission","hostId","member","releaseFlag","feature","reason","locked","featureLocked","lockdownFeature","rate","rateLimit","key","limit","windowMs","allowed","ipRate","tooYoung","getUser","firebase","tenant","now","firestore","reservation","Date","console","refusedBy","meter","allotment","refusal","scope","quota","overage","text","hardCapped","budgetUsd"],"mappings":"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"}
@@ -2071,6 +2071,12 @@
2071
2071
  'static',
2072
2072
  'sticky'
2073
2073
  ]
2074
+ },
2075
+ shrinkOnScroll: {
2076
+ type: 'boolean'
2077
+ },
2078
+ hideOnScroll: {
2079
+ type: 'boolean'
2074
2080
  }
2075
2081
  },
2076
2082
  required: [],
@@ -2083,7 +2089,9 @@
2083
2089
  component: 'select',
2084
2090
  ariaLabel: 'text-field',
2085
2091
  color: 'select',
2086
- position: 'select'
2092
+ position: 'select',
2093
+ shrinkOnScroll: 'switch',
2094
+ hideOnScroll: 'switch'
2087
2095
  },
2088
2096
  textLimits: {
2089
2097
  ariaLabel: 200
@@ -5088,11 +5096,11 @@
5088
5096
  ]
5089
5097
  };
5090
5098
  /** The prompt catalog of each surface. */ export const AI_PALETTE_CATALOG = {
5091
- screen: "Surface: screen. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries: Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch: A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form: Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField: One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- image: A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a page or URL. — leaf — children=text≤40, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Page Link): Links to another page by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox: A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section: Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks: A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- video: Plays a video file from your media library or any URL. — leaf — src=media, poster=media\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack).",
5099
+ screen: "Surface: screen. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries: Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch: A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form: Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField: One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- image: A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky, shrinkOnScroll=boolean\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a page or URL. — leaf — children=text≤40, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Page Link): Links to another page by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox: A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section: Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks: A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- video: Plays a video file from your media library or any URL. — leaf — src=media, poster=media\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack).",
5092
5100
  email: 'Surface: email. Root: the document wrapper (componentId "div", id "_@_"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- emailButton: A call-to-action button that survives every mail client. — leaf — children=text≤40, href=url, backgroundColor=string, color=string, align=left|center|right\n- emailDivider: A horizontal rule between email sections. — leaf — color=string\n- emailImage: An email-safe image, capped at 600px wide, optionally linked. — leaf — src=media, alt=text≤200, width=text≤200, href=url, align=left|center|right\n- emailProduct: A product card resolved at send time, so it never goes stale. — leaf — buttonLabel=text≤40\n- emailSection: An email-safe band with its own background color and padding. — holds any — backgroundColor=string, padding=text≤200, align=left|center|right\n- emailSpacer: Fixed vertical whitespace between email blocks. — leaf — height=text≤200\n- emailText: A paragraph of email copy in one of the preset styles. — leaf — children=text≤2000, variant=heading|subheading|body|caption, color=string, align=left|center|right\nNamed blocks to imitate: Header (emailSection); Footer (emailSection).',
5093
5101
  form: 'Surface: form. Root: one "form" node.\nElements (id (name): purpose — children — props; * = required):\n- form: Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField: One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\nNamed blocks to imitate: Contact Form (form).',
5094
- layout: "Surface: layout. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries: Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch: A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form: Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField: One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- image: A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- layoutSlot: Marks where each bound page's content renders inside this layout. — leaf — caption=text≤200, component=main|div|section|article\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a page or URL. — leaf — children=text≤40, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Page Link): Links to another page by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox: A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section: Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks: A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- video: Plays a video file from your media library or any URL. — leaf — src=media, poster=media\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack).",
5095
- component: "Surface: component. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries: Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch: A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form: Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField: One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- icon: A single icon from the library, at the size and color you pick. — leaf — iconId=string, size=text≤200, color=text≤200\n- image: A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a page or URL. — leaf — children=text≤40, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Page Link): Links to another page by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox: A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section: Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks: A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- video: Plays a video file from your media library or any URL. — leaf — src=media, poster=media\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack)."
5102
+ layout: "Surface: layout. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries: Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch: A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form: Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField: One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- image: A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- layoutSlot: Marks where each bound page's content renders inside this layout. — leaf — caption=text≤200, component=main|div|section|article\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky, shrinkOnScroll=boolean\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a page or URL. — leaf — children=text≤40, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Page Link): Links to another page by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox: A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section: Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks: A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- video: Plays a video file from your media library or any URL. — leaf — src=media, poster=media\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack).",
5103
+ component: "Surface: component. Root: the document wrapper (componentId \"div\", id \"_@_\"); every other node carries an allowed componentId.\nElements (id (name): purpose — children — props; * = required):\n- collectionCategories (Category Pills): A pill per collection category, each filtering the listing. — leaf — collectionSlug=text≤200, allLabel=text≤200\n- collectionEntries: Repeats its children once per entry in a content collection. — holds any — collectionSlug=text≤200, entriesLimit=text≤200, filterCategory=text≤200, filterTag=text≤200, searchMode=filter|suggest\n- collectionEntryAuthor (Entry Author): The author card for an entry — portrait, byline and bio. — leaf — name=text≤200, bio=text≤2000, image=media, pageUrl=url, url=url\n- collectionEntryBody (Entry Body): The current entry's markdown body, on an entry template. — leaf — markdown=text≤200\n- collectionEntryMeta (Entry Meta): The byline row for an entry — author, date, category and tags. — leaf — date=text≤200, dateFormat=default|monthYear|mediumDate|longDate|iso, author=text≤200, category=text≤200, authorPageUrl=url\n- collectionRelated (Related Posts): Other entries sharing this one's category or tags. — leaf — heading=text≤200, layout=list|cards, headingVariant=displayXl|h1|h2|h3|h4|h5|…, titleVariant=displayXl|h1|h2|h3|h4|h5|…, dateFormat=default|monthYear|mediumDate|longDate|iso\n- collectionSearch: A search box for a content collection, with a suggestions dropdown. — leaf — collectionSlug=text≤200, searchPlaceholder=text≤200\n- collectionShare (Share Bar): Share buttons for the current page, plus a copy link. — leaf — heading=text≤200\n- contentAuthorProfile (Author Profile): Who an author page is about — portrait, name, role, bio and links. — leaf — name=text≤200, bio=text≤2000, image=media, jobTitle=text≤200, url=url\n- dataTable (Table): A grid of rows and columns — a feature matrix, a spec sheet, a comparison. — leaf — headerRow=boolean, emphasizeColumn=text≤200\n- form: Collects its Form Fields and sends the answers to you, or into a dataset. — holds any — formName=text≤200, datasetName=text≤200, afterSubmit=message|redirect|reveal, redirectScreenId=screen, redirectUrl=url\n- formField: One input inside a form — text, select, radio, checkbox or rating. — leaf — fieldName=text≤200, label=text≤200, placeholder=text≤200, fieldType=text|email|textarea|select|radio|checkbox|…, options=text≤2000\n- icon: A single icon from the library, at the size and color you pick. — leaf — iconId=string, size=text≤200, color=text≤200\n- image: A picture from your media library or any URL, with fit, size and an optional link. — leaf — src=media, objectFit=cover|contain|fill|none|scale-down, loading=lazy|eager, screenId=screen, href=url\n- muiAccordion (Accordion): Header that expands to reveal its details — FAQs, long forms, anything progressive. — holds only muiAccordionSummary|muiAccordionDetails — defaultExpanded=boolean, disableGutters=boolean, disabled=boolean\n- muiAccordionDetails (Accordion Details): The region an accordion reveals when expanded. — holds any — no props\n- muiAccordionSummary (Accordion Summary): The clickable header row of an accordion. — leaf — children=text≤2000, screenId=screen\n- muiAppBar (App Bar): The bar across the top of the site — put a Toolbar Content inside it. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, color=default|inherit|transparent|primary|secondary|tertiary, position=absolute|fixed|relative|static|sticky, shrinkOnScroll=boolean\n- muiBox (Box): Generic container with no styling of its own — style it from the styles panel. — holds any — component=div|span|p|figure|figcaption|blockquote|…, colorScheme=site|light|dark\n- muiBreadcrumbs (Breadcrumbs): Trail showing where a page sits. — holds any — separator=text≤200, maxItems=text≤200, itemsBeforeCollapse=text≤200, itemsAfterCollapse=text≤200, expandText=text≤200\n- muiButton (Button): A button — or a link styled as one, if you point it at a page or URL. — leaf — children=text≤40, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large, variant=text|outlined|contained, target=_self|_blank|custom\n- muiCard (Card): Surface that groups a header, content and actions about one subject. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, variant=elevation|outlined, elevation=text≤200\n- muiCardActions (Card Actions): The button row at the bottom of a card. — holds any — disableSpacing=boolean\n- muiCardContent (Card Content): The padded body region of a card. — holds any — no props\n- muiCardHeader (Card Header): The title and subheader at the top of a card. — leaf — title=text≤200, subheader=text≤200\n- muiContainer (Container): Centers your page content and caps how wide it gets. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, fixed=boolean, maxWidth=xs|sm|md|lg|xl, colorScheme=site|light|dark\n- muiGrid (Grid): Responsive 12-column layout. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, direction=row|row-reverse, wrap=wrap|nowrap|wrap-reverse, colorScheme=site|light|dark\n- muiImageList (Image List): Dense grid of images — standard, quilted, masonry or woven. — holds only muiImageListItem — variant=standard|quilted|masonry|woven, cols=text≤200, gap=text≤200, rowHeight=text≤200\n- muiImageListItem (Image List Item): One tile of an image list, with an optional caption bar. — holds any — title=text≤200, subtitle=text≤200, barPosition=bottom|top|below, cols=text≤200, rows=text≤200\n- muiList (List): Vertical list of items, optionally under a sticky heading. — holds only muiListItem — subheader=text≤200, dense=boolean, disablePadding=boolean\n- muiListItem (List Item): One row of a list. — holds only muiListItemText — divider=boolean, alignItems=center|flex-start, dense=boolean, disableGutters=boolean, disablePadding=boolean\n- muiListItemText (List Item Text): The primary and secondary text pair inside a list row. — leaf — primary=text≤2000, secondary=text≤2000, inset=boolean\n- muiScreenLink (Page Link): Links to another page by id, so it survives a slug change. — leaf — children=text≤40, target=_self|_blank|custom, renderAs=button|link|linkButton, color=inherit|primary|secondary|tertiary|success|error|…, size=inherit|small|medium|large\n- muiStack (Stack): Lays children out in a row or column with even spacing — the workhorse for most layouts. — holds any — component=div|section|article|aside|nav|header|…, direction=column|column-reverse|row|row-reverse, justifyContent=flex-start|center|flex-end|space-between|space-around|space-evenly, alignItems=stretch|flex-start|center|flex-end|baseline, flexWrap=nowrap|wrap|wrap-reverse\n- muiToolbar (Toolbar Content): The content row inside an App Bar — brand, links, actions. — holds any — component=div|section|article|aside|nav|header|…, ariaLabel=text≤200, disableGutters=boolean, variant=dense|regular\n- muiTypography (Typography): A block of text in one of the theme's type styles. — leaf — children=text≤2000, variant=displayXl|h1|h2|h3|h4|h5|…, component=h1|h2|h3|h4|h5|h6|…, align=inherit|left|center|right|justify, noWrap=boolean\n- searchBox: A search field that opens the site's search page. — leaf — placeholder=text≤200, defaultValue=text≤200\n- section: Groups components inside a semantic HTML element (section, article, nav, …). — holds any — element=section|div|article|aside|nav|header|…, ariaLabel=text≤200, colorScheme=site|light|dark\n- socialLinks: A row of icon links to whichever social profiles you fill in. — leaf — twitter=text≤200, instagram=text≤200, facebook=text≤200, linkedin=text≤200, youtube=text≤200\n- video: Plays a video file from your media library or any URL. — leaf — src=media, poster=media\n- videoEmbed (Video): Embeds a YouTube or Vimeo player from the video's URL. — leaf — url=url\nNamed blocks to imitate: App Bar (muiAppBar); List (muiList); List Item (muiListItem); Image Gallery (muiStack); Testimonials (muiStack); Pricing Table (muiStack); FAQ (muiStack); Announcement Bar (muiStack); Nav Bar (muiAppBar); Hero (muiStack); Feature Grid (muiStack); Image + Text (muiStack); Call to Action (muiStack); Footer (section); Collection Entries (collectionEntries); Dropdown Panel (muiStack); Grid (muiGrid); Grid Cell (muiGrid); Card (muiCard); Card Outlined (muiCard); Accordion (muiAccordion); Image List (muiImageList); Image List Masonry (muiImageList); Image List Item (muiImageListItem); Breadcrumbs (muiBreadcrumbs); Contact Form (form); Contact Section (muiStack)."
5096
5104
  };
5097
5105
 
5098
5106
  //# sourceMappingURL=ai-palette.generated.js.map