@aglyn/plugins-ai 1.0.0-beta.214 → 1.0.0-beta.215

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-ai",
3
- "version": "1.0.0-beta.214",
3
+ "version": "1.0.0-beta.215",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,22 +25,22 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.214",
29
- "@aglyn/shared-data-enums": "1.0.0-beta.214",
30
- "@aglyn/shared-data-mdi": "1.0.0-beta.214",
31
- "@aglyn/shared-data-types": "1.0.0-beta.214",
32
- "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.214",
33
- "@aglyn/shared-ui-jsx": "1.0.0-beta.214",
34
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.214",
35
- "@aglyn/shared-ui-theme": "1.0.0-beta.214",
36
- "@aglyn/shared-util-email": "1.0.0-beta.214",
37
- "@aglyn/shared-util-errors": "1.0.0-beta.214",
38
- "@aglyn/shared-util-http": "1.0.0-beta.214",
39
- "@aglyn/shared-util-timestamp": "1.0.0-beta.214",
40
- "@aglyn/shared-util-tools": "1.0.0-beta.214",
41
- "@aglyn/tenant-data-admin": "1.0.0-beta.214",
42
- "@aglyn/tenant-feature-instance": "1.0.0-beta.214",
43
- "@aglyn/tenant-runtime": "1.0.0-beta.214",
28
+ "@aglyn/aglyn": "1.0.0-beta.215",
29
+ "@aglyn/shared-data-enums": "1.0.0-beta.215",
30
+ "@aglyn/shared-data-mdi": "1.0.0-beta.215",
31
+ "@aglyn/shared-data-types": "1.0.0-beta.215",
32
+ "@aglyn/shared-ui-email-campaigns": "1.0.0-beta.215",
33
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.215",
34
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.215",
35
+ "@aglyn/shared-ui-theme": "1.0.0-beta.215",
36
+ "@aglyn/shared-util-email": "1.0.0-beta.215",
37
+ "@aglyn/shared-util-errors": "1.0.0-beta.215",
38
+ "@aglyn/shared-util-http": "1.0.0-beta.215",
39
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.215",
40
+ "@aglyn/shared-util-tools": "1.0.0-beta.215",
41
+ "@aglyn/tenant-data-admin": "1.0.0-beta.215",
42
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.215",
43
+ "@aglyn/tenant-runtime": "1.0.0-beta.215",
44
44
  "@swc/helpers": "0.5.23"
45
45
  },
