@vectoriox/iox-ui 5.56.1 → 5.58.0
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/composite-core/index.d.ts +3 -3
- package/design-core/index.d.ts +40 -2
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +6 -10
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +46 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +4231 -897
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs +24 -10
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +27 -1
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/generation-core/index.d.ts +3774 -1101
- package/harness-core/index.d.ts +24 -9
- package/index.d.ts +22 -1
- package/package.json +1 -1
- package/src/composite-core/accordion-runtime.ts +2 -1
- package/src/composite-core/lightbox.ts +5 -6
- package/src/design-core/entrance-timing.ts +56 -0
- package/src/design-core/public-api.ts +1 -0
- package/src/generation-core/ai-run.ts +21 -0
- package/src/generation-core/audit-site.ts +32 -0
- package/src/generation-core/compose-site.ts +213 -42
- package/src/generation-core/entrances.ts +82 -13
- package/src/generation-core/patterns.ts +127 -15
- package/src/generation-core/public-api.ts +3 -2
- package/src/generation-core/showcase.ts +81 -0
- package/src/generation-core/site-plan-schema.ts +21 -8
- package/src/generation-core/site-plan.model.ts +75 -4
- package/src/generation-core/site-spec.generated.ts +4019 -1174
- package/src/generation-core/validate-site-plan.ts +129 -21
- package/src/harness-core/harness-spec.generated.ts +3 -3
- package/src/harness-core/public-api.ts +1 -0
- package/src/harness-core/reference-colour.ts +15 -3
- package/src/harness-core/site-audit-view.ts +10 -0
- package/src/harness-core/starting-design.ts +8 -4
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { flavorPackById, parseHexColor, SPEC_AXES } from '@vectoriox/iox-ui/design-core';
|
|
2
|
+
import { formPresetById } from '@vectoriox/iox-ui/forms-core';
|
|
3
|
+
import { SEO_DESCRIPTION_MAX, SEO_TITLE_MAX } from '@vectoriox/iox-ui/seo-core';
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
|
-
|
|
6
|
+
BindScope, patternById, patternHasForm, patternLinkSlots, patternNeedsBinding, patternOptionalParts, patternRoleNeeds, patternTakesBinding, patternScopes,
|
|
7
|
+
patternSlots, PatternGlobalPosition, slotBudget,
|
|
5
8
|
} from './patterns';
|
|
6
9
|
import {
|
|
7
|
-
FACT_KEYS, GlobalSlot, PlanDataSourceMode, PlanDataSourceType, RelativeOp,
|
|
10
|
+
FACT_KEYS, GlobalSlot, PAGE_ARCHETYPES, PlanDataSourceMode, PlanDataSourceType, RelativeOp, REQUIRED_CONTENT_ROLES, SitePlan, SitePlanSourceKind,
|
|
11
|
+
SITE_LOCALES, SITE_PLAN_VERSION,
|
|
8
12
|
} from './site-plan.model';
|
|
9
13
|
import { SPEC_SITE_PLAN_SHAPE } from './site-spec.generated';
|
|
10
14
|
import {
|
|
@@ -43,9 +47,26 @@ export enum SitePlanIssueCode {
|
|
|
43
47
|
UnknownOptionalPart = 'unknown-optional-part',
|
|
44
48
|
Binding = 'binding',
|
|
45
49
|
MissingScope = 'missing-scope',
|
|
46
|
-
/** A pattern
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
/** A pattern reads a role (`@title`) the bound item's content type does not name (L0-3), or a type names no title. */
|
|
51
|
+
MissingRole = 'missing-role',
|
|
52
|
+
/** A role the contract does not know, or one played by a field the type does not have. */
|
|
53
|
+
UnknownRole = 'unknown-role',
|
|
54
|
+
/** A page type that does not exist; or (`archetypeIssues`, the plan stage's own check) sections that do not fit it. */
|
|
55
|
+
Archetype = 'archetype',
|
|
56
|
+
/** A slot's words are longer than its budget under the look's density (L0-2, M1-6c): rewrite, never cut. */
|
|
57
|
+
SlotTooLong = 'slot-too-long',
|
|
58
|
+
/** A section re-points a slot that does not link, or to a route the plan has no page for. */
|
|
59
|
+
InvalidLink = 'invalid-link',
|
|
60
|
+
/** A section's form preset is not a forms-core preset, or the pattern holds no form. */
|
|
61
|
+
InvalidForm = 'invalid-form',
|
|
62
|
+
/** A section's entrance is not a How animated level. */
|
|
63
|
+
InvalidEnter = 'invalid-enter',
|
|
64
|
+
/** The plan's language is not a site locale. */
|
|
65
|
+
InvalidLocale = 'invalid-locale',
|
|
66
|
+
/** A page's SEO words are longer than a search result shows (seo-core limits). */
|
|
67
|
+
InvalidSeo = 'invalid-seo',
|
|
68
|
+
/** A page key that is a global slot's name (`header`, `footer`): its sections' ids would be the global's. */
|
|
69
|
+
ReservedPageKey = 'reserved-page-key',
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
export type SitePlanIssue = PathIssue<SitePlanIssueCode>;
|
|
@@ -75,6 +96,9 @@ const BUSINESS_KEYS = shapeKeys(SHAPE['business']);
|
|
|
75
96
|
const DESIGN_REQUIRED = requiredShapeKeys(SHAPE['design']);
|
|
76
97
|
const DESIGN_KEYS = shapeKeys(SHAPE['design']);
|
|
77
98
|
const CONTENT_TYPE_KEYS = shapeKeys(SHAPE['contentType']);
|
|
99
|
+
const ROLE_KEYS = shapeKeys(SHAPE['roles']);
|
|
100
|
+
const PAGE_SEO_KEYS = shapeKeys(SHAPE['pageSeo']);
|
|
101
|
+
const ARRAY_TYPE = 'array';
|
|
78
102
|
const FIELD_KEYS = shapeKeys(SHAPE['field']);
|
|
79
103
|
const SEED_FIXED_KEYS = shapeKeys(SHAPE['seed']);
|
|
80
104
|
const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
|
|
@@ -129,6 +153,12 @@ function validateBusiness(c: Collector, business: unknown): void {
|
|
|
129
153
|
}
|
|
130
154
|
}
|
|
131
155
|
|
|
156
|
+
function validateLocale(c: Collector, locale: unknown): void {
|
|
157
|
+
if (locale !== undefined && (typeof locale !== 'string' || !has(SITE_LOCALES, locale))) {
|
|
158
|
+
c.add(SitePlanIssueCode.InvalidLocale, 'locale', `must be one of ${Object.keys(SITE_LOCALES).join(', ')}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
132
162
|
function validateDesign(c: Collector, design: unknown): void {
|
|
133
163
|
if (!isObject(design)) return c.add(SitePlanIssueCode.Shape, 'design', 'must be an object');
|
|
134
164
|
c.keys(design, DESIGN_KEYS, 'design');
|
|
@@ -155,7 +185,7 @@ function validateDesign(c: Collector, design: unknown): void {
|
|
|
155
185
|
if (design['fonts'] !== undefined && !isObject(design['fonts'])) bad('fonts', 'must be an object');
|
|
156
186
|
}
|
|
157
187
|
|
|
158
|
-
interface ContentTypeInfo { fields: Set<string>;
|
|
188
|
+
interface ContentTypeInfo { fields: Set<string>; roles: Record<string, string>; }
|
|
159
189
|
|
|
160
190
|
function validateContentTypes(c: Collector, list: unknown[]): Map<string, ContentTypeInfo> {
|
|
161
191
|
const types = new Map<string, ContentTypeInfo>();
|
|
@@ -177,12 +207,8 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
|
|
|
177
207
|
});
|
|
178
208
|
c.unique(names, j => `${path}.fields[${j}].name`, 'field');
|
|
179
209
|
const fieldSet = new Set(names.filter((n): n is string => !!n));
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
const v = c.string(raw, key, path, false);
|
|
183
|
-
if (v && !fieldSet.has(v)) c.add(SitePlanIssueCode.UnknownField, `${path}.${key}`, `"${v}" is not a field of ${name}`);
|
|
184
|
-
preview[key] = v && fieldSet.has(v) ? v : null;
|
|
185
|
-
}
|
|
210
|
+
const fieldTypes = new Map(fields.filter(isObject).map(f => [f['name'], f['type']]));
|
|
211
|
+
const roles = validateRoles(c, raw['roles'], `${path}.roles`, name, fieldSet, fieldTypes);
|
|
186
212
|
const seed = raw['seed'];
|
|
187
213
|
if (seed !== undefined) {
|
|
188
214
|
if (!isObject(seed)) c.add(SitePlanIssueCode.Shape, `${path}.seed`, 'must be an object');
|
|
@@ -195,11 +221,25 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
|
|
|
195
221
|
}
|
|
196
222
|
}
|
|
197
223
|
if (name && types.has(name)) c.add(SitePlanIssueCode.Duplicate, `${path}.name`, `content type "${name}" is used twice`);
|
|
198
|
-
else if (name) types.set(name, { fields: fieldSet,
|
|
224
|
+
else if (name) types.set(name, { fields: fieldSet, roles });
|
|
199
225
|
});
|
|
200
226
|
return types;
|
|
201
227
|
}
|
|
202
228
|
|
|
229
|
+
/** A content type's roles (L0-3): known roles only, each played by a field of the type; `title` always; `gallery` a list. */
|
|
230
|
+
function validateRoles(c: Collector, roles: unknown, path: string, typeName: string | null, fields: Set<string>, fieldTypes: Map<unknown, unknown>): Record<string, string> {
|
|
231
|
+
if (!isObject(roles)) { c.add(SitePlanIssueCode.Shape, path, 'must be an object (which field plays each role)'); return {}; }
|
|
232
|
+
const out: Record<string, string> = {};
|
|
233
|
+
for (const [role, field] of Object.entries(roles)) {
|
|
234
|
+
if (!ROLE_KEYS.includes(role)) { c.add(SitePlanIssueCode.UnknownRole, `${path}.${role}`, `"${role}" is not a role (${ROLE_KEYS.join(', ')})`); continue; }
|
|
235
|
+
if (typeof field !== 'string' || !fields.has(field)) { c.add(SitePlanIssueCode.UnknownRole, `${path}.${role}`, `"${String(field)}" is not a field of ${typeName}`); continue; }
|
|
236
|
+
if (role === 'gallery' && fieldTypes.get(field) !== ARRAY_TYPE) { c.add(SitePlanIssueCode.UnknownRole, `${path}.${role}`, `"${field}" is not a list field — a gallery is a list of pictures`); continue; }
|
|
237
|
+
out[role] = field;
|
|
238
|
+
}
|
|
239
|
+
for (const role of REQUIRED_CONTENT_ROLES) if (!out[role] && !has(roles, role)) c.add(SitePlanIssueCode.MissingRole, `${path}.${role}`, `every content type names its ${role} field`);
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
203
243
|
interface SourceInfo { type: string; mode?: string; contentType?: string; routeParam?: string; of?: string; op?: string; }
|
|
204
244
|
|
|
205
245
|
function validateDataSources(c: Collector, list: unknown[], types: Map<string, ContentTypeInfo>): Map<string, SourceInfo> {
|
|
@@ -255,13 +295,17 @@ function validateDataSources(c: Collector, list: unknown[], types: Map<string, C
|
|
|
255
295
|
return sources;
|
|
256
296
|
}
|
|
257
297
|
|
|
258
|
-
function validatePages(c: Collector, list: unknown[], types: Map<string, ContentTypeInfo>, sources: Map<string, SourceInfo
|
|
298
|
+
function validatePages(c: Collector, list: unknown[], types: Map<string, ContentTypeInfo>, sources: Map<string, SourceInfo>, density: string | null): void {
|
|
259
299
|
const homes: number[] = [];
|
|
300
|
+
const routes = new Set(list.map(p => (isObject(p) && typeof p['route'] === 'string' ? p['route'] : null)).filter((r): r is string => !!r));
|
|
260
301
|
list.forEach((raw, i) => {
|
|
261
302
|
const path = `pages[${i}]`;
|
|
262
303
|
if (!isObject(raw)) return c.add(SitePlanIssueCode.Shape, path, 'must be an object');
|
|
263
304
|
c.keys(raw, PAGE_KEYS, path);
|
|
264
|
-
c.string(raw, 'key', path);
|
|
305
|
+
const key = c.string(raw, 'key', path);
|
|
306
|
+
if (key && (Object.values(GlobalSlot) as string[]).includes(key)) {
|
|
307
|
+
c.add(SitePlanIssueCode.ReservedPageKey, `${path}.key`, `"${key}" names the site's ${key} — a page's sections would share its ids`);
|
|
308
|
+
}
|
|
265
309
|
c.string(raw, 'name', path);
|
|
266
310
|
for (const key of ['isHome', 'required', 'inNav']) c.boolean(raw, key, path);
|
|
267
311
|
const route = c.string(raw, 'route', path);
|
|
@@ -288,6 +332,10 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
|
|
|
288
332
|
}
|
|
289
333
|
}
|
|
290
334
|
|
|
335
|
+
const archetype = c.string(raw, 'archetype', path, false);
|
|
336
|
+
if (archetype && !has(PAGE_ARCHETYPES, archetype)) c.add(SitePlanIssueCode.Archetype, `${path}.archetype`, `"${archetype}" is not a page type (${Object.keys(PAGE_ARCHETYPES).join(', ')})`);
|
|
337
|
+
validatePageSeo(c, raw['seo'], `${path}.seo`);
|
|
338
|
+
|
|
291
339
|
c.array(raw, 'sections', path).forEach((s, j) => {
|
|
292
340
|
const sp = `${path}.sections[${j}]`;
|
|
293
341
|
if (!isObject(s)) return c.add(SitePlanIssueCode.Shape, sp, 'must be an object');
|
|
@@ -305,9 +353,29 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
|
|
|
305
353
|
for (const [slot, value] of Object.entries(slots)) {
|
|
306
354
|
if (!declared.includes(slot)) c.add(SitePlanIssueCode.UnknownSlot, `${sp}.slots.${slot}`, `"${patternId}" has no slot "${slot}" (${declared.join(', ') || 'none'})`);
|
|
307
355
|
else if (typeof value !== 'string') c.add(SitePlanIssueCode.Shape, `${sp}.slots.${slot}`, 'must be a string');
|
|
356
|
+
else {
|
|
357
|
+
const budget = slotBudget(pattern, slot, density);
|
|
358
|
+
if (budget !== null && value.length > budget) c.add(SitePlanIssueCode.SlotTooLong, `${sp}.slots.${slot}`, `${value.length} characters — the slot holds ${budget} at the ${density ?? 'regular'} density`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const links = s['links'];
|
|
364
|
+
if (links !== undefined) {
|
|
365
|
+
if (!isObject(links)) c.add(SitePlanIssueCode.Shape, `${sp}.links`, 'must be an object');
|
|
366
|
+
else {
|
|
367
|
+
const linkSlots = patternLinkSlots(pattern);
|
|
368
|
+
for (const [slot, route] of Object.entries(links)) {
|
|
369
|
+
if (!linkSlots.includes(slot)) c.add(SitePlanIssueCode.InvalidLink, `${sp}.links.${slot}`, `"${patternId}" has no linked slot "${slot}" (${linkSlots.join(', ') || 'none'})`);
|
|
370
|
+
else if (typeof route !== 'string' || !routes.has(route) || routeParams(route).length) c.add(SitePlanIssueCode.InvalidLink, `${sp}.links.${slot}`, `"${String(route)}" is not a page route of the plan`);
|
|
308
371
|
}
|
|
309
372
|
}
|
|
310
373
|
}
|
|
374
|
+
const form = c.string(s, 'form', sp, false);
|
|
375
|
+
if (form && !patternHasForm(pattern)) c.add(SitePlanIssueCode.InvalidForm, `${sp}.form`, `"${patternId}" holds no form`);
|
|
376
|
+
else if (form && !formPresetById(form)) c.add(SitePlanIssueCode.InvalidForm, `${sp}.form`, `"${form}" is not a form preset`);
|
|
377
|
+
const enter = c.string(s, 'enter', sp, false);
|
|
378
|
+
if (enter && !has(SPEC_AXES.animations, enter)) c.add(SitePlanIssueCode.InvalidEnter, `${sp}.enter`, `must be one of ${Object.keys(SPEC_AXES.animations).join(', ')}`);
|
|
311
379
|
const without = s['without'];
|
|
312
380
|
if (without !== undefined) {
|
|
313
381
|
const parts = patternOptionalParts(pattern);
|
|
@@ -337,13 +405,15 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
|
|
|
337
405
|
c.add(SitePlanIssueCode.MissingScope, `${sp}.pattern`, `"${patternId}" reads the next item — the plan needs a ${RelativeOp.SiblingsNext} source of ${routeSourceAlias}`);
|
|
338
406
|
}
|
|
339
407
|
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
|
|
408
|
+
// L0-3: the roles a pattern reads must be roles of the item's type — the section's list (`item`), the route
|
|
409
|
+
// item (`route`) or its next sibling (`next`, the same type). An optional one (`@image?`) is only left out.
|
|
410
|
+
for (const need of patternRoleNeeds(pattern)) {
|
|
411
|
+
if (need.optional) continue;
|
|
412
|
+
const alias = need.scope === BindScope.Item ? binding : routeSourceOk ? routeSourceAlias : null;
|
|
343
413
|
const typeName = alias ? sources.get(alias)?.contentType : undefined;
|
|
344
414
|
const type = typeName ? types.get(typeName) : undefined;
|
|
345
|
-
if (type && !type.
|
|
346
|
-
c.add(SitePlanIssueCode.
|
|
415
|
+
if (type && !type.roles[need.role]) {
|
|
416
|
+
c.add(SitePlanIssueCode.MissingRole, `${sp}.pattern`, `"${patternId}" shows each ${typeName} item's ${need.role} — ${typeName} names no ${need.role} field (roles.${need.role})`);
|
|
347
417
|
}
|
|
348
418
|
}
|
|
349
419
|
});
|
|
@@ -353,6 +423,18 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
|
|
|
353
423
|
if (homes.length !== 1) c.add(SitePlanIssueCode.Home, 'pages', `exactly one page is the home page (found ${homes.length})`);
|
|
354
424
|
}
|
|
355
425
|
|
|
426
|
+
|
|
427
|
+
function validatePageSeo(c: Collector, seo: unknown, path: string): void {
|
|
428
|
+
if (seo === undefined) return;
|
|
429
|
+
if (!isObject(seo)) return c.add(SitePlanIssueCode.Shape, path, 'must be an object');
|
|
430
|
+
c.keys(seo, PAGE_SEO_KEYS, path);
|
|
431
|
+
const limits: Readonly<Record<string, number>> = { title: SEO_TITLE_MAX, description: SEO_DESCRIPTION_MAX };
|
|
432
|
+
for (const key of PAGE_SEO_KEYS) {
|
|
433
|
+
const value = c.string(seo, key, path, false);
|
|
434
|
+
if (value && value.length > limits[key]) c.add(SitePlanIssueCode.InvalidSeo, `${path}.${key}`, `${value.length} characters — a search result shows ${limits[key]}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
356
438
|
/** The plan's `$siblings.next` source of `routeSource` — what a pattern's `next.*` reads. */
|
|
357
439
|
export function nextSourceOf(sources: Map<string, { type: string; of?: string; op?: string }>, routeSource: string): string | null {
|
|
358
440
|
for (const [alias, s] of sources) if (s.type === PlanDataSourceType.Relative && s.of === routeSource && s.op === RelativeOp.SiblingsNext) return alias;
|
|
@@ -381,10 +463,12 @@ export function validateSitePlan(plan: unknown): SitePlanValidation {
|
|
|
381
463
|
if (plan['version'] !== SITE_PLAN_VERSION) c.add(SitePlanIssueCode.Version, 'version', `must be "${SITE_PLAN_VERSION}"`);
|
|
382
464
|
validateSource(c, plan['source']);
|
|
383
465
|
validateBusiness(c, plan['business']);
|
|
466
|
+
validateLocale(c, plan['locale']);
|
|
384
467
|
validateDesign(c, plan['design']);
|
|
385
468
|
const types = validateContentTypes(c, c.array(plan, 'contentTypes', ''));
|
|
386
469
|
const sources = validateDataSources(c, c.array(plan, 'dataSources', ''), types);
|
|
387
|
-
|
|
470
|
+
const design = isObject(plan['design']) ? plan['design'] : {};
|
|
471
|
+
validatePages(c, c.array(plan, 'pages', ''), types, sources, typeof design['density'] === 'string' ? design['density'] : null);
|
|
388
472
|
validateGlobals(c, plan['globals']);
|
|
389
473
|
return { valid: c.issues.length === 0, issues: c.issues };
|
|
390
474
|
}
|
|
@@ -394,3 +478,27 @@ export function assertValidSitePlan(plan: unknown): void {
|
|
|
394
478
|
const result = validateSitePlan(plan);
|
|
395
479
|
if (!result.valid) throw new SitePlanError(result.issues);
|
|
396
480
|
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* A plan's pages against their page types (L0-2): every required slot filled, every section of a family the type takes.
|
|
484
|
+
* NOT part of `validateSitePlan` — a page type is what the model READS to give a page its shape, and the plan stage
|
|
485
|
+
* holds the model's plan to it; the owner may then make any page their own (merge About into Contact, drop the
|
|
486
|
+
* closing band), which the gate must not refuse. Pure; issues carry the same paths.
|
|
487
|
+
*/
|
|
488
|
+
export function archetypeIssues(plan: SitePlan): SitePlanIssue[] {
|
|
489
|
+
const c = new Collector();
|
|
490
|
+
plan.pages.forEach((page, i) => {
|
|
491
|
+
const type = page.archetype ? PAGE_ARCHETYPES[page.archetype] : undefined;
|
|
492
|
+
if (!type) return;
|
|
493
|
+
const families = page.sections.map(s => patternById(s.pattern)?.family ?? '');
|
|
494
|
+
for (const slot of type.slots) {
|
|
495
|
+
if (slot.required && !families.some(f => slot.families.includes(f))) {
|
|
496
|
+
c.add(SitePlanIssueCode.Archetype, `pages[${i}].sections`, `a ${page.archetype} page needs a ${slot.label} section (${slot.families.join(' / ')})`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
families.forEach((family, j) => {
|
|
500
|
+
if (!type.slots.some(slot => slot.families.includes(family))) c.add(SitePlanIssueCode.Archetype, `pages[${i}].sections[${j}]`, `a ${family} section does not belong on a ${page.archetype} page`);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
return c.issues;
|
|
504
|
+
}
|
|
@@ -138,7 +138,7 @@ export const SPEC_STRUCTURES = {
|
|
|
138
138
|
"About",
|
|
139
139
|
"Contact"
|
|
140
140
|
],
|
|
141
|
-
"shipped":
|
|
141
|
+
"shipped": true
|
|
142
142
|
},
|
|
143
143
|
"place": {
|
|
144
144
|
"label": "Place",
|
|
@@ -146,11 +146,11 @@ export const SPEC_STRUCTURES = {
|
|
|
146
146
|
"pages": [
|
|
147
147
|
"Home",
|
|
148
148
|
"Menu",
|
|
149
|
-
"Visit (hours
|
|
149
|
+
"Visit (hours and address)",
|
|
150
150
|
"About",
|
|
151
151
|
"Contact"
|
|
152
152
|
],
|
|
153
|
-
"shipped":
|
|
153
|
+
"shipped": true
|
|
154
154
|
}
|
|
155
155
|
} as const;
|
|
156
156
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { auditDesignContrast, designTokenRows, parseHexColor, presetChoices, stylePresetById } from '@vectoriox/iox-ui/design-core';
|
|
1
|
+
import { auditDesignContrast, designTokenRows, parseHexColor, presetChoices, resolveTokenValues, stylePresetById, wcagContrastRatio } from '@vectoriox/iox-ui/design-core';
|
|
2
2
|
import type { DesignChoices } from '@vectoriox/iox-ui/design-core';
|
|
3
3
|
|
|
4
4
|
import { StyleRead } from './rank';
|
|
@@ -21,6 +21,17 @@ export interface ReferenceSeed {
|
|
|
21
21
|
used: boolean;
|
|
22
22
|
/** The modes the reference colour failed in (empty when used, or when there is no reference). */
|
|
23
23
|
failsIn: Array<'light' | 'dark'>;
|
|
24
|
+
/** Information only: the RAW reference colour against the style's page ground (its first mode); null without one. */
|
|
25
|
+
rawRatio: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The raw colour's ratio against the preset's own page ground, in its first mode — shown, never decisive. */
|
|
29
|
+
function rawRatioOf(presetId: string, hex: string): number | null {
|
|
30
|
+
const preset = stylePresetById(presetId);
|
|
31
|
+
const choices = preset ? presetChoices(preset.id, preset.modes[0]) : null;
|
|
32
|
+
if (!preset || !choices) return null;
|
|
33
|
+
const ground = resolveTokenValues(designTokenRows(choices), preset.modes[0])['page-bg'];
|
|
34
|
+
return ground && parseHexColor(ground) ? Math.round(wcagContrastRatio(hex, ground) * 100) / 100 : null;
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
/** Every mode `presetId` offers passes the audit with `seed`. */
|
|
@@ -39,7 +50,8 @@ export function referenceSeed(read: Pick<StyleRead, 'brand'> | null | undefined,
|
|
|
39
50
|
const preset = stylePresetById(presetId);
|
|
40
51
|
if (!preset) return null;
|
|
41
52
|
const reference = read?.brand && parseHexColor(read.brand) ? read.brand : null;
|
|
42
|
-
if (!reference) return { seed: preset.seed, reference: null, used: false, failsIn: [] };
|
|
53
|
+
if (!reference) return { seed: preset.seed, reference: null, used: false, failsIn: [], rawRatio: null };
|
|
43
54
|
const { passes, failsIn } = seedPassesPreset(preset.id, reference);
|
|
44
|
-
|
|
55
|
+
const rawRatio = rawRatioOf(preset.id, reference);
|
|
56
|
+
return passes ? { seed: reference, reference, used: true, failsIn: [], rawRatio } : { seed: preset.seed, reference, used: false, failsIn, rawRatio };
|
|
45
57
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SiteAudit } from '@vectoriox/iox-ui/generation-core';
|
|
2
|
+
|
|
3
|
+
import type { LintFinding } from './lint';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `GET …/webbuilder/site-audit?page=` (AI plan §3e, M1-7): the eight checks (generation-core `auditSite`) and the design
|
|
7
|
+
* advice beside them (`lintDesign` — never blocks, "Keep it" dismisses). ONE definition: iox-cms answers it, the Publish
|
|
8
|
+
* dialog and Needs attention read it.
|
|
9
|
+
*/
|
|
10
|
+
export type SiteAuditView = SiteAudit & { advice: LintFinding[] };
|
|
@@ -12,7 +12,11 @@ import { referenceSeed } from './reference-colour';
|
|
|
12
12
|
* the style offers (`referenceSeed`). Pure.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* A preset for this business: its choices, Motion from the business (`motionFor`), the references' colour as its seed
|
|
17
|
+
* when that colour passes AA in every mode the preset offers (`referenceSeed` — judged as used), and what the owner
|
|
18
|
+
* already set kept. The ONE rule for a Look pick and for Build with these.
|
|
19
|
+
*/
|
|
16
20
|
export function presetDesign(
|
|
17
21
|
presetId: string,
|
|
18
22
|
input: MotionInput | string | null,
|
|
@@ -23,7 +27,8 @@ export function presetDesign(
|
|
|
23
27
|
if (!base) return null;
|
|
24
28
|
const motionInput: MotionInput = typeof input === 'object' && input !== null ? input : { businessType: input as string | null };
|
|
25
29
|
const start = motionFor(motionInput, presetId, read);
|
|
26
|
-
|
|
30
|
+
const seed = referenceSeed(read, presetId);
|
|
31
|
+
return { ...base, ...(seed?.used ? { seed: seed.seed } : {}), scroll: start.scroll, animate: start.animate, between: start.between, ...kept };
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export interface StartingDesign {
|
|
@@ -39,6 +44,5 @@ export function startingDesign(input: (RankInput & MotionInput) | string | null,
|
|
|
39
44
|
const top = rankWithReferences(rankInput, read)[0];
|
|
40
45
|
const design = top ? presetDesign(top.id, rankInput, {}, read) : null;
|
|
41
46
|
if (!top || !design) return null;
|
|
42
|
-
|
|
43
|
-
return seed?.used ? { presetId: top.id, design: { ...design, seed: seed.seed }, referenceColour: true } : { presetId: top.id, design, referenceColour: false };
|
|
47
|
+
return { presetId: top.id, design, referenceColour: !!referenceSeed(read, top.id)?.used };
|
|
44
48
|
}
|