Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 34x 1220x 2x 2x 610x 610x 610x 26230x 4457x 2992x 610x 610x 139x 138x 1x 610x 610x 610x 3x 2x 1x 610x 26230x 3127x 9150x 26230x 9150x 610x | // SPDX-License-Identifier: MIT
/**
* Consumer theme curation API.
*
* `createThemeCatalog()` provides a single, composable surface for selecting a
* subset of themes. It composes with the existing `flavors` (tokens) and
* `VENDOR_GROUPS` (metadata) so consumers no longer need to hand-slice arrays.
*
* Filter precedence:
* all themes -> vendors/appearances -> include -> exclude
*
* Invalid IDs supplied to `include`/`exclude` warn in development (never
* throw), so a typo cannot silently drop or duplicate a theme.
*/
import type { ThemeFlavor } from '../themes/types.js';
import type { ThemeId } from '../themes/theme-ids.js';
import { flavors, themeIds as allThemeIds } from '../tokens/index.js';
import { VENDOR_GROUPS, type VendorGroup } from '../themes/metadata.js';
// Bundlers (esbuild, Vite, webpack) replace `process.env.NODE_ENV` with a
// string literal at build time; this declaration lets tsc compile without
// @types/node while preserving dead-code elimination in production bundles.
declare const process: { readonly env: { readonly NODE_ENV?: string } } | undefined;
/** Options controlling which themes a {@link ThemeCatalog} contains. */
export interface ThemeCatalogOptions {
/**
* Restrict to these vendors (e.g. `['catppuccin', 'dracula']`). When
* omitted, all vendors are eligible; an explicit empty array selects none,
* which is useful for building an include-only catalog.
*/
vendors?: readonly string[];
/**
* Restrict to these appearances. When omitted, both appearances are
* eligible; an explicit empty array selects none.
*/
appearances?: readonly ('light' | 'dark')[];
/** Theme IDs to add after vendor/appearance filtering. */
include?: readonly string[];
/** Theme IDs to remove after `include` has been applied. */
exclude?: readonly string[];
}
/** A curated view over the theme catalog. */
export interface ThemeCatalog {
/** Selected theme IDs, in canonical catalog order. */
readonly themeIds: readonly ThemeId[];
/** Full flavor objects for the selected themes, in canonical order. */
readonly flavors: readonly ThemeFlavor[];
/** Vendor groups (for dropdown UIs) restricted to the selected themes. */
readonly vendorGroups: readonly VendorGroup[];
}
const KNOWN_THEME_IDS: ReadonlySet<string> = new Set(allThemeIds);
/**
* Emit a dev-time warning for unknown IDs. The `typeof` guard is safe even
* when `process` is an undeclared identifier (browsers/workers without a
* shim), and keeps the contiguous `process.env.NODE_ENV` token sequence so
* bundler define-replacement and dead-code elimination still apply.
*/
function warnInvalidIds(source: 'include' | 'exclude', ids: readonly string[]): void {
if (ids.length === 0) return;
Iif (typeof process !== 'undefined' && process.env.NODE_ENV === 'production') return;
console.warn(
`[catalog] createThemeCatalog: ignoring unknown ${source} theme ID(s): ${ids.join(', ')}. ` +
'Valid IDs come from the exported `themeIds`.',
);
}
/**
* Build a curated {@link ThemeCatalog} from the full theme set.
*
* @param options - Vendor/appearance filters plus explicit include/exclude IDs.
* @returns A catalog exposing `themeIds`, `flavors`, and `vendorGroups`.
*/
export function createThemeCatalog(options: ThemeCatalogOptions = {}): ThemeCatalog {
const { vendors, appearances, include = [], exclude = [] } = options;
const selected = new Set<string>();
for (const flavor of flavors) {
if (vendors !== undefined && !vendors.includes(flavor.vendor)) continue;
if (appearances !== undefined && !appearances.includes(flavor.appearance)) continue;
selected.add(flavor.id);
}
const invalidInclude: string[] = [];
for (const id of include) {
if (KNOWN_THEME_IDS.has(id)) {
selected.add(id);
} else {
invalidInclude.push(id);
}
}
warnInvalidIds('include', invalidInclude);
const invalidExclude: string[] = [];
for (const id of exclude) {
if (KNOWN_THEME_IDS.has(id)) {
selected.delete(id);
} else {
invalidExclude.push(id);
}
}
warnInvalidIds('exclude', invalidExclude);
const selectedFlavors = flavors.filter((flavor) => selected.has(flavor.id));
const selectedThemeIds = selectedFlavors.map((flavor) => flavor.id as ThemeId);
const vendorGroups = VENDOR_GROUPS.map((group) => ({
...group,
themeIds: group.themeIds.filter((id) => selected.has(id)),
})).filter((group) => group.themeIds.length > 0);
return {
themeIds: selectedThemeIds,
flavors: selectedFlavors,
vendorGroups,
};
}
|