46
46
  "peerDependencies": {
@@ -101,23 +101,40 @@ const SOURCE_WORDS = {
101
101
  return (_ref = (_JSON_stringify = JSON.stringify(value)) == null ? void 0 : _JSON_stringify.slice(0, 60)) != null ? _ref : '';
102
102
  };
103
103
  for (const [component, override] of Object.entries((_ref = theme == null ? void 0 : theme.components) != null ? _ref : {})){
104
- var _ref1, _ref2;
104
+ var _ref1;
105
+ var _override_variants;
105
106
  for (const [prop, value] of Object.entries((_ref1 = override == null ? void 0 : override.defaultProps) != null ? _ref1 : {})){
106
107
  leaves.push(`${component} default ${prop} = ${text(value)}`);
107
108
  }
108
- for (const [slot, styles] of Object.entries((_ref2 = override == null ? void 0 : override.styleOverrides) != null ? _ref2 : {})){
109
- if (!styles || typeof styles !== 'object') continue;
110
- for (const [property, value] of Object.entries(styles)){
111
- if (value && typeof value === 'object') {
112
- for (const [nested, nestedValue] of Object.entries(value)){
113
- var _media_property;
114
- leaves.push(`${component} ${slot} ${nested} = ${text(nestedValue)} (${(_media_property = media[property]) != null ? _media_property : property})`);
109
+ // `sx` leaves are marked so the model writes them back to `sx`: the same
110
+ // number is a different length in each (AGL-3403).
111
+ for (const [target, slots] of [
112
+ [
113
+ '',
114
+ override == null ? void 0 : override.styleOverrides
115
+ ],
116
+ [
117
+ ' sx',
118
+ override == null ? void 0 : override.sx
119
+ ]
120
+ ]){
121
+ for (const [slot, styles] of Object.entries(slots != null ? slots : {})){
122
+ if (!styles || typeof styles !== 'object') continue;
123
+ for (const [property, value] of Object.entries(styles)){
124
+ if (value && typeof value === 'object') {
125
+ for (const [nested, nestedValue] of Object.entries(value)){
126
+ var _media_property;
127
+ leaves.push(`${component}${target} ${slot} ${nested} = ${text(nestedValue)} (${(_media_property = media[property]) != null ? _media_property : property})`);
128
+ }
129
+ } else {
130
+ leaves.push(`${component}${target} ${slot} ${property} = ${text(value)}`);
115
131
  }
116
- } else {
117
- leaves.push(`${component} ${slot} ${property} = ${text(value)}`);
118
132
  }
119
133
  }
120
134
  }
135
+ if (override == null ? void 0 : (_override_variants = override.variants) == null ? void 0 : _override_variants.length) {
136
+ leaves.push(`${component} ${override.variants.length} props-matched variant style(s)`);
137
+ }
121
138
  }
122
139
  return leaves;
123
140
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-theme-step.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 {\n hostThemeSource,\n resolveSiteTheme,\n type HostThemeSource,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport {\n INHERITED_BORDER_RADIUS,\n INHERITED_FONT_FAMILY,\n INHERITED_SPACING,\n INHERITED_TOOLBAR_HEIGHTS,\n inheritedThemeColor,\n} from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_MEDIA_QUERIES,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n buildAiThemeProposal,\n type AiThemeProposal,\n type AiThemeProposalMode,\n} from '../model/ai-theme-proposal'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n runValidatedGeneration,\n type AiCustomGenerationInput,\n type AiGenerationCheck,\n} from '../runtime/ai-doctrine'\nimport type { AiSystemBlock, AiUsage } from '../runtime/ai-runtime'\nimport {\n AI_THEME_TOOL_NAME,\n aiThemeTool,\n parseAiThemeToolInput,\n type AiThemeToolParse,\n} from '../tools/ai-theme-tool'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { AI_JOB_THEME_STEP_BUDGET } from './ai-job-theme-budget'\nimport {\n AI_THEME_BRAND_SOURCE_WORDS,\n gatherAiThemeBrandInputs,\n type AiThemeBrandColor,\n} from './ai-theme-brand-inputs'\n\n/**\n * The `theme` step (AGL-2938): a brief and a site's current theme in, a theme\n * proposal out — every control the theme editor exposes, and nothing else.\n *\n * The step writes nothing to the site. A site's theme is fields on its host\n * document, and writing those is the save the Theme section's editor\n * performs; the step's output is the proposal, which a person puts in the\n * editor, previews against the current theme, and saves or discards.\n *\n * The request is shaped for the cache: the rules and the tool are static and\n * sit in the cached prefix, and everything about this site — its theme, its\n * brand colors, the brief — rides in the user turn. The model answers through\n * one strict tool derived from the editor's own catalog, and the answer is\n * held to that catalog and to the doctrine (a targeted diff, dark values,\n * contrast) before it becomes a proposal.\n */\n\n/**\n * The routing table's answer for `job.theme`: the model the theme step runs on\n * when the machine hands it no `modelFor`.\n */\nexport function aiJobThemeModel(): string {\n return aiModelForStep('job.theme')\n}\n\n/**\n * The output budget, from the routing table. The largest answer the tool\n * accepts — every color in both schemes, the full run of component leaves,\n * the longest summary — is about 2,500 tokens of JSON\n * (`ai-job-theme-step.spec.ts` measures it), and the rest is room for the\n * model to think before it answers.\n */\nexport const AI_JOB_THEME_MAX_TOKENS = AI_ROUTING_TABLE['job.theme'].maxTokens\n\n/**\n * The theme's answer ceiling on the model a job runs (AGL-3035): the most\n * whose worst case — the answer, its re-ask and the brand reads — fits the\n * least time the step registers (`ai-job-theme-budget.ts`), and never more\n * than the routing table's.\n */\nexport function aiJobThemeMaxTokens(model: string): number {\n return AI_JOB_THEME_STEP_BUDGET.maxTokens(model)\n}\n\n/** How many of a site's own component override leaves the prompt lists. */\nexport const AI_JOB_THEME_INVENTORY_LEAVES = 40\n\n/** The customer-safe sentences a failed theme step leaves on the job. */\nexport const AI_JOB_THEME_NO_SITE_COPY =\n 'This theme job names a site this workspace does not have.'\nexport const AI_JOB_THEME_NO_ANSWER_COPY =\n 'The AI could not produce a theme proposal from this brief. Try describing the change differently.'\n\n/**\n * The theme step's own instructions, byte-identical on every request so they\n * cache. The acceptable-use rules are not in them: they belong to the block\n * every generator shares, which the generation call puts ahead of these.\n */\nexport const AI_JOB_THEME_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You change the theme of a website built with a site builder. You are given the ' +\n \"site's current theme — every control its theme editor has, with the value the site \" +\n 'set or the default it inherits — any brand colors that belong to the site, and a ' +\n 'brief from the person who owns it.\\n\\n' +\n `Answer by calling ${AI_THEME_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n '- In modify mode, name only the controls the brief is about. Every control you leave ' +\n 'out keeps its current value. \"Warmer\" is about color, not about fonts or corners.\\n' +\n '- In create mode, design a complete theme: the primary, secondary and tertiary ' +\n 'accents, the page background and paper, the text colors, and their dark values.\\n' +\n \"- Colors are hex values on the theme's own tokens. A theme is where the building rules \" +\n 'send every color, so this is the one answer that writes a color as a value. A component ' +\n 'style never carries a literal color; color belongs to the palette.\\n' +\n '- Give every color you change a dark value beside its light value unless the dark ' +\n 'scheme is off. A dark value keeps the hue and reads on a dark background.\\n' +\n '- Keep text readable: body text needs 4.5:1 against the background and the paper, and ' +\n 'the primary color 3:1 against the background.\\n' +\n '- A hex color written in the brief is used exactly as written. When the brief asks to ' +\n 'match the brand, build on the brand colors you are given.\\n' +\n '- Fonts come from the list the tool offers, and numbers stay inside the ranges it ' +\n 'states.\\n' +\n '- Write the summary in the language of the brief.',\n cacheBreakpoint: true,\n },\n]\n\nconst ZERO_USAGE: AiUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n}\n\n/** `create` when the job asked for a whole design; `modify` otherwise. */\nexport function aiJobThemeMode(inputs: AiJob['inputs'] | undefined): AiThemeProposalMode {\n return inputs?.['mode'] === 'create' ? 'create' : 'modify'\n}\n\nconst SOURCE_WORDS: Record<HostThemeSource, string> = {\n installed:\n \"an installed marketplace theme; a change is stored as the site's own edits on top of it\",\n custom: \"the site's own theme\",\n default: \"the platform's default theme; a change stores only the values it sets\",\n}\n\n/** One value as the inventory shows it: the site's own, or the default it inherits. */\nconst shown = (own: string | number | null | undefined, inherited: string | number | undefined) =>\n own === null || own === undefined ? `${inherited ?? 'unset'} (default)` : `${own} (set)`\n\n/** The site's own component overrides, one bounded line a leaf. */\nfunction overrideLeaves(theme: HostTheme | undefined): string[] {\n const leaves: string[] = []\n const media = Object.fromEntries(\n Object.entries(THEME_EDITOR_MEDIA_QUERIES).map(([name, query]) => [query, name]),\n )\n const text = (value: unknown) => JSON.stringify(value)?.slice(0, 60) ?? ''\n for (const [component, override] of Object.entries(theme?.components ?? {})) {\n for (const [prop, value] of Object.entries(override?.defaultProps ?? {})) {\n leaves.push(`${component} default ${prop} = ${text(value)}`)\n }\n for (const [slot, styles] of Object.entries(override?.styleOverrides ?? {})) {\n if (!styles || typeof styles !== 'object') continue\n for (const [property, value] of Object.entries(styles as Record<string, unknown>)) {\n if (value && typeof value === 'object') {\n for (const [nested, nestedValue] of Object.entries(value as Record<string, unknown>)) {\n leaves.push(\n `${component} ${slot} ${nested} = ${text(nestedValue)} (${media[property] ?? property})`,\n )\n }\n } else {\n leaves.push(`${component} ${slot} ${property} = ${text(value)}`)\n }\n }\n }\n }\n return leaves\n}\n\n/**\n * The site's theme as the model reads it: compact, one control a line, each\n * value marked as the site's own or the default it inherits, so \"make the\n * background warmer\" starts from the color the visitor actually sees.\n */\nexport function aiJobThemeInventory(theme: HostTheme | undefined, source: HostThemeSource): string {\n const lines = [`Source: ${SOURCE_WORDS[source]}.`, `Dark scheme: ${readDarkScheme(theme)}.`]\n lines.push('Colors, light | dark:')\n for (const { token } of THEME_COLOR_FIELDS) {\n const value = (scheme: HostThemeScheme) =>\n shown(readThemeColor(theme, scheme, token), inheritedThemeColor(scheme, token))\n lines.push(`- ${token}: ${value('light')} | ${value('dark')}`)\n }\n const font = readFontFamily(theme)\n lines.push(`Font family: ${shown(font === SYSTEM_FONT_VALUE ? null : font, INHERITED_FONT_FAMILY)}`)\n lines.push(`Border radius: ${shown(theme?.shape?.borderRadius, INHERITED_BORDER_RADIUS)}`)\n lines.push(`Spacing unit: ${shown(theme?.spacing, INHERITED_SPACING)}`)\n lines.push(\n `Nav height: mobile ${shown(theme && readToolbarHeight(theme, 'xs'), INHERITED_TOOLBAR_HEIGHTS.xs)}, ` +\n `desktop ${shown(theme && readToolbarHeight(theme, 'sm'), INHERITED_TOOLBAR_HEIGHTS.sm)}`,\n )\n const leaves = overrideLeaves(theme)\n if (!leaves.length) {\n lines.push('Component overrides: none.')\n } else {\n lines.push('Component overrides:')\n for (const leaf of leaves.slice(0, AI_JOB_THEME_INVENTORY_LEAVES)) lines.push(`- ${leaf}`)\n if (leaves.length > AI_JOB_THEME_INVENTORY_LEAVES) {\n lines.push(`- and ${leaves.length - AI_JOB_THEME_INVENTORY_LEAVES} more`)\n }\n }\n return lines.join('\\n')\n}\n\n/** The user turn: the mode, the site's theme, its brand colors, and the brief. */\nexport function aiJobThemePrompt(input: {\n brief: string\n mode: AiThemeProposalMode\n theme: HostTheme | undefined\n source: HostThemeSource\n brand: readonly AiThemeBrandColor[]\n}): string {\n const sections = [\n input.mode === 'create'\n ? 'Mode: create. Design a complete theme over every control.'\n : 'Mode: modify. Change only what the brief is about.',\n `Current theme:\\n${aiJobThemeInventory(input.theme, input.source)}`,\n ]\n if (input.brand.length) {\n sections.push(\n `Brand colors:\\n${input.brand\n .map((color) => `- ${color.hex} (${AI_THEME_BRAND_SOURCE_WORDS[color.source]})`)\n .join('\\n')}`,\n )\n }\n sections.push(`Brief: ${input.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`)\n return sections.join('\\n\\n')\n}\n\n/**\n * The theme tool's answer held to the editor — the check the doctrine's loop\n * runs on each answer (AGL-2935), which also holds rule 13 on it. Usable when\n * anything in it survives; refused, naming what was refused, when nothing\n * does. A call that proposes no change and refuses nothing is a real answer —\n * the theme already does what the brief asks.\n */\nexport const aiJobThemeCheck: AiGenerationCheck<AiThemeToolParse> = (answer) => {\n const parse = parseAiThemeToolInput(answer)\n const nothingUsable =\n !parse.changes.length && !parse.components.length && !parse.resetComponents\n if (!nothingUsable || !parse.dropped.length) return { value: parse, violations: [] }\n return {\n value: null,\n violations: parse.dropped.map((message) => ({ rule: null, code: 'theme-control', message })),\n }\n}\n\n/** What a theme generation is asked from, once the site and its brand have been read. */\nexport interface AiJobThemeRequest {\n brief: string\n mode: AiThemeProposalMode\n theme: HostTheme | undefined\n source: HostThemeSource\n brand: readonly AiThemeBrandColor[]\n model: string\n signal?: AbortSignal\n}\n\n/**\n * The theme step's generation as the doctrine's loop runs it: its cached\n * block, which carries the acceptable-use rules, then this step's\n * instructions; the site's theme in the user turn rather than a site\n * inventory; the strict tool, the ceiling and the check. The eval harness\n * records a theme answer through this same call.\n */\nexport function aiJobThemeGeneration(\n request: AiJobThemeRequest,\n): AiCustomGenerationInput<AiThemeToolParse> {\n return {\n step: 'job.theme',\n model: request.model,\n instructions: AI_JOB_THEME_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiJobThemePrompt(request) }],\n tool: aiThemeTool(),\n maxTokens: aiJobThemeMaxTokens(request.model),\n ...(AI_ROUTING_TABLE['job.theme'].thinking ? { thinking: AI_ROUTING_TABLE['job.theme'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.theme'].effort ? { effort: AI_ROUTING_TABLE['job.theme'].effort } : {}),\n ...(request.signal ? { signal: request.signal } : {}),\n check: aiJobThemeCheck,\n }\n}\n\n/** The site a theme job changes, read and checked against the job's org. */\nasync function loadThemeSite(\n firestore: FirebaseFirestore.Firestore,\n job: AiJob,\n): Promise<\n | { host: ThemeHostDocument & Record<string, unknown>; theme: HostTheme | undefined; source: HostThemeSource }\n | { failure: string }\n> {\n if (!job.hostId) return { failure: AI_JOB_THEME_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists\n ? (snapshot.data() as ThemeHostDocument & Record<string, unknown>)\n : null\n // The route checks the caller's membership of the org the job is metered\n // against, not that the site it named belongs to that org. A site of some\n // other org is refused here, before anything about it reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_JOB_THEME_NO_SITE_COPY }\n return { host, theme: resolveSiteTheme(host), source: hostThemeSource(host) }\n}\n\n/** How many edits a proposal makes, for its label. */\nfunction proposalSize(proposal: AiThemeProposal): number {\n return proposal.changes.length + proposal.components.length + (proposal.resetComponents ? 1 : 0)\n}\n\nexport const runAiJobThemeStep: AiJobStepRunner = async ({\n job,\n signal,\n firestore,\n org,\n modelFor,\n}) => {\n if (!firestore) {\n // The machine always hands one in; a runner called without it is a\n // wiring fault, not a customer path.\n throw new Error('the theme step reads the site it changes and was given no Firestore')\n }\n // The model switch's answer for this job (AGL-2942): the creator's pick\n // where the plan, the org restriction and the allotment allowlists allow\n // it, and Auto held to those same lists otherwise. Only a runner called\n // without a resolver asks the routing table directly.\n const model = modelFor?.('job.theme') ?? aiJobThemeModel()\n const site = await loadThemeSite(firestore, job)\n if ('failure' in site) {\n return {\n outputs: [],\n usage: ZERO_USAGE,\n estCostUsd: 0,\n model,\n stopReason: null,\n failure: site.failure,\n }\n }\n const mode = aiJobThemeMode(job.inputs)\n const brand = await gatherAiThemeBrandInputs({\n firestore,\n org: org ?? null,\n hostId: job.hostId as string,\n host: site.host,\n brief: job.brief,\n signal,\n })\n // A second refused answer ends in this step's own sentence, below.\n const generation = await runValidatedGeneration(\n 'theme',\n aiJobThemeGeneration({\n brief: job.brief,\n mode,\n theme: site.theme,\n source: site.source,\n brand: brand.colors,\n model,\n ...(signal ? { signal } : {}),\n }),\n )\n const spent: Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason' | 'effort'> = {\n usage: generation.usage,\n estCostUsd: generation.estCostUsd,\n model: generation.model,\n stopReason: generation.stopReason,\n ...(generation.effort ? { effort: generation.effort } : {}),\n }\n if (generation.status === 'refused') return { outputs: [], ...spent, refused: true }\n if (generation.status === 'needs_input') {\n return { outputs: [], ...spent, failure: AI_JOB_THEME_NO_ANSWER_COPY }\n }\n\n const answer = generation.value\n const proposal = buildAiThemeProposal({\n base: site.theme,\n source: site.source,\n mode,\n brief: job.brief,\n summary: answer.summary,\n changes: answer.changes,\n components: answer.components,\n resetComponents: answer.resetComponents,\n dropped: answer.dropped,\n notes: [...brand.notes, ...answer.notes],\n })\n const size = proposalSize(proposal)\n const output: AiJobOutput = {\n resource: 'theme',\n // A proposal has no document of its own; the id names what it is within\n // the job, as a text output's `draft` does.\n id: 'proposal',\n hostId: job.hostId ?? null,\n hostSubdomain: typeof site.host['subdomain'] === 'string' ? site.host['subdomain'] : null,\n label: `Theme proposal · ${size} ${size === 1 ? 'change' : 'changes'}`,\n // Through JSON so no `undefined` reaches the document write.\n proposal: JSON.parse(JSON.stringify(proposal)) as Record<string, unknown>,\n }\n return { outputs: [output], ...spent }\n}\n"],"names":["hostThemeSource","resolveSiteTheme","INHERITED_BORDER_RADIUS","INHERITED_FONT_FAMILY","INHERITED_SPACING","INHERITED_TOOLBAR_HEIGHTS","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_MEDIA_QUERIES","buildAiThemeProposal","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","AI_THEME_TOOL_NAME","aiThemeTool","parseAiThemeToolInput","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_THEME_STEP_BUDGET","AI_THEME_BRAND_SOURCE_WORDS","gatherAiThemeBrandInputs","aiJobThemeModel","AI_JOB_THEME_MAX_TOKENS","maxTokens","aiJobThemeMaxTokens","model","AI_JOB_THEME_INVENTORY_LEAVES","AI_JOB_THEME_NO_SITE_COPY","AI_JOB_THEME_NO_ANSWER_COPY","AI_JOB_THEME_INSTRUCTIONS","text","cacheBreakpoint","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","aiJobThemeMode","inputs","SOURCE_WORDS","installed","custom","default","shown","own","inherited","undefined","overrideLeaves","theme","leaves","media","Object","fromEntries","entries","map","name","query","value","JSON","stringify","slice","component","override","components","prop","defaultProps","push","slot","styles","styleOverrides","property","nested","nestedValue","aiJobThemeInventory","source","lines","token","scheme","font","shape","borderRadius","spacing","xs","sm","length","leaf","join","aiJobThemePrompt","input","sections","mode","brand","color","hex","brief","aiJobThemeCheck","answer","parse","nothingUsable","changes","resetComponents","dropped","violations","message","rule","code","aiJobThemeGeneration","request","step","instructions","messages","role","content","tool","thinking","effort","signal","check","loadThemeSite","firestore","job","hostId","failure","snapshot","collection","doc","get","host","exists","data","orgId","proposalSize","proposal","runAiJobThemeStep","org","modelFor","Error","site","outputs","usage","estCostUsd","stopReason","generation","colors","spent","status","refused","base","summary","notes","size","output","resource","id","hostSubdomain","label"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-theme-step.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 {\n hostThemeSource,\n resolveSiteTheme,\n type HostThemeSource,\n type ThemeHostDocument,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport {\n INHERITED_BORDER_RADIUS,\n INHERITED_FONT_FAMILY,\n INHERITED_SPACING,\n INHERITED_TOOLBAR_HEIGHTS,\n inheritedThemeColor,\n} from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_MEDIA_QUERIES,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport {\n buildAiThemeProposal,\n type AiThemeProposal,\n type AiThemeProposalMode,\n} from '../model/ai-theme-proposal'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport {\n runValidatedGeneration,\n type AiCustomGenerationInput,\n type AiGenerationCheck,\n} from '../runtime/ai-doctrine'\nimport type { AiSystemBlock, AiUsage } from '../runtime/ai-runtime'\nimport {\n AI_THEME_TOOL_NAME,\n aiThemeTool,\n parseAiThemeToolInput,\n type AiThemeToolParse,\n} from '../tools/ai-theme-tool'\nimport {\n AI_JOB_BRIEF_MAX_CHARS,\n type AiJobStepOutcome,\n type AiJobStepRunner,\n} from './ai-job-text-step'\nimport { AI_JOB_THEME_STEP_BUDGET } from './ai-job-theme-budget'\nimport {\n AI_THEME_BRAND_SOURCE_WORDS,\n gatherAiThemeBrandInputs,\n type AiThemeBrandColor,\n} from './ai-theme-brand-inputs'\n\n/**\n * The `theme` step (AGL-2938): a brief and a site's current theme in, a theme\n * proposal out — every control the theme editor exposes, and nothing else.\n *\n * The step writes nothing to the site. A site's theme is fields on its host\n * document, and writing those is the save the Theme section's editor\n * performs; the step's output is the proposal, which a person puts in the\n * editor, previews against the current theme, and saves or discards.\n *\n * The request is shaped for the cache: the rules and the tool are static and\n * sit in the cached prefix, and everything about this site — its theme, its\n * brand colors, the brief — rides in the user turn. The model answers through\n * one strict tool derived from the editor's own catalog, and the answer is\n * held to that catalog and to the doctrine (a targeted diff, dark values,\n * contrast) before it becomes a proposal.\n */\n\n/**\n * The routing table's answer for `job.theme`: the model the theme step runs on\n * when the machine hands it no `modelFor`.\n */\nexport function aiJobThemeModel(): string {\n return aiModelForStep('job.theme')\n}\n\n/**\n * The output budget, from the routing table. The largest answer the tool\n * accepts — every color in both schemes, the full run of component leaves,\n * the longest summary — is about 2,500 tokens of JSON\n * (`ai-job-theme-step.spec.ts` measures it), and the rest is room for the\n * model to think before it answers.\n */\nexport const AI_JOB_THEME_MAX_TOKENS = AI_ROUTING_TABLE['job.theme'].maxTokens\n\n/**\n * The theme's answer ceiling on the model a job runs (AGL-3035): the most\n * whose worst case — the answer, its re-ask and the brand reads — fits the\n * least time the step registers (`ai-job-theme-budget.ts`), and never more\n * than the routing table's.\n */\nexport function aiJobThemeMaxTokens(model: string): number {\n return AI_JOB_THEME_STEP_BUDGET.maxTokens(model)\n}\n\n/** How many of a site's own component override leaves the prompt lists. */\nexport const AI_JOB_THEME_INVENTORY_LEAVES = 40\n\n/** The customer-safe sentences a failed theme step leaves on the job. */\nexport const AI_JOB_THEME_NO_SITE_COPY =\n 'This theme job names a site this workspace does not have.'\nexport const AI_JOB_THEME_NO_ANSWER_COPY =\n 'The AI could not produce a theme proposal from this brief. Try describing the change differently.'\n\n/**\n * The theme step's own instructions, byte-identical on every request so they\n * cache. The acceptable-use rules are not in them: they belong to the block\n * every generator shares, which the generation call puts ahead of these.\n */\nexport const AI_JOB_THEME_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You change the theme of a website built with a site builder. You are given the ' +\n \"site's current theme — every control its theme editor has, with the value the site \" +\n 'set or the default it inherits — any brand colors that belong to the site, and a ' +\n 'brief from the person who owns it.\\n\\n' +\n `Answer by calling ${AI_THEME_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n '- In modify mode, name only the controls the brief is about. Every control you leave ' +\n 'out keeps its current value. \"Warmer\" is about color, not about fonts or corners.\\n' +\n '- In create mode, design a complete theme: the primary, secondary and tertiary ' +\n 'accents, the page background and paper, the text colors, and their dark values.\\n' +\n \"- Colors are hex values on the theme's own tokens. A theme is where the building rules \" +\n 'send every color, so this is the one answer that writes a color as a value. A component ' +\n 'style never carries a literal color; color belongs to the palette.\\n' +\n '- Give every color you change a dark value beside its light value unless the dark ' +\n 'scheme is off. A dark value keeps the hue and reads on a dark background.\\n' +\n '- Keep text readable: body text needs 4.5:1 against the background and the paper, and ' +\n 'the primary color 3:1 against the background.\\n' +\n '- A hex color written in the brief is used exactly as written. When the brief asks to ' +\n 'match the brand, build on the brand colors you are given.\\n' +\n '- Fonts come from the list the tool offers, and numbers stay inside the ranges it ' +\n 'states.\\n' +\n '- Write the summary in the language of the brief.',\n cacheBreakpoint: true,\n },\n]\n\nconst ZERO_USAGE: AiUsage = {\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n}\n\n/** `create` when the job asked for a whole design; `modify` otherwise. */\nexport function aiJobThemeMode(inputs: AiJob['inputs'] | undefined): AiThemeProposalMode {\n return inputs?.['mode'] === 'create' ? 'create' : 'modify'\n}\n\nconst SOURCE_WORDS: Record<HostThemeSource, string> = {\n installed:\n \"an installed marketplace theme; a change is stored as the site's own edits on top of it\",\n custom: \"the site's own theme\",\n default: \"the platform's default theme; a change stores only the values it sets\",\n}\n\n/** One value as the inventory shows it: the site's own, or the default it inherits. */\nconst shown = (own: string | number | null | undefined, inherited: string | number | undefined) =>\n own === null || own === undefined ? `${inherited ?? 'unset'} (default)` : `${own} (set)`\n\n/** The site's own component overrides, one bounded line a leaf. */\nfunction overrideLeaves(theme: HostTheme | undefined): string[] {\n const leaves: string[] = []\n const media = Object.fromEntries(\n Object.entries(THEME_EDITOR_MEDIA_QUERIES).map(([name, query]) => [query, name]),\n )\n const text = (value: unknown) => JSON.stringify(value)?.slice(0, 60) ?? ''\n for (const [component, override] of Object.entries(theme?.components ?? {})) {\n for (const [prop, value] of Object.entries(override?.defaultProps ?? {})) {\n leaves.push(`${component} default ${prop} = ${text(value)}`)\n }\n // `sx` leaves are marked so the model writes them back to `sx`: the same\n // number is a different length in each (AGL-3403).\n for (const [target, slots] of [\n ['', override?.styleOverrides],\n [' sx', override?.sx],\n ] as const) {\n for (const [slot, styles] of Object.entries(slots ?? {})) {\n if (!styles || typeof styles !== 'object') continue\n for (const [property, value] of Object.entries(styles as Record<string, unknown>)) {\n if (value && typeof value === 'object') {\n for (const [nested, nestedValue] of Object.entries(value as Record<string, unknown>)) {\n leaves.push(\n `${component}${target} ${slot} ${nested} = ${text(nestedValue)} (${media[property] ?? property})`,\n )\n }\n } else {\n leaves.push(`${component}${target} ${slot} ${property} = ${text(value)}`)\n }\n }\n }\n }\n if (override?.variants?.length) {\n leaves.push(`${component} ${override.variants.length} props-matched variant style(s)`)\n }\n }\n return leaves\n}\n\n/**\n * The site's theme as the model reads it: compact, one control a line, each\n * value marked as the site's own or the default it inherits, so \"make the\n * background warmer\" starts from the color the visitor actually sees.\n */\nexport function aiJobThemeInventory(theme: HostTheme | undefined, source: HostThemeSource): string {\n const lines = [`Source: ${SOURCE_WORDS[source]}.`, `Dark scheme: ${readDarkScheme(theme)}.`]\n lines.push('Colors, light | dark:')\n for (const { token } of THEME_COLOR_FIELDS) {\n const value = (scheme: HostThemeScheme) =>\n shown(readThemeColor(theme, scheme, token), inheritedThemeColor(scheme, token))\n lines.push(`- ${token}: ${value('light')} | ${value('dark')}`)\n }\n const font = readFontFamily(theme)\n lines.push(`Font family: ${shown(font === SYSTEM_FONT_VALUE ? null : font, INHERITED_FONT_FAMILY)}`)\n lines.push(`Border radius: ${shown(theme?.shape?.borderRadius, INHERITED_BORDER_RADIUS)}`)\n lines.push(`Spacing unit: ${shown(theme?.spacing, INHERITED_SPACING)}`)\n lines.push(\n `Nav height: mobile ${shown(theme && readToolbarHeight(theme, 'xs'), INHERITED_TOOLBAR_HEIGHTS.xs)}, ` +\n `desktop ${shown(theme && readToolbarHeight(theme, 'sm'), INHERITED_TOOLBAR_HEIGHTS.sm)}`,\n )\n const leaves = overrideLeaves(theme)\n if (!leaves.length) {\n lines.push('Component overrides: none.')\n } else {\n lines.push('Component overrides:')\n for (const leaf of leaves.slice(0, AI_JOB_THEME_INVENTORY_LEAVES)) lines.push(`- ${leaf}`)\n if (leaves.length > AI_JOB_THEME_INVENTORY_LEAVES) {\n lines.push(`- and ${leaves.length - AI_JOB_THEME_INVENTORY_LEAVES} more`)\n }\n }\n return lines.join('\\n')\n}\n\n/** The user turn: the mode, the site's theme, its brand colors, and the brief. */\nexport function aiJobThemePrompt(input: {\n brief: string\n mode: AiThemeProposalMode\n theme: HostTheme | undefined\n source: HostThemeSource\n brand: readonly AiThemeBrandColor[]\n}): string {\n const sections = [\n input.mode === 'create'\n ? 'Mode: create. Design a complete theme over every control.'\n : 'Mode: modify. Change only what the brief is about.',\n `Current theme:\\n${aiJobThemeInventory(input.theme, input.source)}`,\n ]\n if (input.brand.length) {\n sections.push(\n `Brand colors:\\n${input.brand\n .map((color) => `- ${color.hex} (${AI_THEME_BRAND_SOURCE_WORDS[color.source]})`)\n .join('\\n')}`,\n )\n }\n sections.push(`Brief: ${input.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)}`)\n return sections.join('\\n\\n')\n}\n\n/**\n * The theme tool's answer held to the editor — the check the doctrine's loop\n * runs on each answer (AGL-2935), which also holds rule 13 on it. Usable when\n * anything in it survives; refused, naming what was refused, when nothing\n * does. A call that proposes no change and refuses nothing is a real answer —\n * the theme already does what the brief asks.\n */\nexport const aiJobThemeCheck: AiGenerationCheck<AiThemeToolParse> = (answer) => {\n const parse = parseAiThemeToolInput(answer)\n const nothingUsable =\n !parse.changes.length && !parse.components.length && !parse.resetComponents\n if (!nothingUsable || !parse.dropped.length) return { value: parse, violations: [] }\n return {\n value: null,\n violations: parse.dropped.map((message) => ({ rule: null, code: 'theme-control', message })),\n }\n}\n\n/** What a theme generation is asked from, once the site and its brand have been read. */\nexport interface AiJobThemeRequest {\n brief: string\n mode: AiThemeProposalMode\n theme: HostTheme | undefined\n source: HostThemeSource\n brand: readonly AiThemeBrandColor[]\n model: string\n signal?: AbortSignal\n}\n\n/**\n * The theme step's generation as the doctrine's loop runs it: its cached\n * block, which carries the acceptable-use rules, then this step's\n * instructions; the site's theme in the user turn rather than a site\n * inventory; the strict tool, the ceiling and the check. The eval harness\n * records a theme answer through this same call.\n */\nexport function aiJobThemeGeneration(\n request: AiJobThemeRequest,\n): AiCustomGenerationInput<AiThemeToolParse> {\n return {\n step: 'job.theme',\n model: request.model,\n instructions: AI_JOB_THEME_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiJobThemePrompt(request) }],\n tool: aiThemeTool(),\n maxTokens: aiJobThemeMaxTokens(request.model),\n ...(AI_ROUTING_TABLE['job.theme'].thinking ? { thinking: AI_ROUTING_TABLE['job.theme'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.theme'].effort ? { effort: AI_ROUTING_TABLE['job.theme'].effort } : {}),\n ...(request.signal ? { signal: request.signal } : {}),\n check: aiJobThemeCheck,\n }\n}\n\n/** The site a theme job changes, read and checked against the job's org. */\nasync function loadThemeSite(\n firestore: FirebaseFirestore.Firestore,\n job: AiJob,\n): Promise<\n | { host: ThemeHostDocument & Record<string, unknown>; theme: HostTheme | undefined; source: HostThemeSource }\n | { failure: string }\n> {\n if (!job.hostId) return { failure: AI_JOB_THEME_NO_SITE_COPY }\n const snapshot = await firestore.collection('hosts').doc(job.hostId).get()\n const host = snapshot.exists\n ? (snapshot.data() as ThemeHostDocument & Record<string, unknown>)\n : null\n // The route checks the caller's membership of the org the job is metered\n // against, not that the site it named belongs to that org. A site of some\n // other org is refused here, before anything about it reaches a prompt.\n if (!host || host['orgId'] !== job.orgId) return { failure: AI_JOB_THEME_NO_SITE_COPY }\n return { host, theme: resolveSiteTheme(host), source: hostThemeSource(host) }\n}\n\n/** How many edits a proposal makes, for its label. */\nfunction proposalSize(proposal: AiThemeProposal): number {\n return proposal.changes.length + proposal.components.length + (proposal.resetComponents ? 1 : 0)\n}\n\nexport const runAiJobThemeStep: AiJobStepRunner = async ({\n job,\n signal,\n firestore,\n org,\n modelFor,\n}) => {\n if (!firestore) {\n // The machine always hands one in; a runner called without it is a\n // wiring fault, not a customer path.\n throw new Error('the theme step reads the site it changes and was given no Firestore')\n }\n // The model switch's answer for this job (AGL-2942): the creator's pick\n // where the plan, the org restriction and the allotment allowlists allow\n // it, and Auto held to those same lists otherwise. Only a runner called\n // without a resolver asks the routing table directly.\n const model = modelFor?.('job.theme') ?? aiJobThemeModel()\n const site = await loadThemeSite(firestore, job)\n if ('failure' in site) {\n return {\n outputs: [],\n usage: ZERO_USAGE,\n estCostUsd: 0,\n model,\n stopReason: null,\n failure: site.failure,\n }\n }\n const mode = aiJobThemeMode(job.inputs)\n const brand = await gatherAiThemeBrandInputs({\n firestore,\n org: org ?? null,\n hostId: job.hostId as string,\n host: site.host,\n brief: job.brief,\n signal,\n })\n // A second refused answer ends in this step's own sentence, below.\n const generation = await runValidatedGeneration(\n 'theme',\n aiJobThemeGeneration({\n brief: job.brief,\n mode,\n theme: site.theme,\n source: site.source,\n brand: brand.colors,\n model,\n ...(signal ? { signal } : {}),\n }),\n )\n const spent: Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason' | 'effort'> = {\n usage: generation.usage,\n estCostUsd: generation.estCostUsd,\n model: generation.model,\n stopReason: generation.stopReason,\n ...(generation.effort ? { effort: generation.effort } : {}),\n }\n if (generation.status === 'refused') return { outputs: [], ...spent, refused: true }\n if (generation.status === 'needs_input') {\n return { outputs: [], ...spent, failure: AI_JOB_THEME_NO_ANSWER_COPY }\n }\n\n const answer = generation.value\n const proposal = buildAiThemeProposal({\n base: site.theme,\n source: site.source,\n mode,\n brief: job.brief,\n summary: answer.summary,\n changes: answer.changes,\n components: answer.components,\n resetComponents: answer.resetComponents,\n dropped: answer.dropped,\n notes: [...brand.notes, ...answer.notes],\n })\n const size = proposalSize(proposal)\n const output: AiJobOutput = {\n resource: 'theme',\n // A proposal has no document of its own; the id names what it is within\n // the job, as a text output's `draft` does.\n id: 'proposal',\n hostId: job.hostId ?? null,\n hostSubdomain: typeof site.host['subdomain'] === 'string' ? site.host['subdomain'] : null,\n label: `Theme proposal · ${size} ${size === 1 ? 'change' : 'changes'}`,\n // Through JSON so no `undefined` reaches the document write.\n proposal: JSON.parse(JSON.stringify(proposal)) as Record<string, unknown>,\n }\n return { outputs: [output], ...spent }\n}\n"],"names":["hostThemeSource","resolveSiteTheme","INHERITED_BORDER_RADIUS","INHERITED_FONT_FAMILY","INHERITED_SPACING","INHERITED_TOOLBAR_HEIGHTS","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_MEDIA_QUERIES","buildAiThemeProposal","AI_ROUTING_TABLE","aiModelForStep","runValidatedGeneration","AI_THEME_TOOL_NAME","aiThemeTool","parseAiThemeToolInput","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_THEME_STEP_BUDGET","AI_THEME_BRAND_SOURCE_WORDS","gatherAiThemeBrandInputs","aiJobThemeModel","AI_JOB_THEME_MAX_TOKENS","maxTokens","aiJobThemeMaxTokens","model","AI_JOB_THEME_INVENTORY_LEAVES","AI_JOB_THEME_NO_SITE_COPY","AI_JOB_THEME_NO_ANSWER_COPY","AI_JOB_THEME_INSTRUCTIONS","text","cacheBreakpoint","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","aiJobThemeMode","inputs","SOURCE_WORDS","installed","custom","default","shown","own","inherited","undefined","overrideLeaves","theme","leaves","media","Object","fromEntries","entries","map","name","query","value","JSON","stringify","slice","component","override","components","prop","defaultProps","push","target","slots","styleOverrides","sx","slot","styles","property","nested","nestedValue","variants","length","aiJobThemeInventory","source","lines","token","scheme","font","shape","borderRadius","spacing","xs","sm","leaf","join","aiJobThemePrompt","input","sections","mode","brand","color","hex","brief","aiJobThemeCheck","answer","parse","nothingUsable","changes","resetComponents","dropped","violations","message","rule","code","aiJobThemeGeneration","request","step","instructions","messages","role","content","tool","thinking","effort","signal","check","loadThemeSite","firestore","job","hostId","failure","snapshot","collection","doc","get","host","exists","data","orgId","proposalSize","proposal","runAiJobThemeStep","org","modelFor","Error","site","outputs","usage","estCostUsd","stopReason","generation","colors","spent","status","refused","base","summary","notes","size","output","resource","id","hostSubdomain","label"],"mappings":"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"}
@@ -460,7 +460,7 @@ const VALUE_CONTROLS = new Set(AI_THEME_VALUE_CONTROLS);
460
460
  const components = (Array.isArray(value['components']) ? value['components'] : []).filter(isRecord).filter((leaf)=>{
461
461
  var _ref;
462
462
  var _THEME_EDITOR_CONTROLS_find;
463
- return ((_ref = (_THEME_EDITOR_CONTROLS_find = THEME_EDITOR_CONTROLS.find((control)=>control.id === 'components')) == null ? void 0 : _THEME_EDITOR_CONTROLS_find.options) != null ? _ref : []).includes(String(leaf['component'])) && (leaf['target'] === 'styleOverrides' || leaf['target'] === 'defaultProps') && typeof leaf['property'] === 'string' && [
463
+ return ((_ref = (_THEME_EDITOR_CONTROLS_find = THEME_EDITOR_CONTROLS.find((control)=>control.id === 'components')) == null ? void 0 : _THEME_EDITOR_CONTROLS_find.options) != null ? _ref : []).includes(String(leaf['component'])) && (leaf['target'] === 'styleOverrides' || leaf['target'] === 'sx' || leaf['target'] === 'defaultProps') && typeof leaf['property'] === 'string' && [
464
464
  'string',
465
465
  'number',
466
466
  'boolean'
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-theme-proposal.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 {\n CONTRAST_AA,\n CONTRAST_AA_LARGE,\n contrastRatio,\n type HostThemeSource,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport { accessibleShade } from '@aglyn/shared-ui-theme/util/accessible-shade'\nimport { inheritedThemeColor } from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n resetComponentOverrides,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n THEME_EDITOR_SCHEMES,\n writeBorderRadius,\n writeComponentOverride,\n writeDarkScheme,\n writeFontFamily,\n writeSpacing,\n writeThemeColor,\n writeToolbarHeight,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControlId,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { prefersDarkInk } from '@aglyn/shared-util-tools/contrast'\n\n/**\n * A theme proposal (AGL-2938): what a theme job hands a person to apply.\n *\n * Not a theme. A proposal is a set of theme editor controls set to values,\n * applied to whatever the site's theme is when the person applies it, through\n * the same writes the editor performs. A proposal that names three colors\n * changes three colors, and every control it does not name is untouched by\n * construction rather than by the model's restraint — which is what makes\n * \"make it feel warmer\" a targeted diff instead of a regenerated theme.\n *\n * Pure and free of anything server-side: the job step builds a proposal, and\n * the Theme section's widget previews and applies one, from this module.\n */\n\nexport const AI_THEME_PROPOSAL_VERSION = 1\n\n/**\n * `modify` changes what the brief is about and nothing else; `create` is a\n * whole design over every control the editor has.\n */\nexport type AiThemeProposalMode = 'modify' | 'create'\n\nexport const AI_THEME_PROPOSAL_MODES: readonly AiThemeProposalMode[] = ['modify', 'create']\n\n/** The parts of a theme a brief can be about. */\nexport type AiThemeChangeGroup =\n | 'color'\n | 'darkScheme'\n | 'typography'\n | 'shape'\n | 'spacing'\n | 'navigation'\n | 'components'\n\nexport const AI_THEME_CHANGE_GROUPS: readonly AiThemeChangeGroup[] = [\n 'color',\n 'darkScheme',\n 'typography',\n 'shape',\n 'spacing',\n 'navigation',\n 'components',\n]\n\n/** The controls a proposal sets one value on; component overrides are leaves of their own. */\nexport type AiThemeValueControl = Exclude<ThemeEditorControlId, 'components'>\n\n/** Every value control, as the catalog lists them. */\nexport const AI_THEME_VALUE_CONTROLS: readonly AiThemeValueControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.id !== 'components',\n).map((control) => control.id as AiThemeValueControl)\n\n/** One editor control set to a value, or back to its default with `null`. */\nexport interface AiThemeControlChange {\n control: AiThemeValueControl\n /** The scheme a color is set for; `null` for every other control. */\n scheme: HostThemeScheme | null\n value: string | number | null\n /** What the site had set when the proposal was made; `null` when it inherited. */\n before: string | number | null\n}\n\n/** A value the proposal carries that the model did not write, and why. */\nexport interface AiThemeCorrection {\n control: `color.${ThemeColorToken}`\n scheme: HostThemeScheme\n /** The value before the correction; `null` when the slot inherited. */\n from: string | null\n to: string\n /**\n * `contrast` — the pair fell below its bar and this is the nearest shade\n * of the foreground that clears it. `dark-value` — the proposal changed an\n * accent for light and gave no dark value, and this keeps the same hue\n * readable on the dark background.\n */\n reason: 'contrast' | 'dark-value'\n /** The colors a contrast correction was measured against. */\n against?: string[]\n /** The worst ratio before the correction. */\n ratio?: number\n /** The bar the pair has to clear. */\n required?: number\n}\n\nexport interface AiThemeProposal {\n version: typeof AI_THEME_PROPOSAL_VERSION\n mode: AiThemeProposalMode\n /** One sentence naming what the proposal changes, in the brief's language. */\n summary: string\n /**\n * Where the site's theme came from when the proposal was made, which is\n * how the editor's Save stores it: an installed theme's edits become its\n * override patch, a default theme stores only the changed values, and a\n * site's own theme changes in place.\n */\n source: HostThemeSource\n changes: AiThemeControlChange[]\n components: ThemeComponentOverrideLeaf[]\n /** The site's own component overrides are dropped before `components` apply. */\n resetComponents: boolean\n corrections: AiThemeCorrection[]\n /** What the model proposed that the proposal leaves out, in words. */\n dropped: string[]\n /** Anything else a person should read before applying, in words. */\n notes: string[]\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading and applying\n * ------------------------------------------------------------------------ */\n\n/** The color token a control writes, or `null` for a control that is not a color. */\nexport function aiThemeColorToken(control: string): ThemeColorToken | null {\n return control.startsWith('color.')\n ? (control.slice('color.'.length) as ThemeColorToken)\n : null\n}\n\n/** What a control holds on a theme, as a proposal's `before` names it. */\nexport function readAiThemeControl(\n theme: HostTheme | undefined,\n control: AiThemeValueControl,\n scheme: HostThemeScheme | null,\n): string | number | null {\n const token = aiThemeColorToken(control)\n if (token) return readThemeColor(theme, scheme ?? 'light', token) ?? null\n switch (control) {\n case 'darkScheme':\n return readDarkScheme(theme) === 'off' ? 'off' : null\n case 'fontFamily': {\n const family = readFontFamily(theme)\n return family === SYSTEM_FONT_VALUE ? null : family\n }\n case 'borderRadius':\n return typeof theme?.shape?.borderRadius === 'number' ? theme.shape.borderRadius : null\n case 'spacing':\n return typeof theme?.spacing === 'number' ? theme.spacing : null\n case 'navHeight.xs':\n return (theme && readToolbarHeight(theme, 'xs')) ?? null\n case 'navHeight.sm':\n return (theme && readToolbarHeight(theme, 'sm')) ?? null\n default:\n return null\n }\n}\n\nfunction applyControlChange(\n theme: HostTheme,\n change: Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>,\n): HostTheme {\n const { control, scheme, value } = change\n const token = aiThemeColorToken(control)\n if (token) {\n return writeThemeColor(\n theme,\n scheme ?? 'light',\n token,\n typeof value === 'string' ? value : undefined,\n )\n }\n const number = typeof value === 'number' ? value : undefined\n switch (control) {\n case 'darkScheme':\n return writeDarkScheme(theme, value === 'off' ? 'off' : 'auto')\n case 'fontFamily':\n return writeFontFamily(theme, typeof value === 'string' ? value : SYSTEM_FONT_VALUE)\n case 'borderRadius':\n return writeBorderRadius(theme, number)\n case 'spacing':\n return writeSpacing(theme, number)\n case 'navHeight.xs':\n return writeToolbarHeight(theme, 'xs', number)\n case 'navHeight.sm':\n return writeToolbarHeight(theme, 'sm', number)\n default:\n return theme\n }\n}\n\n/**\n * The theme a set of changes produces on top of `theme`: each change written\n * the way the editor writes it, then the component overrides. Returns a new\n * object; `theme` is not mutated.\n */\nexport function applyAiThemeChanges(\n theme: HostTheme | undefined,\n proposal: {\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n },\n): HostTheme {\n let next: HostTheme = { ...(theme ?? {}) }\n for (const change of proposal.changes) next = applyControlChange(next, change)\n if (proposal.resetComponents) next = resetComponentOverrides(next)\n for (const leaf of proposal.components) next = writeComponentOverride(next, leaf)\n return next\n}\n\n/* ------------------------------------------------------------------------ *\n * Scope: which parts of the theme a brief is about\n * ------------------------------------------------------------------------ */\n\n/** The part of the theme a value control belongs to. */\nexport function aiThemeControlGroup(control: AiThemeValueControl): AiThemeChangeGroup {\n if (aiThemeColorToken(control)) return 'color'\n switch (control) {\n case 'darkScheme':\n return 'darkScheme'\n case 'fontFamily':\n return 'typography'\n case 'borderRadius':\n return 'shape'\n case 'spacing':\n return 'spacing'\n default:\n return 'navigation'\n }\n}\n\n/**\n * The part of the theme a component override belongs to: the type ramp's\n * overrides are typography, the app bar's and the toolbar's are navigation,\n * and the rest are components.\n */\nexport function aiThemeComponentGroup(\n component: ThemeComponentOverrideLeaf['component'],\n): AiThemeChangeGroup {\n if (component === 'MuiTypography') return 'typography'\n if (component === 'MuiAppBar' || component === 'MuiToolbar') return 'navigation'\n return 'components'\n}\n\n/**\n * The words that tie a brief to a part of the theme, matched case-insensitively\n * on whole words. A brief can name several parts.\n */\nconst BRIEF_VOCABULARY: Record<AiThemeChangeGroup, RegExp> = {\n color:\n /(#[0-9a-f]{3,8}\\b)|\\b(colou?rs?|palettes?|hues?|warm(er|th|ly)?|cool(er)?|cold(er)?|contrast\\w*|bright(er|ness)?|dark(er|en)?|light(er|en)?|saturat\\w*|desaturat\\w*|muted|vibrant|pastels?|tones?|tints?|shades?|accents?|primary|secondary|tertiary|backgrounds?|surfaces?|brand\\w*|earthy|neon|moody|red|orange|yellow|green|blue|teal|purple|violet|pink|magenta|brown|beige|cream|gold|navy|black|white|gr[ae]y|hex)\\b/i,\n darkScheme: /\\b(dark (mode|scheme|theme)|light (mode|scheme)|always light|night mode)\\b/i,\n typography:\n /\\b(fonts?|typefaces?|typography|serif|sans|monospace|headings?|headlines?|titles?|type ?scale|text size|font size|lettering|weights?|bold(er)?|editorial)\\b/i,\n shape: /\\b(radius|radii|round(ed|er|ness)?|corners?|sharp(er)?|square(d|r)?|pills?|curv\\w*)\\b/i,\n spacing:\n /\\b(spac\\w*|padding|margins?|roomy|airy|tight(er)?|compact|dense|breathing room|white ?space|gaps?)\\b/i,\n navigation: /\\b(nav\\w*|header height|toolbar|app ?bar|menu bar|top bar)\\b/i,\n components:\n /\\b(buttons?|cards?|chips?|inputs?|text ?fields?|links?|tabs?|switch(es)?|sliders?|avatars?|badges?|tooltips?|dividers?|menus?|components?|uppercase|capitali[sz]\\w*|shadows?|elevation|outlined|underlin\\w*)\\b/i,\n}\n\n/**\n * The parts of the theme a job may change. A `create` job is a whole design.\n * A `modify` job may change only what its brief is about, so \"make it feel\n * warmer\" reaches the colors and nothing else; a brief that names no part at\n * all — in another language, or in words this does not know — is taken to\n * be about the whole design rather than refused.\n */\nexport function aiThemeBriefScope(\n brief: string,\n mode: AiThemeProposalMode,\n): ReadonlySet<AiThemeChangeGroup> {\n if (mode === 'create') return new Set(AI_THEME_CHANGE_GROUPS)\n const named = AI_THEME_CHANGE_GROUPS.filter((group) => BRIEF_VOCABULARY[group].test(brief))\n return new Set(named.length ? named : AI_THEME_CHANGE_GROUPS)\n}\n\nconst GROUP_WORDS: Record<AiThemeChangeGroup, string> = {\n color: 'colors',\n darkScheme: 'the dark scheme',\n typography: 'typography',\n shape: 'corners',\n spacing: 'spacing',\n navigation: 'navigation',\n components: 'component styles',\n}\n\n/* ------------------------------------------------------------------------ *\n * Colors as the site renders them, and dark values\n * ------------------------------------------------------------------------ */\n\nconst colorKey = (scheme: HostThemeScheme, token: ThemeColorToken) => `${scheme}:${token}`\n\n/** A color as the site renders it in one scheme: its own value, else the brand's. */\nexport function effectiveThemeColor(\n theme: HostTheme | undefined,\n scheme: HostThemeScheme,\n token: ThemeColorToken,\n): string | undefined {\n return readThemeColor(theme, scheme, token) ?? inheritedThemeColor(scheme, token)\n}\n\n/** The accents: the colors whose dark value can follow their light one. */\nconst ACCENT_TOKENS: ReadonlySet<ThemeColorToken> = new Set(\n THEME_COLOR_FIELDS.filter((field) => field.group === 'palette').map((field) => field.token),\n)\n\n/**\n * The colors a set of changes gives a light value and no dark one, while the\n * site's dark scheme is on. Each is a visitor in dark mode seeing half a\n * change.\n */\nexport function aiThemeMissingDarkValues(\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n themeAfter: HostTheme,\n): ThemeColorToken[] {\n if (readDarkScheme(themeAfter) === 'off') return []\n const set = new Set(\n changes\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n return THEME_COLOR_FIELDS.map(({ token }) => token).filter(\n (token) => set.has(colorKey('light', token)) && !set.has(colorKey('dark', token)),\n )\n}\n\n/**\n * The dark value an accent keeps when a proposal gave none: the same hue,\n * lightened only as far as it takes to read on the dark background as a fill\n * or an indicator. Surfaces, text and tints have no such value — a pale light\n * surface lightened is still a light surface — so they go on inheriting the\n * platform's dark palette, which is what a site with no dark design renders.\n */\nexport function deriveAiThemeDarkAccent(light: string, themeAfter: HostTheme): string | null {\n const background = effectiveThemeColor(themeAfter, 'dark', 'background.default')\n if (!background) return null\n try {\n return accessibleShade(light, [background], 'lighten', { minContrast: CONTRAST_AA_LARGE })\n } catch {\n return null\n }\n}\n\n/* ------------------------------------------------------------------------ *\n * Contrast\n * ------------------------------------------------------------------------ */\n\n/** A foreground that has to read on each of its backgrounds. */\nexport interface AiThemeContrastPair {\n foreground: ThemeColorToken\n backgrounds: readonly ThemeColorToken[]\n required: number\n}\n\n/**\n * The pairs a proposal is held to. Body text on the page and on paper, and\n * secondary text on the page, are the pairs the theme publish check\n * (`validateThemeForPublish`) measures, at its bars; the primary color on\n * the page is held to the bar a fill or an indicator owes.\n */\nexport const AI_THEME_CONTRAST_PAIRS: readonly AiThemeContrastPair[] = [\n {\n foreground: 'text.primary',\n backgrounds: ['background.default', 'background.paper'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'text.secondary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'primary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA_LARGE,\n },\n]\n\nconst round2 = (ratio: number) => Math.round(ratio * 100) / 100\n\n/**\n * Whether dark ink reads better than light ink on a background. A hex takes\n * the shared helper; anything else a theme can hold (`rgba()`) is measured\n * against black and white directly, which is the same comparison.\n */\nfunction darkInkReads(background: string): boolean {\n const verdict = prefersDarkInk(background)\n if (verdict !== null) return verdict\n return (contrastRatio('#000000', background) ?? 0) >= (contrastRatio('#ffffff', background) ?? 0)\n}\n\nexport interface AiThemeContrastResult {\n theme: HostTheme\n corrections: AiThemeCorrection[]\n notes: string[]\n}\n\n/**\n * Every pair a set of changes takes below its bar, corrected to the nearest\n * shade of its foreground that clears it, and named.\n *\n * Only pairs the changes touched are corrected. A pair that already fell\n * short with neither of its colors changed is named in `notes` and left\n * alone, because correcting it would change a color the brief did not ask\n * about. A dark scheme the site has switched off is not measured: no visitor\n * sees it.\n */\nexport function correctAiThemeContrast(\n themeAfter: HostTheme,\n touchedChanges: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n): AiThemeContrastResult {\n const touched = new Set(\n touchedChanges\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n const schemes = THEME_EDITOR_SCHEMES.filter(\n (scheme) => scheme === 'light' || readDarkScheme(themeAfter) !== 'off',\n )\n let theme = themeAfter\n const corrections: AiThemeCorrection[] = []\n const notes: string[] = []\n for (const scheme of schemes) {\n for (const pair of AI_THEME_CONTRAST_PAIRS) {\n const foreground = effectiveThemeColor(theme, scheme, pair.foreground)\n const backgrounds = pair.backgrounds\n .map((token) => effectiveThemeColor(theme, scheme, token))\n .filter((value): value is string => typeof value === 'string')\n if (!foreground || backgrounds.length !== pair.backgrounds.length) continue\n const ratios = backgrounds.map((background) => contrastRatio(foreground, background))\n if (ratios.some((ratio) => ratio === null)) continue\n const worst = Math.min(...(ratios as number[]))\n if (worst >= pair.required) continue\n const involved = [pair.foreground, ...pair.backgrounds].some((token) =>\n touched.has(colorKey(scheme, token)),\n )\n if (!involved) {\n notes.push(\n `${pair.foreground} on ${pair.backgrounds.join(' and ')} in ${scheme} was already ` +\n `${round2(worst)}:1 before this proposal, below ${pair.required}:1.`,\n )\n continue\n }\n let corrected: string\n try {\n corrected = accessibleShade(\n foreground,\n backgrounds,\n darkInkReads(backgrounds[0]) ? 'darken' : 'lighten',\n { minContrast: pair.required },\n )\n } catch {\n continue\n }\n const clears = backgrounds.every(\n (background) => (contrastRatio(corrected, background) ?? 0) >= pair.required,\n )\n if (!clears) {\n notes.push(\n `${pair.foreground} cannot reach ${pair.required}:1 on the ${scheme} background by ` +\n 'changing its shade alone; the background needs to change too.',\n )\n continue\n }\n corrections.push({\n control: `color.${pair.foreground}`,\n scheme,\n from: readThemeColor(theme, scheme, pair.foreground) ?? null,\n to: corrected,\n reason: 'contrast',\n against: backgrounds,\n ratio: round2(worst),\n required: pair.required,\n })\n theme = writeThemeColor(theme, scheme, pair.foreground, corrected)\n }\n }\n return { theme, corrections, notes }\n}\n\n/** Folds a correction into a change list: the change it corrects, or a new one. */\nfunction withCorrection(\n changes: AiThemeControlChange[],\n correction: AiThemeCorrection,\n base: HostTheme | undefined,\n): AiThemeControlChange[] {\n const index = changes.findIndex(\n (change) => change.control === correction.control && change.scheme === correction.scheme,\n )\n if (index >= 0) {\n return changes.map((change, at) => (at === index ? { ...change, value: correction.to } : change))\n }\n return [\n ...changes,\n {\n control: correction.control,\n scheme: correction.scheme,\n value: correction.to,\n before: readAiThemeControl(base, correction.control, correction.scheme),\n },\n ]\n}\n\n/* ------------------------------------------------------------------------ *\n * Building a proposal\n * ------------------------------------------------------------------------ */\n\nexport interface BuildAiThemeProposalInput {\n /** The site's theme as the editor shows it, overrides resolved. */\n base: HostTheme | undefined\n source: HostThemeSource\n mode: AiThemeProposalMode\n brief: string\n summary: string\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What reading the model's answer already left out, in words. */\n dropped?: readonly string[]\n notes?: readonly string[]\n}\n\n/**\n * A proposal from what the model proposed, held to the editor and to the\n * doctrine: changes that change nothing are gone, a modify brief reaches\n * only the parts of the theme it names, accents keep a dark value, and every\n * pair the proposal touches clears its contrast bar.\n */\nexport function buildAiThemeProposal(input: BuildAiThemeProposalInput): AiThemeProposal {\n const dropped = [...(input.dropped ?? [])]\n const notes = [...(input.notes ?? [])]\n const scope = aiThemeBriefScope(input.brief, input.mode)\n const outOfScope = (group: AiThemeChangeGroup, what: string) => {\n dropped.push(`${what}: the brief did not ask about ${GROUP_WORDS[group]}`)\n }\n\n let changes: AiThemeControlChange[] = []\n for (const change of input.changes) {\n const before = readAiThemeControl(input.base, change.control, change.scheme)\n if (before === change.value) continue\n const group = aiThemeControlGroup(change.control)\n if (!scope.has(group)) {\n outOfScope(group, change.scheme ? `${change.control} (${change.scheme})` : change.control)\n continue\n }\n changes.push({ ...change, before })\n }\n const components = input.components.filter((leaf) => {\n const group = aiThemeComponentGroup(leaf.component)\n if (scope.has(group)) return true\n outOfScope(group, `${leaf.component}.${leaf.property}`)\n return false\n })\n let resetComponents = input.resetComponents\n if (resetComponents && !scope.has('components')) {\n outOfScope('components', 'Resetting the component overrides')\n resetComponents = false\n }\n\n const corrections: AiThemeCorrection[] = []\n let after = applyAiThemeChanges(input.base, { changes, components, resetComponents })\n for (const token of aiThemeMissingDarkValues(changes, after)) {\n const light = changes.find(\n (change) => change.scheme === 'light' && aiThemeColorToken(change.control) === token,\n )\n if (!ACCENT_TOKENS.has(token) || typeof light?.value !== 'string') {\n notes.push(`${token} keeps its current dark value; only its light value changes.`)\n continue\n }\n const dark = deriveAiThemeDarkAccent(light.value, after)\n if (!dark) continue\n const correction: AiThemeCorrection = {\n control: `color.${token}`,\n scheme: 'dark',\n from: readThemeColor(input.base, 'dark', token) ?? null,\n to: dark,\n reason: 'dark-value',\n }\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n after = writeThemeColor(after, 'dark', token, dark)\n }\n\n const contrast = correctAiThemeContrast(after, changes)\n for (const correction of contrast.corrections) {\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n }\n notes.push(...contrast.notes)\n\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode: input.mode,\n summary: input.summary,\n source: input.source,\n changes,\n components,\n resetComponents,\n corrections,\n dropped,\n notes,\n }\n}\n\n/**\n * What applying a proposal to the site's CURRENT theme produces, held to the\n * contrast bars again: the theme may have changed since the proposal was\n * made, and a proposal that cleared its bars against the old background can\n * fall short against the new one. The corrections this makes are named\n * beside the proposal's own.\n */\nexport function aiThemeProposalAfter(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes' | 'components' | 'resetComponents'>,\n): AiThemeContrastResult {\n const applied = applyAiThemeChanges(theme, proposal)\n return correctAiThemeContrast(applied, proposal.changes)\n}\n\n/** The changes whose `before` no longer matches the theme a proposal is applied to. */\nexport function aiThemeStaleChanges(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes'>,\n): AiThemeControlChange[] {\n return proposal.changes.filter(\n (change) => readAiThemeControl(theme, change.control, change.scheme) !== change.before,\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading a stored proposal back\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nconst scalar = (value: unknown): string | number | null =>\n typeof value === 'string' || typeof value === 'number' ? value : null\n\nconst VALUE_CONTROLS: ReadonlySet<string> = new Set(AI_THEME_VALUE_CONTROLS)\n\n/**\n * A proposal as a job output stores it, or `null` for anything that is not\n * one. The job writes these through the Admin SDK and nothing else can, but\n * a reader that applies what it reads to a theme checks the shape first.\n */\nexport function readAiThemeProposal(value: unknown): AiThemeProposal | null {\n if (!isRecord(value) || value['version'] !== AI_THEME_PROPOSAL_VERSION) return null\n const mode = value['mode'] === 'create' ? 'create' : 'modify'\n const source =\n value['source'] === 'installed' || value['source'] === 'custom' ? value['source'] : 'default'\n const changes = (Array.isArray(value['changes']) ? value['changes'] : [])\n .filter(isRecord)\n .filter((change) => VALUE_CONTROLS.has(String(change['control'])))\n .map(\n (change): AiThemeControlChange => ({\n control: change['control'] as AiThemeValueControl,\n scheme: change['scheme'] === 'light' || change['scheme'] === 'dark' ? change['scheme'] : null,\n value: scalar(change['value']),\n before: scalar(change['before']),\n }),\n )\n const components = (Array.isArray(value['components']) ? value['components'] : [])\n .filter(isRecord)\n .filter(\n (leaf) =>\n (THEME_EDITOR_CONTROLS.find((control) => control.id === 'components')?.options ?? []).includes(\n String(leaf['component']),\n ) &&\n (leaf['target'] === 'styleOverrides' || leaf['target'] === 'defaultProps') &&\n typeof leaf['property'] === 'string' &&\n ['string', 'number', 'boolean'].includes(typeof leaf['value']) &&\n (leaf['media'] === null || String(leaf['media']) in THEME_EDITOR_MEDIA_QUERIES),\n )\n .map(\n (leaf): ThemeComponentOverrideLeaf => ({\n component: leaf['component'] as ThemeComponentOverrideLeaf['component'],\n target: leaf['target'] as ThemeComponentOverrideLeaf['target'],\n slot: typeof leaf['slot'] === 'string' ? leaf['slot'] : null,\n property: leaf['property'] as string,\n media: (leaf['media'] as ThemeComponentOverrideLeaf['media']) ?? null,\n value: leaf['value'] as string | number | boolean,\n }),\n )\n const corrections = (Array.isArray(value['corrections']) ? value['corrections'] : [])\n .filter(isRecord)\n .filter(\n (correction) =>\n aiThemeColorToken(String(correction['control'])) !== null &&\n (correction['scheme'] === 'light' || correction['scheme'] === 'dark') &&\n typeof correction['to'] === 'string',\n )\n .map(\n (correction): AiThemeCorrection => ({\n control: correction['control'] as AiThemeCorrection['control'],\n scheme: correction['scheme'] as HostThemeScheme,\n from: typeof correction['from'] === 'string' ? correction['from'] : null,\n to: correction['to'] as string,\n reason: correction['reason'] === 'dark-value' ? 'dark-value' : 'contrast',\n ...(Array.isArray(correction['against']) ? { against: strings(correction['against']) } : {}),\n ...(typeof correction['ratio'] === 'number' ? { ratio: correction['ratio'] } : {}),\n ...(typeof correction['required'] === 'number' ? { required: correction['required'] } : {}),\n }),\n )\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode,\n summary: typeof value['summary'] === 'string' ? value['summary'] : '',\n source,\n changes,\n components,\n resetComponents: value['resetComponents'] === true,\n corrections,\n dropped: strings(value['dropped']),\n notes: strings(value['notes']),\n }\n}\n"],"names":["CONTRAST_AA","CONTRAST_AA_LARGE","contrastRatio","accessibleShade","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","resetComponentOverrides","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","THEME_EDITOR_SCHEMES","writeBorderRadius","writeComponentOverride","writeDarkScheme","writeFontFamily","writeSpacing","writeThemeColor","writeToolbarHeight","prefersDarkInk","AI_THEME_PROPOSAL_VERSION","AI_THEME_PROPOSAL_MODES","AI_THEME_CHANGE_GROUPS","AI_THEME_VALUE_CONTROLS","filter","control","id","map","aiThemeColorToken","startsWith","slice","length","readAiThemeControl","theme","scheme","token","family","shape","borderRadius","spacing","applyControlChange","change","value","undefined","number","applyAiThemeChanges","proposal","next","changes","resetComponents","leaf","components","aiThemeControlGroup","aiThemeComponentGroup","component","BRIEF_VOCABULARY","color","darkScheme","typography","navigation","aiThemeBriefScope","brief","mode","Set","named","group","test","GROUP_WORDS","colorKey","effectiveThemeColor","ACCENT_TOKENS","field","aiThemeMissingDarkValues","themeAfter","set","has","deriveAiThemeDarkAccent","light","background","minContrast","AI_THEME_CONTRAST_PAIRS","foreground","backgrounds","required","round2","ratio","Math","round","darkInkReads","verdict","correctAiThemeContrast","touchedChanges","touched","schemes","corrections","notes","pair","ratios","some","worst","min","involved","push","join","corrected","clears","every","from","to","reason","against","withCorrection","correction","base","index","findIndex","at","before","buildAiThemeProposal","input","dropped","scope","outOfScope","what","property","after","find","dark","contrast","version","summary","source","aiThemeProposalAfter","applied","aiThemeStaleChanges","isRecord","Array","isArray","strings","entry","scalar","VALUE_CONTROLS","readAiThemeProposal","String","options","includes","target","slot","media"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-theme-proposal.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 {\n CONTRAST_AA,\n CONTRAST_AA_LARGE,\n contrastRatio,\n type HostThemeSource,\n} from '@aglyn/aglyn/app-utils/marketplace-theme'\nimport type { HostTheme, HostThemeScheme } from '@aglyn/shared-data-types'\nimport { accessibleShade } from '@aglyn/shared-ui-theme/util/accessible-shade'\nimport { inheritedThemeColor } from '@aglyn/shared-ui-theme/util/theme-editor-defaults'\nimport {\n readDarkScheme,\n readFontFamily,\n readThemeColor,\n readToolbarHeight,\n resetComponentOverrides,\n SYSTEM_FONT_VALUE,\n THEME_COLOR_FIELDS,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n THEME_EDITOR_SCHEMES,\n writeBorderRadius,\n writeComponentOverride,\n writeDarkScheme,\n writeFontFamily,\n writeSpacing,\n writeThemeColor,\n writeToolbarHeight,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControlId,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport { prefersDarkInk } from '@aglyn/shared-util-tools/contrast'\n\n/**\n * A theme proposal (AGL-2938): what a theme job hands a person to apply.\n *\n * Not a theme. A proposal is a set of theme editor controls set to values,\n * applied to whatever the site's theme is when the person applies it, through\n * the same writes the editor performs. A proposal that names three colors\n * changes three colors, and every control it does not name is untouched by\n * construction rather than by the model's restraint — which is what makes\n * \"make it feel warmer\" a targeted diff instead of a regenerated theme.\n *\n * Pure and free of anything server-side: the job step builds a proposal, and\n * the Theme section's widget previews and applies one, from this module.\n */\n\nexport const AI_THEME_PROPOSAL_VERSION = 1\n\n/**\n * `modify` changes what the brief is about and nothing else; `create` is a\n * whole design over every control the editor has.\n */\nexport type AiThemeProposalMode = 'modify' | 'create'\n\nexport const AI_THEME_PROPOSAL_MODES: readonly AiThemeProposalMode[] = ['modify', 'create']\n\n/** The parts of a theme a brief can be about. */\nexport type AiThemeChangeGroup =\n | 'color'\n | 'darkScheme'\n | 'typography'\n | 'shape'\n | 'spacing'\n | 'navigation'\n | 'components'\n\nexport const AI_THEME_CHANGE_GROUPS: readonly AiThemeChangeGroup[] = [\n 'color',\n 'darkScheme',\n 'typography',\n 'shape',\n 'spacing',\n 'navigation',\n 'components',\n]\n\n/** The controls a proposal sets one value on; component overrides are leaves of their own. */\nexport type AiThemeValueControl = Exclude<ThemeEditorControlId, 'components'>\n\n/** Every value control, as the catalog lists them. */\nexport const AI_THEME_VALUE_CONTROLS: readonly AiThemeValueControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.id !== 'components',\n).map((control) => control.id as AiThemeValueControl)\n\n/** One editor control set to a value, or back to its default with `null`. */\nexport interface AiThemeControlChange {\n control: AiThemeValueControl\n /** The scheme a color is set for; `null` for every other control. */\n scheme: HostThemeScheme | null\n value: string | number | null\n /** What the site had set when the proposal was made; `null` when it inherited. */\n before: string | number | null\n}\n\n/** A value the proposal carries that the model did not write, and why. */\nexport interface AiThemeCorrection {\n control: `color.${ThemeColorToken}`\n scheme: HostThemeScheme\n /** The value before the correction; `null` when the slot inherited. */\n from: string | null\n to: string\n /**\n * `contrast` — the pair fell below its bar and this is the nearest shade\n * of the foreground that clears it. `dark-value` — the proposal changed an\n * accent for light and gave no dark value, and this keeps the same hue\n * readable on the dark background.\n */\n reason: 'contrast' | 'dark-value'\n /** The colors a contrast correction was measured against. */\n against?: string[]\n /** The worst ratio before the correction. */\n ratio?: number\n /** The bar the pair has to clear. */\n required?: number\n}\n\nexport interface AiThemeProposal {\n version: typeof AI_THEME_PROPOSAL_VERSION\n mode: AiThemeProposalMode\n /** One sentence naming what the proposal changes, in the brief's language. */\n summary: string\n /**\n * Where the site's theme came from when the proposal was made, which is\n * how the editor's Save stores it: an installed theme's edits become its\n * override patch, a default theme stores only the changed values, and a\n * site's own theme changes in place.\n */\n source: HostThemeSource\n changes: AiThemeControlChange[]\n components: ThemeComponentOverrideLeaf[]\n /** The site's own component overrides are dropped before `components` apply. */\n resetComponents: boolean\n corrections: AiThemeCorrection[]\n /** What the model proposed that the proposal leaves out, in words. */\n dropped: string[]\n /** Anything else a person should read before applying, in words. */\n notes: string[]\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading and applying\n * ------------------------------------------------------------------------ */\n\n/** The color token a control writes, or `null` for a control that is not a color. */\nexport function aiThemeColorToken(control: string): ThemeColorToken | null {\n return control.startsWith('color.')\n ? (control.slice('color.'.length) as ThemeColorToken)\n : null\n}\n\n/** What a control holds on a theme, as a proposal's `before` names it. */\nexport function readAiThemeControl(\n theme: HostTheme | undefined,\n control: AiThemeValueControl,\n scheme: HostThemeScheme | null,\n): string | number | null {\n const token = aiThemeColorToken(control)\n if (token) return readThemeColor(theme, scheme ?? 'light', token) ?? null\n switch (control) {\n case 'darkScheme':\n return readDarkScheme(theme) === 'off' ? 'off' : null\n case 'fontFamily': {\n const family = readFontFamily(theme)\n return family === SYSTEM_FONT_VALUE ? null : family\n }\n case 'borderRadius':\n return typeof theme?.shape?.borderRadius === 'number' ? theme.shape.borderRadius : null\n case 'spacing':\n return typeof theme?.spacing === 'number' ? theme.spacing : null\n case 'navHeight.xs':\n return (theme && readToolbarHeight(theme, 'xs')) ?? null\n case 'navHeight.sm':\n return (theme && readToolbarHeight(theme, 'sm')) ?? null\n default:\n return null\n }\n}\n\nfunction applyControlChange(\n theme: HostTheme,\n change: Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>,\n): HostTheme {\n const { control, scheme, value } = change\n const token = aiThemeColorToken(control)\n if (token) {\n return writeThemeColor(\n theme,\n scheme ?? 'light',\n token,\n typeof value === 'string' ? value : undefined,\n )\n }\n const number = typeof value === 'number' ? value : undefined\n switch (control) {\n case 'darkScheme':\n return writeDarkScheme(theme, value === 'off' ? 'off' : 'auto')\n case 'fontFamily':\n return writeFontFamily(theme, typeof value === 'string' ? value : SYSTEM_FONT_VALUE)\n case 'borderRadius':\n return writeBorderRadius(theme, number)\n case 'spacing':\n return writeSpacing(theme, number)\n case 'navHeight.xs':\n return writeToolbarHeight(theme, 'xs', number)\n case 'navHeight.sm':\n return writeToolbarHeight(theme, 'sm', number)\n default:\n return theme\n }\n}\n\n/**\n * The theme a set of changes produces on top of `theme`: each change written\n * the way the editor writes it, then the component overrides. Returns a new\n * object; `theme` is not mutated.\n */\nexport function applyAiThemeChanges(\n theme: HostTheme | undefined,\n proposal: {\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n },\n): HostTheme {\n let next: HostTheme = { ...(theme ?? {}) }\n for (const change of proposal.changes) next = applyControlChange(next, change)\n if (proposal.resetComponents) next = resetComponentOverrides(next)\n for (const leaf of proposal.components) next = writeComponentOverride(next, leaf)\n return next\n}\n\n/* ------------------------------------------------------------------------ *\n * Scope: which parts of the theme a brief is about\n * ------------------------------------------------------------------------ */\n\n/** The part of the theme a value control belongs to. */\nexport function aiThemeControlGroup(control: AiThemeValueControl): AiThemeChangeGroup {\n if (aiThemeColorToken(control)) return 'color'\n switch (control) {\n case 'darkScheme':\n return 'darkScheme'\n case 'fontFamily':\n return 'typography'\n case 'borderRadius':\n return 'shape'\n case 'spacing':\n return 'spacing'\n default:\n return 'navigation'\n }\n}\n\n/**\n * The part of the theme a component override belongs to: the type ramp's\n * overrides are typography, the app bar's and the toolbar's are navigation,\n * and the rest are components.\n */\nexport function aiThemeComponentGroup(\n component: ThemeComponentOverrideLeaf['component'],\n): AiThemeChangeGroup {\n if (component === 'MuiTypography') return 'typography'\n if (component === 'MuiAppBar' || component === 'MuiToolbar') return 'navigation'\n return 'components'\n}\n\n/**\n * The words that tie a brief to a part of the theme, matched case-insensitively\n * on whole words. A brief can name several parts.\n */\nconst BRIEF_VOCABULARY: Record<AiThemeChangeGroup, RegExp> = {\n color:\n /(#[0-9a-f]{3,8}\\b)|\\b(colou?rs?|palettes?|hues?|warm(er|th|ly)?|cool(er)?|cold(er)?|contrast\\w*|bright(er|ness)?|dark(er|en)?|light(er|en)?|saturat\\w*|desaturat\\w*|muted|vibrant|pastels?|tones?|tints?|shades?|accents?|primary|secondary|tertiary|backgrounds?|surfaces?|brand\\w*|earthy|neon|moody|red|orange|yellow|green|blue|teal|purple|violet|pink|magenta|brown|beige|cream|gold|navy|black|white|gr[ae]y|hex)\\b/i,\n darkScheme: /\\b(dark (mode|scheme|theme)|light (mode|scheme)|always light|night mode)\\b/i,\n typography:\n /\\b(fonts?|typefaces?|typography|serif|sans|monospace|headings?|headlines?|titles?|type ?scale|text size|font size|lettering|weights?|bold(er)?|editorial)\\b/i,\n shape: /\\b(radius|radii|round(ed|er|ness)?|corners?|sharp(er)?|square(d|r)?|pills?|curv\\w*)\\b/i,\n spacing:\n /\\b(spac\\w*|padding|margins?|roomy|airy|tight(er)?|compact|dense|breathing room|white ?space|gaps?)\\b/i,\n navigation: /\\b(nav\\w*|header height|toolbar|app ?bar|menu bar|top bar)\\b/i,\n components:\n /\\b(buttons?|cards?|chips?|inputs?|text ?fields?|links?|tabs?|switch(es)?|sliders?|avatars?|badges?|tooltips?|dividers?|menus?|components?|uppercase|capitali[sz]\\w*|shadows?|elevation|outlined|underlin\\w*)\\b/i,\n}\n\n/**\n * The parts of the theme a job may change. A `create` job is a whole design.\n * A `modify` job may change only what its brief is about, so \"make it feel\n * warmer\" reaches the colors and nothing else; a brief that names no part at\n * all — in another language, or in words this does not know — is taken to\n * be about the whole design rather than refused.\n */\nexport function aiThemeBriefScope(\n brief: string,\n mode: AiThemeProposalMode,\n): ReadonlySet<AiThemeChangeGroup> {\n if (mode === 'create') return new Set(AI_THEME_CHANGE_GROUPS)\n const named = AI_THEME_CHANGE_GROUPS.filter((group) => BRIEF_VOCABULARY[group].test(brief))\n return new Set(named.length ? named : AI_THEME_CHANGE_GROUPS)\n}\n\nconst GROUP_WORDS: Record<AiThemeChangeGroup, string> = {\n color: 'colors',\n darkScheme: 'the dark scheme',\n typography: 'typography',\n shape: 'corners',\n spacing: 'spacing',\n navigation: 'navigation',\n components: 'component styles',\n}\n\n/* ------------------------------------------------------------------------ *\n * Colors as the site renders them, and dark values\n * ------------------------------------------------------------------------ */\n\nconst colorKey = (scheme: HostThemeScheme, token: ThemeColorToken) => `${scheme}:${token}`\n\n/** A color as the site renders it in one scheme: its own value, else the brand's. */\nexport function effectiveThemeColor(\n theme: HostTheme | undefined,\n scheme: HostThemeScheme,\n token: ThemeColorToken,\n): string | undefined {\n return readThemeColor(theme, scheme, token) ?? inheritedThemeColor(scheme, token)\n}\n\n/** The accents: the colors whose dark value can follow their light one. */\nconst ACCENT_TOKENS: ReadonlySet<ThemeColorToken> = new Set(\n THEME_COLOR_FIELDS.filter((field) => field.group === 'palette').map((field) => field.token),\n)\n\n/**\n * The colors a set of changes gives a light value and no dark one, while the\n * site's dark scheme is on. Each is a visitor in dark mode seeing half a\n * change.\n */\nexport function aiThemeMissingDarkValues(\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n themeAfter: HostTheme,\n): ThemeColorToken[] {\n if (readDarkScheme(themeAfter) === 'off') return []\n const set = new Set(\n changes\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n return THEME_COLOR_FIELDS.map(({ token }) => token).filter(\n (token) => set.has(colorKey('light', token)) && !set.has(colorKey('dark', token)),\n )\n}\n\n/**\n * The dark value an accent keeps when a proposal gave none: the same hue,\n * lightened only as far as it takes to read on the dark background as a fill\n * or an indicator. Surfaces, text and tints have no such value — a pale light\n * surface lightened is still a light surface — so they go on inheriting the\n * platform's dark palette, which is what a site with no dark design renders.\n */\nexport function deriveAiThemeDarkAccent(light: string, themeAfter: HostTheme): string | null {\n const background = effectiveThemeColor(themeAfter, 'dark', 'background.default')\n if (!background) return null\n try {\n return accessibleShade(light, [background], 'lighten', { minContrast: CONTRAST_AA_LARGE })\n } catch {\n return null\n }\n}\n\n/* ------------------------------------------------------------------------ *\n * Contrast\n * ------------------------------------------------------------------------ */\n\n/** A foreground that has to read on each of its backgrounds. */\nexport interface AiThemeContrastPair {\n foreground: ThemeColorToken\n backgrounds: readonly ThemeColorToken[]\n required: number\n}\n\n/**\n * The pairs a proposal is held to. Body text on the page and on paper, and\n * secondary text on the page, are the pairs the theme publish check\n * (`validateThemeForPublish`) measures, at its bars; the primary color on\n * the page is held to the bar a fill or an indicator owes.\n */\nexport const AI_THEME_CONTRAST_PAIRS: readonly AiThemeContrastPair[] = [\n {\n foreground: 'text.primary',\n backgrounds: ['background.default', 'background.paper'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'text.secondary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA,\n },\n {\n foreground: 'primary',\n backgrounds: ['background.default'],\n required: CONTRAST_AA_LARGE,\n },\n]\n\nconst round2 = (ratio: number) => Math.round(ratio * 100) / 100\n\n/**\n * Whether dark ink reads better than light ink on a background. A hex takes\n * the shared helper; anything else a theme can hold (`rgba()`) is measured\n * against black and white directly, which is the same comparison.\n */\nfunction darkInkReads(background: string): boolean {\n const verdict = prefersDarkInk(background)\n if (verdict !== null) return verdict\n return (contrastRatio('#000000', background) ?? 0) >= (contrastRatio('#ffffff', background) ?? 0)\n}\n\nexport interface AiThemeContrastResult {\n theme: HostTheme\n corrections: AiThemeCorrection[]\n notes: string[]\n}\n\n/**\n * Every pair a set of changes takes below its bar, corrected to the nearest\n * shade of its foreground that clears it, and named.\n *\n * Only pairs the changes touched are corrected. A pair that already fell\n * short with neither of its colors changed is named in `notes` and left\n * alone, because correcting it would change a color the brief did not ask\n * about. A dark scheme the site has switched off is not measured: no visitor\n * sees it.\n */\nexport function correctAiThemeContrast(\n themeAfter: HostTheme,\n touchedChanges: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme'>>,\n): AiThemeContrastResult {\n const touched = new Set(\n touchedChanges\n .filter((change) => aiThemeColorToken(change.control))\n .map((change) =>\n colorKey(change.scheme ?? 'light', aiThemeColorToken(change.control) as ThemeColorToken),\n ),\n )\n const schemes = THEME_EDITOR_SCHEMES.filter(\n (scheme) => scheme === 'light' || readDarkScheme(themeAfter) !== 'off',\n )\n let theme = themeAfter\n const corrections: AiThemeCorrection[] = []\n const notes: string[] = []\n for (const scheme of schemes) {\n for (const pair of AI_THEME_CONTRAST_PAIRS) {\n const foreground = effectiveThemeColor(theme, scheme, pair.foreground)\n const backgrounds = pair.backgrounds\n .map((token) => effectiveThemeColor(theme, scheme, token))\n .filter((value): value is string => typeof value === 'string')\n if (!foreground || backgrounds.length !== pair.backgrounds.length) continue\n const ratios = backgrounds.map((background) => contrastRatio(foreground, background))\n if (ratios.some((ratio) => ratio === null)) continue\n const worst = Math.min(...(ratios as number[]))\n if (worst >= pair.required) continue\n const involved = [pair.foreground, ...pair.backgrounds].some((token) =>\n touched.has(colorKey(scheme, token)),\n )\n if (!involved) {\n notes.push(\n `${pair.foreground} on ${pair.backgrounds.join(' and ')} in ${scheme} was already ` +\n `${round2(worst)}:1 before this proposal, below ${pair.required}:1.`,\n )\n continue\n }\n let corrected: string\n try {\n corrected = accessibleShade(\n foreground,\n backgrounds,\n darkInkReads(backgrounds[0]) ? 'darken' : 'lighten',\n { minContrast: pair.required },\n )\n } catch {\n continue\n }\n const clears = backgrounds.every(\n (background) => (contrastRatio(corrected, background) ?? 0) >= pair.required,\n )\n if (!clears) {\n notes.push(\n `${pair.foreground} cannot reach ${pair.required}:1 on the ${scheme} background by ` +\n 'changing its shade alone; the background needs to change too.',\n )\n continue\n }\n corrections.push({\n control: `color.${pair.foreground}`,\n scheme,\n from: readThemeColor(theme, scheme, pair.foreground) ?? null,\n to: corrected,\n reason: 'contrast',\n against: backgrounds,\n ratio: round2(worst),\n required: pair.required,\n })\n theme = writeThemeColor(theme, scheme, pair.foreground, corrected)\n }\n }\n return { theme, corrections, notes }\n}\n\n/** Folds a correction into a change list: the change it corrects, or a new one. */\nfunction withCorrection(\n changes: AiThemeControlChange[],\n correction: AiThemeCorrection,\n base: HostTheme | undefined,\n): AiThemeControlChange[] {\n const index = changes.findIndex(\n (change) => change.control === correction.control && change.scheme === correction.scheme,\n )\n if (index >= 0) {\n return changes.map((change, at) => (at === index ? { ...change, value: correction.to } : change))\n }\n return [\n ...changes,\n {\n control: correction.control,\n scheme: correction.scheme,\n value: correction.to,\n before: readAiThemeControl(base, correction.control, correction.scheme),\n },\n ]\n}\n\n/* ------------------------------------------------------------------------ *\n * Building a proposal\n * ------------------------------------------------------------------------ */\n\nexport interface BuildAiThemeProposalInput {\n /** The site's theme as the editor shows it, overrides resolved. */\n base: HostTheme | undefined\n source: HostThemeSource\n mode: AiThemeProposalMode\n brief: string\n summary: string\n changes: ReadonlyArray<Pick<AiThemeControlChange, 'control' | 'scheme' | 'value'>>\n components: readonly ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What reading the model's answer already left out, in words. */\n dropped?: readonly string[]\n notes?: readonly string[]\n}\n\n/**\n * A proposal from what the model proposed, held to the editor and to the\n * doctrine: changes that change nothing are gone, a modify brief reaches\n * only the parts of the theme it names, accents keep a dark value, and every\n * pair the proposal touches clears its contrast bar.\n */\nexport function buildAiThemeProposal(input: BuildAiThemeProposalInput): AiThemeProposal {\n const dropped = [...(input.dropped ?? [])]\n const notes = [...(input.notes ?? [])]\n const scope = aiThemeBriefScope(input.brief, input.mode)\n const outOfScope = (group: AiThemeChangeGroup, what: string) => {\n dropped.push(`${what}: the brief did not ask about ${GROUP_WORDS[group]}`)\n }\n\n let changes: AiThemeControlChange[] = []\n for (const change of input.changes) {\n const before = readAiThemeControl(input.base, change.control, change.scheme)\n if (before === change.value) continue\n const group = aiThemeControlGroup(change.control)\n if (!scope.has(group)) {\n outOfScope(group, change.scheme ? `${change.control} (${change.scheme})` : change.control)\n continue\n }\n changes.push({ ...change, before })\n }\n const components = input.components.filter((leaf) => {\n const group = aiThemeComponentGroup(leaf.component)\n if (scope.has(group)) return true\n outOfScope(group, `${leaf.component}.${leaf.property}`)\n return false\n })\n let resetComponents = input.resetComponents\n if (resetComponents && !scope.has('components')) {\n outOfScope('components', 'Resetting the component overrides')\n resetComponents = false\n }\n\n const corrections: AiThemeCorrection[] = []\n let after = applyAiThemeChanges(input.base, { changes, components, resetComponents })\n for (const token of aiThemeMissingDarkValues(changes, after)) {\n const light = changes.find(\n (change) => change.scheme === 'light' && aiThemeColorToken(change.control) === token,\n )\n if (!ACCENT_TOKENS.has(token) || typeof light?.value !== 'string') {\n notes.push(`${token} keeps its current dark value; only its light value changes.`)\n continue\n }\n const dark = deriveAiThemeDarkAccent(light.value, after)\n if (!dark) continue\n const correction: AiThemeCorrection = {\n control: `color.${token}`,\n scheme: 'dark',\n from: readThemeColor(input.base, 'dark', token) ?? null,\n to: dark,\n reason: 'dark-value',\n }\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n after = writeThemeColor(after, 'dark', token, dark)\n }\n\n const contrast = correctAiThemeContrast(after, changes)\n for (const correction of contrast.corrections) {\n corrections.push(correction)\n changes = withCorrection(changes, correction, input.base)\n }\n notes.push(...contrast.notes)\n\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode: input.mode,\n summary: input.summary,\n source: input.source,\n changes,\n components,\n resetComponents,\n corrections,\n dropped,\n notes,\n }\n}\n\n/**\n * What applying a proposal to the site's CURRENT theme produces, held to the\n * contrast bars again: the theme may have changed since the proposal was\n * made, and a proposal that cleared its bars against the old background can\n * fall short against the new one. The corrections this makes are named\n * beside the proposal's own.\n */\nexport function aiThemeProposalAfter(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes' | 'components' | 'resetComponents'>,\n): AiThemeContrastResult {\n const applied = applyAiThemeChanges(theme, proposal)\n return correctAiThemeContrast(applied, proposal.changes)\n}\n\n/** The changes whose `before` no longer matches the theme a proposal is applied to. */\nexport function aiThemeStaleChanges(\n theme: HostTheme | undefined,\n proposal: Pick<AiThemeProposal, 'changes'>,\n): AiThemeControlChange[] {\n return proposal.changes.filter(\n (change) => readAiThemeControl(theme, change.control, change.scheme) !== change.before,\n )\n}\n\n/* ------------------------------------------------------------------------ *\n * Reading a stored proposal back\n * ------------------------------------------------------------------------ */\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst strings = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n\nconst scalar = (value: unknown): string | number | null =>\n typeof value === 'string' || typeof value === 'number' ? value : null\n\nconst VALUE_CONTROLS: ReadonlySet<string> = new Set(AI_THEME_VALUE_CONTROLS)\n\n/**\n * A proposal as a job output stores it, or `null` for anything that is not\n * one. The job writes these through the Admin SDK and nothing else can, but\n * a reader that applies what it reads to a theme checks the shape first.\n */\nexport function readAiThemeProposal(value: unknown): AiThemeProposal | null {\n if (!isRecord(value) || value['version'] !== AI_THEME_PROPOSAL_VERSION) return null\n const mode = value['mode'] === 'create' ? 'create' : 'modify'\n const source =\n value['source'] === 'installed' || value['source'] === 'custom' ? value['source'] : 'default'\n const changes = (Array.isArray(value['changes']) ? value['changes'] : [])\n .filter(isRecord)\n .filter((change) => VALUE_CONTROLS.has(String(change['control'])))\n .map(\n (change): AiThemeControlChange => ({\n control: change['control'] as AiThemeValueControl,\n scheme: change['scheme'] === 'light' || change['scheme'] === 'dark' ? change['scheme'] : null,\n value: scalar(change['value']),\n before: scalar(change['before']),\n }),\n )\n const components = (Array.isArray(value['components']) ? value['components'] : [])\n .filter(isRecord)\n .filter(\n (leaf) =>\n (THEME_EDITOR_CONTROLS.find((control) => control.id === 'components')?.options ?? []).includes(\n String(leaf['component']),\n ) &&\n (leaf['target'] === 'styleOverrides' ||\n leaf['target'] === 'sx' ||\n leaf['target'] === 'defaultProps') &&\n typeof leaf['property'] === 'string' &&\n ['string', 'number', 'boolean'].includes(typeof leaf['value']) &&\n (leaf['media'] === null || String(leaf['media']) in THEME_EDITOR_MEDIA_QUERIES),\n )\n .map(\n (leaf): ThemeComponentOverrideLeaf => ({\n component: leaf['component'] as ThemeComponentOverrideLeaf['component'],\n target: leaf['target'] as ThemeComponentOverrideLeaf['target'],\n slot: typeof leaf['slot'] === 'string' ? leaf['slot'] : null,\n property: leaf['property'] as string,\n media: (leaf['media'] as ThemeComponentOverrideLeaf['media']) ?? null,\n value: leaf['value'] as string | number | boolean,\n }),\n )\n const corrections = (Array.isArray(value['corrections']) ? value['corrections'] : [])\n .filter(isRecord)\n .filter(\n (correction) =>\n aiThemeColorToken(String(correction['control'])) !== null &&\n (correction['scheme'] === 'light' || correction['scheme'] === 'dark') &&\n typeof correction['to'] === 'string',\n )\n .map(\n (correction): AiThemeCorrection => ({\n control: correction['control'] as AiThemeCorrection['control'],\n scheme: correction['scheme'] as HostThemeScheme,\n from: typeof correction['from'] === 'string' ? correction['from'] : null,\n to: correction['to'] as string,\n reason: correction['reason'] === 'dark-value' ? 'dark-value' : 'contrast',\n ...(Array.isArray(correction['against']) ? { against: strings(correction['against']) } : {}),\n ...(typeof correction['ratio'] === 'number' ? { ratio: correction['ratio'] } : {}),\n ...(typeof correction['required'] === 'number' ? { required: correction['required'] } : {}),\n }),\n )\n return {\n version: AI_THEME_PROPOSAL_VERSION,\n mode,\n summary: typeof value['summary'] === 'string' ? value['summary'] : '',\n source,\n changes,\n components,\n resetComponents: value['resetComponents'] === true,\n corrections,\n dropped: strings(value['dropped']),\n notes: strings(value['notes']),\n }\n}\n"],"names":["CONTRAST_AA","CONTRAST_AA_LARGE","contrastRatio","accessibleShade","inheritedThemeColor","readDarkScheme","readFontFamily","readThemeColor","readToolbarHeight","resetComponentOverrides","SYSTEM_FONT_VALUE","THEME_COLOR_FIELDS","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","THEME_EDITOR_SCHEMES","writeBorderRadius","writeComponentOverride","writeDarkScheme","writeFontFamily","writeSpacing","writeThemeColor","writeToolbarHeight","prefersDarkInk","AI_THEME_PROPOSAL_VERSION","AI_THEME_PROPOSAL_MODES","AI_THEME_CHANGE_GROUPS","AI_THEME_VALUE_CONTROLS","filter","control","id","map","aiThemeColorToken","startsWith","slice","length","readAiThemeControl","theme","scheme","token","family","shape","borderRadius","spacing","applyControlChange","change","value","undefined","number","applyAiThemeChanges","proposal","next","changes","resetComponents","leaf","components","aiThemeControlGroup","aiThemeComponentGroup","component","BRIEF_VOCABULARY","color","darkScheme","typography","navigation","aiThemeBriefScope","brief","mode","Set","named","group","test","GROUP_WORDS","colorKey","effectiveThemeColor","ACCENT_TOKENS","field","aiThemeMissingDarkValues","themeAfter","set","has","deriveAiThemeDarkAccent","light","background","minContrast","AI_THEME_CONTRAST_PAIRS","foreground","backgrounds","required","round2","ratio","Math","round","darkInkReads","verdict","correctAiThemeContrast","touchedChanges","touched","schemes","corrections","notes","pair","ratios","some","worst","min","involved","push","join","corrected","clears","every","from","to","reason","against","withCorrection","correction","base","index","findIndex","at","before","buildAiThemeProposal","input","dropped","scope","outOfScope","what","property","after","find","dark","contrast","version","summary","source","aiThemeProposalAfter","applied","aiThemeStaleChanges","isRecord","Array","isArray","strings","entry","scalar","VALUE_CONTROLS","readAiThemeProposal","String","options","includes","target","slot","media"],"mappings":"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"}
@@ -262,7 +262,7 @@ function colorValue(scheme) {
262
262
  navHeightDesktop: numberProperty('navHeight.sm', 'The navigation bar height from the tablet breakpoint up'),
263
263
  componentOverrides: {
264
264
  type: 'array',
265
- description: 'Component styles beyond the palette and typography, one leaf each. A style names a ' + 'slot and a camelCase CSS property; a default names a prop. A style never carries a ' + 'literal color: color belongs to the palette.',
265
+ description: 'Component styles beyond the palette and typography, one leaf each. A style names a ' + 'slot and a camelCase CSS property; a default names a prop. A style never carries a ' + 'literal color: color belongs to the palette. Target sx to write the style in theme ' + 'terms, where a color property takes a palette path (primary.main, divider, ' + 'text.secondary) and a length a spacing unit.',
266
266
  items: {
267
267
  type: 'object',
268
268
  properties: {
@@ -276,6 +276,7 @@ function colorValue(scheme) {
276
276
  type: 'string',
277
277
  enum: [
278
278
  'styleOverrides',
279
+ 'sx',
279
280
  'defaultProps'
280
281
  ]
281
282
  },
@@ -376,9 +377,9 @@ function parseComponentLeaf(raw, index, dropped) {
376
377
  dropped.push(`${where}: ${component || 'the component'} is not one a theme may override`);
377
378
  return null;
378
379
  }
379
- const target = raw['target'] === 'defaultProps' || raw['target'] === 'styleOverrides' ? raw['target'] : null;
380
+ const target = raw['target'] === 'defaultProps' || raw['target'] === 'styleOverrides' || raw['target'] === 'sx' ? raw['target'] : null;
380
381
  if (!target) {
381
- dropped.push(`${where}: target must be styleOverrides or defaultProps`);
382
+ dropped.push(`${where}: target must be styleOverrides, sx or defaultProps`);
382
383
  return null;
383
384
  }
384
385
  const property = String((_raw_property = raw['property']) != null ? _raw_property : '');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-theme-tool.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 { HostThemeScheme } from '@aglyn/shared-data-types'\nimport {\n COMPONENT_OVERRIDES_FIELD,\n SYSTEM_FONT_VALUE,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n themeEditorControl,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControl,\n type ThemeEditorControlId,\n type ThemeEditorMedia,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport type { AiThemeControlChange, AiThemeValueControl } from '../model/ai-theme-proposal'\nimport type { AiTool } from '../providers/contract'\nimport { AI_SX_ALLOWED_KEYS, HOSTILE_TEXT, SX_VALUE } from '../runtime/ai-node-tree'\n\n/**\n * The theme tool (AGL-2938): the one structured-output tool a theme job asks\n * the model to call, DERIVED from the theme editor's field catalog.\n *\n * Every property of the schema carries editor controls, and every editor\n * control is carried by a property — `ai-theme-tool.spec.ts` holds both\n * directions — so the model is offered exactly what a person editing the\n * theme is offered: the same color tokens, the same fonts, the same bounds\n * and the same component whitelist. A control added to the editor fails that\n * spec until it is offered here too.\n *\n * The schema is strict, which on the providers that enforce it means every\n * object forbids extra keys, every key is required, and \"no change\" is said\n * with `null`. Numeric bounds are not keywords a strict schema accepts, so\n * they are stated in the descriptions and enforced by\n * {@link parseAiThemeToolInput}, from the same catalog entries.\n */\n\nexport const AI_THEME_TOOL_NAME = 'propose_theme_changes'\n\n/** The value that returns a color or a number to its default. */\nexport const AI_THEME_TOOL_DEFAULT = 'default'\n\n/** The catalog's color controls, which the `colors` property carries. */\nexport const AI_THEME_COLOR_CONTROLS: readonly ThemeEditorControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.kind === 'color',\n)\n\n/** How many component override leaves one call may carry. */\nexport const AI_THEME_TOOL_MAX_COMPONENT_LEAVES = 40\n\n/** The longest summary a proposal keeps. */\nexport const AI_THEME_SUMMARY_MAX_CHARS = 300\n\n/**\n * Which schema property carries which editor controls. `colors` carries\n * every color control in both schemes; the rest carry one control each, and\n * the two component properties both carry the JSON overrides control.\n */\nexport const AI_THEME_TOOL_PROPERTIES: ReadonlyArray<{\n property: string\n controls: readonly ThemeEditorControlId[]\n}> = [\n { property: 'colors', controls: AI_THEME_COLOR_CONTROLS.map((control) => control.id) },\n { property: 'darkScheme', controls: ['darkScheme'] },\n { property: 'fontFamily', controls: ['fontFamily'] },\n { property: 'borderRadius', controls: ['borderRadius'] },\n { property: 'spacing', controls: ['spacing'] },\n { property: 'navHeightMobile', controls: ['navHeight.xs'] },\n { property: 'navHeightDesktop', controls: ['navHeight.sm'] },\n { property: 'componentOverrides', controls: ['components'] },\n { property: 'resetComponentOverrides', controls: ['components'] },\n]\n\n/** Properties that carry no editor control: what the call says about itself. */\nexport const AI_THEME_TOOL_META_PROPERTIES = ['summary'] as const\n\n/**\n * Parity between the editor's catalog and the tool, in both directions: the\n * controls the tool does not offer, and the control ids it offers that the\n * catalog does not have. Both are empty, or the tool and the editor offer\n * different things.\n */\nexport function aiThemeToolParity(\n controls: ReadonlyArray<Pick<ThemeEditorControl, 'id'>> = THEME_EDITOR_CONTROLS,\n): { unoffered: string[]; unknown: string[] } {\n const offered = new Set<string>(AI_THEME_TOOL_PROPERTIES.flatMap((entry) => entry.controls))\n const catalog = new Set<string>(controls.map((control) => control.id))\n return {\n unoffered: [...catalog].filter((id) => !offered.has(id)),\n unknown: [...offered].filter((id) => !catalog.has(id)),\n }\n}\n\n/**\n * The style properties a component override may set: the node validator's\n * allowlist less the `sx` shorthands (`p`, `bgcolor`, `typography`…), which a\n * style override does not expand.\n */\nconst SX_SHORTHANDS = new Set([\n 'm', 'mt', 'mr', 'mb', 'ml', 'mx', 'my',\n 'p', 'pt', 'pr', 'pb', 'pl', 'px', 'py',\n 'bgcolor', 'typography',\n])\nexport const AI_THEME_STYLE_PROPERTIES: readonly string[] = [...AI_SX_ALLOWED_KEYS].filter(\n (key) => !SX_SHORTHANDS.has(key),\n)\n\n/**\n * The component props a theme may set a default for: the ones that choose\n * among a component's own looks. Nothing that changes what an element IS — an\n * `href`, a `component`, a handler — is a theme's to default.\n */\nexport const AI_THEME_DEFAULT_PROPS: readonly string[] = [\n 'align',\n 'centered',\n 'color',\n 'disableElevation',\n 'disableRipple',\n 'elevation',\n 'fullWidth',\n 'gutterBottom',\n 'indicatorColor',\n 'margin',\n 'noWrap',\n 'size',\n 'square',\n 'textColor',\n 'underline',\n 'variant',\n]\n\n/** A literal color written as a hex or a color function. */\nconst LITERAL_COLOR = /#[0-9a-f]{3,8}\\b|\\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\\s*\\(/i\n\n/** The CSS named colors, which are literal colors as surely as a hex is. */\nconst NAMED_COLORS: ReadonlySet<string> = new Set(\n (\n 'aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue ' +\n 'blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk ' +\n 'crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki ' +\n 'darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen ' +\n 'darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue ' +\n 'dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite ' +\n 'gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki ' +\n 'lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan ' +\n 'lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen ' +\n 'lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen ' +\n 'magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen ' +\n 'mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream ' +\n 'mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid ' +\n 'palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum ' +\n 'powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown ' +\n 'seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen ' +\n 'steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen'\n ).split(' '),\n)\n\n/** Whether a CSS value carries a literal color, which belongs to the palette. */\nexport function carriesLiteralColor(value: string): boolean {\n if (LITERAL_COLOR.test(value)) return true\n return (value.toLowerCase().match(/[a-z]+/g) ?? []).some((word) => NAMED_COLORS.has(word))\n}\n\nconst HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i\nconst IDENTIFIER = /^[A-Za-z][A-Za-z0-9]{0,63}$/\nconst PROP_TOKEN = /^[A-Za-z][A-Za-z0-9-]{0,39}$/\n\nconst nullable = (schema: Record<string, unknown>) => ({\n anyOf: [schema, { type: 'null' }],\n})\n\nconst rangeText = (control: ThemeEditorControl) => `from ${control.min} to ${control.max}`\n\nfunction numberProperty(id: ThemeEditorControlId, what: string): Record<string, unknown> {\n const control = themeEditorControl(id)\n return {\n anyOf: [\n { type: 'integer' },\n { type: 'string', enum: [AI_THEME_TOOL_DEFAULT] },\n { type: 'null' },\n ],\n description:\n `${what}, in px, a whole number ${rangeText(control)}. ` +\n `\"${AI_THEME_TOOL_DEFAULT}\" returns it to the theme default; null leaves it as it is.`,\n }\n}\n\nfunction colorValue(scheme: HostThemeScheme): Record<string, unknown> {\n return {\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description:\n `The ${scheme} scheme's value: a hex color #rrggbb, \"${AI_THEME_TOOL_DEFAULT}\" to return ` +\n `it to the theme default, or null to leave the ${scheme} value as it is.`,\n }\n}\n\n/**\n * The tool, built from the catalog. Deterministic for a given catalog, so its\n * bytes are the same on every request and it sits inside the cached prefix.\n */\nexport function aiThemeTool(): AiTool {\n const darkScheme = themeEditorControl('darkScheme')\n const fontFamily = themeEditorControl('fontFamily')\n const properties: Record<string, Record<string, unknown>> = {\n summary: {\n type: 'string',\n description: 'One sentence, in the language of the brief, naming what this proposal changes.',\n },\n colors: {\n type: 'array',\n description:\n 'Color changes, one entry per token. Give the light and the dark value together: a ' +\n 'color changed for one scheme and left unsaid for the other is half a change.',\n items: {\n type: 'object',\n properties: {\n token: {\n type: 'string',\n enum: AI_THEME_COLOR_CONTROLS.map((control) => control.token as ThemeColorToken),\n description:\n 'A palette color (primary, secondary, tertiary, surface, error, warning, info, ' +\n 'success), a background (background.default is the page, background.paper cards ' +\n 'and menus), a text color, a pale accent tint, or the divider.',\n },\n light: colorValue('light'),\n dark: colorValue('dark'),\n },\n required: ['token', 'light', 'dark'],\n additionalProperties: false,\n },\n },\n darkScheme: nullable({\n type: 'string',\n enum: darkScheme.options,\n description:\n '\"auto\" lets visitors in dark mode see the dark scheme; \"off\" keeps every visitor on ' +\n 'light. null leaves it as it is.',\n }),\n fontFamily: nullable({\n type: 'string',\n enum: fontFamily.options,\n description:\n `The site's font, from the curated list; \"${SYSTEM_FONT_VALUE}\" is the theme default. ` +\n 'null leaves it as it is.',\n }),\n borderRadius: numberProperty('borderRadius', 'The corner radius'),\n spacing: numberProperty('spacing', 'The spacing unit every gap and padding is a multiple of'),\n navHeightMobile: numberProperty('navHeight.xs', 'The navigation bar height on phones'),\n navHeightDesktop: numberProperty(\n 'navHeight.sm',\n 'The navigation bar height from the tablet breakpoint up',\n ),\n componentOverrides: {\n type: 'array',\n description:\n 'Component styles beyond the palette and typography, one leaf each. A style names a ' +\n 'slot and a camelCase CSS property; a default names a prop. A style never carries a ' +\n 'literal color: color belongs to the palette.',\n items: {\n type: 'object',\n properties: {\n component: { type: 'string', enum: [...COMPONENT_OVERRIDES_FIELD.components] },\n target: { type: 'string', enum: ['styleOverrides', 'defaultProps'] },\n slot: nullable({\n type: 'string',\n description: 'The style slot (root, contained, h1); null for a default prop.',\n }),\n property: {\n type: 'string',\n description: 'A camelCase CSS property for a style, or the prop for a default.',\n },\n media: nullable({\n type: 'string',\n enum: Object.keys(THEME_EDITOR_MEDIA_QUERIES),\n description: 'Scopes a style to phones or to wider screens; null for every width.',\n }),\n value: { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }] },\n },\n required: ['component', 'target', 'slot', 'property', 'media', 'value'],\n additionalProperties: false,\n },\n },\n resetComponentOverrides: {\n type: 'boolean',\n description:\n \"true drops the site's own component overrides before any above apply, so the theme's \" +\n 'defaults show again.',\n },\n }\n return {\n name: AI_THEME_TOOL_NAME,\n description:\n \"Propose changes to the site's theme. Name only the controls the brief is about; every \" +\n 'control left out keeps its current value. Call this once.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties,\n required: Object.keys(properties),\n additionalProperties: false,\n },\n }\n}\n\n/** What a tool call proposes, held to the editor's constraints. */\nexport interface AiThemeToolParse {\n summary: string\n changes: Array<Omit<AiThemeControlChange, 'before'>>\n components: ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What the call wrote that the editor would not accept, in words. */\n dropped: string[]\n /** What was kept only once it was brought inside the editor's bounds. */\n notes: string[]\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst quoted = (value: unknown) => `\"${String(value).slice(0, 40)}\"`\n\n/** `#abc` or `#aabbcc` as the editor's picker writes it: six lowercase digits. */\nexport function normalizeThemeHex(value: string): string | null {\n const trimmed = value.trim()\n if (!HEX.test(trimmed)) return null\n const digits = trimmed.slice(1).toLowerCase()\n return `#${digits.length === 3 ? digits.replace(/./g, (digit) => digit + digit) : digits}`\n}\n\nfunction parseColorValue(raw: unknown, where: string, dropped: string[]): string | null | undefined {\n if (raw === null || raw === undefined) return undefined\n if (raw === AI_THEME_TOOL_DEFAULT) return null\n const hex = typeof raw === 'string' ? normalizeThemeHex(raw) : null\n if (hex) return hex\n dropped.push(`${where}: ${quoted(raw)} is not a hex color`)\n return undefined\n}\n\nfunction parseNumber(\n raw: unknown,\n id: ThemeEditorControlId,\n dropped: string[],\n notes: string[],\n): number | null | undefined {\n if (raw === null || raw === undefined) return undefined\n if (raw === AI_THEME_TOOL_DEFAULT) return null\n const control = themeEditorControl(id)\n if (typeof raw !== 'number' || !Number.isFinite(raw)) {\n dropped.push(`${control.label}: ${quoted(raw)} is not a number`)\n return undefined\n }\n const bounded = Math.min(control.max as number, Math.max(control.min as number, Math.round(raw)))\n if (bounded !== raw) {\n notes.push(`${control.label}: ${raw} is outside ${rangeText(control)}, so ${bounded} is proposed.`)\n }\n return bounded\n}\n\nfunction parseComponentLeaf(\n raw: unknown,\n index: number,\n dropped: string[],\n): ThemeComponentOverrideLeaf | null {\n const where = `componentOverrides[${index}]`\n if (!isRecord(raw)) {\n dropped.push(`${where}: not an object`)\n return null\n }\n const component = String(raw['component'] ?? '')\n if (!(COMPONENT_OVERRIDES_FIELD.components as readonly string[]).includes(component)) {\n dropped.push(`${where}: ${component || 'the component'} is not one a theme may override`)\n return null\n }\n const target =\n raw['target'] === 'defaultProps' || raw['target'] === 'styleOverrides' ? raw['target'] : null\n if (!target) {\n dropped.push(`${where}: target must be styleOverrides or defaultProps`)\n return null\n }\n const property = String(raw['property'] ?? '')\n const value = raw['value']\n const named = `${component}.${property || '?'}`\n if (target === 'defaultProps') {\n if (!AI_THEME_DEFAULT_PROPS.includes(property)) {\n dropped.push(`${named}: not a prop a theme may set a default for`)\n return null\n }\n const valid =\n typeof value === 'boolean' ||\n (typeof value === 'number' && Number.isFinite(value)) ||\n (typeof value === 'string' && PROP_TOKEN.test(value))\n if (!valid) {\n dropped.push(`${named}: ${quoted(value)} is not a value a prop default may take`)\n return null\n }\n return {\n component: component as ThemeComponentOverrideLeaf['component'],\n target,\n slot: null,\n property,\n media: null,\n value: value as string | number | boolean,\n }\n }\n if (!AI_THEME_STYLE_PROPERTIES.includes(property)) {\n dropped.push(`${named}: not a style a theme may set`)\n return null\n }\n const slot = raw['slot'] === null || raw['slot'] === undefined ? 'root' : String(raw['slot'])\n if (!IDENTIFIER.test(slot)) {\n dropped.push(`${named}: ${quoted(slot)} is not a style slot`)\n return null\n }\n const media =\n raw['media'] === null || raw['media'] === undefined\n ? null\n : String(raw['media']) in THEME_EDITOR_MEDIA_QUERIES\n ? (raw['media'] as ThemeEditorMedia)\n : undefined\n if (media === undefined) {\n dropped.push(`${named}: media must be mobile, desktop or null`)\n return null\n }\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n dropped.push(`${named}: not a finite number`)\n return null\n }\n return {\n component: component as ThemeComponentOverrideLeaf['component'],\n target,\n slot,\n property,\n media,\n value,\n }\n }\n if (typeof value !== 'string') {\n dropped.push(`${named}: a style takes a string or a number`)\n return null\n }\n const trimmed = value.trim()\n if (!trimmed || HOSTILE_TEXT.test(trimmed) || !SX_VALUE.test(trimmed)) {\n dropped.push(`${named}: ${quoted(trimmed)} is not a value a style may take`)\n return null\n }\n if (carriesLiteralColor(trimmed)) {\n dropped.push(`${named}: a color belongs to the palette, not to a component style`)\n return null\n }\n return {\n component: component as ThemeComponentOverrideLeaf['component'],\n target,\n slot,\n property,\n media,\n value: trimmed,\n }\n}\n\n/**\n * A tool call's input, held to the editor's constraints: tokens and fonts\n * from the catalog, hex colors as the picker writes them, numbers inside the\n * editor's bounds, and component leaves on the whitelist with safe values\n * and no literal color. What does not hold is dropped and named rather than\n * guessed at; a number out of bounds is brought inside them and named.\n */\nexport function parseAiThemeToolInput(input: Record<string, unknown>): AiThemeToolParse {\n const dropped: string[] = []\n const notes: string[] = []\n const changes: AiThemeToolParse['changes'] = []\n const summary = String(input['summary'] ?? '').trim().slice(0, AI_THEME_SUMMARY_MAX_CHARS)\n\n const colorControls = new Set<string>(AI_THEME_COLOR_CONTROLS.map((control) => control.id))\n const rawColors = Array.isArray(input['colors']) ? input['colors'] : []\n const seen = new Set<string>()\n for (const [index, entry] of rawColors.slice(0, AI_THEME_COLOR_CONTROLS.length * 2).entries()) {\n if (!isRecord(entry)) {\n dropped.push(`colors[${index}]: not an object`)\n continue\n }\n const control = `color.${String(entry['token'] ?? '')}`\n if (!colorControls.has(control)) {\n dropped.push(`colors[${index}]: ${quoted(entry['token'])} is not a theme color`)\n continue\n }\n if (seen.has(control)) {\n dropped.push(`${control}: named twice; the first value is kept`)\n continue\n }\n seen.add(control)\n for (const scheme of ['light', 'dark'] as const) {\n const value = parseColorValue(entry[scheme], `${control} (${scheme})`, dropped)\n if (value === undefined) continue\n changes.push({ control: control as AiThemeValueControl, scheme, value })\n }\n }\n\n const darkScheme = input['darkScheme']\n if (darkScheme === 'auto' || darkScheme === 'off') {\n changes.push({ control: 'darkScheme', scheme: null, value: darkScheme === 'off' ? 'off' : null })\n } else if (darkScheme !== null && darkScheme !== undefined) {\n dropped.push(`darkScheme: ${quoted(darkScheme)} is not auto or off`)\n }\n\n const fontFamily = input['fontFamily']\n if (typeof fontFamily === 'string' && themeEditorControl('fontFamily').options?.includes(fontFamily)) {\n changes.push({\n control: 'fontFamily',\n scheme: null,\n value: fontFamily === SYSTEM_FONT_VALUE ? null : fontFamily,\n })\n } else if (fontFamily !== null && fontFamily !== undefined) {\n dropped.push(`fontFamily: ${quoted(fontFamily)} is not in the font list`)\n }\n\n const numbers: Array<[string, AiThemeValueControl]> = [\n ['borderRadius', 'borderRadius'],\n ['spacing', 'spacing'],\n ['navHeightMobile', 'navHeight.xs'],\n ['navHeightDesktop', 'navHeight.sm'],\n ]\n for (const [property, control] of numbers) {\n const value = parseNumber(input[property], control, dropped, notes)\n if (value !== undefined) changes.push({ control, scheme: null, value })\n }\n\n const rawLeaves = Array.isArray(input['componentOverrides']) ? input['componentOverrides'] : []\n if (rawLeaves.length > AI_THEME_TOOL_MAX_COMPONENT_LEAVES) {\n dropped.push(\n `componentOverrides: ${rawLeaves.length - AI_THEME_TOOL_MAX_COMPONENT_LEAVES} past the ` +\n `${AI_THEME_TOOL_MAX_COMPONENT_LEAVES}-leaf limit`,\n )\n }\n const components = rawLeaves\n .slice(0, AI_THEME_TOOL_MAX_COMPONENT_LEAVES)\n .map((leaf, index) => parseComponentLeaf(leaf, index, dropped))\n .filter((leaf): leaf is ThemeComponentOverrideLeaf => leaf !== null)\n\n return {\n summary,\n changes,\n components,\n resetComponents: input['resetComponentOverrides'] === true,\n dropped,\n notes,\n }\n}\n"],"names":["COMPONENT_OVERRIDES_FIELD","SYSTEM_FONT_VALUE","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","themeEditorControl","AI_SX_ALLOWED_KEYS","HOSTILE_TEXT","SX_VALUE","AI_THEME_TOOL_NAME","AI_THEME_TOOL_DEFAULT","AI_THEME_COLOR_CONTROLS","filter","control","kind","AI_THEME_TOOL_MAX_COMPONENT_LEAVES","AI_THEME_SUMMARY_MAX_CHARS","AI_THEME_TOOL_PROPERTIES","property","controls","map","id","AI_THEME_TOOL_META_PROPERTIES","aiThemeToolParity","offered","Set","flatMap","entry","catalog","unoffered","has","unknown","SX_SHORTHANDS","AI_THEME_STYLE_PROPERTIES","key","AI_THEME_DEFAULT_PROPS","LITERAL_COLOR","NAMED_COLORS","split","carriesLiteralColor","value","test","toLowerCase","match","some","word","HEX","IDENTIFIER","PROP_TOKEN","nullable","schema","anyOf","type","rangeText","min","max","numberProperty","what","enum","description","colorValue","scheme","aiThemeTool","darkScheme","fontFamily","properties","summary","colors","items","token","light","dark","required","additionalProperties","options","borderRadius","spacing","navHeightMobile","navHeightDesktop","componentOverrides","component","components","target","slot","media","Object","keys","resetComponentOverrides","name","strict","inputSchema","isRecord","Array","isArray","quoted","String","slice","normalizeThemeHex","trimmed","trim","digits","length","replace","digit","parseColorValue","raw","where","dropped","undefined","hex","push","parseNumber","notes","Number","isFinite","label","bounded","Math","round","parseComponentLeaf","index","includes","named","valid","parseAiThemeToolInput","input","changes","colorControls","rawColors","seen","entries","add","numbers","rawLeaves","leaf","resetComponents"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-theme-tool.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 { HostThemeScheme } from '@aglyn/shared-data-types'\nimport {\n COMPONENT_OVERRIDES_FIELD,\n SYSTEM_FONT_VALUE,\n THEME_EDITOR_CONTROLS,\n THEME_EDITOR_MEDIA_QUERIES,\n themeEditorControl,\n type ThemeColorToken,\n type ThemeComponentOverrideLeaf,\n type ThemeEditorControl,\n type ThemeEditorControlId,\n type ThemeEditorMedia,\n} from '@aglyn/shared-ui-theme/util/theme-editor-fields'\nimport type { AiThemeControlChange, AiThemeValueControl } from '../model/ai-theme-proposal'\nimport type { AiTool } from '../providers/contract'\nimport { AI_SX_ALLOWED_KEYS, HOSTILE_TEXT, SX_VALUE } from '../runtime/ai-node-tree'\n\n/**\n * The theme tool (AGL-2938): the one structured-output tool a theme job asks\n * the model to call, DERIVED from the theme editor's field catalog.\n *\n * Every property of the schema carries editor controls, and every editor\n * control is carried by a property — `ai-theme-tool.spec.ts` holds both\n * directions — so the model is offered exactly what a person editing the\n * theme is offered: the same color tokens, the same fonts, the same bounds\n * and the same component whitelist. A control added to the editor fails that\n * spec until it is offered here too.\n *\n * The schema is strict, which on the providers that enforce it means every\n * object forbids extra keys, every key is required, and \"no change\" is said\n * with `null`. Numeric bounds are not keywords a strict schema accepts, so\n * they are stated in the descriptions and enforced by\n * {@link parseAiThemeToolInput}, from the same catalog entries.\n */\n\nexport const AI_THEME_TOOL_NAME = 'propose_theme_changes'\n\n/** The value that returns a color or a number to its default. */\nexport const AI_THEME_TOOL_DEFAULT = 'default'\n\n/** The catalog's color controls, which the `colors` property carries. */\nexport const AI_THEME_COLOR_CONTROLS: readonly ThemeEditorControl[] = THEME_EDITOR_CONTROLS.filter(\n (control) => control.kind === 'color',\n)\n\n/** How many component override leaves one call may carry. */\nexport const AI_THEME_TOOL_MAX_COMPONENT_LEAVES = 40\n\n/** The longest summary a proposal keeps. */\nexport const AI_THEME_SUMMARY_MAX_CHARS = 300\n\n/**\n * Which schema property carries which editor controls. `colors` carries\n * every color control in both schemes; the rest carry one control each, and\n * the two component properties both carry the JSON overrides control.\n */\nexport const AI_THEME_TOOL_PROPERTIES: ReadonlyArray<{\n property: string\n controls: readonly ThemeEditorControlId[]\n}> = [\n { property: 'colors', controls: AI_THEME_COLOR_CONTROLS.map((control) => control.id) },\n { property: 'darkScheme', controls: ['darkScheme'] },\n { property: 'fontFamily', controls: ['fontFamily'] },\n { property: 'borderRadius', controls: ['borderRadius'] },\n { property: 'spacing', controls: ['spacing'] },\n { property: 'navHeightMobile', controls: ['navHeight.xs'] },\n { property: 'navHeightDesktop', controls: ['navHeight.sm'] },\n { property: 'componentOverrides', controls: ['components'] },\n { property: 'resetComponentOverrides', controls: ['components'] },\n]\n\n/** Properties that carry no editor control: what the call says about itself. */\nexport const AI_THEME_TOOL_META_PROPERTIES = ['summary'] as const\n\n/**\n * Parity between the editor's catalog and the tool, in both directions: the\n * controls the tool does not offer, and the control ids it offers that the\n * catalog does not have. Both are empty, or the tool and the editor offer\n * different things.\n */\nexport function aiThemeToolParity(\n controls: ReadonlyArray<Pick<ThemeEditorControl, 'id'>> = THEME_EDITOR_CONTROLS,\n): { unoffered: string[]; unknown: string[] } {\n const offered = new Set<string>(AI_THEME_TOOL_PROPERTIES.flatMap((entry) => entry.controls))\n const catalog = new Set<string>(controls.map((control) => control.id))\n return {\n unoffered: [...catalog].filter((id) => !offered.has(id)),\n unknown: [...offered].filter((id) => !catalog.has(id)),\n }\n}\n\n/**\n * The style properties a component override may set: the node validator's\n * allowlist less the `sx` shorthands (`p`, `bgcolor`, `typography`…), which a\n * style override does not expand.\n */\nconst SX_SHORTHANDS = new Set([\n 'm', 'mt', 'mr', 'mb', 'ml', 'mx', 'my',\n 'p', 'pt', 'pr', 'pb', 'pl', 'px', 'py',\n 'bgcolor', 'typography',\n])\nexport const AI_THEME_STYLE_PROPERTIES: readonly string[] = [...AI_SX_ALLOWED_KEYS].filter(\n (key) => !SX_SHORTHANDS.has(key),\n)\n\n/**\n * The component props a theme may set a default for: the ones that choose\n * among a component's own looks. Nothing that changes what an element IS — an\n * `href`, a `component`, a handler — is a theme's to default.\n */\nexport const AI_THEME_DEFAULT_PROPS: readonly string[] = [\n 'align',\n 'centered',\n 'color',\n 'disableElevation',\n 'disableRipple',\n 'elevation',\n 'fullWidth',\n 'gutterBottom',\n 'indicatorColor',\n 'margin',\n 'noWrap',\n 'size',\n 'square',\n 'textColor',\n 'underline',\n 'variant',\n]\n\n/** A literal color written as a hex or a color function. */\nconst LITERAL_COLOR = /#[0-9a-f]{3,8}\\b|\\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\\s*\\(/i\n\n/** The CSS named colors, which are literal colors as surely as a hex is. */\nconst NAMED_COLORS: ReadonlySet<string> = new Set(\n (\n 'aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue ' +\n 'blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk ' +\n 'crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki ' +\n 'darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen ' +\n 'darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue ' +\n 'dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite ' +\n 'gold goldenrod gray green greenyellow grey honeydew hotpink indianred indigo ivory khaki ' +\n 'lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan ' +\n 'lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen ' +\n 'lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen ' +\n 'magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen ' +\n 'mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream ' +\n 'mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid ' +\n 'palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum ' +\n 'powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown ' +\n 'seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen ' +\n 'steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen'\n ).split(' '),\n)\n\n/** Whether a CSS value carries a literal color, which belongs to the palette. */\nexport function carriesLiteralColor(value: string): boolean {\n if (LITERAL_COLOR.test(value)) return true\n return (value.toLowerCase().match(/[a-z]+/g) ?? []).some((word) => NAMED_COLORS.has(word))\n}\n\nconst HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i\nconst IDENTIFIER = /^[A-Za-z][A-Za-z0-9]{0,63}$/\nconst PROP_TOKEN = /^[A-Za-z][A-Za-z0-9-]{0,39}$/\n\nconst nullable = (schema: Record<string, unknown>) => ({\n anyOf: [schema, { type: 'null' }],\n})\n\nconst rangeText = (control: ThemeEditorControl) => `from ${control.min} to ${control.max}`\n\nfunction numberProperty(id: ThemeEditorControlId, what: string): Record<string, unknown> {\n const control = themeEditorControl(id)\n return {\n anyOf: [\n { type: 'integer' },\n { type: 'string', enum: [AI_THEME_TOOL_DEFAULT] },\n { type: 'null' },\n ],\n description:\n `${what}, in px, a whole number ${rangeText(control)}. ` +\n `\"${AI_THEME_TOOL_DEFAULT}\" returns it to the theme default; null leaves it as it is.`,\n }\n}\n\nfunction colorValue(scheme: HostThemeScheme): Record<string, unknown> {\n return {\n anyOf: [{ type: 'string' }, { type: 'null' }],\n description:\n `The ${scheme} scheme's value: a hex color #rrggbb, \"${AI_THEME_TOOL_DEFAULT}\" to return ` +\n `it to the theme default, or null to leave the ${scheme} value as it is.`,\n }\n}\n\n/**\n * The tool, built from the catalog. Deterministic for a given catalog, so its\n * bytes are the same on every request and it sits inside the cached prefix.\n */\nexport function aiThemeTool(): AiTool {\n const darkScheme = themeEditorControl('darkScheme')\n const fontFamily = themeEditorControl('fontFamily')\n const properties: Record<string, Record<string, unknown>> = {\n summary: {\n type: 'string',\n description: 'One sentence, in the language of the brief, naming what this proposal changes.',\n },\n colors: {\n type: 'array',\n description:\n 'Color changes, one entry per token. Give the light and the dark value together: a ' +\n 'color changed for one scheme and left unsaid for the other is half a change.',\n items: {\n type: 'object',\n properties: {\n token: {\n type: 'string',\n enum: AI_THEME_COLOR_CONTROLS.map((control) => control.token as ThemeColorToken),\n description:\n 'A palette color (primary, secondary, tertiary, surface, error, warning, info, ' +\n 'success), a background (background.default is the page, background.paper cards ' +\n 'and menus), a text color, a pale accent tint, or the divider.',\n },\n light: colorValue('light'),\n dark: colorValue('dark'),\n },\n required: ['token', 'light', 'dark'],\n additionalProperties: false,\n },\n },\n darkScheme: nullable({\n type: 'string',\n enum: darkScheme.options,\n description:\n '\"auto\" lets visitors in dark mode see the dark scheme; \"off\" keeps every visitor on ' +\n 'light. null leaves it as it is.',\n }),\n fontFamily: nullable({\n type: 'string',\n enum: fontFamily.options,\n description:\n `The site's font, from the curated list; \"${SYSTEM_FONT_VALUE}\" is the theme default. ` +\n 'null leaves it as it is.',\n }),\n borderRadius: numberProperty('borderRadius', 'The corner radius'),\n spacing: numberProperty('spacing', 'The spacing unit every gap and padding is a multiple of'),\n navHeightMobile: numberProperty('navHeight.xs', 'The navigation bar height on phones'),\n navHeightDesktop: numberProperty(\n 'navHeight.sm',\n 'The navigation bar height from the tablet breakpoint up',\n ),\n componentOverrides: {\n type: 'array',\n description:\n 'Component styles beyond the palette and typography, one leaf each. A style names a ' +\n 'slot and a camelCase CSS property; a default names a prop. A style never carries a ' +\n 'literal color: color belongs to the palette. Target sx to write the style in theme ' +\n 'terms, where a color property takes a palette path (primary.main, divider, ' +\n 'text.secondary) and a length a spacing unit.',\n items: {\n type: 'object',\n properties: {\n component: { type: 'string', enum: [...COMPONENT_OVERRIDES_FIELD.components] },\n target: { type: 'string', enum: ['styleOverrides', 'sx', 'defaultProps'] },\n slot: nullable({\n type: 'string',\n description: 'The style slot (root, contained, h1); null for a default prop.',\n }),\n property: {\n type: 'string',\n description: 'A camelCase CSS property for a style, or the prop for a default.',\n },\n media: nullable({\n type: 'string',\n enum: Object.keys(THEME_EDITOR_MEDIA_QUERIES),\n description: 'Scopes a style to phones or to wider screens; null for every width.',\n }),\n value: { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }] },\n },\n required: ['component', 'target', 'slot', 'property', 'media', 'value'],\n additionalProperties: false,\n },\n },\n resetComponentOverrides: {\n type: 'boolean',\n description:\n \"true drops the site's own component overrides before any above apply, so the theme's \" +\n 'defaults show again.',\n },\n }\n return {\n name: AI_THEME_TOOL_NAME,\n description:\n \"Propose changes to the site's theme. Name only the controls the brief is about; every \" +\n 'control left out keeps its current value. Call this once.',\n strict: true,\n inputSchema: {\n type: 'object',\n properties,\n required: Object.keys(properties),\n additionalProperties: false,\n },\n }\n}\n\n/** What a tool call proposes, held to the editor's constraints. */\nexport interface AiThemeToolParse {\n summary: string\n changes: Array<Omit<AiThemeControlChange, 'before'>>\n components: ThemeComponentOverrideLeaf[]\n resetComponents: boolean\n /** What the call wrote that the editor would not accept, in words. */\n dropped: string[]\n /** What was kept only once it was brought inside the editor's bounds. */\n notes: string[]\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\nconst quoted = (value: unknown) => `\"${String(value).slice(0, 40)}\"`\n\n/** `#abc` or `#aabbcc` as the editor's picker writes it: six lowercase digits. */\nexport function normalizeThemeHex(value: string): string | null {\n const trimmed = value.trim()\n if (!HEX.test(trimmed)) return null\n const digits = trimmed.slice(1).toLowerCase()\n return `#${digits.length === 3 ? digits.replace(/./g, (digit) => digit + digit) : digits}`\n}\n\nfunction parseColorValue(raw: unknown, where: string, dropped: string[]): string | null | undefined {\n if (raw === null || raw === undefined) return undefined\n if (raw === AI_THEME_TOOL_DEFAULT) return null\n const hex = typeof raw === 'string' ? normalizeThemeHex(raw) : null\n if (hex) return hex\n dropped.push(`${where}: ${quoted(raw)} is not a hex color`)\n return undefined\n}\n\nfunction parseNumber(\n raw: unknown,\n id: ThemeEditorControlId,\n dropped: string[],\n notes: string[],\n): number | null | undefined {\n if (raw === null || raw === undefined) return undefined\n if (raw === AI_THEME_TOOL_DEFAULT) return null\n const control = themeEditorControl(id)\n if (typeof raw !== 'number' || !Number.isFinite(raw)) {\n dropped.push(`${control.label}: ${quoted(raw)} is not a number`)\n return undefined\n }\n const bounded = Math.min(control.max as number, Math.max(control.min as number, Math.round(raw)))\n if (bounded !== raw) {\n notes.push(`${control.label}: ${raw} is outside ${rangeText(control)}, so ${bounded} is proposed.`)\n }\n return bounded\n}\n\nfunction parseComponentLeaf(\n raw: unknown,\n index: number,\n dropped: string[],\n): ThemeComponentOverrideLeaf | null {\n const where = `componentOverrides[${index}]`\n if (!isRecord(raw)) {\n dropped.push(`${where}: not an object`)\n return null\n }\n const component = String(raw['component'] ?? '')\n if (!(COMPONENT_OVERRIDES_FIELD.components as readonly string[]).includes(component)) {\n dropped.push(`${where}: ${component || 'the component'} is not one a theme may override`)\n return null\n }\n const target =\n raw['target'] === 'defaultProps' ||\n raw['target'] === 'styleOverrides' ||\n raw['target'] === 'sx'\n ? raw['target']\n : null\n if (!target) {\n dropped.push(`${where}: target must be styleOverrides, sx or defaultProps`)\n return null\n }\n const property = String(raw['property'] ?? '')\n const value = raw['value']\n const named = `${component}.${property || '?'}`\n if (target === 'defaultProps') {\n if (!AI_THEME_DEFAULT_PROPS.includes(property)) {\n dropped.push(`${named}: not a prop a theme may set a default for`)\n return null\n }\n const valid =\n typeof value === 'boolean' ||\n (typeof value === 'number' && Number.isFinite(value)) ||\n (typeof value === 'string' && PROP_TOKEN.test(value))\n if (!valid) {\n dropped.push(`${named}: ${quoted(value)} is not a value a prop default may take`)\n return null\n }\n return {\n component: component as ThemeComponentOverrideLeaf['component'],\n target,\n slot: null,\n property,\n media: null,\n value: value as string | number | boolean,\n }\n }\n if (!AI_THEME_STYLE_PROPERTIES.includes(property)) {\n dropped.push(`${named}: not a style a theme may set`)\n return null\n }\n const slot = raw['slot'] === null || raw['slot'] === undefined ? 'root' : String(raw['slot'])\n if (!IDENTIFIER.test(slot)) {\n dropped.push(`${named}: ${quoted(slot)} is not a style slot`)\n return null\n }\n const media =\n raw['media'] === null || raw['media'] === undefined\n ? null\n : String(raw['media']) in THEME_EDITOR_MEDIA_QUERIES\n ? (raw['media'] as ThemeEditorMedia)\n : undefined\n if (media === undefined) {\n dropped.push(`${named}: media must be mobile, desktop or null`)\n return null\n }\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n dropped.push(`${named}: not a finite number`)\n return null\n }\n return {\n component: component as ThemeComponentOverrideLeaf['component'],\n target,\n slot,\n property,\n media,\n value,\n }\n }\n if (typeof value !== 'string') {\n dropped.push(`${named}: a style takes a string or a number`)\n return null\n }\n const trimmed = value.trim()\n if (!trimmed || HOSTILE_TEXT.test(trimmed) || !SX_VALUE.test(trimmed)) {\n dropped.push(`${named}: ${quoted(trimmed)} is not a value a style may take`)\n return null\n }\n if (carriesLiteralColor(trimmed)) {\n dropped.push(`${named}: a color belongs to the palette, not to a component style`)\n return null\n }\n return {\n component: component as ThemeComponentOverrideLeaf['component'],\n target,\n slot,\n property,\n media,\n value: trimmed,\n }\n}\n\n/**\n * A tool call's input, held to the editor's constraints: tokens and fonts\n * from the catalog, hex colors as the picker writes them, numbers inside the\n * editor's bounds, and component leaves on the whitelist with safe values\n * and no literal color. What does not hold is dropped and named rather than\n * guessed at; a number out of bounds is brought inside them and named.\n */\nexport function parseAiThemeToolInput(input: Record<string, unknown>): AiThemeToolParse {\n const dropped: string[] = []\n const notes: string[] = []\n const changes: AiThemeToolParse['changes'] = []\n const summary = String(input['summary'] ?? '').trim().slice(0, AI_THEME_SUMMARY_MAX_CHARS)\n\n const colorControls = new Set<string>(AI_THEME_COLOR_CONTROLS.map((control) => control.id))\n const rawColors = Array.isArray(input['colors']) ? input['colors'] : []\n const seen = new Set<string>()\n for (const [index, entry] of rawColors.slice(0, AI_THEME_COLOR_CONTROLS.length * 2).entries()) {\n if (!isRecord(entry)) {\n dropped.push(`colors[${index}]: not an object`)\n continue\n }\n const control = `color.${String(entry['token'] ?? '')}`\n if (!colorControls.has(control)) {\n dropped.push(`colors[${index}]: ${quoted(entry['token'])} is not a theme color`)\n continue\n }\n if (seen.has(control)) {\n dropped.push(`${control}: named twice; the first value is kept`)\n continue\n }\n seen.add(control)\n for (const scheme of ['light', 'dark'] as const) {\n const value = parseColorValue(entry[scheme], `${control} (${scheme})`, dropped)\n if (value === undefined) continue\n changes.push({ control: control as AiThemeValueControl, scheme, value })\n }\n }\n\n const darkScheme = input['darkScheme']\n if (darkScheme === 'auto' || darkScheme === 'off') {\n changes.push({ control: 'darkScheme', scheme: null, value: darkScheme === 'off' ? 'off' : null })\n } else if (darkScheme !== null && darkScheme !== undefined) {\n dropped.push(`darkScheme: ${quoted(darkScheme)} is not auto or off`)\n }\n\n const fontFamily = input['fontFamily']\n if (typeof fontFamily === 'string' && themeEditorControl('fontFamily').options?.includes(fontFamily)) {\n changes.push({\n control: 'fontFamily',\n scheme: null,\n value: fontFamily === SYSTEM_FONT_VALUE ? null : fontFamily,\n })\n } else if (fontFamily !== null && fontFamily !== undefined) {\n dropped.push(`fontFamily: ${quoted(fontFamily)} is not in the font list`)\n }\n\n const numbers: Array<[string, AiThemeValueControl]> = [\n ['borderRadius', 'borderRadius'],\n ['spacing', 'spacing'],\n ['navHeightMobile', 'navHeight.xs'],\n ['navHeightDesktop', 'navHeight.sm'],\n ]\n for (const [property, control] of numbers) {\n const value = parseNumber(input[property], control, dropped, notes)\n if (value !== undefined) changes.push({ control, scheme: null, value })\n }\n\n const rawLeaves = Array.isArray(input['componentOverrides']) ? input['componentOverrides'] : []\n if (rawLeaves.length > AI_THEME_TOOL_MAX_COMPONENT_LEAVES) {\n dropped.push(\n `componentOverrides: ${rawLeaves.length - AI_THEME_TOOL_MAX_COMPONENT_LEAVES} past the ` +\n `${AI_THEME_TOOL_MAX_COMPONENT_LEAVES}-leaf limit`,\n )\n }\n const components = rawLeaves\n .slice(0, AI_THEME_TOOL_MAX_COMPONENT_LEAVES)\n .map((leaf, index) => parseComponentLeaf(leaf, index, dropped))\n .filter((leaf): leaf is ThemeComponentOverrideLeaf => leaf !== null)\n\n return {\n summary,\n changes,\n components,\n resetComponents: input['resetComponentOverrides'] === true,\n dropped,\n notes,\n }\n}\n"],"names":["COMPONENT_OVERRIDES_FIELD","SYSTEM_FONT_VALUE","THEME_EDITOR_CONTROLS","THEME_EDITOR_MEDIA_QUERIES","themeEditorControl","AI_SX_ALLOWED_KEYS","HOSTILE_TEXT","SX_VALUE","AI_THEME_TOOL_NAME","AI_THEME_TOOL_DEFAULT","AI_THEME_COLOR_CONTROLS","filter","control","kind","AI_THEME_TOOL_MAX_COMPONENT_LEAVES","AI_THEME_SUMMARY_MAX_CHARS","AI_THEME_TOOL_PROPERTIES","property","controls","map","id","AI_THEME_TOOL_META_PROPERTIES","aiThemeToolParity","offered","Set","flatMap","entry","catalog","unoffered","has","unknown","SX_SHORTHANDS","AI_THEME_STYLE_PROPERTIES","key","AI_THEME_DEFAULT_PROPS","LITERAL_COLOR","NAMED_COLORS","split","carriesLiteralColor","value","test","toLowerCase","match","some","word","HEX","IDENTIFIER","PROP_TOKEN","nullable","schema","anyOf","type","rangeText","min","max","numberProperty","what","enum","description","colorValue","scheme","aiThemeTool","darkScheme","fontFamily","properties","summary","colors","items","token","light","dark","required","additionalProperties","options","borderRadius","spacing","navHeightMobile","navHeightDesktop","componentOverrides","component","components","target","slot","media","Object","keys","resetComponentOverrides","name","strict","inputSchema","isRecord","Array","isArray","quoted","String","slice","normalizeThemeHex","trimmed","trim","digits","length","replace","digit","parseColorValue","raw","where","dropped","undefined","hex","push","parseNumber","notes","Number","isFinite","label","bounded","Math","round","parseComponentLeaf","index","includes","named","valid","parseAiThemeToolInput","input","changes","colorControls","rawColors","seen","entries","add","numbers","rawLeaves","leaf","resetComponents"],"mappings":"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"}