@aglyn/aglyn 1.0.0-beta.287 → 1.0.0-beta.289

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/business-profile-values.d.ts +15 -3
  3. package/src/lib/app-utils/business-profile-values.js +19 -3
  4. package/src/lib/app-utils/business-profile-values.js.map +1 -1
  5. package/src/lib/app-utils/business-profile.d.ts +5 -3
  6. package/src/lib/app-utils/business-profile.js +2 -1
  7. package/src/lib/app-utils/business-profile.js.map +1 -1
  8. package/src/lib/app-utils/docs-help-sections.generated.js +5 -0
  9. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  10. package/src/lib/app-utils/docs-help.generated.d.ts +7 -1
  11. package/src/lib/app-utils/docs-help.generated.js +24 -0
  12. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  13. package/src/lib/app-utils/docs-index.generated.js +111 -27
  14. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  15. package/src/lib/app-utils/element-motion.js +1 -0
  16. package/src/lib/app-utils/element-motion.js.map +1 -1
  17. package/src/lib/app-utils/host-permissions.d.ts +31 -1
  18. package/src/lib/app-utils/host-permissions.js +45 -5
  19. package/src/lib/app-utils/host-permissions.js.map +1 -1
  20. package/src/lib/app-utils/host-status.d.ts +2 -1
  21. package/src/lib/app-utils/host-status.js +10 -0
  22. package/src/lib/app-utils/host-status.js.map +1 -1
  23. package/src/lib/app-utils/org-permissions.d.ts +8 -0
  24. package/src/lib/app-utils/org-permissions.js +2 -0
  25. package/src/lib/app-utils/org-permissions.js.map +1 -1
  26. package/src/lib/app-utils/org-role-grants.js +3 -3
  27. package/src/lib/app-utils/org-role-grants.js.map +1 -1
  28. package/src/lib/app-utils/screen-slug.d.ts +5 -5
  29. package/src/lib/app-utils/screen-slug.js +5 -5
  30. package/src/lib/app-utils/screen-slug.js.map +1 -1
  31. package/src/lib/app-utils/search-indexing.d.ts +18 -0
  32. package/src/lib/app-utils/search-indexing.js +16 -2
  33. package/src/lib/app-utils/search-indexing.js.map +1 -1
  34. package/src/lib/app-utils/server.d.ts +1 -0
  35. package/src/lib/app-utils/server.js +2 -0
  36. package/src/lib/app-utils/server.js.map +1 -1
  37. package/src/lib/app-utils/site-preview.d.ts +153 -0
  38. package/src/lib/app-utils/site-preview.js +167 -0
  39. package/src/lib/app-utils/site-preview.js.map +1 -0
  40. package/src/lib/foundation/definitions/platform.types.d.ts +27 -0
  41. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  42. package/src/lib/plugin-manager/feature-plugins.d.ts +17 -1
  43. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  44. package/src/lib/plugin-manager/plugin-entitlements.d.ts +12 -0
  45. package/src/lib/plugin-manager/plugin-entitlements.js +4 -2
  46. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/aglyn",
3
- "version": "1.0.0-beta.287",
3
+ "version": "1.0.0-beta.289",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -37,16 +37,16 @@
37
37
  "./package.json": "./package.json"
38
38
  },
