@halftone-ds/tokens 1.2.0 → 1.3.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.
@@ -0,0 +1,168 @@
1
+ /** Types for @halftone-ds/tokens/system (index.mjs): the HalftoneSystem v1 document. */
2
+ import type { Mode, Neutral, Recipe, RecipeInput, Theme, TokensJson } from '../theme/index.mjs';
3
+
4
+ export type ColorRole = 'brand' | 'neutral' | 'danger' | 'success' | 'warning' | 'info';
5
+ export type StatusRole = 'danger' | 'success' | 'warning' | 'info';
6
+ export type Step = '50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | '950';
7
+ /** The neutral also has '0' (white) and '1000' (black). */
8
+ export type NeutralStep = Step | '0' | '1000';
9
+ export type TextStyleName = 'h1' | 'h2' | 'h3' | 'h4' | 'lead' | 'p' | 'large' | 'small' | 'muted' | 'code' | 'blockquote';
10
+ export type FontRole = 'heading' | 'body' | 'mono';
11
+ export type RadiusStep = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
12
+ export type ShadowLevel = '1' | '2' | '3';
13
+ export type BorderStyle = 'solid' | 'dashed' | 'dotted';
14
+ export type ExtraKind = 'color' | 'text' | 'radius' | 'shadow' | 'border';
15
+ /** A 6-digit lowercase hex colour, '#2563eb'. */
16
+ export type Hex = `#${string}`;
17
+ /** A ramp step ('brand.600', 'neutral.0') or a hex. */
18
+ export type RoleValue = `${ColorRole}.${NeutralStep}` | Hex;
19
+
20
+ /**
21
+ * One colour role. brand: its seed is recipe.brand (a seed here folds into it). neutral: a hex seed makes a custom
22
+ * neutral; a preset name folds into recipe.neutral. A status role without a seed keeps the base file's ramp.
23
+ */
24
+ export type ColorOverride = {
25
+ seed?: Hex;
26
+ /** The step the seed is pinned to, when not its nearest one. */
27
+ anchor?: Step;
28
+ /** Steps edited by hand. */
29
+ steps?: Partial<Record<NeutralStep, Hex>>;
30
+ };
31
+ export type RoleOverride = { light?: RoleValue; dark?: RoleValue };
32
+ /** size px, leading × size, tracking em. */
33
+ export type TextStyle = { font: FontRole; weight: number; size: number; leading: number; tracking: number };
34
+ export type ShadowLayer = { x: number; y: number; blur: number; spread: number; color: Hex; alpha: number; inset?: true };
35
+ export type Shadow = Record<Mode, ShadowLayer[]>;
36
+ export type BorderOverride = { width?: number; style?: BorderStyle };
37
+
38
+ export type ExtraColor = { name: string } & ({ value: Hex | Record<Mode, Hex> } | { seed: Hex; anchor?: Step; steps?: Partial<Record<Step, Hex>> });
39
+ export type ExtraText = { name: string } & TextStyle;
40
+ export type ExtraRadius = { name: string; value: number };
41
+ export type ExtraShadow = { name: string } & Shadow;
42
+ export type ExtraBorder = { name: string; width: number; style?: BorderStyle };
43
+ export type Extras = { color: ExtraColor[]; text: ExtraText[]; radius: ExtraRadius[]; shadow: ExtraShadow[]; border: ExtraBorder[] };
44
+
45
+ /** As the docs site's brand store keeps it (apps/docs lib/brand/svg.ts). */
46
+ export type BrandImage = { svg?: string; png?: string; mono?: boolean; aspect: number };
47
+ export type IconBackground = 'brand' | 'white' | 'black' | Hex;
48
+ export type SystemBrand = {
49
+ name: string;
50
+ iconBackground: IconBackground;
51
+ logo?: BrandImage;
52
+ logoDark?: BrandImage;
53
+ mark?: BrandImage;
54
+ markDark?: BrandImage;
55
+ };
56
+
57
+ /** A normalised system: every section present, overrides only. */
58
+ export type HalftoneSystem = {
59
+ kind: 'halftone.system';
60
+ v: 1;
61
+ id: string;
62
+ name: string;
63
+ preset?: string;
64
+ updatedAt?: string;
65
+ recipe: Recipe;
66
+ colors: Partial<Record<ColorRole, ColorOverride>>;
67
+ roles: Partial<Record<string, RoleOverride>>;
68
+ type: Partial<Record<TextStyleName, Partial<TextStyle>>>;
69
+ /** Any Google Fonts family. */
70
+ fonts: Partial<Record<FontRole, string>>;
71
+ radius: Partial<Record<RadiusStep, number>>;
72
+ shadow: Partial<Record<ShadowLevel, Shadow>>;
73
+ border: BorderOverride;
74
+ extras: Extras;
75
+ brand: SystemBrand;
76
+ /** Component-level edits (Button, Chip, Badge, Input), overrides only. Button and Input styles fold into the recipe. */
77
+ components: SystemComponents;
78
+ };
79
+
80
+ /** A component colour: a role (any semantic colour), or 'none' for a fill or border. */
81
+ export type ComponentEdit = {
82
+ style?: string;
83
+ /** A Foundations corner, or px. */
84
+ radius?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | number;
85
+ size?: Partial<Record<'sm' | 'md' | 'lg' | 'x', number>>;
86
+ /** A text style (h1…blockquote). */
87
+ text?: string;
88
+ variants?: Record<string, Partial<Record<'fill' | 'label' | 'border' | 'pressed', string>>>;
89
+ };
90
+ export type SystemComponents = Partial<Record<'button' | 'chip' | 'badge' | 'input', ComponentEdit>>;
91
+
92
+ /** What normalizeSystem accepts: any section may be missing, a shadow may be a plain layer list, a layer partial. */
93
+ export type HalftoneSystemInput = Partial<Omit<HalftoneSystem, 'recipe' | 'colors' | 'shadow' | 'extras' | 'brand' | 'components' | 'kind' | 'v'>> & {
94
+ kind?: 'halftone.system';
95
+ v?: number;
96
+ recipe?: RecipeInput;
97
+ colors?: Partial<Record<Exclude<ColorRole, 'neutral'>, ColorOverride>> & { neutral?: Omit<ColorOverride, 'seed'> & { seed?: Hex | Neutral } };
98
+ shadow?: Partial<Record<ShadowLevel, Partial<ShadowLayer>[] | { light?: Partial<ShadowLayer>[]; dark?: Partial<ShadowLayer>[] }>>;
99
+ extras?: Partial<Record<ExtraKind, Array<Record<string, unknown> & { name: string }>>>;
100
+ brand?: Partial<SystemBrand>;
101
+ components?: SystemComponents;
102
+ };
103
+
104
+ /** A contrast pairing the fixer left failing because a colour in it was chosen by hand. */
105
+ export type ContrastWarning = { mode: Mode; label: string; fg: string; bg: string; ratio: number; threshold: number; locked: string[] };
106
+
107
+ /** A colour role's ramp as the Studio draws it. */
108
+ export type SystemRamp = {
109
+ /** The seed (a neutral preset's name for a preset neutral). */
110
+ seed: string;
111
+ /** The step the seed sits on (null for a preset neutral). */
112
+ anchor: Step | null;
113
+ source: 'recipe' | 'seed' | 'base';
114
+ /** Before edits. */
115
+ generated: Record<string, string>;
116
+ /** After edits. */
117
+ values: Record<string, string>;
118
+ edited: string[];
119
+ };
120
+ export type ResolvedTextStyle = TextStyle & { edited: (keyof TextStyle)[] };
121
+
122
+ export const SYSTEM_KIND: 'halftone.system';
123
+ export const SYSTEM_VERSION: 1;
124
+ export const COLOR_ROLES: ColorRole[];
125
+ export const STATUS_ROLES: StatusRole[];
126
+ /** Every semantic colour a role mapping can set. */
127
+ export const ROLE_NAMES: string[];
128
+ export const TEXT_STYLES: TextStyleName[];
129
+ export const TEXT_KEYS: (keyof TextStyle)[];
130
+ export const TEXT_DEFAULTS: Record<TextStyleName, Omit<TextStyle, 'leading'> & { leading: number | 'prose' }>;
131
+ export const FONT_ROLES: FontRole[];
132
+ export const RADIUS_STEPS: RadiusStep[];
133
+ export const SHADOW_LEVELS: ShadowLevel[];
134
+ export const BORDER_STYLES: BorderStyle[];
135
+ export const EXTRA_KINDS: ExtraKind[];
136
+
137
+ export class SystemError extends Error {
138
+ /** Every problem, as 'path: message'. */
139
+ errors: string[];
140
+ }
141
+ export function isSystem(value: unknown): boolean;
142
+ /** Why an extra can't take this name (a built-in slot, a Tailwind class it would shadow, a theme variable, `taken`), or null. */
143
+ export function extraNameProblem(kind: ExtraKind, name: string, taken?: string[]): string | null;
144
+ export function newSystemId(): string;
145
+ export function normalizeSystem(input: HalftoneSystemInput | HalftoneSystem): HalftoneSystem;
146
+ export function validateSystem(input: unknown): { ok: boolean; errors: string[]; system: HalftoneSystem | null };
147
+ /** A new system with a fresh id and the time. */
148
+ export function createSystem(input?: HalftoneSystemInput): HalftoneSystem;
149
+ /** Every override as a path ('colors.danger.seed', 'roles.primary.dark', 'extras.color.sunset'). */
150
+ export function systemOverrides(system: HalftoneSystemInput | HalftoneSystem): string[];
151
+ export function hasOverrides(system: HalftoneSystemInput | HalftoneSystem): boolean;
152
+ export function withoutBrandImages(system: HalftoneSystem): HalftoneSystem;
153
+
154
+ /** The theme for a system: generateTheme's shape; `extra` holds the extras as a DTCG group. */
155
+ export function generateSystem(system: HalftoneSystemInput | HalftoneSystem, base: TokensJson, options?: { fix?: boolean }): Theme & { extra?: Record<string, unknown> };
156
+ /** The ht- code, or null when the system has overrides a code can't carry. */
157
+ export function systemCode(system: HalftoneSystemInput | HalftoneSystem): string | null;
158
+ export function systemRamps(system: HalftoneSystemInput | HalftoneSystem, base: TokensJson): Record<ColorRole, SystemRamp>;
159
+ export function textStyles(system: HalftoneSystemInput | HalftoneSystem): Record<TextStyleName, ResolvedTextStyle>;
160
+ /** rounded-sm…2xl in px. */
161
+ export function systemRadii(system: HalftoneSystemInput | HalftoneSystem, base: TokensJson, theme?: Theme): Record<RadiusStep, number>;
162
+
163
+ /** A system from a recipe (or an ht- code) and the Studio's brand; a document is normalised. */
164
+ export function migrate(input: RecipeInput | string | HalftoneSystemInput, brand?: Partial<SystemBrand>, options?: { id?: string; name?: string; updatedAt?: string }): HalftoneSystem;
165
+ /** The recipe under a system, its code (null with overrides) and the overrides a recipe loses. */
166
+ export function systemToRecipe(system: HalftoneSystemInput | HalftoneSystem): { recipe: Recipe; code: string | null; lost: string[] };
167
+ /** Commit a system into tokens.json; the record keeps the document without brand images. */
168
+ export function applySystem(tokens: TokensJson, system: HalftoneSystemInput | HalftoneSystem): { tokens: TokensJson; theme: Theme; system: HalftoneSystem };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @halftone-ds/tokens/system — the HalftoneSystem document: one design system (a recipe plus everything edited
3
+ * by hand on top of it), as the Studio keeps it and `halftone-tokens theme apply ./name.halftone.json` commits it.
4
+ * Pure ESM with no Node APIs. See schema.mjs for the shape.
5
+ */
6
+ export {
7
+ BORDER_STYLES,
8
+ COLOR_ROLES,
9
+ createSystem,
10
+ EXTRA_KINDS,
11
+ extraNameProblem,
12
+ FONT_ROLES,
13
+ hasOverrides,
14
+ isSystem,
15
+ newSystemId,
16
+ normalizeSystem,
17
+ RADIUS_STEPS,
18
+ ROLE_NAMES,
19
+ SHADOW_LEVELS,
20
+ STATUS_ROLES,
21
+ SYSTEM_KIND,
22
+ SYSTEM_VERSION,
23
+ SystemError,
24
+ systemOverrides,
25
+ TEXT_DEFAULTS,
26
+ TEXT_KEYS,
27
+ TEXT_STYLES,
28
+ validateSystem,
29
+ withoutBrandImages,
30
+ } from './schema.mjs';
31
+ export { generateSystem, systemCode, systemRadii, systemRamps, textStyles } from './generate.mjs';
32
+ export { migrate, systemToRecipe } from './migrate.mjs';
33
+ export { applySystem } from '../theme/apply.mjs';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * migrate — a HalftoneSystem from what came before it: a theme recipe (or its ht- code) and the Studio's brand.
3
+ * A document already is cleaned up and brought to the current version. Pure ESM.
4
+ *
5
+ * migrate(recipe | code | system, brand?, { id?, name?, updatedAt? }) → HalftoneSystem
6
+ * systemToRecipe(system) → { recipe, code, lost }: the other way; `lost` lists the overrides a recipe can't
7
+ * hold (code is null when there are any)
8
+ *
9
+ * A recipe migrates to a system with no overrides, so it round-trips: systemToRecipe(migrate(r)).recipe is r, and
10
+ * generateSystem(migrate(r), base) is generateTheme(r, base).
11
+ */
12
+ import { decodeRecipe, encodeRecipe, isThemeCode } from '../theme/codec.mjs';
13
+ import { presetById } from '../theme/presets.mjs';
14
+ import { normalizeRecipe } from '../theme/recipe.mjs';
15
+ import { isSystem, newSystemId, normalizeSystem, systemOverrides } from './schema.mjs';
16
+
17
+ export function migrate(input, brand, { id, name, updatedAt } = {}) {
18
+ if (isSystem(input)) {
19
+ const doc = normalizeSystem(input);
20
+ return { ...doc, ...(id ? { id } : {}), ...(name ? { name } : {}), ...(updatedAt ? { updatedAt } : {}), ...(brand ? { brand: normalizeSystem({ brand }).brand } : {}) };
21
+ }
22
+ const recipe = typeof input === 'string' && isThemeCode(input) ? decodeRecipe(input) : normalizeRecipe(input ?? {});
23
+ const preset = recipe.preset ? presetById(recipe.preset) : undefined;
24
+ return normalizeSystem({
25
+ id: id ?? newSystemId(),
26
+ name: name ?? (brand?.name?.trim() || preset?.name || 'My system'),
27
+ ...(recipe.preset ? { preset: recipe.preset } : {}),
28
+ updatedAt: updatedAt ?? new Date().toISOString(),
29
+ recipe,
30
+ ...(brand ? { brand } : {}),
31
+ });
32
+ }
33
+
34
+ export function systemToRecipe(input) {
35
+ const doc = normalizeSystem(input);
36
+ const lost = systemOverrides(doc);
37
+ return { recipe: doc.recipe, code: lost.length ? null : encodeRecipe(doc.recipe), lost };
38
+ }