@zpress/core 0.3.0 → 0.5.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/dist/index.d.ts CHANGED
@@ -69,12 +69,22 @@ export declare interface CardConfig {
69
69
  };
70
70
  }
71
71
 
72
+ /**
73
+ * All valid color modes — used for validation.
74
+ */
75
+ export declare const COLOR_MODES: readonly ColorMode[];
76
+
77
+ /**
78
+ * How dark/light mode is controlled.
79
+ */
80
+ export declare type ColorMode = 'dark' | 'light' | 'toggle';
81
+
72
82
  /**
73
83
  * Error produced during config validation in `defineConfig`.
74
84
  */
75
85
  export declare interface ConfigError {
76
86
  readonly _tag: 'ConfigError';
77
- readonly type: 'empty_sections' | 'missing_field' | 'duplicate_prefix' | 'invalid_icon' | 'invalid_entry';
87
+ readonly type: 'empty_sections' | 'missing_field' | 'duplicate_prefix' | 'invalid_icon' | 'invalid_entry' | 'invalid_theme';
78
88
  readonly message: string;
79
89
  }
80
90
 
@@ -98,21 +108,15 @@ export declare type ConfigResult<T> = readonly [ConfigError, null] | readonly [n
98
108
  export declare function createPaths(dir: string): Paths;
99
109
 
100
110
  /**
101
- * Type-safe config helper with validation.
102
- *
103
- * Validates the config structure and exits with a clear error message
104
- * if any issues are found. This is the primary entry point for user-
105
- * provided config — validation happens at the boundary.
111
+ * Type-safe config helper for user config files.
106
112
  *
107
- * `defineConfig` is called in user config files (e.g. `zpress.config.ts`)
108
- * where the return value is consumed by the c12 config loader, which
109
- * expects a plain `ZpressConfig` object not a `Result` tuple.
110
- * Because this is the outermost user-facing boundary (not a library
111
- * function), `process.exit(1)` is acceptable here: the user must fix
112
- * their config before any downstream code can run.
113
+ * This is a passthrough that provides type safety and editor
114
+ * autocompletion in `zpress.config.ts`. Validation is deferred to
115
+ * `loadConfig` at CLI runtime, so errors surface with structured
116
+ * feedback rather than a raw `process.exit`.
113
117
  *
114
118
  * @param config - Raw zpress config object
115
- * @returns The validated config (unchanged)
119
+ * @returns The config (unchanged)
116
120
  */
117
121
  export declare function defineConfig(config: ZpressConfig): ZpressConfig;
118
122
 
@@ -251,6 +255,11 @@ export declare interface Entry {
251
255
  * @default "overview"
252
256
  */
253
257
  readonly indexFile?: string;
258
+ /**
259
+ * Iconify icon identifier (e.g. `"pixelarticons:speed-fast"`).
260
+ * Used on home page feature cards when auto-generated from sections.
261
+ */
262
+ readonly icon?: string;
254
263
  /**
255
264
  * Card display metadata for the parent section's auto-generated landing page.
256
265
  *
@@ -395,10 +404,14 @@ declare type GlobPattern = string;
395
404
  export declare function hasGlobChars(s: string): boolean;
396
405
 
397
406
  /**
398
- * Load zpress config at runtime via c12.
407
+ * Load and validate zpress config at runtime via c12.
408
+ *
409
+ * Returns a `ConfigResult` tuple — the CLI boundary is responsible for
410
+ * surfacing any error and exiting. Validation runs here (not in
411
+ * `defineConfig`) so every consumer gets structured error feedback.
399
412
  *
400
- * Returns a `ConfigResult` tuple instead of calling `process.exit` — the
401
- * CLI boundary is responsible for surfacing the error and exiting.
413
+ * @param dir - Repository root directory to search for `zpress.config.*`
414
+ * @returns A `ConfigResult` tuple `[null, config]` on success or `[ConfigError, null]` on failure
402
415
  */
403
416
  export declare function loadConfig(dir: string): Promise<ConfigResult<ZpressConfig>>;
404
417
 
@@ -510,6 +523,14 @@ export declare interface Paths {
510
523
  readonly cacheDir: string;
511
524
  }
512
525
 
526
+ /**
527
+ * Resolve the default color mode for a given theme name.
528
+ *
529
+ * @param theme - Built-in theme identifier
530
+ * @returns The theme's natural color mode
531
+ */
532
+ export declare function resolveDefaultColorMode(theme: ThemeName): ColorMode;
533
+
513
534
  /**
514
535
  * Internal resolved node — produced by the resolver, consumed by copy + sidebar/nav generators.
515
536
  */
@@ -747,11 +768,86 @@ export declare interface SyncResult {
747
768
  readonly elapsed: number;
748
769
  }
749
770
 
771
+ /**
772
+ * All valid theme names — used for validation.
773
+ */
774
+ export declare const THEME_NAMES: readonly ThemeName[];
775
+
776
+ /**
777
+ * Optional color overrides keyed to CSS custom properties.
778
+ *
779
+ * Each key maps to one or more `--zp-c-*` / `--rp-c-*` variables.
780
+ * Values must be valid CSS color strings (hex or rgba).
781
+ */
782
+ export declare interface ThemeColors {
783
+ readonly brand?: string;
784
+ readonly brandLight?: string;
785
+ readonly brandDark?: string;
786
+ readonly brandSoft?: string;
787
+ readonly bg?: string;
788
+ readonly bgAlt?: string;
789
+ readonly bgElv?: string;
790
+ readonly bgSoft?: string;
791
+ readonly text1?: string;
792
+ readonly text2?: string;
793
+ readonly text3?: string;
794
+ readonly divider?: string;
795
+ readonly border?: string;
796
+ readonly homeBg?: string;
797
+ }
798
+
799
+ /**
800
+ * Top-level theme configuration for `zpress.config.ts`.
801
+ */
802
+ export declare interface ThemeConfig {
803
+ /**
804
+ * Built-in theme to use.
805
+ * @default 'base'
806
+ */
807
+ readonly name?: ThemeName;
808
+ /**
809
+ * Color mode behavior. Defaults to the theme's natural mode.
810
+ */
811
+ readonly colorMode?: ColorMode;
812
+ /**
813
+ * Show the theme switcher dropdown in the nav bar.
814
+ * @default false
815
+ */
816
+ readonly switcher?: boolean;
817
+ /**
818
+ * Partial color overrides applied in light mode (or base mode).
819
+ */
820
+ readonly colors?: ThemeColors;
821
+ /**
822
+ * Partial color overrides applied only in dark mode.
823
+ */
824
+ readonly darkColors?: ThemeColors;
825
+ }
826
+
827
+ /**
828
+ * Theme types and default resolution.
829
+ *
830
+ * Defines the built-in theme palette system: theme names, color modes,
831
+ * per-theme color overrides, and the top-level ThemeConfig shape.
832
+ */
833
+ /**
834
+ * Built-in theme identifiers.
835
+ */
836
+ export declare type ThemeName = 'base' | 'midnight' | 'arcade';
837
+
750
838
  /**
751
839
  * URL path (e.g. `"/guides/add-api-route"`)
752
840
  */
753
841
  declare type UrlPath = string;
754
842
 
843
+ /**
844
+ * Validate the entire config, returning the first error found.
845
+ *
846
+ * @param config - Raw zpress config object to validate
847
+ * @returns A `ConfigResult` tuple — `[null, config]` on success or `[ConfigError, null]` on failure
848
+ */
849
+ export declare function validateConfig(config: ZpressConfig): ConfigResult<ZpressConfig>;
850
+
755
851
  /**
756
852
  * A named group of workspace items for custom workspace categories.
757
853
  *
@@ -906,6 +1002,11 @@ export declare interface ZpressConfig {
906
1002
  * Site meta description. Used as the hero headline on the home page.
907
1003
  */
908
1004
  readonly description?: string;
1005
+ /**
1006
+ * Theme configuration.
1007
+ * Controls the visual theme, color mode, and optional color overrides.
1008
+ */
1009
+ readonly theme?: ThemeConfig;
909
1010
  /**
910
1011
  * Path to a custom favicon file served from `.zpress/public/`.
911
1012
  * When omitted, defaults to the auto-generated `/icon.svg`.
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
+ import { P, match } from "ts-pattern";
1
2
  import { loadConfig } from "c12";
2
3
  import promises from "node:fs/promises";
3
4
  import node_path from "node:path";
4
5
  import { log } from "@clack/prompts";
5
- import { P, match } from "ts-pattern";
6
6
  import { capitalize, groupBy, isUndefined, omitBy, range, words } from "es-toolkit";
7
7
  import { createHash } from "node:crypto";
8
8
  import gray_matter from "gray-matter";
@@ -47,12 +47,20 @@ function collectResults(results) {
47
47
  []
48
48
  ]);
49
49
  }
50
+ const THEME_NAMES = [
51
+ 'base',
52
+ 'midnight',
53
+ 'arcade'
54
+ ];
55
+ const COLOR_MODES = [
56
+ 'dark',
57
+ 'light',
58
+ 'toggle'
59
+ ];
60
+ function resolveDefaultColorMode(theme) {
61
+ return match(theme).with('base', ()=>'toggle').with('midnight', ()=>'dark').with('arcade', ()=>'dark').exhaustive();
62
+ }
50
63
  function defineConfig(config) {
51
- const [err] = validateConfig(config);
52
- if (err) {
53
- process.stderr.write(`[zpress] ${err.message}\n`);
54
- process.exit(1);
55
- }
56
64
  return config;
57
65
  }
58
66
  function validateConfig(config) {
@@ -90,6 +98,11 @@ function validateConfig(config) {
90
98
  featErr,
91
99
  null
92
100
  ];
101
+ const [themeErr] = validateTheme(config.theme);
102
+ if (themeErr) return [
103
+ themeErr,
104
+ null
105
+ ];
93
106
  return [
94
107
  null,
95
108
  config
@@ -222,6 +235,71 @@ function validateFeature(feature) {
222
235
  if (feature.icon && !feature.icon.includes(':')) return configError('invalid_icon', `Feature "${feature.text}": icon must be an Iconify identifier (e.g. "pixelarticons:speed-fast")`);
223
236
  return null;
224
237
  }
238
+ function validateThemeColors(colors, label) {
239
+ const colorPattern = /^(?:#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})|rgba?\([^)]*\))$/;
240
+ const keys = [
241
+ 'brand',
242
+ 'brandLight',
243
+ 'brandDark',
244
+ 'brandSoft',
245
+ 'bg',
246
+ 'bgAlt',
247
+ 'bgElv',
248
+ 'bgSoft',
249
+ 'text1',
250
+ 'text2',
251
+ 'text3',
252
+ 'divider',
253
+ 'border',
254
+ 'homeBg'
255
+ ];
256
+ const firstError = keys.reduce((acc, key)=>{
257
+ if (acc) return acc;
258
+ const value = colors[key];
259
+ if (void 0 !== value && !colorPattern.test(value)) return configError('invalid_theme', `theme.${label}.${key}: "${value}" is not a valid color (use hex #xxx/#xxxxxx or rgba())`);
260
+ return null;
261
+ }, null);
262
+ if (firstError) return [
263
+ firstError,
264
+ null
265
+ ];
266
+ return [
267
+ null,
268
+ true
269
+ ];
270
+ }
271
+ function validateTheme(theme) {
272
+ if (void 0 === theme) return [
273
+ null,
274
+ true
275
+ ];
276
+ if (void 0 !== theme.name && !THEME_NAMES.includes(theme.name)) return [
277
+ configError('invalid_theme', `theme.name: "${theme.name}" is not a valid theme (use ${THEME_NAMES.map((n)=>`"${n}"`).join(', ')})`),
278
+ null
279
+ ];
280
+ if (void 0 !== theme.colorMode && !COLOR_MODES.includes(theme.colorMode)) return [
281
+ configError('invalid_theme', `theme.colorMode: "${theme.colorMode}" is not valid (use ${COLOR_MODES.map((m)=>`"${m}"`).join(', ')})`),
282
+ null
283
+ ];
284
+ if (theme.colors) {
285
+ const [colorsErr] = validateThemeColors(theme.colors, 'colors');
286
+ if (colorsErr) return [
287
+ colorsErr,
288
+ null
289
+ ];
290
+ }
291
+ if (theme.darkColors) {
292
+ const [darkErr] = validateThemeColors(theme.darkColors, 'darkColors');
293
+ if (darkErr) return [
294
+ darkErr,
295
+ null
296
+ ];
297
+ }
298
+ return [
299
+ null,
300
+ true
301
+ ];
302
+ }
225
303
  async function config_loadConfig(dir) {
226
304
  const { config } = await loadConfig({
227
305
  cwd: dir,
@@ -235,10 +313,7 @@ async function config_loadConfig(dir) {
235
313
  configError('empty_sections', 'Failed to load zpress.config — no sections found'),
236
314
  null
237
315
  ];
238
- return [
239
- null,
240
- config
241
- ];
316
+ return validateConfig(config);
242
317
  }
243
318
  const FIGLET_CHARS = Object.freeze({
244
319
  A: [
@@ -1429,7 +1504,7 @@ function buildFeatures(sections, repoRoot) {
1429
1504
  return Promise.all(sections.slice(0, 3).map(async (section, index)=>{
1430
1505
  const link = section.link ?? findFirstChildLink(section);
1431
1506
  const details = await extractSectionDescription(section, repoRoot);
1432
- const iconId = null;
1507
+ const iconId = section.icon ?? null;
1433
1508
  const iconColor = ICON_COLORS[index % ICON_COLORS.length];
1434
1509
  return {
1435
1510
  title: section.text,
@@ -2839,4 +2914,4 @@ function createPaths(dir) {
2839
2914
  cacheDir: node_path.resolve(outputRoot, 'cache')
2840
2915
  };
2841
2916
  }
2842
- export { configError, config_loadConfig as loadConfig, createPaths, defineConfig, generateAssets, generateBannerSvg, generateIconSvg, generateLogoSvg, hasGlobChars, loadManifest, resolveEntries, sync, syncError };
2917
+ export { COLOR_MODES, THEME_NAMES, configError, config_loadConfig as loadConfig, createPaths, defineConfig, generateAssets, generateBannerSvg, generateIconSvg, generateLogoSvg, hasGlobChars, loadManifest, resolveDefaultColorMode, resolveEntries, sync, syncError, validateConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zpress/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Config loading, sync engine, and asset utilities for zpress",
5
5
  "keywords": [
6
6
  "config",