@notty/theme-api 0.14.0 → 1.0.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/README.md +27 -4
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +1 -7
- package/dist/index.d.ts +1 -7
- package/dist/index.js +2 -2
- package/dist/testing/index.cjs +321 -0
- package/dist/testing/index.d.cts +150 -0
- package/dist/testing/index.d.ts +150 -0
- package/dist/testing/index.js +291 -0
- package/package.json +18 -12
package/README.md
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
1
|
# @notty/theme-api
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Public SDK for building Notty admin themes.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Current Status
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
- Theme authoring workflow is available through `defineTheme()`
|
|
8
|
+
- Includes theme manifest types, branding tokens, layout overrides, and white-label metadata
|
|
9
|
+
- Active theme branding now flows into the admin `ui-config` endpoint and white-label branding pipeline
|
|
10
|
+
- The API is usable now, but should still be treated as early-stage and evolving, especially for full token/layout runtime application
|
|
11
|
+
- Distribution remains restricted to the `@notty` npm scope
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineTheme } from '@notty/theme-api';
|
|
17
|
+
|
|
18
|
+
export default defineTheme({
|
|
19
|
+
manifest: {
|
|
20
|
+
name: '@acme/theme-brand',
|
|
21
|
+
displayName: 'Brand Theme',
|
|
22
|
+
version: '0.1.0',
|
|
23
|
+
kind: 'theme',
|
|
24
|
+
engines: { notty: '>=0.16.0' },
|
|
25
|
+
branding: {
|
|
26
|
+
title: 'Acme CMS',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
9
30
|
```
|
|
10
31
|
|
|
32
|
+
For scaffolding, use `create-notty --theme`.
|
|
33
|
+
|
|
11
34
|
## License
|
|
12
35
|
|
|
13
36
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -20,11 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
|
|
23
|
+
defineTheme: () => import_types.defineTheme
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(index_exports);
|
|
26
|
-
var
|
|
26
|
+
var import_types = require("@notty/types");
|
|
27
27
|
// Annotate the CommonJS export names for ESM import in node:
|
|
28
28
|
0 && (module.exports = {
|
|
29
|
-
|
|
29
|
+
defineTheme
|
|
30
30
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* @notty/theme-api
|
|
3
|
-
* Theme SDK for ACms
|
|
4
|
-
*/
|
|
5
|
-
declare const THEME_API_VERSION = "0.1.0";
|
|
6
|
-
|
|
7
|
-
export { THEME_API_VERSION };
|
|
1
|
+
export { ManifestValidationResult, PluginAuthor, PluginConfigSchema, PluginContext, PluginEngines, PluginLifecycleHooks, PluginManifest, PluginSettingField, ThemeBranding, ThemeColorToken, ThemeConfig, ThemeDefinition, ThemeManifest, ThemeRadiusToken, ThemeShadowToken, ThemeSpacingToken, ThemeTokens, ThemeTypographyToken, defineTheme } from '@notty/types';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* @notty/theme-api
|
|
3
|
-
* Theme SDK for ACms
|
|
4
|
-
*/
|
|
5
|
-
declare const THEME_API_VERSION = "0.1.0";
|
|
6
|
-
|
|
7
|
-
export { THEME_API_VERSION };
|
|
1
|
+
export { ManifestValidationResult, PluginAuthor, PluginConfigSchema, PluginContext, PluginEngines, PluginLifecycleHooks, PluginManifest, PluginSettingField, ThemeBranding, ThemeColorToken, ThemeConfig, ThemeDefinition, ThemeManifest, ThemeRadiusToken, ThemeShadowToken, ThemeSpacingToken, ThemeTokens, ThemeTypographyToken, defineTheme } from '@notty/types';
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/testing/index.ts
|
|
21
|
+
var testing_exports = {};
|
|
22
|
+
__export(testing_exports, {
|
|
23
|
+
assertValidBranding: () => assertValidBranding,
|
|
24
|
+
assertValidTokens: () => assertValidTokens,
|
|
25
|
+
createTestThemeContext: () => createTestThemeContext,
|
|
26
|
+
createTestThemeHarness: () => createTestThemeHarness,
|
|
27
|
+
makeTestThemeDefinition: () => makeTestThemeDefinition,
|
|
28
|
+
makeTestThemeManifest: () => makeTestThemeManifest
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(testing_exports);
|
|
31
|
+
function createMockLogger() {
|
|
32
|
+
const entries = [];
|
|
33
|
+
function log(level) {
|
|
34
|
+
return (message, ...args) => {
|
|
35
|
+
entries.push({ level, message, args });
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
info: log("info"),
|
|
40
|
+
warn: log("warn"),
|
|
41
|
+
error: log("error"),
|
|
42
|
+
debug: log("debug"),
|
|
43
|
+
get entries() {
|
|
44
|
+
return entries;
|
|
45
|
+
},
|
|
46
|
+
entriesOf: (level) => entries.filter((e) => e.level === level),
|
|
47
|
+
clear: () => {
|
|
48
|
+
entries.length = 0;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function noop() {
|
|
53
|
+
}
|
|
54
|
+
function createNoopRouteRegistrar() {
|
|
55
|
+
return { get: noop, post: noop, put: noop, delete: noop, patch: noop, add: noop };
|
|
56
|
+
}
|
|
57
|
+
function createNoopServiceRegistrar() {
|
|
58
|
+
return { register: noop, get: () => void 0, has: () => false };
|
|
59
|
+
}
|
|
60
|
+
function createNoopHookRegistrar() {
|
|
61
|
+
return {
|
|
62
|
+
on: noop,
|
|
63
|
+
once: noop,
|
|
64
|
+
emit: (_name, payload) => Promise.resolve(payload)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function createNoopMiddlewareRegistrar() {
|
|
68
|
+
return { addAdmin: noop, addContent: noop, addSystem: noop };
|
|
69
|
+
}
|
|
70
|
+
function createNoopPolicyRegistrar() {
|
|
71
|
+
return { register: noop };
|
|
72
|
+
}
|
|
73
|
+
function createNoopDataLayerRegistrar() {
|
|
74
|
+
return {
|
|
75
|
+
contributeContentType: noop,
|
|
76
|
+
contributeComponent: noop,
|
|
77
|
+
addMigration: noop,
|
|
78
|
+
addSeed: noop
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createCapturingAdminRegistrar() {
|
|
82
|
+
const captured = {
|
|
83
|
+
navigationItems: [],
|
|
84
|
+
pages: [],
|
|
85
|
+
settingsSections: [],
|
|
86
|
+
dashboardWidgets: [],
|
|
87
|
+
injectionZones: [],
|
|
88
|
+
fieldExtensions: []
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
captured,
|
|
92
|
+
addNavigationItem: (item) => {
|
|
93
|
+
captured.navigationItems.push(item);
|
|
94
|
+
},
|
|
95
|
+
addPage: (page) => {
|
|
96
|
+
captured.pages.push(page);
|
|
97
|
+
},
|
|
98
|
+
addSettingsSection: (section) => {
|
|
99
|
+
captured.settingsSections.push(section);
|
|
100
|
+
},
|
|
101
|
+
addDashboardWidget: (widget) => {
|
|
102
|
+
captured.dashboardWidgets.push(widget);
|
|
103
|
+
},
|
|
104
|
+
addInjectionZoneEntry: (entry) => {
|
|
105
|
+
captured.injectionZones.push(entry);
|
|
106
|
+
},
|
|
107
|
+
addFieldExtension: (ext) => {
|
|
108
|
+
captured.fieldExtensions.push(ext);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
var DEFAULT_THEME_MANIFEST = {
|
|
113
|
+
name: "@test/theme",
|
|
114
|
+
displayName: "Test Theme",
|
|
115
|
+
version: "1.0.0",
|
|
116
|
+
kind: "theme",
|
|
117
|
+
engines: { notty: ">=0.14.0" },
|
|
118
|
+
tokens: {
|
|
119
|
+
colors: {
|
|
120
|
+
brand: { primary: { value: "#3b82f6" } },
|
|
121
|
+
surface: {
|
|
122
|
+
background: { value: "#ffffff" },
|
|
123
|
+
foreground: { value: "#1a1a2e" }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
function makeTestThemeManifest(overrides) {
|
|
129
|
+
return { ...DEFAULT_THEME_MANIFEST, ...overrides };
|
|
130
|
+
}
|
|
131
|
+
function makeTestThemeDefinition(overrides) {
|
|
132
|
+
const { manifest: manifestOverrides, ...hooks } = overrides ?? {};
|
|
133
|
+
return {
|
|
134
|
+
manifest: makeTestThemeManifest(manifestOverrides),
|
|
135
|
+
...hooks
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function createTestThemeContext(options) {
|
|
139
|
+
const manifest = makeTestThemeManifest(options?.manifest);
|
|
140
|
+
const config = options?.config ?? {};
|
|
141
|
+
const admin = createCapturingAdminRegistrar();
|
|
142
|
+
return {
|
|
143
|
+
manifest,
|
|
144
|
+
config,
|
|
145
|
+
log: createMockLogger(),
|
|
146
|
+
routes: createNoopRouteRegistrar(),
|
|
147
|
+
services: createNoopServiceRegistrar(),
|
|
148
|
+
hooks: createNoopHookRegistrar(),
|
|
149
|
+
middleware: createNoopMiddlewareRegistrar(),
|
|
150
|
+
policies: createNoopPolicyRegistrar(),
|
|
151
|
+
dataLayer: createNoopDataLayerRegistrar(),
|
|
152
|
+
admin
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function createTestThemeHarness(definition, options) {
|
|
156
|
+
const mergedManifest = {
|
|
157
|
+
...DEFAULT_THEME_MANIFEST,
|
|
158
|
+
...definition.manifest,
|
|
159
|
+
...options?.manifest
|
|
160
|
+
};
|
|
161
|
+
const mergedDefinition = {
|
|
162
|
+
...definition,
|
|
163
|
+
manifest: mergedManifest
|
|
164
|
+
};
|
|
165
|
+
const s = {
|
|
166
|
+
ctx: createTestThemeContext({ manifest: mergedManifest, config: options?.config }),
|
|
167
|
+
phase: "discovered",
|
|
168
|
+
errors: []
|
|
169
|
+
};
|
|
170
|
+
function toError(err) {
|
|
171
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
172
|
+
}
|
|
173
|
+
function hasFailed() {
|
|
174
|
+
return s.phase === "failed";
|
|
175
|
+
}
|
|
176
|
+
function runRegister() {
|
|
177
|
+
if (s.phase !== "discovered") return;
|
|
178
|
+
try {
|
|
179
|
+
if (mergedDefinition.register) mergedDefinition.register(s.ctx);
|
|
180
|
+
s.phase = "registered";
|
|
181
|
+
} catch (err) {
|
|
182
|
+
s.phase = "failed";
|
|
183
|
+
s.errors.push(toError(err));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function runInit() {
|
|
187
|
+
if (s.phase === "discovered") runRegister();
|
|
188
|
+
if (s.phase !== "registered") return;
|
|
189
|
+
try {
|
|
190
|
+
if (mergedDefinition.init) await mergedDefinition.init(s.ctx);
|
|
191
|
+
s.phase = "initialized";
|
|
192
|
+
} catch (err) {
|
|
193
|
+
s.phase = "failed";
|
|
194
|
+
s.errors.push(toError(err));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function runBoot() {
|
|
198
|
+
if (s.phase === "discovered") runRegister();
|
|
199
|
+
if (s.phase === "registered") await runInit();
|
|
200
|
+
if (s.phase !== "initialized") return;
|
|
201
|
+
try {
|
|
202
|
+
if (mergedDefinition.boot) await mergedDefinition.boot(s.ctx);
|
|
203
|
+
s.phase = "active";
|
|
204
|
+
} catch (err) {
|
|
205
|
+
s.phase = "failed";
|
|
206
|
+
s.errors.push(toError(err));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function runAll() {
|
|
210
|
+
runRegister();
|
|
211
|
+
if (hasFailed()) return;
|
|
212
|
+
await runInit();
|
|
213
|
+
if (hasFailed()) return;
|
|
214
|
+
await runBoot();
|
|
215
|
+
}
|
|
216
|
+
async function runDestroy() {
|
|
217
|
+
const startPhase = s.phase;
|
|
218
|
+
try {
|
|
219
|
+
if (mergedDefinition.destroy) {
|
|
220
|
+
await mergedDefinition.destroy(s.ctx);
|
|
221
|
+
}
|
|
222
|
+
if (startPhase !== "failed") {
|
|
223
|
+
s.phase = "disabled";
|
|
224
|
+
}
|
|
225
|
+
} catch (err) {
|
|
226
|
+
s.phase = "failed";
|
|
227
|
+
s.errors.push(toError(err));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function reset() {
|
|
231
|
+
s.ctx = createTestThemeContext({ manifest: mergedManifest, config: options?.config });
|
|
232
|
+
s.phase = "discovered";
|
|
233
|
+
s.errors = [];
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
get ctx() {
|
|
237
|
+
return s.ctx;
|
|
238
|
+
},
|
|
239
|
+
get definition() {
|
|
240
|
+
return mergedDefinition;
|
|
241
|
+
},
|
|
242
|
+
get phase() {
|
|
243
|
+
return s.phase;
|
|
244
|
+
},
|
|
245
|
+
get errors() {
|
|
246
|
+
return s.errors;
|
|
247
|
+
},
|
|
248
|
+
runRegister,
|
|
249
|
+
runInit,
|
|
250
|
+
runBoot,
|
|
251
|
+
runDestroy,
|
|
252
|
+
runAll,
|
|
253
|
+
reset
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function assertValidTokens(tokens) {
|
|
257
|
+
const issues = [];
|
|
258
|
+
if (!tokens) {
|
|
259
|
+
return { valid: true, issues: [] };
|
|
260
|
+
}
|
|
261
|
+
if (tokens.colors) {
|
|
262
|
+
for (const [group, colorGroup] of Object.entries(tokens.colors)) {
|
|
263
|
+
if (!colorGroup || typeof colorGroup !== "object") continue;
|
|
264
|
+
for (const [name, token] of Object.entries(colorGroup)) {
|
|
265
|
+
if (!token) continue;
|
|
266
|
+
if (!token.value || typeof token.value !== "string" || token.value.trim() === "") {
|
|
267
|
+
issues.push(`colors.${group}.${name}: missing or empty value`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (tokens.typography) {
|
|
273
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
274
|
+
if (!token) continue;
|
|
275
|
+
const hasAny = token.fontFamily || token.fontSize || token.fontWeight || token.lineHeight || token.letterSpacing;
|
|
276
|
+
if (!hasAny) {
|
|
277
|
+
issues.push(`typography.${name}: no properties set`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const valueSections = [
|
|
282
|
+
["spacing", tokens.spacing],
|
|
283
|
+
["shadows", tokens.shadows],
|
|
284
|
+
["radii", tokens.radii]
|
|
285
|
+
];
|
|
286
|
+
for (const [section, sectionTokens] of valueSections) {
|
|
287
|
+
if (!sectionTokens) continue;
|
|
288
|
+
for (const [name, token] of Object.entries(sectionTokens)) {
|
|
289
|
+
if (!token) continue;
|
|
290
|
+
if (!token.value || typeof token.value !== "string" || token.value.trim() === "") {
|
|
291
|
+
issues.push(`${section}.${name}: missing or empty value`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { valid: issues.length === 0, issues };
|
|
296
|
+
}
|
|
297
|
+
function assertValidBranding(branding) {
|
|
298
|
+
const issues = [];
|
|
299
|
+
if (!branding) {
|
|
300
|
+
return { valid: true, issues: [] };
|
|
301
|
+
}
|
|
302
|
+
if (branding.logo && typeof branding.logo !== "string") {
|
|
303
|
+
issues.push("branding.logo: must be a string URL/path");
|
|
304
|
+
}
|
|
305
|
+
if (branding.favicon && typeof branding.favicon !== "string") {
|
|
306
|
+
issues.push("branding.favicon: must be a string URL/path");
|
|
307
|
+
}
|
|
308
|
+
if (branding.title && typeof branding.title !== "string") {
|
|
309
|
+
issues.push("branding.title: must be a string");
|
|
310
|
+
}
|
|
311
|
+
return { valid: issues.length === 0, issues };
|
|
312
|
+
}
|
|
313
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
314
|
+
0 && (module.exports = {
|
|
315
|
+
assertValidBranding,
|
|
316
|
+
assertValidTokens,
|
|
317
|
+
createTestThemeContext,
|
|
318
|
+
createTestThemeHarness,
|
|
319
|
+
makeTestThemeDefinition,
|
|
320
|
+
makeTestThemeManifest
|
|
321
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { AdminNavigationItem, AdminPage, AdminSettingsSection, AdminDashboardWidget, AdminInjectionZoneEntry, AdminFieldExtension, PluginContext, ThemeManifest, ThemeDefinition, PluginLifecyclePhase, ThemeTokens, ThemeBranding } from '@notty/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @notty/theme-api/testing
|
|
5
|
+
*
|
|
6
|
+
* Test utilities for Notty theme authors.
|
|
7
|
+
*
|
|
8
|
+
* Re-exports core plugin testing utilities from @notty/plugin-api/testing
|
|
9
|
+
* plus theme-specific helpers for design tokens and branding.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import {
|
|
14
|
+
* createTestThemeContext,
|
|
15
|
+
* createTestThemeHarness,
|
|
16
|
+
* makeTestThemeManifest,
|
|
17
|
+
* assertValidTokens,
|
|
18
|
+
* } from '@notty/theme-api/testing';
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface LogEntry {
|
|
23
|
+
level: 'info' | 'warn' | 'error' | 'debug';
|
|
24
|
+
message: string;
|
|
25
|
+
args: unknown[];
|
|
26
|
+
}
|
|
27
|
+
interface MockLogger {
|
|
28
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
29
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
30
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
31
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
32
|
+
readonly entries: ReadonlyArray<LogEntry>;
|
|
33
|
+
entriesOf(level: LogEntry['level']): LogEntry[];
|
|
34
|
+
clear(): void;
|
|
35
|
+
}
|
|
36
|
+
/** Captured admin extensions for theme testing */
|
|
37
|
+
interface CapturedAdminExtensions {
|
|
38
|
+
navigationItems: AdminNavigationItem[];
|
|
39
|
+
pages: AdminPage[];
|
|
40
|
+
settingsSections: AdminSettingsSection[];
|
|
41
|
+
dashboardWidgets: AdminDashboardWidget[];
|
|
42
|
+
injectionZones: AdminInjectionZoneEntry[];
|
|
43
|
+
fieldExtensions: AdminFieldExtension[];
|
|
44
|
+
}
|
|
45
|
+
interface TestThemeContext extends PluginContext {
|
|
46
|
+
log: MockLogger;
|
|
47
|
+
admin: PluginContext['admin'] & {
|
|
48
|
+
captured: CapturedAdminExtensions;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface CreateTestThemeContextOptions {
|
|
52
|
+
manifest?: Partial<ThemeManifest>;
|
|
53
|
+
config?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create a valid ThemeManifest with sensible defaults including design tokens.
|
|
57
|
+
*/
|
|
58
|
+
declare function makeTestThemeManifest(overrides?: Partial<ThemeManifest>): ThemeManifest;
|
|
59
|
+
/**
|
|
60
|
+
* Create a valid ThemeDefinition with sensible defaults.
|
|
61
|
+
*/
|
|
62
|
+
declare function makeTestThemeDefinition(overrides?: Partial<ThemeDefinition> & {
|
|
63
|
+
manifest?: Partial<ThemeManifest>;
|
|
64
|
+
}): ThemeDefinition;
|
|
65
|
+
/**
|
|
66
|
+
* Create a mock PluginContext tailored for theme testing.
|
|
67
|
+
*
|
|
68
|
+
* Backend registrars (routes, services, hooks, etc.) are no-op stubs.
|
|
69
|
+
* The admin registrar captures calls so you can verify theme admin extensions.
|
|
70
|
+
* The logger captures entries for inspection.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { createTestThemeContext } from '@notty/theme-api/testing';
|
|
75
|
+
*
|
|
76
|
+
* test('theme registers settings section', () => {
|
|
77
|
+
* const ctx = createTestThemeContext({ manifest: { name: '@my/dark-theme' } });
|
|
78
|
+
*
|
|
79
|
+
* myTheme.register(ctx);
|
|
80
|
+
*
|
|
81
|
+
* expect(ctx.admin.captured.settingsSections).toHaveLength(1);
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function createTestThemeContext(options?: CreateTestThemeContextOptions): TestThemeContext;
|
|
86
|
+
interface ThemeTestHarness {
|
|
87
|
+
readonly ctx: TestThemeContext;
|
|
88
|
+
readonly definition: ThemeDefinition;
|
|
89
|
+
readonly phase: PluginLifecyclePhase;
|
|
90
|
+
readonly errors: ReadonlyArray<Error>;
|
|
91
|
+
/** Run register phase */
|
|
92
|
+
runRegister(): void;
|
|
93
|
+
/** Run init phase (auto-registers if needed) */
|
|
94
|
+
runInit(): Promise<void>;
|
|
95
|
+
/** Run boot phase (auto-registers and inits if needed) */
|
|
96
|
+
runBoot(): Promise<void>;
|
|
97
|
+
/** Run destroy phase from any current phase */
|
|
98
|
+
runDestroy(): Promise<void>;
|
|
99
|
+
/** Run all phases */
|
|
100
|
+
runAll(): Promise<void>;
|
|
101
|
+
/** Reset to initial state */
|
|
102
|
+
reset(): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create a test harness for a theme definition.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* import { createTestThemeHarness } from '@notty/theme-api/testing';
|
|
110
|
+
* import myTheme from '../src/index';
|
|
111
|
+
*
|
|
112
|
+
* test('theme lifecycle completes', async () => {
|
|
113
|
+
* const harness = createTestThemeHarness(myTheme);
|
|
114
|
+
* await harness.runAll();
|
|
115
|
+
* expect(harness.phase).toBe('active');
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
declare function createTestThemeHarness(definition: ThemeDefinition, options?: CreateTestThemeContextOptions): ThemeTestHarness;
|
|
120
|
+
interface TokenValidationResult {
|
|
121
|
+
valid: boolean;
|
|
122
|
+
issues: string[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validate that theme tokens follow expected structure.
|
|
126
|
+
*
|
|
127
|
+
* Checks:
|
|
128
|
+
* - Color tokens have non-empty `value` strings
|
|
129
|
+
* - Typography tokens have at least one property set
|
|
130
|
+
* - Spacing/shadow/radius tokens have non-empty `value` strings
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const result = assertValidTokens(myTheme.manifest.tokens);
|
|
135
|
+
* expect(result.valid).toBe(true);
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
declare function assertValidTokens(tokens?: ThemeTokens): TokenValidationResult;
|
|
139
|
+
/**
|
|
140
|
+
* Validate that theme branding has required fields set.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const result = assertValidBranding(myTheme.manifest.branding);
|
|
145
|
+
* expect(result.valid).toBe(true);
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
declare function assertValidBranding(branding?: ThemeBranding): TokenValidationResult;
|
|
149
|
+
|
|
150
|
+
export { type CapturedAdminExtensions, type CreateTestThemeContextOptions, type LogEntry, type MockLogger, type TestThemeContext, type ThemeTestHarness, type TokenValidationResult, assertValidBranding, assertValidTokens, createTestThemeContext, createTestThemeHarness, makeTestThemeDefinition, makeTestThemeManifest };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { AdminNavigationItem, AdminPage, AdminSettingsSection, AdminDashboardWidget, AdminInjectionZoneEntry, AdminFieldExtension, PluginContext, ThemeManifest, ThemeDefinition, PluginLifecyclePhase, ThemeTokens, ThemeBranding } from '@notty/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @notty/theme-api/testing
|
|
5
|
+
*
|
|
6
|
+
* Test utilities for Notty theme authors.
|
|
7
|
+
*
|
|
8
|
+
* Re-exports core plugin testing utilities from @notty/plugin-api/testing
|
|
9
|
+
* plus theme-specific helpers for design tokens and branding.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import {
|
|
14
|
+
* createTestThemeContext,
|
|
15
|
+
* createTestThemeHarness,
|
|
16
|
+
* makeTestThemeManifest,
|
|
17
|
+
* assertValidTokens,
|
|
18
|
+
* } from '@notty/theme-api/testing';
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface LogEntry {
|
|
23
|
+
level: 'info' | 'warn' | 'error' | 'debug';
|
|
24
|
+
message: string;
|
|
25
|
+
args: unknown[];
|
|
26
|
+
}
|
|
27
|
+
interface MockLogger {
|
|
28
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
29
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
30
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
31
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
32
|
+
readonly entries: ReadonlyArray<LogEntry>;
|
|
33
|
+
entriesOf(level: LogEntry['level']): LogEntry[];
|
|
34
|
+
clear(): void;
|
|
35
|
+
}
|
|
36
|
+
/** Captured admin extensions for theme testing */
|
|
37
|
+
interface CapturedAdminExtensions {
|
|
38
|
+
navigationItems: AdminNavigationItem[];
|
|
39
|
+
pages: AdminPage[];
|
|
40
|
+
settingsSections: AdminSettingsSection[];
|
|
41
|
+
dashboardWidgets: AdminDashboardWidget[];
|
|
42
|
+
injectionZones: AdminInjectionZoneEntry[];
|
|
43
|
+
fieldExtensions: AdminFieldExtension[];
|
|
44
|
+
}
|
|
45
|
+
interface TestThemeContext extends PluginContext {
|
|
46
|
+
log: MockLogger;
|
|
47
|
+
admin: PluginContext['admin'] & {
|
|
48
|
+
captured: CapturedAdminExtensions;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
interface CreateTestThemeContextOptions {
|
|
52
|
+
manifest?: Partial<ThemeManifest>;
|
|
53
|
+
config?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create a valid ThemeManifest with sensible defaults including design tokens.
|
|
57
|
+
*/
|
|
58
|
+
declare function makeTestThemeManifest(overrides?: Partial<ThemeManifest>): ThemeManifest;
|
|
59
|
+
/**
|
|
60
|
+
* Create a valid ThemeDefinition with sensible defaults.
|
|
61
|
+
*/
|
|
62
|
+
declare function makeTestThemeDefinition(overrides?: Partial<ThemeDefinition> & {
|
|
63
|
+
manifest?: Partial<ThemeManifest>;
|
|
64
|
+
}): ThemeDefinition;
|
|
65
|
+
/**
|
|
66
|
+
* Create a mock PluginContext tailored for theme testing.
|
|
67
|
+
*
|
|
68
|
+
* Backend registrars (routes, services, hooks, etc.) are no-op stubs.
|
|
69
|
+
* The admin registrar captures calls so you can verify theme admin extensions.
|
|
70
|
+
* The logger captures entries for inspection.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { createTestThemeContext } from '@notty/theme-api/testing';
|
|
75
|
+
*
|
|
76
|
+
* test('theme registers settings section', () => {
|
|
77
|
+
* const ctx = createTestThemeContext({ manifest: { name: '@my/dark-theme' } });
|
|
78
|
+
*
|
|
79
|
+
* myTheme.register(ctx);
|
|
80
|
+
*
|
|
81
|
+
* expect(ctx.admin.captured.settingsSections).toHaveLength(1);
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
declare function createTestThemeContext(options?: CreateTestThemeContextOptions): TestThemeContext;
|
|
86
|
+
interface ThemeTestHarness {
|
|
87
|
+
readonly ctx: TestThemeContext;
|
|
88
|
+
readonly definition: ThemeDefinition;
|
|
89
|
+
readonly phase: PluginLifecyclePhase;
|
|
90
|
+
readonly errors: ReadonlyArray<Error>;
|
|
91
|
+
/** Run register phase */
|
|
92
|
+
runRegister(): void;
|
|
93
|
+
/** Run init phase (auto-registers if needed) */
|
|
94
|
+
runInit(): Promise<void>;
|
|
95
|
+
/** Run boot phase (auto-registers and inits if needed) */
|
|
96
|
+
runBoot(): Promise<void>;
|
|
97
|
+
/** Run destroy phase from any current phase */
|
|
98
|
+
runDestroy(): Promise<void>;
|
|
99
|
+
/** Run all phases */
|
|
100
|
+
runAll(): Promise<void>;
|
|
101
|
+
/** Reset to initial state */
|
|
102
|
+
reset(): void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Create a test harness for a theme definition.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* import { createTestThemeHarness } from '@notty/theme-api/testing';
|
|
110
|
+
* import myTheme from '../src/index';
|
|
111
|
+
*
|
|
112
|
+
* test('theme lifecycle completes', async () => {
|
|
113
|
+
* const harness = createTestThemeHarness(myTheme);
|
|
114
|
+
* await harness.runAll();
|
|
115
|
+
* expect(harness.phase).toBe('active');
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
declare function createTestThemeHarness(definition: ThemeDefinition, options?: CreateTestThemeContextOptions): ThemeTestHarness;
|
|
120
|
+
interface TokenValidationResult {
|
|
121
|
+
valid: boolean;
|
|
122
|
+
issues: string[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validate that theme tokens follow expected structure.
|
|
126
|
+
*
|
|
127
|
+
* Checks:
|
|
128
|
+
* - Color tokens have non-empty `value` strings
|
|
129
|
+
* - Typography tokens have at least one property set
|
|
130
|
+
* - Spacing/shadow/radius tokens have non-empty `value` strings
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const result = assertValidTokens(myTheme.manifest.tokens);
|
|
135
|
+
* expect(result.valid).toBe(true);
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
declare function assertValidTokens(tokens?: ThemeTokens): TokenValidationResult;
|
|
139
|
+
/**
|
|
140
|
+
* Validate that theme branding has required fields set.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const result = assertValidBranding(myTheme.manifest.branding);
|
|
145
|
+
* expect(result.valid).toBe(true);
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
declare function assertValidBranding(branding?: ThemeBranding): TokenValidationResult;
|
|
149
|
+
|
|
150
|
+
export { type CapturedAdminExtensions, type CreateTestThemeContextOptions, type LogEntry, type MockLogger, type TestThemeContext, type ThemeTestHarness, type TokenValidationResult, assertValidBranding, assertValidTokens, createTestThemeContext, createTestThemeHarness, makeTestThemeDefinition, makeTestThemeManifest };
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// src/testing/index.ts
|
|
2
|
+
function createMockLogger() {
|
|
3
|
+
const entries = [];
|
|
4
|
+
function log(level) {
|
|
5
|
+
return (message, ...args) => {
|
|
6
|
+
entries.push({ level, message, args });
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
info: log("info"),
|
|
11
|
+
warn: log("warn"),
|
|
12
|
+
error: log("error"),
|
|
13
|
+
debug: log("debug"),
|
|
14
|
+
get entries() {
|
|
15
|
+
return entries;
|
|
16
|
+
},
|
|
17
|
+
entriesOf: (level) => entries.filter((e) => e.level === level),
|
|
18
|
+
clear: () => {
|
|
19
|
+
entries.length = 0;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function noop() {
|
|
24
|
+
}
|
|
25
|
+
function createNoopRouteRegistrar() {
|
|
26
|
+
return { get: noop, post: noop, put: noop, delete: noop, patch: noop, add: noop };
|
|
27
|
+
}
|
|
28
|
+
function createNoopServiceRegistrar() {
|
|
29
|
+
return { register: noop, get: () => void 0, has: () => false };
|
|
30
|
+
}
|
|
31
|
+
function createNoopHookRegistrar() {
|
|
32
|
+
return {
|
|
33
|
+
on: noop,
|
|
34
|
+
once: noop,
|
|
35
|
+
emit: (_name, payload) => Promise.resolve(payload)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function createNoopMiddlewareRegistrar() {
|
|
39
|
+
return { addAdmin: noop, addContent: noop, addSystem: noop };
|
|
40
|
+
}
|
|
41
|
+
function createNoopPolicyRegistrar() {
|
|
42
|
+
return { register: noop };
|
|
43
|
+
}
|
|
44
|
+
function createNoopDataLayerRegistrar() {
|
|
45
|
+
return {
|
|
46
|
+
contributeContentType: noop,
|
|
47
|
+
contributeComponent: noop,
|
|
48
|
+
addMigration: noop,
|
|
49
|
+
addSeed: noop
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function createCapturingAdminRegistrar() {
|
|
53
|
+
const captured = {
|
|
54
|
+
navigationItems: [],
|
|
55
|
+
pages: [],
|
|
56
|
+
settingsSections: [],
|
|
57
|
+
dashboardWidgets: [],
|
|
58
|
+
injectionZones: [],
|
|
59
|
+
fieldExtensions: []
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
captured,
|
|
63
|
+
addNavigationItem: (item) => {
|
|
64
|
+
captured.navigationItems.push(item);
|
|
65
|
+
},
|
|
66
|
+
addPage: (page) => {
|
|
67
|
+
captured.pages.push(page);
|
|
68
|
+
},
|
|
69
|
+
addSettingsSection: (section) => {
|
|
70
|
+
captured.settingsSections.push(section);
|
|
71
|
+
},
|
|
72
|
+
addDashboardWidget: (widget) => {
|
|
73
|
+
captured.dashboardWidgets.push(widget);
|
|
74
|
+
},
|
|
75
|
+
addInjectionZoneEntry: (entry) => {
|
|
76
|
+
captured.injectionZones.push(entry);
|
|
77
|
+
},
|
|
78
|
+
addFieldExtension: (ext) => {
|
|
79
|
+
captured.fieldExtensions.push(ext);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var DEFAULT_THEME_MANIFEST = {
|
|
84
|
+
name: "@test/theme",
|
|
85
|
+
displayName: "Test Theme",
|
|
86
|
+
version: "1.0.0",
|
|
87
|
+
kind: "theme",
|
|
88
|
+
engines: { notty: ">=0.14.0" },
|
|
89
|
+
tokens: {
|
|
90
|
+
colors: {
|
|
91
|
+
brand: { primary: { value: "#3b82f6" } },
|
|
92
|
+
surface: {
|
|
93
|
+
background: { value: "#ffffff" },
|
|
94
|
+
foreground: { value: "#1a1a2e" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function makeTestThemeManifest(overrides) {
|
|
100
|
+
return { ...DEFAULT_THEME_MANIFEST, ...overrides };
|
|
101
|
+
}
|
|
102
|
+
function makeTestThemeDefinition(overrides) {
|
|
103
|
+
const { manifest: manifestOverrides, ...hooks } = overrides ?? {};
|
|
104
|
+
return {
|
|
105
|
+
manifest: makeTestThemeManifest(manifestOverrides),
|
|
106
|
+
...hooks
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function createTestThemeContext(options) {
|
|
110
|
+
const manifest = makeTestThemeManifest(options?.manifest);
|
|
111
|
+
const config = options?.config ?? {};
|
|
112
|
+
const admin = createCapturingAdminRegistrar();
|
|
113
|
+
return {
|
|
114
|
+
manifest,
|
|
115
|
+
config,
|
|
116
|
+
log: createMockLogger(),
|
|
117
|
+
routes: createNoopRouteRegistrar(),
|
|
118
|
+
services: createNoopServiceRegistrar(),
|
|
119
|
+
hooks: createNoopHookRegistrar(),
|
|
120
|
+
middleware: createNoopMiddlewareRegistrar(),
|
|
121
|
+
policies: createNoopPolicyRegistrar(),
|
|
122
|
+
dataLayer: createNoopDataLayerRegistrar(),
|
|
123
|
+
admin
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function createTestThemeHarness(definition, options) {
|
|
127
|
+
const mergedManifest = {
|
|
128
|
+
...DEFAULT_THEME_MANIFEST,
|
|
129
|
+
...definition.manifest,
|
|
130
|
+
...options?.manifest
|
|
131
|
+
};
|
|
132
|
+
const mergedDefinition = {
|
|
133
|
+
...definition,
|
|
134
|
+
manifest: mergedManifest
|
|
135
|
+
};
|
|
136
|
+
const s = {
|
|
137
|
+
ctx: createTestThemeContext({ manifest: mergedManifest, config: options?.config }),
|
|
138
|
+
phase: "discovered",
|
|
139
|
+
errors: []
|
|
140
|
+
};
|
|
141
|
+
function toError(err) {
|
|
142
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
143
|
+
}
|
|
144
|
+
function hasFailed() {
|
|
145
|
+
return s.phase === "failed";
|
|
146
|
+
}
|
|
147
|
+
function runRegister() {
|
|
148
|
+
if (s.phase !== "discovered") return;
|
|
149
|
+
try {
|
|
150
|
+
if (mergedDefinition.register) mergedDefinition.register(s.ctx);
|
|
151
|
+
s.phase = "registered";
|
|
152
|
+
} catch (err) {
|
|
153
|
+
s.phase = "failed";
|
|
154
|
+
s.errors.push(toError(err));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function runInit() {
|
|
158
|
+
if (s.phase === "discovered") runRegister();
|
|
159
|
+
if (s.phase !== "registered") return;
|
|
160
|
+
try {
|
|
161
|
+
if (mergedDefinition.init) await mergedDefinition.init(s.ctx);
|
|
162
|
+
s.phase = "initialized";
|
|
163
|
+
} catch (err) {
|
|
164
|
+
s.phase = "failed";
|
|
165
|
+
s.errors.push(toError(err));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function runBoot() {
|
|
169
|
+
if (s.phase === "discovered") runRegister();
|
|
170
|
+
if (s.phase === "registered") await runInit();
|
|
171
|
+
if (s.phase !== "initialized") return;
|
|
172
|
+
try {
|
|
173
|
+
if (mergedDefinition.boot) await mergedDefinition.boot(s.ctx);
|
|
174
|
+
s.phase = "active";
|
|
175
|
+
} catch (err) {
|
|
176
|
+
s.phase = "failed";
|
|
177
|
+
s.errors.push(toError(err));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function runAll() {
|
|
181
|
+
runRegister();
|
|
182
|
+
if (hasFailed()) return;
|
|
183
|
+
await runInit();
|
|
184
|
+
if (hasFailed()) return;
|
|
185
|
+
await runBoot();
|
|
186
|
+
}
|
|
187
|
+
async function runDestroy() {
|
|
188
|
+
const startPhase = s.phase;
|
|
189
|
+
try {
|
|
190
|
+
if (mergedDefinition.destroy) {
|
|
191
|
+
await mergedDefinition.destroy(s.ctx);
|
|
192
|
+
}
|
|
193
|
+
if (startPhase !== "failed") {
|
|
194
|
+
s.phase = "disabled";
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
s.phase = "failed";
|
|
198
|
+
s.errors.push(toError(err));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function reset() {
|
|
202
|
+
s.ctx = createTestThemeContext({ manifest: mergedManifest, config: options?.config });
|
|
203
|
+
s.phase = "discovered";
|
|
204
|
+
s.errors = [];
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
get ctx() {
|
|
208
|
+
return s.ctx;
|
|
209
|
+
},
|
|
210
|
+
get definition() {
|
|
211
|
+
return mergedDefinition;
|
|
212
|
+
},
|
|
213
|
+
get phase() {
|
|
214
|
+
return s.phase;
|
|
215
|
+
},
|
|
216
|
+
get errors() {
|
|
217
|
+
return s.errors;
|
|
218
|
+
},
|
|
219
|
+
runRegister,
|
|
220
|
+
runInit,
|
|
221
|
+
runBoot,
|
|
222
|
+
runDestroy,
|
|
223
|
+
runAll,
|
|
224
|
+
reset
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function assertValidTokens(tokens) {
|
|
228
|
+
const issues = [];
|
|
229
|
+
if (!tokens) {
|
|
230
|
+
return { valid: true, issues: [] };
|
|
231
|
+
}
|
|
232
|
+
if (tokens.colors) {
|
|
233
|
+
for (const [group, colorGroup] of Object.entries(tokens.colors)) {
|
|
234
|
+
if (!colorGroup || typeof colorGroup !== "object") continue;
|
|
235
|
+
for (const [name, token] of Object.entries(colorGroup)) {
|
|
236
|
+
if (!token) continue;
|
|
237
|
+
if (!token.value || typeof token.value !== "string" || token.value.trim() === "") {
|
|
238
|
+
issues.push(`colors.${group}.${name}: missing or empty value`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (tokens.typography) {
|
|
244
|
+
for (const [name, token] of Object.entries(tokens.typography)) {
|
|
245
|
+
if (!token) continue;
|
|
246
|
+
const hasAny = token.fontFamily || token.fontSize || token.fontWeight || token.lineHeight || token.letterSpacing;
|
|
247
|
+
if (!hasAny) {
|
|
248
|
+
issues.push(`typography.${name}: no properties set`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const valueSections = [
|
|
253
|
+
["spacing", tokens.spacing],
|
|
254
|
+
["shadows", tokens.shadows],
|
|
255
|
+
["radii", tokens.radii]
|
|
256
|
+
];
|
|
257
|
+
for (const [section, sectionTokens] of valueSections) {
|
|
258
|
+
if (!sectionTokens) continue;
|
|
259
|
+
for (const [name, token] of Object.entries(sectionTokens)) {
|
|
260
|
+
if (!token) continue;
|
|
261
|
+
if (!token.value || typeof token.value !== "string" || token.value.trim() === "") {
|
|
262
|
+
issues.push(`${section}.${name}: missing or empty value`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { valid: issues.length === 0, issues };
|
|
267
|
+
}
|
|
268
|
+
function assertValidBranding(branding) {
|
|
269
|
+
const issues = [];
|
|
270
|
+
if (!branding) {
|
|
271
|
+
return { valid: true, issues: [] };
|
|
272
|
+
}
|
|
273
|
+
if (branding.logo && typeof branding.logo !== "string") {
|
|
274
|
+
issues.push("branding.logo: must be a string URL/path");
|
|
275
|
+
}
|
|
276
|
+
if (branding.favicon && typeof branding.favicon !== "string") {
|
|
277
|
+
issues.push("branding.favicon: must be a string URL/path");
|
|
278
|
+
}
|
|
279
|
+
if (branding.title && typeof branding.title !== "string") {
|
|
280
|
+
issues.push("branding.title: must be a string");
|
|
281
|
+
}
|
|
282
|
+
return { valid: issues.length === 0, issues };
|
|
283
|
+
}
|
|
284
|
+
export {
|
|
285
|
+
assertValidBranding,
|
|
286
|
+
assertValidTokens,
|
|
287
|
+
createTestThemeContext,
|
|
288
|
+
createTestThemeHarness,
|
|
289
|
+
makeTestThemeDefinition,
|
|
290
|
+
makeTestThemeManifest
|
|
291
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notty/theme-api",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Theme API and testing toolkit for Notty CMS themes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -11,27 +11,33 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js",
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./testing": {
|
|
16
|
+
"types": "./dist/testing/index.d.ts",
|
|
17
|
+
"import": "./dist/testing/index.js",
|
|
18
|
+
"require": "./dist/testing/index.cjs"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"files": [
|
|
17
22
|
"dist"
|
|
18
23
|
],
|
|
19
24
|
"publishConfig": {
|
|
20
|
-
"access": "
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts src/testing/index.ts --format cjs,esm --dts",
|
|
29
|
+
"build:libs": "tsup src/index.ts src/testing/index.ts --format cjs,esm --dts",
|
|
30
|
+
"dev": "tsup src/index.ts src/testing/index.ts --format cjs,esm --dts --watch",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"lint": "eslint src",
|
|
33
|
+
"type-check": "tsc --noEmit",
|
|
34
|
+
"clean": "rm -rf dist"
|
|
21
35
|
},
|
|
22
36
|
"dependencies": {
|
|
23
|
-
"@notty/types": "0.
|
|
37
|
+
"@notty/types": "1.0.0"
|
|
24
38
|
},
|
|
25
39
|
"devDependencies": {
|
|
26
40
|
"tsup": "^8.3.0",
|
|
27
41
|
"typescript": "^5.6.3"
|
|
28
|
-
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
31
|
-
"build:libs": "tsup src/index.ts --format cjs,esm --dts",
|
|
32
|
-
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
33
|
-
"lint": "eslint src",
|
|
34
|
-
"type-check": "tsc --noEmit",
|
|
35
|
-
"clean": "rm -rf dist"
|
|
36
42
|
}
|
|
37
43
|
}
|