39
39
  "dependencies": {
40
- "@aglyn/shared-data-enums": "1.0.0-beta.287",
41
- "@aglyn/shared-data-mdi": "1.0.0-beta.287",
42
- "@aglyn/shared-data-types": "1.0.0-beta.287",
43
- "@aglyn/shared-util-email": "1.0.0-beta.287",
44
- "@aglyn/shared-util-first-touch": "1.0.0-beta.287",
45
- "@aglyn/shared-util-http": "1.0.0-beta.287",
46
- "@aglyn/shared-util-logger": "1.0.0-beta.287",
47
- "@aglyn/shared-util-timestamp": "1.0.0-beta.287",
48
- "@aglyn/shared-util-tools": "1.0.0-beta.287",
49
- "@aglyn/shared-util-vendor": "1.0.0-beta.287",
40
+ "@aglyn/shared-data-enums": "1.0.0-beta.289",
41
+ "@aglyn/shared-data-mdi": "1.0.0-beta.289",
42
+ "@aglyn/shared-data-types": "1.0.0-beta.289",
43
+ "@aglyn/shared-util-email": "1.0.0-beta.289",
44
+ "@aglyn/shared-util-first-touch": "1.0.0-beta.289",
45
+ "@aglyn/shared-util-http": "1.0.0-beta.289",
46
+ "@aglyn/shared-util-logger": "1.0.0-beta.289",
47
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.289",
48
+ "@aglyn/shared-util-tools": "1.0.0-beta.289",
49
+ "@aglyn/shared-util-vendor": "1.0.0-beta.289",
50
50
  "@data-driven-forms/react-form-renderer": "^4.2.0",
51
51
  "@msgpack/msgpack": "^3.1.3",
52
52
  "@types/unist": "^3.0.3",
@@ -38,8 +38,12 @@ export declare const BUSINESS_TONES: readonly ["friendly", "professional", "play
38
38
  export type BusinessTone = (typeof BUSINESS_TONES)[number];
39
39
  /** Each tone as the owner picks it and as a writer is told it. */
40
40
  export declare const BUSINESS_TONE_LABELS: Readonly<Record<BusinessTone, string>>;
41
- /** The fields the profile document stores itself. */
42
- export declare const BUSINESS_PROFILE_FIELDS: readonly ["whatYouDo", "services", "serviceArea", "audience", "tone", "toneNotes"];
41
+ /**
42
+ * The fields the profile document stores itself. `founded` is the year the
43
+ * business started (AGL-4092): the guided start asks it, and every job that
44
+ * writes about the business reads it here rather than marking a gap for it.
45
+ */
46
+ export declare const BUSINESS_PROFILE_FIELDS: readonly ["whatYouDo", "services", "serviceArea", "audience", "tone", "toneNotes", "founded"];
43
47
  export type BusinessProfileField = (typeof BUSINESS_PROFILE_FIELDS)[number];
44
48
  /** Each field's ceiling, in characters; `services` is per item. */
45
49
  export declare const BUSINESS_PROFILE_MAX_CHARS: Readonly<Record<Exclude<BusinessProfileField, 'tone'>, number>>;
@@ -53,16 +57,24 @@ export interface BusinessProfileDoc {
53
57
  audience?: string;
54
58
  tone?: BusinessTone | null;
55
59
  toneNotes?: string;
60
+ /** The year the business started, four digits: `2019`. */
61
+ founded?: string;
56
62
  /** Who wrote each stored value; a value without one is the owner's. */
57
63
  sources?: Partial<Record<BusinessProfileField, BusinessProfileSource>>;
58
64
  updatedAt?: unknown;
59
65
  updatedBy?: string | null;
60
66
  }
61
67
  /** The values a write carries, before they are sanitized. */
62
- export type BusinessProfileValues = Partial<Pick<BusinessProfileDoc, 'whatYouDo' | 'services' | 'serviceArea' | 'audience' | 'tone' | 'toneNotes'>>;
68
+ export type BusinessProfileValues = Partial<Pick<BusinessProfileDoc, 'whatYouDo' | 'services' | 'serviceArea' | 'audience' | 'tone' | 'toneNotes' | 'founded'>>;
63
69
  export declare const text: (value: unknown, max: number) => string;
64
70
  /** One field's stored value, sanitized; empty is `''`, `[]` or `null`. */
65
71
  export declare function fieldValue(doc: BusinessProfileValues | null | undefined, field: BusinessProfileField): unknown;
72
+ /**
73
+ * What is wrong with a year founded as the owner typed it (AGL-4092), or
74
+ * `null` when nothing is: empty, or four digits no later than this year —
75
+ * the guided start's own rule.
76
+ */
77
+ export declare function businessProfileFoundedProblem(value: unknown, now?: Date): string | null;
66
78
  export declare const isEmpty: (value: unknown) => boolean;
67
79
  /** The source a stored, non-empty value carries; the owner's when none is recorded. */
68
80
  export declare function businessProfileSourceOf(doc: BusinessProfileDoc | null | undefined, field: BusinessProfileField): BusinessProfileSource | null;
@@ -42,20 +42,26 @@ export const BUSINESS_TONES = [
42
42
  premium: 'Premium and refined',
43
43
  plain: 'Plain and direct'
44
44
  };
45
- /** The fields the profile document stores itself. */ export const BUSINESS_PROFILE_FIELDS = [
45
+ /**
46
+ * The fields the profile document stores itself. `founded` is the year the
47
+ * business started (AGL-4092): the guided start asks it, and every job that
48
+ * writes about the business reads it here rather than marking a gap for it.
49
+ */ export const BUSINESS_PROFILE_FIELDS = [
46
50
  'whatYouDo',
47
51
  'services',
48
52
  'serviceArea',
49
53
  'audience',
50
54
  'tone',
51
- 'toneNotes'
55
+ 'toneNotes',
56
+ 'founded'
52
57
  ];
53
58
  /** Each field's ceiling, in characters; `services` is per item. */ export const BUSINESS_PROFILE_MAX_CHARS = {
54
59
  whatYouDo: 200,
55
60
  services: 60,
56
61
  serviceArea: 120,
57
62
  audience: 160,
58
- toneNotes: 200
63
+ toneNotes: 200,
64
+ /** A year, four digits, as the guided start asks it. */ founded: 4
59
65
  };
60
66
  /** At most this many services are kept. */ export const BUSINESS_PROFILE_MAX_SERVICES = 12;
61
67
  export const text = (value, max)=>typeof value === 'string' ? value.replace(/\s+/g, ' ').trim().slice(0, max) : '';
@@ -84,6 +90,16 @@ const services = (value)=>{
84
90
  return text(doc == null ? void 0 : doc[field], BUSINESS_PROFILE_MAX_CHARS[field]);
85
91
  }
86
92
  }
93
+ /**
94
+ * What is wrong with a year founded as the owner typed it (AGL-4092), or
95
+ * `null` when nothing is: empty, or four digits no later than this year —
96
+ * the guided start's own rule.
97
+ */ export function businessProfileFoundedProblem(value, now = new Date()) {
98
+ const year = typeof value === 'string' ? value.trim() : '';
99
+ if (!year) return null;
100
+ const latest = now.getFullYear();
101
+ return /^\d{4}$/.test(year) && Number(year) >= 1000 && Number(year) <= latest ? null : `Enter the year as four digits, like ${latest - 10}.`;
102
+ }
87
103
  export const isEmpty = (value)=>value === null || value === '' || Array.isArray(value) && value.length === 0;
88
104
  /** The source a stored, non-empty value carries; the owner's when none is recorded. */ export function businessProfileSourceOf(doc, field) {
89
105
  var _doc_sources;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/business-profile-values.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The pure part of the business profile: its fields, limits, sources and the\n * write rules (what the owner typed is theirs; a changed field becomes the\n * owner's). It imports nothing, so the native apps replay it from\n * `function-cases.generated.json` (AGL-3671); `business-profile.ts` re-exports\n * all of it, so every existing import is unchanged.\n */\n\n/** The collection a profile document lives in, under a host or an org. */\nexport const BUSINESS_PROFILE_SUBCOLLECTION = 'businessProfile'\n/** The site's profile document id. */\nexport const BUSINESS_PROFILE_SITE_DOC = 'profile'\n/** The workspace defaults document id. */\nexport const BUSINESS_PROFILE_WORKSPACE_DOC = 'defaults'\n\n/** Who wrote a stored value: the owner, the guided start's answers, or a generator. */\nexport type BusinessProfileSource = 'owner' | 'start' | 'ai'\n\n/**\n * Where a RESOLVED value came from: a stored source, the site's own settings\n * (`seo.entity`, `business`, the display name), or the workspace defaults.\n */\nexport type BusinessProfileOrigin = BusinessProfileSource | 'site' | 'workspace'\n\n/** How much a source's words weigh: a higher rank is never replaced by a lower one. */\nconst SOURCE_RANK: Readonly<Record<BusinessProfileSource, number>> = {\n ai: 1,\n start: 2,\n owner: 3,\n}\n\nexport const BUSINESS_TONES = ['friendly', 'professional', 'playful', 'premium', 'plain'] as const\nexport type BusinessTone = (typeof BUSINESS_TONES)[number]\n\n/** Each tone as the owner picks it and as a writer is told it. */\nexport const BUSINESS_TONE_LABELS: Readonly<Record<BusinessTone, string>> = {\n friendly: 'Friendly and warm',\n professional: 'Professional and clear',\n playful: 'Playful and upbeat',\n premium: 'Premium and refined',\n plain: 'Plain and direct',\n}\n\n/** The fields the profile document stores itself. */\nexport const BUSINESS_PROFILE_FIELDS = [\n 'whatYouDo',\n 'services',\n 'serviceArea',\n 'audience',\n 'tone',\n 'toneNotes',\n] as const\nexport type BusinessProfileField = (typeof BUSINESS_PROFILE_FIELDS)[number]\n\n/** Each field's ceiling, in characters; `services` is per item. */\nexport const BUSINESS_PROFILE_MAX_CHARS: Readonly<Record<Exclude<BusinessProfileField, 'tone'>, number>> = {\n whatYouDo: 200,\n services: 60,\n serviceArea: 120,\n audience: 160,\n toneNotes: 200,\n}\n\n/** At most this many services are kept. */\nexport const BUSINESS_PROFILE_MAX_SERVICES = 12\n\n/** The stored document, at either level. */\nexport interface BusinessProfileDoc {\n whatYouDo?: string\n services?: string[]\n serviceArea?: string\n audience?: string\n tone?: BusinessTone | null\n toneNotes?: string\n /** Who wrote each stored value; a value without one is the owner's. */\n sources?: Partial<Record<BusinessProfileField, BusinessProfileSource>>\n updatedAt?: unknown\n updatedBy?: string | null\n}\n\n/** The values a write carries, before they are sanitized. */\nexport type BusinessProfileValues = Partial<\n Pick<BusinessProfileDoc, 'whatYouDo' | 'services' | 'serviceArea' | 'audience' | 'tone' | 'toneNotes'>\n>\n\nexport const text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim().slice(0, max) : ''\n\nconst tone = (value: unknown): BusinessTone | null =>\n (BUSINESS_TONES as readonly unknown[]).includes(value) ? (value as BusinessTone) : null\n\nconst services = (value: unknown): string[] => {\n const items = Array.isArray(value) ? value : typeof value === 'string' ? value.split(/[,\\n]/) : []\n const seen = new Set<string>()\n const kept: string[] = []\n for (const item of items) {\n const name = text(item, BUSINESS_PROFILE_MAX_CHARS.services)\n const key = name.toLowerCase()\n if (!name || seen.has(key)) continue\n seen.add(key)\n kept.push(name)\n if (kept.length >= BUSINESS_PROFILE_MAX_SERVICES) break\n }\n return kept\n}\n\n/** One field's stored value, sanitized; empty is `''`, `[]` or `null`. */\nexport function fieldValue(doc: BusinessProfileValues | null | undefined, field: BusinessProfileField): unknown {\n switch (field) {\n case 'services':\n return services(doc?.services)\n case 'tone':\n return tone(doc?.tone)\n default:\n return text(doc?.[field], BUSINESS_PROFILE_MAX_CHARS[field])\n }\n}\n\nexport const isEmpty = (value: unknown): boolean =>\n value === null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** The source a stored, non-empty value carries; the owner's when none is recorded. */\nexport function businessProfileSourceOf(\n doc: BusinessProfileDoc | null | undefined,\n field: BusinessProfileField,\n): BusinessProfileSource | null {\n if (isEmpty(fieldValue(doc, field))) return null\n const source = doc?.sources?.[field]\n return source && source in SOURCE_RANK ? source : 'owner'\n}\n\n/** Every value a write may carry, sanitized; an absent field stays absent. */\nexport function sanitizeBusinessProfileValues(values: BusinessProfileValues): BusinessProfileValues {\n const clean: BusinessProfileValues = {}\n for (const field of BUSINESS_PROFILE_FIELDS) {\n if (!(field in values)) continue\n ;(clean as Record<string, unknown>)[field] = fieldValue(values, field)\n }\n return clean\n}\n\n/**\n * The document a generator's or the guided start's values make of the stored\n * one: each value fills a field that is empty or was written by a source that\n * weighs less, and nothing else. `null` when nothing would change, so a caller\n * writes only when there is something to write.\n */\nexport function mergeBusinessProfilePrefill(\n current: BusinessProfileDoc | null | undefined,\n values: BusinessProfileValues,\n source: Exclude<BusinessProfileSource, 'owner'>,\n): BusinessProfileDoc | null {\n const clean = sanitizeBusinessProfileValues(values)\n const next: BusinessProfileDoc = { ...(current ?? {}), sources: { ...(current?.sources ?? {}) } }\n let changed = false\n for (const field of BUSINESS_PROFILE_FIELDS) {\n if (!(field in clean)) continue\n const value = (clean as Record<string, unknown>)[field]\n if (isEmpty(value)) continue\n // An explicit source counts even on an empty value: a field the owner\n // cleared stays cleared rather than being refilled behind them.\n const recorded = current?.sources?.[field]\n const held = recorded && recorded in SOURCE_RANK ? recorded : businessProfileSourceOf(current, field)\n if (held && SOURCE_RANK[held] >= SOURCE_RANK[source]) continue\n if (JSON.stringify(fieldValue(current, field)) === JSON.stringify(value)) continue\n ;(next as Record<string, unknown>)[field] = value\n next.sources![field] = source\n changed = true\n }\n return changed ? next : null\n}\n\n/**\n * The document the owner's save makes: every field they changed becomes\n * theirs, and a field they left as it was keeps the source it had — so saving\n * the form after correcting one line does not claim every suggestion on it.\n */\nexport function businessProfileOwnerWrite(\n current: BusinessProfileDoc | null | undefined,\n edited: BusinessProfileValues,\n): Pick<BusinessProfileDoc, BusinessProfileField | 'sources'> {\n const clean = sanitizeBusinessProfileValues(edited)\n const next: Record<string, unknown> = {}\n const sources: Partial<Record<BusinessProfileField, BusinessProfileSource>> = {}\n for (const field of BUSINESS_PROFILE_FIELDS) {\n const before = fieldValue(current, field)\n const after = field in clean ? (clean as Record<string, unknown>)[field] : before\n next[field] = after\n const same = JSON.stringify(before) === JSON.stringify(after)\n if (isEmpty(after)) {\n // A suggestion the owner deleted is their decision, and recorded as\n // one, so the next prefill does not put it back.\n if (!same) sources[field] = 'owner'\n else if (current?.sources?.[field] === 'owner') sources[field] = 'owner'\n continue\n }\n sources[field] = same ? (businessProfileSourceOf(current, field) ?? 'owner') : 'owner'\n }\n return { ...(next as Pick<BusinessProfileDoc, BusinessProfileField>), sources }\n}\n"],"names":["BUSINESS_PROFILE_SUBCOLLECTION","BUSINESS_PROFILE_SITE_DOC","BUSINESS_PROFILE_WORKSPACE_DOC","SOURCE_RANK","ai","start","owner","BUSINESS_TONES","BUSINESS_TONE_LABELS","friendly","professional","playful","premium","plain","BUSINESS_PROFILE_FIELDS","BUSINESS_PROFILE_MAX_CHARS","whatYouDo","services","serviceArea","audience","toneNotes","BUSINESS_PROFILE_MAX_SERVICES","text","value","max","replace","trim","slice","tone","includes","items","Array","isArray","split","seen","Set","kept","item","name","key","toLowerCase","has","add","push","length","fieldValue","doc","field","isEmpty","businessProfileSourceOf","source","sources","sanitizeBusinessProfileValues","values","clean","mergeBusinessProfilePrefill","current","next","changed","recorded","held","JSON","stringify","businessProfileOwnerWrite","edited","before","after","same"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/business-profile-values.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The pure part of the business profile: its fields, limits, sources and the\n * write rules (what the owner typed is theirs; a changed field becomes the\n * owner's). It imports nothing, so the native apps replay it from\n * `function-cases.generated.json` (AGL-3671); `business-profile.ts` re-exports\n * all of it, so every existing import is unchanged.\n */\n\n/** The collection a profile document lives in, under a host or an org. */\nexport const BUSINESS_PROFILE_SUBCOLLECTION = 'businessProfile'\n/** The site's profile document id. */\nexport const BUSINESS_PROFILE_SITE_DOC = 'profile'\n/** The workspace defaults document id. */\nexport const BUSINESS_PROFILE_WORKSPACE_DOC = 'defaults'\n\n/** Who wrote a stored value: the owner, the guided start's answers, or a generator. */\nexport type BusinessProfileSource = 'owner' | 'start' | 'ai'\n\n/**\n * Where a RESOLVED value came from: a stored source, the site's own settings\n * (`seo.entity`, `business`, the display name), or the workspace defaults.\n */\nexport type BusinessProfileOrigin = BusinessProfileSource | 'site' | 'workspace'\n\n/** How much a source's words weigh: a higher rank is never replaced by a lower one. */\nconst SOURCE_RANK: Readonly<Record<BusinessProfileSource, number>> = {\n ai: 1,\n start: 2,\n owner: 3,\n}\n\nexport const BUSINESS_TONES = ['friendly', 'professional', 'playful', 'premium', 'plain'] as const\nexport type BusinessTone = (typeof BUSINESS_TONES)[number]\n\n/** Each tone as the owner picks it and as a writer is told it. */\nexport const BUSINESS_TONE_LABELS: Readonly<Record<BusinessTone, string>> = {\n friendly: 'Friendly and warm',\n professional: 'Professional and clear',\n playful: 'Playful and upbeat',\n premium: 'Premium and refined',\n plain: 'Plain and direct',\n}\n\n/**\n * The fields the profile document stores itself. `founded` is the year the\n * business started (AGL-4092): the guided start asks it, and every job that\n * writes about the business reads it here rather than marking a gap for it.\n */\nexport const BUSINESS_PROFILE_FIELDS = [\n 'whatYouDo',\n 'services',\n 'serviceArea',\n 'audience',\n 'tone',\n 'toneNotes',\n 'founded',\n] as const\nexport type BusinessProfileField = (typeof BUSINESS_PROFILE_FIELDS)[number]\n\n/** Each field's ceiling, in characters; `services` is per item. */\nexport const BUSINESS_PROFILE_MAX_CHARS: Readonly<Record<Exclude<BusinessProfileField, 'tone'>, number>> = {\n whatYouDo: 200,\n services: 60,\n serviceArea: 120,\n audience: 160,\n toneNotes: 200,\n /** A year, four digits, as the guided start asks it. */\n founded: 4,\n}\n\n/** At most this many services are kept. */\nexport const BUSINESS_PROFILE_MAX_SERVICES = 12\n\n/** The stored document, at either level. */\nexport interface BusinessProfileDoc {\n whatYouDo?: string\n services?: string[]\n serviceArea?: string\n audience?: string\n tone?: BusinessTone | null\n toneNotes?: string\n /** The year the business started, four digits: `2019`. */\n founded?: string\n /** Who wrote each stored value; a value without one is the owner's. */\n sources?: Partial<Record<BusinessProfileField, BusinessProfileSource>>\n updatedAt?: unknown\n updatedBy?: string | null\n}\n\n/** The values a write carries, before they are sanitized. */\nexport type BusinessProfileValues = Partial<\n Pick<BusinessProfileDoc, 'whatYouDo' | 'services' | 'serviceArea' | 'audience' | 'tone' | 'toneNotes' | 'founded'>\n>\n\nexport const text = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim().slice(0, max) : ''\n\nconst tone = (value: unknown): BusinessTone | null =>\n (BUSINESS_TONES as readonly unknown[]).includes(value) ? (value as BusinessTone) : null\n\nconst services = (value: unknown): string[] => {\n const items = Array.isArray(value) ? value : typeof value === 'string' ? value.split(/[,\\n]/) : []\n const seen = new Set<string>()\n const kept: string[] = []\n for (const item of items) {\n const name = text(item, BUSINESS_PROFILE_MAX_CHARS.services)\n const key = name.toLowerCase()\n if (!name || seen.has(key)) continue\n seen.add(key)\n kept.push(name)\n if (kept.length >= BUSINESS_PROFILE_MAX_SERVICES) break\n }\n return kept\n}\n\n/** One field's stored value, sanitized; empty is `''`, `[]` or `null`. */\nexport function fieldValue(doc: BusinessProfileValues | null | undefined, field: BusinessProfileField): unknown {\n switch (field) {\n case 'services':\n return services(doc?.services)\n case 'tone':\n return tone(doc?.tone)\n default:\n return text(doc?.[field], BUSINESS_PROFILE_MAX_CHARS[field])\n }\n}\n\n/**\n * What is wrong with a year founded as the owner typed it (AGL-4092), or\n * `null` when nothing is: empty, or four digits no later than this year —\n * the guided start's own rule.\n */\nexport function businessProfileFoundedProblem(value: unknown, now: Date = new Date()): string | null {\n const year = typeof value === 'string' ? value.trim() : ''\n if (!year) return null\n const latest = now.getFullYear()\n return /^\\d{4}$/.test(year) && Number(year) >= 1000 && Number(year) <= latest\n ? null\n : `Enter the year as four digits, like ${latest - 10}.`\n}\n\nexport const isEmpty = (value: unknown): boolean =>\n value === null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** The source a stored, non-empty value carries; the owner's when none is recorded. */\nexport function businessProfileSourceOf(\n doc: BusinessProfileDoc | null | undefined,\n field: BusinessProfileField,\n): BusinessProfileSource | null {\n if (isEmpty(fieldValue(doc, field))) return null\n const source = doc?.sources?.[field]\n return source && source in SOURCE_RANK ? source : 'owner'\n}\n\n/** Every value a write may carry, sanitized; an absent field stays absent. */\nexport function sanitizeBusinessProfileValues(values: BusinessProfileValues): BusinessProfileValues {\n const clean: BusinessProfileValues = {}\n for (const field of BUSINESS_PROFILE_FIELDS) {\n if (!(field in values)) continue\n ;(clean as Record<string, unknown>)[field] = fieldValue(values, field)\n }\n return clean\n}\n\n/**\n * The document a generator's or the guided start's values make of the stored\n * one: each value fills a field that is empty or was written by a source that\n * weighs less, and nothing else. `null` when nothing would change, so a caller\n * writes only when there is something to write.\n */\nexport function mergeBusinessProfilePrefill(\n current: BusinessProfileDoc | null | undefined,\n values: BusinessProfileValues,\n source: Exclude<BusinessProfileSource, 'owner'>,\n): BusinessProfileDoc | null {\n const clean = sanitizeBusinessProfileValues(values)\n const next: BusinessProfileDoc = { ...(current ?? {}), sources: { ...(current?.sources ?? {}) } }\n let changed = false\n for (const field of BUSINESS_PROFILE_FIELDS) {\n if (!(field in clean)) continue\n const value = (clean as Record<string, unknown>)[field]\n if (isEmpty(value)) continue\n // An explicit source counts even on an empty value: a field the owner\n // cleared stays cleared rather than being refilled behind them.\n const recorded = current?.sources?.[field]\n const held = recorded && recorded in SOURCE_RANK ? recorded : businessProfileSourceOf(current, field)\n if (held && SOURCE_RANK[held] >= SOURCE_RANK[source]) continue\n if (JSON.stringify(fieldValue(current, field)) === JSON.stringify(value)) continue\n ;(next as Record<string, unknown>)[field] = value\n next.sources![field] = source\n changed = true\n }\n return changed ? next : null\n}\n\n/**\n * The document the owner's save makes: every field they changed becomes\n * theirs, and a field they left as it was keeps the source it had — so saving\n * the form after correcting one line does not claim every suggestion on it.\n */\nexport function businessProfileOwnerWrite(\n current: BusinessProfileDoc | null | undefined,\n edited: BusinessProfileValues,\n): Pick<BusinessProfileDoc, BusinessProfileField | 'sources'> {\n const clean = sanitizeBusinessProfileValues(edited)\n const next: Record<string, unknown> = {}\n const sources: Partial<Record<BusinessProfileField, BusinessProfileSource>> = {}\n for (const field of BUSINESS_PROFILE_FIELDS) {\n const before = fieldValue(current, field)\n const after = field in clean ? (clean as Record<string, unknown>)[field] : before\n next[field] = after\n const same = JSON.stringify(before) === JSON.stringify(after)\n if (isEmpty(after)) {\n // A suggestion the owner deleted is their decision, and recorded as\n // one, so the next prefill does not put it back.\n if (!same) sources[field] = 'owner'\n else if (current?.sources?.[field] === 'owner') sources[field] = 'owner'\n continue\n }\n sources[field] = same ? (businessProfileSourceOf(current, field) ?? 'owner') : 'owner'\n }\n return { ...(next as Pick<BusinessProfileDoc, BusinessProfileField>), sources }\n}\n"],"names":["BUSINESS_PROFILE_SUBCOLLECTION","BUSINESS_PROFILE_SITE_DOC","BUSINESS_PROFILE_WORKSPACE_DOC","SOURCE_RANK","ai","start","owner","BUSINESS_TONES","BUSINESS_TONE_LABELS","friendly","professional","playful","premium","plain","BUSINESS_PROFILE_FIELDS","BUSINESS_PROFILE_MAX_CHARS","whatYouDo","services","serviceArea","audience","toneNotes","founded","BUSINESS_PROFILE_MAX_SERVICES","text","value","max","replace","trim","slice","tone","includes","items","Array","isArray","split","seen","Set","kept","item","name","key","toLowerCase","has","add","push","length","fieldValue","doc","field","businessProfileFoundedProblem","now","Date","year","latest","getFullYear","test","Number","isEmpty","businessProfileSourceOf","source","sources","sanitizeBusinessProfileValues","values","clean","mergeBusinessProfilePrefill","current","next","changed","recorded","held","JSON","stringify","businessProfileOwnerWrite","edited","before","after","same"],"mappings":"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"}
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { type HostSeoEntity } from './content-authors';
18
18
  import { type BusinessProfileDoc, type BusinessProfileOrigin, type BusinessTone } from './business-profile-values';
19
- export { BUSINESS_PROFILE_FIELDS, BUSINESS_PROFILE_MAX_CHARS, BUSINESS_PROFILE_MAX_SERVICES, BUSINESS_PROFILE_SITE_DOC, BUSINESS_PROFILE_SUBCOLLECTION, BUSINESS_PROFILE_WORKSPACE_DOC, BUSINESS_TONE_LABELS, BUSINESS_TONES, businessProfileOwnerWrite, businessProfileSourceOf, mergeBusinessProfilePrefill, sanitizeBusinessProfileValues, } from './business-profile-values';
19
+ export { BUSINESS_PROFILE_FIELDS, BUSINESS_PROFILE_MAX_CHARS, BUSINESS_PROFILE_MAX_SERVICES, BUSINESS_PROFILE_SITE_DOC, BUSINESS_PROFILE_SUBCOLLECTION, BUSINESS_PROFILE_WORKSPACE_DOC, BUSINESS_TONE_LABELS, BUSINESS_TONES, businessProfileFoundedProblem, businessProfileOwnerWrite, businessProfileSourceOf, mergeBusinessProfilePrefill, sanitizeBusinessProfileValues, } from './business-profile-values';
20
20
  export type { BusinessProfileDoc, BusinessProfileField, BusinessProfileOrigin, BusinessProfileSource, BusinessProfileValues, BusinessTone, } from './business-profile-values';
21
21
  /** The tones as a plain list, in the order the owner picks them, for the native apps' pickers. */
22
22
  export declare const BUSINESS_TONE_ORDER: readonly string[];
@@ -37,8 +37,8 @@ export declare const BUSINESS_PROFILE_FIELD_ORDER: readonly string[];
37
37
  * here would make two places to keep in step, and the copy would drift.
38
38
  *
39
39
  * So the profile document holds only what has no home yet — the services, the
40
- * audience, the tone of voice — plus a line about the business and the area it
41
- * serves for a site whose SEO entity does not say. {@link resolveBusinessProfile}
40
+ * audience, the tone of voice, the year it was founded — plus a line about the
41
+ * business and the area it serves for a site whose SEO entity does not say. {@link resolveBusinessProfile}
42
42
  * reads the two together into one picture, field by field, and says where
43
43
  * each value came from.
44
44
  *
@@ -88,6 +88,8 @@ export interface ResolvedBusinessProfile {
88
88
  audience: ResolvedBusinessField<string> | null;
89
89
  tone: ResolvedBusinessField<BusinessTone> | null;
90
90
  toneNotes: ResolvedBusinessField<string> | null;
91
+ /** The year the business started (AGL-4092). */
92
+ founded: ResolvedBusinessField<string> | null;
91
93
  /** The schema.org business type the SEO entity names, e.g. `Plumber`. */
92
94
  businessType: string | null;
93
95
  /** Real contact details, each only where the owner entered it in the site's settings. */
@@ -17,7 +17,7 @@
17
17
  import { normalizeAreaServed } from "./local-business.js";
18
18
  import { PLATFORM_BRAND_NAME } from "./platform-brand.js";
19
19
  import { BUSINESS_PROFILE_FIELDS, BUSINESS_PROFILE_MAX_CHARS, BUSINESS_TONES, businessProfileSourceOf, fieldValue, isEmpty, text } from "./business-profile-values.js";
20
- export { BUSINESS_PROFILE_FIELDS, BUSINESS_PROFILE_MAX_CHARS, BUSINESS_PROFILE_MAX_SERVICES, BUSINESS_PROFILE_SITE_DOC, BUSINESS_PROFILE_SUBCOLLECTION, BUSINESS_PROFILE_WORKSPACE_DOC, BUSINESS_TONE_LABELS, BUSINESS_TONES, businessProfileOwnerWrite, businessProfileSourceOf, mergeBusinessProfilePrefill, sanitizeBusinessProfileValues } from "./business-profile-values.js";
20
+ export { BUSINESS_PROFILE_FIELDS, BUSINESS_PROFILE_MAX_CHARS, BUSINESS_PROFILE_MAX_SERVICES, BUSINESS_PROFILE_SITE_DOC, BUSINESS_PROFILE_SUBCOLLECTION, BUSINESS_PROFILE_WORKSPACE_DOC, BUSINESS_TONE_LABELS, BUSINESS_TONES, businessProfileFoundedProblem, businessProfileOwnerWrite, businessProfileSourceOf, mergeBusinessProfilePrefill, sanitizeBusinessProfileValues } from "./business-profile-values.js";
21
21
  /** The tones as a plain list, in the order the owner picks them, for the native apps' pickers. */ export const BUSINESS_TONE_ORDER = BUSINESS_TONES;
22
22
  /** The stored fields as a plain list, for the native apps' writers. */ export const BUSINESS_PROFILE_FIELD_ORDER = BUSINESS_PROFILE_FIELDS;
23
23
  /** The postal address as one line: the SEO entity's parts, else the contact card's block. */ function addressLine(host) {
@@ -87,6 +87,7 @@ export { BUSINESS_PROFILE_FIELDS, BUSINESS_PROFILE_MAX_CHARS, BUSINESS_PROFILE_M
87
87
  audience: resolve('audience'),
88
88
  tone: resolve('tone'),
89
89
  toneNotes: resolve('toneNotes'),
90
+ founded: resolve('founded'),
90
91
  businessType: text(entity == null ? void 0 : entity.businessType, 80) || null,
91
92
  contact: {
92
93
  email: text(entity == null ? void 0 : entity.email, 320) || text(host == null ? void 0 : (_host_business = host.business) == null ? void 0 : _host_business.supportEmail, 320) || null,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/business-profile.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 { siteProfileUrls, type HostSeoEntity } from './content-authors'\nimport { normalizeAreaServed } from './local-business'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport {\n BUSINESS_PROFILE_FIELDS,\n BUSINESS_PROFILE_MAX_CHARS,\n BUSINESS_TONES,\n businessProfileSourceOf,\n fieldValue,\n isEmpty,\n text,\n type BusinessProfileDoc,\n type BusinessProfileField,\n type BusinessProfileOrigin,\n type BusinessProfileSource,\n type BusinessProfileValues,\n type BusinessTone,\n} from './business-profile-values'\n\nexport {\n BUSINESS_PROFILE_FIELDS,\n BUSINESS_PROFILE_MAX_CHARS,\n BUSINESS_PROFILE_MAX_SERVICES,\n BUSINESS_PROFILE_SITE_DOC,\n BUSINESS_PROFILE_SUBCOLLECTION,\n BUSINESS_PROFILE_WORKSPACE_DOC,\n BUSINESS_TONE_LABELS,\n BUSINESS_TONES,\n businessProfileOwnerWrite,\n businessProfileSourceOf,\n mergeBusinessProfilePrefill,\n sanitizeBusinessProfileValues,\n} from './business-profile-values'\nexport type {\n BusinessProfileDoc,\n BusinessProfileField,\n BusinessProfileOrigin,\n BusinessProfileSource,\n BusinessProfileValues,\n BusinessTone,\n} from './business-profile-values'\n\n/** The tones as a plain list, in the order the owner picks them, for the native apps' pickers. */\nexport const BUSINESS_TONE_ORDER: readonly string[] = BUSINESS_TONES\n\n/** The stored fields as a plain list, for the native apps' writers. */\nexport const BUSINESS_PROFILE_FIELD_ORDER: readonly string[] = BUSINESS_PROFILE_FIELDS\n\n/**\n * A site's BUSINESS PROFILE (AGL-3661): what the business is, as everything\n * that writes about it needs to know — the site's own pages, the structured\n * data, and any generator a plugin brings.\n *\n * ── Most of it is already stored, so this stores only the rest ─────────────\n *\n * The site's name, its one-line description, its contact details, its hours,\n * the areas it serves and its social profiles all have a home on the host\n * document already: `seo.entity` (the publisher entity the JSON-LD is built\n * from, edited in Setup → SEO) and `business` (the contact card the `host.*`\n * tokens and the footer read, edited in Setup → Basic details). Copying them\n * here would make two places to keep in step, and the copy would drift.\n *\n * So the profile document holds only what has no home yet — the services, the\n * audience, the tone of voice — plus a line about the business and the area it\n * serves for a site whose SEO entity does not say. {@link resolveBusinessProfile}\n * reads the two together into one picture, field by field, and says where\n * each value came from.\n *\n * ── Where it is stored ────────────────────────────────────────────────────\n *\n * `hosts/{hostId}/businessProfile/profile` for a site and\n * `orgs/{orgId}/businessProfile/defaults` for the workspace, whose values a\n * site inherits where it has none of its own. A subcollection document rather\n * than a key on the host: the host document's `business` map is replaced\n * whole by the contact card's save, and a key beside it would be one more\n * thing a partial write could blank.\n *\n * ── Whose words win ───────────────────────────────────────────────────────\n *\n * Every stored value carries its source. What the owner typed is never\n * replaced by anything else; what the guided start was told replaces only\n * what a generator guessed; and a generator's guess fills only what is empty.\n * {@link mergeBusinessProfilePrefill} is the one place that rule is applied.\n */\n\n\n/** The host fields the profile reads. A closed surface, as the host tokens' is. */\nexport interface BusinessProfileHost {\n displayName?: string\n logoUrl?: string\n seo?: { description?: string; entity?: HostSeoEntity | null } | null\n business?: {\n supportEmail?: string\n address?: string\n socialLinks?: Array<{ label?: string; url?: string } | null> | null\n } | null\n}\n\nexport interface ResolvedBusinessField<T> {\n value: T\n origin: BusinessProfileOrigin\n}\n\n/** The picture every reader works from: each field's value and where it came from. */\nexport interface ResolvedBusinessProfile {\n name: ResolvedBusinessField<string> | null\n whatYouDo: ResolvedBusinessField<string> | null\n services: ResolvedBusinessField<string[]> | null\n serviceArea: ResolvedBusinessField<string> | null\n audience: ResolvedBusinessField<string> | null\n tone: ResolvedBusinessField<BusinessTone> | null\n toneNotes: ResolvedBusinessField<string> | null\n /** The schema.org business type the SEO entity names, e.g. `Plumber`. */\n businessType: string | null\n /** Real contact details, each only where the owner entered it in the site's settings. */\n contact: {\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n }\n /** The site's social and other profiles, https only. */\n profiles: string[]\n logo: string | null\n}\n\n\n/** The postal address as one line: the SEO entity's parts, else the contact card's block. */\nfunction addressLine(host: BusinessProfileHost | null | undefined): string {\n const parts = host?.seo?.entity?.address\n const line = [\n parts?.streetAddress,\n parts?.addressLocality,\n parts?.addressRegion,\n parts?.postalCode,\n parts?.addressCountry,\n ]\n .map((part) => text(part, 200))\n .filter(Boolean)\n .join(', ')\n return line || text(host?.business?.address, 400)\n}\n\n/**\n * The profile as every reader sees it, field by field (AGL-3661):\n *\n * 1. what the OWNER typed into the site's profile;\n * 2. what the site's own settings say (the SEO entity, the contact card, the\n * display name), which the owner typed too, elsewhere;\n * 3. what the guided start or a generator stored on the site's profile;\n * 4. the workspace defaults.\n *\n * Contact details, the logo and the profiles come from the site's settings\n * only: nothing but the owner's own entry may say how to reach the business.\n */\nexport function resolveBusinessProfile(input: {\n host: BusinessProfileHost | null | undefined\n site?: BusinessProfileDoc | null\n workspace?: BusinessProfileDoc | null\n}): ResolvedBusinessProfile {\n const { host, site, workspace } = input\n const entity = host?.seo?.entity ?? null\n const fromSite: Partial<Record<BusinessProfileField, unknown>> = {\n whatYouDo: text(entity?.description, BUSINESS_PROFILE_MAX_CHARS.whatYouDo),\n serviceArea: normalizeAreaServed(entity?.areaServed).join(', ').slice(0, BUSINESS_PROFILE_MAX_CHARS.serviceArea),\n }\n\n const resolve = <T>(field: BusinessProfileField): ResolvedBusinessField<T> | null => {\n const stored = businessProfileSourceOf(site, field)\n if (stored === 'owner') return { value: fieldValue(site, field) as T, origin: 'owner' }\n const own = fromSite[field]\n if (own !== undefined && !isEmpty(own)) return { value: own as T, origin: 'site' }\n if (stored) return { value: fieldValue(site, field) as T, origin: stored }\n if (businessProfileSourceOf(workspace, field)) {\n return { value: fieldValue(workspace, field) as T, origin: 'workspace' }\n }\n return null\n }\n\n const name = text(entity?.name, 200) || text(host?.displayName, 200)\n return {\n name: name ? { value: name, origin: 'site' } : null,\n whatYouDo: resolve<string>('whatYouDo'),\n services: resolve<string[]>('services'),\n serviceArea: resolve<string>('serviceArea'),\n audience: resolve<string>('audience'),\n tone: resolve<BusinessTone>('tone'),\n toneNotes: resolve<string>('toneNotes'),\n businessType: text(entity?.businessType, 80) || null,\n contact: {\n email: text(entity?.email, 320) || text(host?.business?.supportEmail, 320) || null,\n phone: text(entity?.telephone, 64) || null,\n address: addressLine(host) || null,\n hours: text(entity?.openingHours, 400) || null,\n },\n profiles: siteProfileUrls(host as Parameters<typeof siteProfileUrls>[0]),\n logo: text(host?.logoUrl, 800) || text(entity?.logo, 800) || null,\n }\n}\n\n/** Each origin as the profile screen labels it. */\nexport const BUSINESS_PROFILE_ORIGIN_LABELS: Readonly<Record<BusinessProfileOrigin, string>> = {\n owner: 'You',\n site: 'Site settings',\n start: 'Your answers when the site was started',\n ai: `Suggested by ${PLATFORM_BRAND_NAME} AI`,\n workspace: 'Workspace default',\n}\n"],"names":["siteProfileUrls","normalizeAreaServed","PLATFORM_BRAND_NAME","BUSINESS_PROFILE_FIELDS","BUSINESS_PROFILE_MAX_CHARS","BUSINESS_TONES","businessProfileSourceOf","fieldValue","isEmpty","text","BUSINESS_PROFILE_MAX_SERVICES","BUSINESS_PROFILE_SITE_DOC","BUSINESS_PROFILE_SUBCOLLECTION","BUSINESS_PROFILE_WORKSPACE_DOC","BUSINESS_TONE_LABELS","businessProfileOwnerWrite","mergeBusinessProfilePrefill","sanitizeBusinessProfileValues","BUSINESS_TONE_ORDER","BUSINESS_PROFILE_FIELD_ORDER","addressLine","host","parts","seo","entity","address","line","streetAddress","addressLocality","addressRegion","postalCode","addressCountry","map","part","filter","Boolean","join","business","resolveBusinessProfile","input","site","workspace","fromSite","whatYouDo","description","serviceArea","areaServed","slice","resolve","field","stored","value","origin","own","undefined","name","displayName","services","audience","tone","toneNotes","businessType","contact","email","supportEmail","phone","telephone","hours","openingHours","profiles","logo","logoUrl","BUSINESS_PROFILE_ORIGIN_LABELS","owner","start","ai"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/business-profile.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 { siteProfileUrls, type HostSeoEntity } from './content-authors'\nimport { normalizeAreaServed } from './local-business'\nimport { PLATFORM_BRAND_NAME } from './platform-brand'\nimport {\n BUSINESS_PROFILE_FIELDS,\n BUSINESS_PROFILE_MAX_CHARS,\n BUSINESS_TONES,\n businessProfileSourceOf,\n fieldValue,\n isEmpty,\n text,\n type BusinessProfileDoc,\n type BusinessProfileField,\n type BusinessProfileOrigin,\n type BusinessProfileSource,\n type BusinessProfileValues,\n type BusinessTone,\n} from './business-profile-values'\n\nexport {\n BUSINESS_PROFILE_FIELDS,\n BUSINESS_PROFILE_MAX_CHARS,\n BUSINESS_PROFILE_MAX_SERVICES,\n BUSINESS_PROFILE_SITE_DOC,\n BUSINESS_PROFILE_SUBCOLLECTION,\n BUSINESS_PROFILE_WORKSPACE_DOC,\n BUSINESS_TONE_LABELS,\n BUSINESS_TONES,\n businessProfileFoundedProblem,\n businessProfileOwnerWrite,\n businessProfileSourceOf,\n mergeBusinessProfilePrefill,\n sanitizeBusinessProfileValues,\n} from './business-profile-values'\nexport type {\n BusinessProfileDoc,\n BusinessProfileField,\n BusinessProfileOrigin,\n BusinessProfileSource,\n BusinessProfileValues,\n BusinessTone,\n} from './business-profile-values'\n\n/** The tones as a plain list, in the order the owner picks them, for the native apps' pickers. */\nexport const BUSINESS_TONE_ORDER: readonly string[] = BUSINESS_TONES\n\n/** The stored fields as a plain list, for the native apps' writers. */\nexport const BUSINESS_PROFILE_FIELD_ORDER: readonly string[] = BUSINESS_PROFILE_FIELDS\n\n/**\n * A site's BUSINESS PROFILE (AGL-3661): what the business is, as everything\n * that writes about it needs to know — the site's own pages, the structured\n * data, and any generator a plugin brings.\n *\n * ── Most of it is already stored, so this stores only the rest ─────────────\n *\n * The site's name, its one-line description, its contact details, its hours,\n * the areas it serves and its social profiles all have a home on the host\n * document already: `seo.entity` (the publisher entity the JSON-LD is built\n * from, edited in Setup → SEO) and `business` (the contact card the `host.*`\n * tokens and the footer read, edited in Setup → Basic details). Copying them\n * here would make two places to keep in step, and the copy would drift.\n *\n * So the profile document holds only what has no home yet — the services, the\n * audience, the tone of voice, the year it was founded — plus a line about the\n * business and the area it serves for a site whose SEO entity does not say. {@link resolveBusinessProfile}\n * reads the two together into one picture, field by field, and says where\n * each value came from.\n *\n * ── Where it is stored ────────────────────────────────────────────────────\n *\n * `hosts/{hostId}/businessProfile/profile` for a site and\n * `orgs/{orgId}/businessProfile/defaults` for the workspace, whose values a\n * site inherits where it has none of its own. A subcollection document rather\n * than a key on the host: the host document's `business` map is replaced\n * whole by the contact card's save, and a key beside it would be one more\n * thing a partial write could blank.\n *\n * ── Whose words win ───────────────────────────────────────────────────────\n *\n * Every stored value carries its source. What the owner typed is never\n * replaced by anything else; what the guided start was told replaces only\n * what a generator guessed; and a generator's guess fills only what is empty.\n * {@link mergeBusinessProfilePrefill} is the one place that rule is applied.\n */\n\n\n/** The host fields the profile reads. A closed surface, as the host tokens' is. */\nexport interface BusinessProfileHost {\n displayName?: string\n logoUrl?: string\n seo?: { description?: string; entity?: HostSeoEntity | null } | null\n business?: {\n supportEmail?: string\n address?: string\n socialLinks?: Array<{ label?: string; url?: string } | null> | null\n } | null\n}\n\nexport interface ResolvedBusinessField<T> {\n value: T\n origin: BusinessProfileOrigin\n}\n\n/** The picture every reader works from: each field's value and where it came from. */\nexport interface ResolvedBusinessProfile {\n name: ResolvedBusinessField<string> | null\n whatYouDo: ResolvedBusinessField<string> | null\n services: ResolvedBusinessField<string[]> | null\n serviceArea: ResolvedBusinessField<string> | null\n audience: ResolvedBusinessField<string> | null\n tone: ResolvedBusinessField<BusinessTone> | null\n toneNotes: ResolvedBusinessField<string> | null\n /** The year the business started (AGL-4092). */\n founded: ResolvedBusinessField<string> | null\n /** The schema.org business type the SEO entity names, e.g. `Plumber`. */\n businessType: string | null\n /** Real contact details, each only where the owner entered it in the site's settings. */\n contact: {\n email: string | null\n phone: string | null\n address: string | null\n hours: string | null\n }\n /** The site's social and other profiles, https only. */\n profiles: string[]\n logo: string | null\n}\n\n\n/** The postal address as one line: the SEO entity's parts, else the contact card's block. */\nfunction addressLine(host: BusinessProfileHost | null | undefined): string {\n const parts = host?.seo?.entity?.address\n const line = [\n parts?.streetAddress,\n parts?.addressLocality,\n parts?.addressRegion,\n parts?.postalCode,\n parts?.addressCountry,\n ]\n .map((part) => text(part, 200))\n .filter(Boolean)\n .join(', ')\n return line || text(host?.business?.address, 400)\n}\n\n/**\n * The profile as every reader sees it, field by field (AGL-3661):\n *\n * 1. what the OWNER typed into the site's profile;\n * 2. what the site's own settings say (the SEO entity, the contact card, the\n * display name), which the owner typed too, elsewhere;\n * 3. what the guided start or a generator stored on the site's profile;\n * 4. the workspace defaults.\n *\n * Contact details, the logo and the profiles come from the site's settings\n * only: nothing but the owner's own entry may say how to reach the business.\n */\nexport function resolveBusinessProfile(input: {\n host: BusinessProfileHost | null | undefined\n site?: BusinessProfileDoc | null\n workspace?: BusinessProfileDoc | null\n}): ResolvedBusinessProfile {\n const { host, site, workspace } = input\n const entity = host?.seo?.entity ?? null\n const fromSite: Partial<Record<BusinessProfileField, unknown>> = {\n whatYouDo: text(entity?.description, BUSINESS_PROFILE_MAX_CHARS.whatYouDo),\n serviceArea: normalizeAreaServed(entity?.areaServed).join(', ').slice(0, BUSINESS_PROFILE_MAX_CHARS.serviceArea),\n }\n\n const resolve = <T>(field: BusinessProfileField): ResolvedBusinessField<T> | null => {\n const stored = businessProfileSourceOf(site, field)\n if (stored === 'owner') return { value: fieldValue(site, field) as T, origin: 'owner' }\n const own = fromSite[field]\n if (own !== undefined && !isEmpty(own)) return { value: own as T, origin: 'site' }\n if (stored) return { value: fieldValue(site, field) as T, origin: stored }\n if (businessProfileSourceOf(workspace, field)) {\n return { value: fieldValue(workspace, field) as T, origin: 'workspace' }\n }\n return null\n }\n\n const name = text(entity?.name, 200) || text(host?.displayName, 200)\n return {\n name: name ? { value: name, origin: 'site' } : null,\n whatYouDo: resolve<string>('whatYouDo'),\n services: resolve<string[]>('services'),\n serviceArea: resolve<string>('serviceArea'),\n audience: resolve<string>('audience'),\n tone: resolve<BusinessTone>('tone'),\n toneNotes: resolve<string>('toneNotes'),\n founded: resolve<string>('founded'),\n businessType: text(entity?.businessType, 80) || null,\n contact: {\n email: text(entity?.email, 320) || text(host?.business?.supportEmail, 320) || null,\n phone: text(entity?.telephone, 64) || null,\n address: addressLine(host) || null,\n hours: text(entity?.openingHours, 400) || null,\n },\n profiles: siteProfileUrls(host as Parameters<typeof siteProfileUrls>[0]),\n logo: text(host?.logoUrl, 800) || text(entity?.logo, 800) || null,\n }\n}\n\n/** Each origin as the profile screen labels it. */\nexport const BUSINESS_PROFILE_ORIGIN_LABELS: Readonly<Record<BusinessProfileOrigin, string>> = {\n owner: 'You',\n site: 'Site settings',\n start: 'Your answers when the site was started',\n ai: `Suggested by ${PLATFORM_BRAND_NAME} AI`,\n workspace: 'Workspace default',\n}\n"],"names":["siteProfileUrls","normalizeAreaServed","PLATFORM_BRAND_NAME","BUSINESS_PROFILE_FIELDS","BUSINESS_PROFILE_MAX_CHARS","BUSINESS_TONES","businessProfileSourceOf","fieldValue","isEmpty","text","BUSINESS_PROFILE_MAX_SERVICES","BUSINESS_PROFILE_SITE_DOC","BUSINESS_PROFILE_SUBCOLLECTION","BUSINESS_PROFILE_WORKSPACE_DOC","BUSINESS_TONE_LABELS","businessProfileFoundedProblem","businessProfileOwnerWrite","mergeBusinessProfilePrefill","sanitizeBusinessProfileValues","BUSINESS_TONE_ORDER","BUSINESS_PROFILE_FIELD_ORDER","addressLine","host","parts","seo","entity","address","line","streetAddress","addressLocality","addressRegion","postalCode","addressCountry","map","part","filter","Boolean","join","business","resolveBusinessProfile","input","site","workspace","fromSite","whatYouDo","description","serviceArea","areaServed","slice","resolve","field","stored","value","origin","own","undefined","name","displayName","services","audience","tone","toneNotes","founded","businessType","contact","email","supportEmail","phone","telephone","hours","openingHours","profiles","logo","logoUrl","BUSINESS_PROFILE_ORIGIN_LABELS","owner","start","ai"],"mappings":"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"}
@@ -95,6 +95,7 @@
95
95
  },
96
96
  aiImport: {
97
97
  '#prove-it-is-yours': 'Use one of these three ways. The status at the top shows Verified once one of them works.',
98
+ '#turned-away': 'Many hosts protect a site with bot protection or a firewall that turns away visitors it takes for robots.',
98
99
  '#waves': 'The first wave brings in your main pages: home, then the pages in your menu, then your other top-level pages.',
99
100
  '#your-current-website': 'Under Website address, type the address people use to reach your site today, for example example.com, and choose Next.'
100
101
  },
@@ -551,6 +552,10 @@
551
552
  '#workspace-licenses': 'This workspace lists the marketplace licenses this workspace owns — installable by any member with install permission, whoever on the team paid for it.',
552
553
  '#your-listings': 'Marketplace → Listings lists everything your organization has published, with its preview image; a row\'s menu is where you Publish new version…, Edit listing, or switch a plugin between private and public.'
553
554
  },
555
+ quotes: {
556
+ '#make-a-quote': 'On the Orders tab, choose New quote in the Quotes card. The quote gets the next number from your store\'s order numbers and is listed as Quote #1042, for example.',
557
+ '#quote-defaults': 'The Quotes card on the Checkout settings page sets what every new quote starts with: whether customers approve the proof before they can pay.'
558
+ },
554
559
  redirects: {
555
560
  '#manage-redirects': 'Create, edit, and delete redirect rules from the redirect manager page.'
556
561
  },