@protoyard/blueprint 0.0.0-stage → 1.1.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/LICENSE +21 -0
- package/README.md +1149 -2
- package/dist/aggregate.d.ts +83 -0
- package/dist/aggregate.js +268 -0
- package/dist/app-model.d.ts +76 -0
- package/dist/app-model.js +287 -0
- package/dist/archetypes/index.d.ts +1 -0
- package/dist/archetypes/index.js +1 -0
- package/dist/archetypes/schema.d.ts +126 -0
- package/dist/archetypes/schema.js +77 -0
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +149 -0
- package/dist/authoring/deck-lint.d.ts +32 -0
- package/dist/authoring/deck-lint.js +567 -0
- package/dist/authoring/index.d.ts +1 -0
- package/dist/authoring/index.js +1 -0
- package/dist/authoring/select-deck-layouts.d.ts +50 -0
- package/dist/authoring/select-deck-layouts.js +213 -0
- package/dist/authoring-apis.d.ts +13 -0
- package/dist/authoring-apis.js +13 -0
- package/dist/authoring-validation.d.ts +65 -0
- package/dist/authoring-validation.js +267 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +30 -0
- package/dist/brand.d.ts +70 -0
- package/dist/brand.js +294 -0
- package/dist/brief/archetypes.d.ts +28 -0
- package/dist/brief/archetypes.js +798 -0
- package/dist/brief/content-synthesize.d.ts +31 -0
- package/dist/brief/content-synthesize.js +210 -0
- package/dist/brief/deck-outlines.d.ts +96 -0
- package/dist/brief/deck-outlines.js +561 -0
- package/dist/brief/draft-bodies.d.ts +29 -0
- package/dist/brief/draft-bodies.js +224 -0
- package/dist/brief/fields.d.ts +19 -0
- package/dist/brief/fields.js +229 -0
- package/dist/brief/index.d.ts +8 -0
- package/dist/brief/index.js +8 -0
- package/dist/brief/kind-detection.d.ts +69 -0
- package/dist/brief/kind-detection.js +353 -0
- package/dist/brief/limits.d.ts +117 -0
- package/dist/brief/limits.js +217 -0
- package/dist/brief/page-doc-outlines.d.ts +80 -0
- package/dist/brief/page-doc-outlines.js +485 -0
- package/dist/brief/synthesize.d.ts +59 -0
- package/dist/brief/synthesize.js +511 -0
- package/dist/brief/text.d.ts +13 -0
- package/dist/brief/text.js +59 -0
- package/dist/canvas-screen.d.ts +31 -0
- package/dist/canvas-screen.js +33 -0
- package/dist/canvas-seed.d.ts +58 -0
- package/dist/canvas-seed.js +82 -0
- package/dist/computed/evaluate.d.ts +11 -0
- package/dist/computed/evaluate.js +101 -0
- package/dist/computed/index.d.ts +4 -0
- package/dist/computed/index.js +3 -0
- package/dist/computed/public.d.ts +14 -0
- package/dist/computed/public.js +19 -0
- package/dist/computed/schema.d.ts +10 -0
- package/dist/computed/schema.js +140 -0
- package/dist/computed/types.d.ts +70 -0
- package/dist/computed/types.js +1 -0
- package/dist/computed/validate.d.ts +6 -0
- package/dist/computed/validate.js +146 -0
- package/dist/csv-guard.d.ts +15 -0
- package/dist/csv-guard.js +29 -0
- package/dist/csv.d.ts +85 -0
- package/dist/csv.js +288 -0
- package/dist/files.d.ts +35 -0
- package/dist/files.js +246 -0
- package/dist/form-schema.d.ts +124 -0
- package/dist/form-schema.js +300 -0
- package/dist/generator-version.d.ts +11 -0
- package/dist/generator-version.js +51 -0
- package/dist/generator.d.ts +34 -0
- package/dist/generator.js +445 -0
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +91 -0
- package/dist/import/csv-shape.d.ts +40 -0
- package/dist/import/csv-shape.js +265 -0
- package/dist/import/json-shape.d.ts +55 -0
- package/dist/import/json-shape.js +491 -0
- package/dist/import/limits.d.ts +13 -0
- package/dist/import/limits.js +14 -0
- package/dist/import/sqlite-schema.d.ts +36 -0
- package/dist/import/sqlite-schema.js +729 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +58 -0
- package/dist/kinds/anchors.d.ts +18 -0
- package/dist/kinds/anchors.js +46 -0
- package/dist/kinds/bodies.d.ts +6 -0
- package/dist/kinds/bodies.js +32 -0
- package/dist/kinds/brand-runtime.d.ts +19 -0
- package/dist/kinds/brand-runtime.js +35 -0
- package/dist/kinds/deck.d.ts +8 -0
- package/dist/kinds/deck.js +34 -0
- package/dist/kinds/doc.d.ts +19 -0
- package/dist/kinds/doc.js +41 -0
- package/dist/kinds/embed/index.d.ts +39 -0
- package/dist/kinds/embed/index.js +91 -0
- package/dist/kinds/envelope.d.ts +159 -0
- package/dist/kinds/envelope.js +78 -0
- package/dist/kinds/index.d.ts +11 -0
- package/dist/kinds/index.js +11 -0
- package/dist/kinds/kind.d.ts +36 -0
- package/dist/kinds/kind.js +55 -0
- package/dist/kinds/limits.d.ts +64 -0
- package/dist/kinds/limits.js +190 -0
- package/dist/kinds/page-doc-runtime.d.ts +37 -0
- package/dist/kinds/page-doc-runtime.js +55 -0
- package/dist/kinds/page.d.ts +16 -0
- package/dist/kinds/page.js +67 -0
- package/dist/kinds/parse.d.ts +18 -0
- package/dist/kinds/parse.js +27 -0
- package/dist/kinds/runtime.d.ts +128 -0
- package/dist/kinds/runtime.js +206 -0
- package/dist/kinds/services.d.ts +49 -0
- package/dist/kinds/services.js +249 -0
- package/dist/kinds/testing.d.ts +9 -0
- package/dist/kinds/testing.js +16 -0
- package/dist/migrations.d.ts +97 -0
- package/dist/migrations.js +351 -0
- package/dist/motion.d.ts +50 -0
- package/dist/motion.js +52 -0
- package/dist/notifications.d.ts +19 -0
- package/dist/notifications.js +177 -0
- package/dist/patch.d.ts +122 -0
- package/dist/patch.js +322 -0
- package/dist/policy.d.ts +23 -0
- package/dist/policy.js +105 -0
- package/dist/runtime-definition.d.ts +39 -0
- package/dist/runtime-definition.js +74 -0
- package/dist/scenarios.d.ts +9 -0
- package/dist/scenarios.js +49 -0
- package/dist/schema/blueprint.schema.json +10191 -0
- package/dist/schema-json.d.ts +6 -0
- package/dist/schema-json.js +36 -0
- package/dist/schema.d.ts +6348 -0
- package/dist/schema.js +1269 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +96 -0
- package/dist/services.d.ts +89 -0
- package/dist/services.js +898 -0
- package/dist/share.d.ts +78 -0
- package/dist/share.js +234 -0
- package/dist/sheet-screen.d.ts +49 -0
- package/dist/sheet-screen.js +91 -0
- package/dist/storage.d.ts +43 -0
- package/dist/storage.js +179 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.js +173 -0
- package/dist/untrusted.d.ts +63 -0
- package/dist/untrusted.js +502 -0
- package/dist/validation-tasks.d.ts +178 -0
- package/dist/validation-tasks.js +199 -0
- package/dist/validation.d.ts +35 -0
- package/dist/validation.js +312 -0
- package/dist/variants.d.ts +112 -0
- package/dist/variants.js +283 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +3 -0
- package/dist/workflows/schema.d.ts +50 -0
- package/dist/workflows/schema.js +34 -0
- package/dist/workflows/transition.d.ts +15 -0
- package/dist/workflows/transition.js +25 -0
- package/dist/workflows/validate.d.ts +17 -0
- package/dist/workflows/validate.js +140 -0
- package/package.json +120 -3
package/dist/brand.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an optional blueprint `brand` block into a full Theme for
|
|
3
|
+
* `protoyard generate`. Palette tokens come from `@protoyard/themes`
|
|
4
|
+
* (`createThemeFromSeed` or a built-in preset); fonts, radius and density
|
|
5
|
+
* come from the brand and/or a named style profile. Every resolved theme is
|
|
6
|
+
* checked against WCAG AA text and UI pairs before it is returned.
|
|
7
|
+
*/
|
|
8
|
+
import { type Theme, type ThemeStyleInput } from '@protoyard/core';
|
|
9
|
+
import type { BlueprintBrand } from './schema.ts';
|
|
10
|
+
export interface StyleProfile {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
radius?: string;
|
|
14
|
+
density?: number;
|
|
15
|
+
font?: string;
|
|
16
|
+
style?: ThemeStyleInput;
|
|
17
|
+
}
|
|
18
|
+
/** Named style knobs applied after the palette (or preset) is chosen. */
|
|
19
|
+
export declare const STYLE_PROFILES: readonly StyleProfile[];
|
|
20
|
+
export declare function getStyleProfile(id: string): StyleProfile | undefined;
|
|
21
|
+
export interface ResolveBrandOptions {
|
|
22
|
+
/** Blueprint product name — used when brand.name / brand.id are omitted. */
|
|
23
|
+
blueprintName?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare class BrandThemeError extends Error {
|
|
26
|
+
readonly failures: readonly BrandContrastFailure[];
|
|
27
|
+
constructor(message: string, failures?: readonly BrandContrastFailure[]);
|
|
28
|
+
}
|
|
29
|
+
export interface BrandContrastFailure {
|
|
30
|
+
mode: 'light' | 'dark';
|
|
31
|
+
foreground: string;
|
|
32
|
+
background: string;
|
|
33
|
+
minimum: number;
|
|
34
|
+
ratio: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Up to `limit` ids from `known` nearest to `id`: ids that contain it (or that
|
|
38
|
+
* it contains) first, then by edit distance, ties in `known` order. Only ids
|
|
39
|
+
* within half the longer length are suggested, so unrelated names stay out.
|
|
40
|
+
*/
|
|
41
|
+
export declare function closestIds(id: string, known: readonly string[], limit?: number): string[];
|
|
42
|
+
/**
|
|
43
|
+
* The library theme a brand names (`brand.libraryTheme`), or `undefined` when
|
|
44
|
+
* the brand builds its own theme. A library brand needs no generated module:
|
|
45
|
+
* the prototype's `defaultTheme` is the library id and the renderer loads it.
|
|
46
|
+
* Throws `BrandThemeError` (with the closest ids) when the library does not
|
|
47
|
+
* know the id.
|
|
48
|
+
*/
|
|
49
|
+
export declare function brandLibraryThemeId(brand: BlueprintBrand): string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Stable theme id from brand or blueprint name (`Acme Labs` → `acme-labs`);
|
|
52
|
+
* a library brand's id is the library theme id.
|
|
53
|
+
*/
|
|
54
|
+
export declare function brandThemeId(brand: BlueprintBrand, blueprintName?: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* `acme-labs` → `acmeLabsTheme` for the generated module export. Throws
|
|
57
|
+
* `BrandThemeError` for an id that cannot become a valid identifier.
|
|
58
|
+
*/
|
|
59
|
+
export declare function brandThemeExportName(themeId: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Build a complete Theme from a validated blueprint brand block.
|
|
62
|
+
* Throws `BrandThemeError` when the result fails AA or a catalog id is unknown,
|
|
63
|
+
* and for a library brand, which ships by id (see `brandLibraryThemeId`).
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveBlueprintBrand(brand: BlueprintBrand, options?: ResolveBrandOptions): Theme;
|
|
66
|
+
/**
|
|
67
|
+
* TypeScript module body for `src/generated/brand-theme.ts` (no header).
|
|
68
|
+
* The CLI wraps it with the generate hash header.
|
|
69
|
+
*/
|
|
70
|
+
export declare function emitBrandThemeModule(theme: Theme): string;
|
package/dist/brand.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an optional blueprint `brand` block into a full Theme for
|
|
3
|
+
* `protoyard generate`. Palette tokens come from `@protoyard/themes`
|
|
4
|
+
* (`createThemeFromSeed` or a built-in preset); fonts, radius and density
|
|
5
|
+
* come from the brand and/or a named style profile. Every resolved theme is
|
|
6
|
+
* checked against WCAG AA text and UI pairs before it is returned.
|
|
7
|
+
*/
|
|
8
|
+
import { defineTheme } from '@protoyard/core';
|
|
9
|
+
import { ALL_LIBRARY_THEME_IDS, BUILTIN_THEMES, contrastRatio, createThemeFromSeed, getFontPairing, isLibraryThemeId, TEXT_CONTRAST_PAIRS, UI_CONTRAST_PAIRS, } from '@protoyard/themes';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
/** Named style knobs applied after the palette (or preset) is chosen. */
|
|
12
|
+
export const STYLE_PROFILES = [
|
|
13
|
+
{
|
|
14
|
+
id: 'soft',
|
|
15
|
+
label: 'Soft',
|
|
16
|
+
radius: '0.75rem',
|
|
17
|
+
density: 1.05,
|
|
18
|
+
style: { elevation: 'soft', shape: { radiusScale: 1.2 } },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'crisp',
|
|
22
|
+
label: 'Crisp',
|
|
23
|
+
radius: '0.375rem',
|
|
24
|
+
density: 0.9,
|
|
25
|
+
style: { elevation: 'crisp', shape: { radiusScale: 0.6, borderWidth: '1px' } },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'flat',
|
|
29
|
+
label: 'Flat',
|
|
30
|
+
radius: '0.5rem',
|
|
31
|
+
density: 1,
|
|
32
|
+
style: { elevation: 'flat', shape: { borderWidth: '1px' } },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'rounded',
|
|
36
|
+
label: 'Rounded',
|
|
37
|
+
radius: '1rem',
|
|
38
|
+
density: 1.05,
|
|
39
|
+
style: { elevation: 'soft', shape: { radiusScale: 1.6 }, surface: 'solid' },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'editorial',
|
|
43
|
+
label: 'Editorial',
|
|
44
|
+
radius: '0.25rem',
|
|
45
|
+
density: 1.05,
|
|
46
|
+
font: 'editorial',
|
|
47
|
+
style: {
|
|
48
|
+
elevation: 'soft',
|
|
49
|
+
typography: {
|
|
50
|
+
scaleRatio: 1.25,
|
|
51
|
+
weights: { body: 400, medium: 500, heading: 600, bold: 700 },
|
|
52
|
+
tracking: { body: '0.01em', heading: '-0.02em' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'mono',
|
|
58
|
+
label: 'Mono',
|
|
59
|
+
radius: '0.375rem',
|
|
60
|
+
density: 0.9,
|
|
61
|
+
font: 'mono',
|
|
62
|
+
style: {
|
|
63
|
+
elevation: 'crisp',
|
|
64
|
+
surface: 'solid',
|
|
65
|
+
shape: { radiusScale: 0.6, borderWidth: '1px' },
|
|
66
|
+
typography: { tracking: { body: '0', heading: '-0.02em' } },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
export function getStyleProfile(id) {
|
|
71
|
+
return STYLE_PROFILES.find((profile) => profile.id === id);
|
|
72
|
+
}
|
|
73
|
+
export class BrandThemeError extends Error {
|
|
74
|
+
failures;
|
|
75
|
+
constructor(message, failures = []) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = 'BrandThemeError';
|
|
78
|
+
this.failures = failures;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Edit distance between two short ids (single-row Levenshtein). */
|
|
82
|
+
function editDistance(a, b) {
|
|
83
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
84
|
+
for (let i = 1; i <= a.length; i++) {
|
|
85
|
+
let diagonal = row[0];
|
|
86
|
+
row[0] = i;
|
|
87
|
+
for (let j = 1; j <= b.length; j++) {
|
|
88
|
+
const above = row[j];
|
|
89
|
+
row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
90
|
+
diagonal = above;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return row[b.length];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Up to `limit` ids from `known` nearest to `id`: ids that contain it (or that
|
|
97
|
+
* it contains) first, then by edit distance, ties in `known` order. Only ids
|
|
98
|
+
* within half the longer length are suggested, so unrelated names stay out.
|
|
99
|
+
*/
|
|
100
|
+
export function closestIds(id, known, limit = 3) {
|
|
101
|
+
return known
|
|
102
|
+
.map((candidate, index) => {
|
|
103
|
+
const contains = candidate.includes(id) || id.includes(candidate);
|
|
104
|
+
return { candidate, index, distance: contains ? 0 : editDistance(id, candidate) };
|
|
105
|
+
})
|
|
106
|
+
.filter(({ candidate, distance }) => distance <= Math.ceil(Math.max(id.length, candidate.length) / 2))
|
|
107
|
+
.sort((a, b) => a.distance - b.distance || a.index - b.index)
|
|
108
|
+
.slice(0, limit)
|
|
109
|
+
.map(({ candidate }) => candidate);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The library theme a brand names (`brand.libraryTheme`), or `undefined` when
|
|
113
|
+
* the brand builds its own theme. A library brand needs no generated module:
|
|
114
|
+
* the prototype's `defaultTheme` is the library id and the renderer loads it.
|
|
115
|
+
* Throws `BrandThemeError` (with the closest ids) when the library does not
|
|
116
|
+
* know the id.
|
|
117
|
+
*/
|
|
118
|
+
export function brandLibraryThemeId(brand) {
|
|
119
|
+
const id = brand.libraryTheme;
|
|
120
|
+
if (id === undefined || isLibraryThemeId(id))
|
|
121
|
+
return id;
|
|
122
|
+
const near = closestIds(id, ALL_LIBRARY_THEME_IDS);
|
|
123
|
+
throw new BrandThemeError(`Unknown library theme "${id}"${near.length > 0 ? ` (closest: ${near.join(', ')})` : ''}.`);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Stable theme id from brand or blueprint name (`Acme Labs` → `acme-labs`);
|
|
127
|
+
* a library brand's id is the library theme id.
|
|
128
|
+
*/
|
|
129
|
+
export function brandThemeId(brand, blueprintName) {
|
|
130
|
+
if (brand.libraryTheme)
|
|
131
|
+
return brand.libraryTheme;
|
|
132
|
+
if (brand.id)
|
|
133
|
+
return brand.id;
|
|
134
|
+
const source = brand.name ?? blueprintName ?? 'brand';
|
|
135
|
+
const slug = source
|
|
136
|
+
.trim()
|
|
137
|
+
.toLowerCase()
|
|
138
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
139
|
+
.replace(/^-+|-+$/g, '');
|
|
140
|
+
if (slug.length === 0)
|
|
141
|
+
return 'brand';
|
|
142
|
+
// Theme ids (and so export names) start with a letter: `3D Print` → `brand-3d-print`.
|
|
143
|
+
const id = /^[a-z]/.test(slug) ? slug : `brand-${slug}`;
|
|
144
|
+
return id.slice(0, 40).replace(/-+$/, '');
|
|
145
|
+
}
|
|
146
|
+
const EXPORT_NAME = /^[A-Za-z_$][\w$]*$/;
|
|
147
|
+
/**
|
|
148
|
+
* `acme-labs` → `acmeLabsTheme` for the generated module export. Throws
|
|
149
|
+
* `BrandThemeError` for an id that cannot become a valid identifier.
|
|
150
|
+
*/
|
|
151
|
+
export function brandThemeExportName(themeId) {
|
|
152
|
+
const camel = themeId.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
153
|
+
const name = `${camel}Theme`;
|
|
154
|
+
if (!EXPORT_NAME.test(name)) {
|
|
155
|
+
throw new BrandThemeError(`Brand theme id "${themeId}" does not map to a valid export name; use lowercase kebab-case.`);
|
|
156
|
+
}
|
|
157
|
+
return name;
|
|
158
|
+
}
|
|
159
|
+
function themeDisplayName(brand, blueprintName) {
|
|
160
|
+
const raw = (brand.name ?? blueprintName ?? 'Brand').trim();
|
|
161
|
+
return raw.slice(0, 40) || 'Brand';
|
|
162
|
+
}
|
|
163
|
+
function assertAa(theme) {
|
|
164
|
+
const failures = [];
|
|
165
|
+
for (const mode of ['light', 'dark']) {
|
|
166
|
+
const palette = theme[mode];
|
|
167
|
+
for (const [foreground, background] of TEXT_CONTRAST_PAIRS) {
|
|
168
|
+
try {
|
|
169
|
+
const ratio = contrastRatio(palette[foreground], palette[background]);
|
|
170
|
+
if (ratio < 4.5) {
|
|
171
|
+
failures.push({ mode, foreground, background, minimum: 4.5, ratio });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
failures.push({ mode, foreground, background, minimum: 4.5, ratio: 0 });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const [foreground, background] of UI_CONTRAST_PAIRS) {
|
|
179
|
+
try {
|
|
180
|
+
const ratio = contrastRatio(palette[foreground], palette[background]);
|
|
181
|
+
if (ratio < 3) {
|
|
182
|
+
failures.push({ mode, foreground, background, minimum: 3, ratio });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
failures.push({ mode, foreground, background, minimum: 3, ratio: 0 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (failures.length > 0) {
|
|
191
|
+
throw new BrandThemeError(`Brand theme "${theme.id}" fails WCAG AA (${failures.length} pair(s)).`, failures);
|
|
192
|
+
}
|
|
193
|
+
return theme;
|
|
194
|
+
}
|
|
195
|
+
function applyFinishes(theme, brand, profile) {
|
|
196
|
+
const fontId = brand.font ?? profile?.font;
|
|
197
|
+
const pairing = fontId ? getFontPairing(fontId) : undefined;
|
|
198
|
+
if (fontId && !pairing) {
|
|
199
|
+
throw new BrandThemeError(`Unknown font pairing "${fontId}".`);
|
|
200
|
+
}
|
|
201
|
+
const radius = brand.radius ?? profile?.radius;
|
|
202
|
+
const density = brand.density ?? profile?.density;
|
|
203
|
+
const style = profile?.style;
|
|
204
|
+
return defineTheme({
|
|
205
|
+
id: theme.id,
|
|
206
|
+
name: theme.name,
|
|
207
|
+
description: theme.description,
|
|
208
|
+
light: theme.light,
|
|
209
|
+
dark: theme.dark,
|
|
210
|
+
fontFamily: pairing?.fontFamily ?? theme.fontFamily,
|
|
211
|
+
...(pairing?.fontFamilyHeading !== undefined
|
|
212
|
+
? { fontFamilyHeading: pairing.fontFamilyHeading }
|
|
213
|
+
: theme.fontFamilyHeading
|
|
214
|
+
? { fontFamilyHeading: theme.fontFamilyHeading }
|
|
215
|
+
: {}),
|
|
216
|
+
fontFamilyMono: pairing?.fontFamilyMono ?? theme.fontFamilyMono,
|
|
217
|
+
radius: radius ?? theme.radius,
|
|
218
|
+
density: density ?? theme.density,
|
|
219
|
+
...(style || theme.style ? { style: { ...(theme.style ?? {}), ...(style ?? {}) } } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Build a complete Theme from a validated blueprint brand block.
|
|
224
|
+
* Throws `BrandThemeError` when the result fails AA or a catalog id is unknown,
|
|
225
|
+
* and for a library brand, which ships by id (see `brandLibraryThemeId`).
|
|
226
|
+
*/
|
|
227
|
+
export function resolveBlueprintBrand(brand, options = {}) {
|
|
228
|
+
try {
|
|
229
|
+
return buildBrandTheme(brand, options);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
// A theme the core schema rejects is a blueprint problem, not an internal error.
|
|
233
|
+
if (error instanceof z.ZodError || error?.name === 'ZodError') {
|
|
234
|
+
const issue = error.issues[0];
|
|
235
|
+
throw new BrandThemeError(`Brand theme is invalid: ${issue ? `${issue.path.map(String).join('.') || '(root)'} — ${issue.message}` : 'unknown error'}`);
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function buildBrandTheme(brand, options) {
|
|
241
|
+
if (brand.libraryTheme) {
|
|
242
|
+
throw new BrandThemeError(`Brand uses library theme "${brand.libraryTheme}"; it ships by id, so there is no theme to build.`);
|
|
243
|
+
}
|
|
244
|
+
const id = brandThemeId(brand, options.blueprintName);
|
|
245
|
+
const name = themeDisplayName(brand, options.blueprintName);
|
|
246
|
+
const profile = brand.styleProfile ? getStyleProfile(brand.styleProfile) : undefined;
|
|
247
|
+
if (brand.styleProfile && !profile) {
|
|
248
|
+
throw new BrandThemeError(`Unknown style profile "${brand.styleProfile}".`);
|
|
249
|
+
}
|
|
250
|
+
let base;
|
|
251
|
+
if (brand.seed) {
|
|
252
|
+
base = createThemeFromSeed({
|
|
253
|
+
id,
|
|
254
|
+
name,
|
|
255
|
+
seed: brand.seed,
|
|
256
|
+
...(brand.secondary ? { secondary: brand.secondary } : {}),
|
|
257
|
+
...(profile?.style ? { style: profile.style } : {}),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const presetId = brand.preset;
|
|
262
|
+
const preset = BUILTIN_THEMES.find((theme) => theme.id === presetId);
|
|
263
|
+
if (!preset) {
|
|
264
|
+
throw new BrandThemeError(`Unknown theme preset "${presetId}".`);
|
|
265
|
+
}
|
|
266
|
+
base = defineTheme({
|
|
267
|
+
...preset,
|
|
268
|
+
id,
|
|
269
|
+
name,
|
|
270
|
+
description: `Brand theme from the ${preset.name} preset.`,
|
|
271
|
+
light: { ...preset.light },
|
|
272
|
+
dark: { ...preset.dark },
|
|
273
|
+
...(preset.style ? { style: { ...preset.style } } : {}),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// When seed already applied profile.style via createThemeFromSeed, still
|
|
277
|
+
// apply font/radius/density (and style again is idempotent via merge).
|
|
278
|
+
const finished = applyFinishes(base, brand, profile);
|
|
279
|
+
return assertAa(finished);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* TypeScript module body for `src/generated/brand-theme.ts` (no header).
|
|
283
|
+
* The CLI wraps it with the generate hash header.
|
|
284
|
+
*/
|
|
285
|
+
export function emitBrandThemeModule(theme) {
|
|
286
|
+
const exportName = brandThemeExportName(theme.id);
|
|
287
|
+
return `import { defineTheme, type Theme } from '@protoyard/core';
|
|
288
|
+
|
|
289
|
+
/** On-brand theme derived from blueprint.json \`brand\` (Task 128). */
|
|
290
|
+
export const ${exportName}: Theme = defineTheme(${JSON.stringify(theme, null, 2)});
|
|
291
|
+
|
|
292
|
+
export default ${exportName};
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { BlueprintArchetypeInput } from '../archetypes/schema.ts';
|
|
2
|
+
import type { BlueprintInput } from '../schema.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Product archetypes the brief synthesizer starts from. Each one carries the
|
|
5
|
+
* keywords that select it and a small entity graph whose fields drive the
|
|
6
|
+
* screens (enums → boards, dates → calendars and timelines, geo → maps,
|
|
7
|
+
* money → dashboards and analytics). The first entity is the focus: brief-wide
|
|
8
|
+
* cues (a location, a price, a deadline) land on it. All names are fictional.
|
|
9
|
+
*/
|
|
10
|
+
export type EntityInput = BlueprintInput['entities'][number];
|
|
11
|
+
export type FieldInput = EntityInput['fields'][number];
|
|
12
|
+
export interface Archetype {
|
|
13
|
+
id: string;
|
|
14
|
+
/** Product noun used in generated names ("Help Desk"). */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Single words or phrases, matched as whole words in the brief. */
|
|
17
|
+
keywords: readonly string[];
|
|
18
|
+
/** High-signal cues can select a kit archetype with one unambiguous phrase. */
|
|
19
|
+
strongCues?: readonly string[];
|
|
20
|
+
/** Portable runtime binding emitted when this brief seed is selected. */
|
|
21
|
+
kitArchetype?: BlueprintArchetypeInput;
|
|
22
|
+
entities: readonly EntityInput[];
|
|
23
|
+
}
|
|
24
|
+
/** Ordered: on equal keyword scores the earlier archetype wins. */
|
|
25
|
+
export declare const ARCHETYPES: readonly Archetype[];
|
|
26
|
+
/** Used when no keyword matches: a plain tracker the extracted nouns extend. */
|
|
27
|
+
export declare const GENERIC_ARCHETYPE: Archetype;
|
|
28
|
+
export declare const ARCHETYPE_IDS: readonly [...string[], string];
|