@phcdevworks/spectre-tokens 2.2.0 → 2.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.
package/README.md CHANGED
@@ -27,6 +27,27 @@ translate those contracts for specific frameworks and runtimes.
27
27
  - Keeps visual meaning centralized so downstream consumers do not redefine token
28
28
  contracts
29
29
 
30
+ ## What this package owns
31
+
32
+ - Visual language expressed as token data in `tokens/`
33
+ - Semantic roles and token contracts consumed downstream
34
+ - Generated token outputs for JavaScript, TypeScript, CSS variables, and
35
+ Tailwind theme exports
36
+ - Theme and mode definitions used by downstream consumers
37
+
38
+ This package is the correct place to define token meaning.
39
+
40
+ ## What this package does not own
41
+
42
+ - Component structure or composition That belongs in downstream UI packages such
43
+ as [`@phcdevworks/spectre-ui`](https://github.com/phcdevworks/spectre-ui).
44
+ - Framework-specific delivery Adapter packages translate Spectre contracts for
45
+ specific frameworks and runtimes.
46
+ - Local redefinition of token meaning Downstream consumers should consume these
47
+ contracts rather than recreate them independently.
48
+ - Example app architecture The `example/` directory documents token usage; it is
49
+ not the contract source and should not become a downstream UI layer.
50
+
30
51
  ## Installation
31
52
 
32
53
  ```bash
@@ -72,17 +93,103 @@ export default {
72
93
  }
73
94
  ```
74
95
 
75
- Prefer semantic tokens such as `surface`, `text`, `component`, `buttons`, and
76
- `forms` for application UI. Raw palette values remain available when fixed color
77
- access is appropriate.
96
+ ## Consumer usage
78
97
 
79
- ## What this package owns
98
+ ### Source of truth
80
99
 
81
- - Visual language expressed as token data in `tokens/`
82
- - Semantic roles and token contracts consumed downstream
83
- - Generated token outputs for JavaScript, TypeScript, CSS variables, and
84
- Tailwind theme exports
85
- - Theme and mode definitions used by downstream consumers
100
+ - Change token data in `tokens/`.
101
+ - Treat generated outputs as derived artifacts.
102
+ - Treat `contract.manifest.json` as the machine-readable contract authority for
103
+ public namespaces and required outputs.
104
+
105
+ ### JavaScript and TypeScript tokens
106
+
107
+ Use the runtime token object when a consumer needs token values directly in
108
+ code.
109
+
110
+ ```ts
111
+ import tokens from '@phcdevworks/spectre-tokens'
112
+
113
+ const card = {
114
+ background: tokens.surface.card,
115
+ color: tokens.text.onSurface.default,
116
+ borderColor: tokens.component.iconBox.border,
117
+ padding: tokens.space['16']
118
+ }
119
+ ```
120
+
121
+ Use named exports when you need generated helpers or Tailwind integration:
122
+
123
+ ```ts
124
+ import tokens, {
125
+ generateCssVariables,
126
+ tailwindPreset,
127
+ tailwindTheme
128
+ } from '@phcdevworks/spectre-tokens'
129
+
130
+ const css = generateCssVariables(tokens)
131
+ ```
132
+
133
+ ### Generated CSS variables
134
+
135
+ Import `index.css` when a downstream package or app wants the generated Spectre
136
+ CSS variable contract.
137
+
138
+ ```css
139
+ @import '@phcdevworks/spectre-tokens/index.css';
140
+
141
+ .card {
142
+ background: var(--sp-surface-card);
143
+ color: var(--sp-text-on-surface-default);
144
+ }
145
+ ```
146
+
147
+ The CSS entry point is intended for consumers that want the token contract as
148
+ variables rather than reading values in JavaScript.
149
+
150
+ ### Tailwind preset
151
+
152
+ Use the Tailwind preset when a consumer wants Tailwind theme values derived from
153
+ the same token contract.
154
+
155
+ ```ts
156
+ import { tailwindPreset } from '@phcdevworks/spectre-tokens'
157
+
158
+ export default {
159
+ presets: [tailwindPreset]
160
+ }
161
+ ```
162
+
163
+ Use `tailwindTheme` directly only when a consumer needs the generated theme
164
+ object outside the preset shape.
165
+
166
+ ### Semantic tokens first
167
+
168
+ Prefer semantic namespaces for downstream UI work:
169
+
170
+ - `surface`
171
+ - `text`
172
+ - `component`
173
+ - `buttons`
174
+ - `forms`
175
+ - `modes`
176
+
177
+ These namespaces are the main consumer-facing contract because they express UI
178
+ meaning rather than raw color selection.
179
+
180
+ ### When raw palette access is acceptable
181
+
182
+ Raw palette access through `colors` is acceptable when a consumer deliberately
183
+ needs fixed palette values.
184
+
185
+ Typical cases:
186
+
187
+ - data visualization or non-semantic decorative use
188
+ - compatibility layers that need a specific raw ramp
189
+ - tooling that inspects or serializes palette data directly
190
+
191
+ Raw palette access should not replace semantic token usage for normal UI
192
+ surfaces, text, buttons, forms, or mode-aware styling.
86
193
 
87
194
  ### Token model
88
195
 
@@ -100,6 +207,8 @@ The generated token object includes these namespaces:
100
207
  - `transitions`
101
208
  - `animations`
102
209
  - `opacity`
210
+ - `aspectRatios`
211
+ - `icons`
103
212
  - `border`
104
213
  - `accessibility`
105
214
  - `buttons`
@@ -113,23 +222,74 @@ The exported runtime token object is a flattened string-based tree generated
113
222
  from `tokens/`. Source-only wrapper fields such as `value` and `metadata` are
114
223
  internal generation details and are not part of the public package contract.
115
224
 
225
+ ## Public contract guarantees
226
+
227
+ `contract.manifest.json` is the machine-readable contract authority for this
228
+ package.
229
+
230
+ It defines:
231
+
232
+ - public namespaces
233
+ - required output surfaces for JavaScript, CSS, and Tailwind
234
+ - protected semantic groups
235
+
236
+ Every contract-facing surface in this repository must match that manifest.
237
+ Validation fails fast on token overwrite across files, undocumented namespaces,
238
+ output drift, and README mismatch with the contract authority.
239
+
116
240
  ### Themes and modes
117
241
 
118
242
  The package includes mode-aware semantic tokens under `modes`, with `default`
119
243
  and `dark` mode definitions in the generated output.
120
244
 
121
- Raw palette tokens are stable values. Semantic tokens are the preferred
122
- interface for theme-aware usage because they can map across modes without
123
- changing consumer code.
245
+ Use semantic mode-aware values when the consumer needs light/dark or
246
+ mode-specific behavior without branching on raw palette values.
124
247
 
125
- ## What this package does not own
248
+ ```ts
249
+ import tokens from '@phcdevworks/spectre-tokens'
126
250
 
127
- - Component structure or composition That belongs in downstream UI packages such
128
- as [`@phcdevworks/spectre-ui`](https://github.com/phcdevworks/spectre-ui).
129
- - Framework-specific delivery Adapter packages translate Spectre contracts for
130
- specific frameworks and runtimes.
131
- - Local redefinition of token meaning Downstream consumers should consume these
132
- contracts rather than recreate them independently.
251
+ const darkPage = tokens.modes.dark.surface.page
252
+ const darkText = tokens.modes.dark.text.onPage.default
253
+ ```
254
+
255
+ Guidance:
256
+
257
+ - Prefer semantic tokens for theme-aware UI.
258
+ - Prefer `modes` when a consumer explicitly needs mode-specific values.
259
+ - Do not invent local light/dark token contracts when this package already
260
+ provides the semantic path.
261
+
262
+ ## Downstream boundaries
263
+
264
+ Downstream packages should never redefine locally:
265
+
266
+ - the meaning of `surface`, `text`, `component`, `buttons`, `forms`, or `modes`
267
+ - protected semantic groups such as `success`, `warning`, `danger`, or CTA /
268
+ brand-action semantics
269
+ - public namespace shape that this package already exports
270
+
271
+ Downstream packages may:
272
+
273
+ - compose UI structure on top of this contract
274
+ - map these tokens into framework-specific delivery
275
+ - use raw palette values when the usage is intentionally non-semantic
276
+
277
+ ## Upgrade expectations for consumers
278
+
279
+ Consumers should treat this package as a SemVer-governed contract.
280
+
281
+ Practical guidance:
282
+
283
+ - additive token paths are intended to be safe for existing consumers
284
+ - semantic shifts may keep the same path but still affect visual meaning
285
+ - renames and removals are breaking
286
+ - generated JS, TS, CSS, and Tailwind outputs are expected to stay aligned
287
+
288
+ If a downstream package depends on specific token paths or semantic meaning:
289
+
290
+ - read `CHANGELOG.md` for contract change classification
291
+ - read `TOKEN_CONTRACT.md` for contract rules
292
+ - prefer documented public namespaces over undocumented internal assumptions
133
293
 
134
294
  ## Package exports / API surface
135
295
 
@@ -140,7 +300,7 @@ changing consumer code.
140
300
  - `default` / `tokens`
141
301
  - `tailwindTheme`
142
302
  - `tailwindPreset`
143
- - `generateCssVariables()`
303
+ - `generateCssVariables`
144
304
  - TypeScript types including `SpectreTokens`, `TailwindTheme`,
145
305
  `SpectreModeTokens`, and `SpectreModeName`
146
306
 
@@ -177,6 +337,18 @@ Spectre keeps responsibilities separate:
177
337
  That separation keeps token meaning centralized while letting the package system
178
338
  expand by responsibility.
179
339
 
340
+ ## Consumer checklist
341
+
342
+ For downstream packages and compatible apps:
343
+
344
+ - import tokens from the package root when you need runtime values
345
+ - import `index.css` when you need generated CSS variables
346
+ - use `tailwindPreset` when you need Tailwind theme integration
347
+ - prefer semantic namespaces for UI behavior
348
+ - use raw palette values only when fixed palette access is intentional
349
+ - treat `tokens/` as source of truth and generated outputs as derived
350
+ - do not redefine Spectre semantic contracts locally
351
+
180
352
  ## Development
181
353
 
182
354
  Regenerate package outputs:
@@ -199,9 +371,9 @@ Key source areas:
199
371
  - `scripts/` for build and validation scripts
200
372
  - `example/` for usage examples
201
373
 
202
- The files in `example/` are illustrative token demos only. They help explain
203
- the token contract, but they are not the package contract itself and should not
204
- be treated as downstream UI primitives.
374
+ The files in `example/` are illustrative token demos only. They help explain the
375
+ token contract, but they are not the package contract itself and should not be
376
+ treated as downstream UI primitives.
205
377
 
206
378
  ## Contributing
207
379
 
@@ -215,6 +387,7 @@ When contributing:
215
387
  - run `npm run build` to regenerate outputs when sources change
216
388
  - run `npm run check` as the full validation gate before opening a pull request
217
389
  - do not modify locked semantic color families without explicit approval
390
+ - keep `README.md`, generated outputs, and `contract.manifest.json` aligned
218
391
 
219
392
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.
220
393
 
package/dist/index.cjs CHANGED
@@ -75,7 +75,7 @@ var coreTokens = {
75
75
  "text": "{colors.neutral.700}",
76
76
  "authorName": "{colors.neutral.900}",
77
77
  "authorTitle": "{colors.neutral.500}",
78
- "quoteMark": "{colors.neutral.300}"
78
+ "quoteMark": "{colors.neutral.500}"
79
79
  },
80
80
  "pricingCard": {
81
81
  "bg": "{colors.white}",
@@ -103,7 +103,7 @@ var coreTokens = {
103
103
  "text": "{colors.white}",
104
104
  "textDisabled": "{colors.neutral.400}",
105
105
  "focusRing": "{colors.info.500} / 0.4",
106
- "focusVisible": "{buttons.primary.focusRing}"
106
+ "focusVisible": "{colors.info.500} / 0.4"
107
107
  },
108
108
  "secondary": {
109
109
  "bg": "{colors.white}",
@@ -115,7 +115,7 @@ var coreTokens = {
115
115
  "border": "{colors.info.700}",
116
116
  "borderDisabled": "{colors.neutral.200}",
117
117
  "focusRing": "{colors.info.500} / 0.4",
118
- "focusVisible": "{buttons.secondary.focusRing}"
118
+ "focusVisible": "{colors.info.500} / 0.4"
119
119
  },
120
120
  "ghost": {
121
121
  "bg": "transparent",
@@ -125,7 +125,7 @@ var coreTokens = {
125
125
  "text": "{colors.info.700}",
126
126
  "textDisabled": "{colors.neutral.400}",
127
127
  "focusRing": "{colors.info.500} / 0.4",
128
- "focusVisible": "{buttons.ghost.focusRing}"
128
+ "focusVisible": "{colors.info.500} / 0.4"
129
129
  },
130
130
  "danger": {
131
131
  "bg": "{colors.error.600}",
@@ -163,7 +163,7 @@ var coreTokens = {
163
163
  "text": "{colors.white}",
164
164
  "textDisabled": "{colors.neutral.400}",
165
165
  "focusRing": "{colors.accent.500} / 0.4",
166
- "focusVisible": "{buttons.accent.focusRing}"
166
+ "focusVisible": "{colors.accent.500} / 0.4"
167
167
  }
168
168
  },
169
169
  "forms": {
@@ -181,8 +181,8 @@ var coreTokens = {
181
181
  "ring": "{colors.info.500}"
182
182
  },
183
183
  "focusVisible": {
184
- "border": "{forms.focus.border}",
185
- "ring": "{forms.focus.ring}"
184
+ "border": "{colors.info.500}",
185
+ "ring": "{colors.info.500}"
186
186
  },
187
187
  "valid": {
188
188
  "border": "{colors.success.500}",
@@ -295,7 +295,7 @@ var coreTokens = {
295
295
  "input": "{colors.neutral.700}",
296
296
  "overlay": "{colors.neutral.800}",
297
297
  "alternate": "{colors.neutral.800}",
298
- "hero": "linear-gradient(135deg, {colors.accent.900} 0%, {colors.accent.700} 100%)"
298
+ "hero": "linear-gradient(135deg, {colors.accent.700} 0%, {colors.accent.900} 100%)"
299
299
  },
300
300
  "text": {
301
301
  "onPage": {
@@ -679,7 +679,8 @@ var coreTokens = {
679
679
  "xs": {
680
680
  "size": "0.75rem",
681
681
  "lineHeight": "1.25rem",
682
- "weight": 400
682
+ "weight": 400,
683
+ "letterSpacing": "0.02em"
683
684
  },
684
685
  "sm": {
685
686
  "size": "0.875rem",
@@ -863,6 +864,10 @@ var createCssVariableMap = (tokens2, options = {}) => {
863
864
  map[name] = resolveValue(tokens2, value);
864
865
  };
865
866
  Object.entries(baseTokens.colors).forEach(([group, scale]) => {
867
+ if (typeof scale === "string" || typeof scale === "number") {
868
+ assign(toVariableName(prefix, "color", group), scale);
869
+ return;
870
+ }
866
871
  Object.entries(scale).forEach(([step, value]) => {
867
872
  assign(toVariableName(prefix, "color", group, step), value);
868
873
  });
@@ -1028,15 +1033,18 @@ var generateCssVariables = (tokens2, options = {}) => {
1028
1033
  addBase(toVariableName(prefix, "surface", "card"), pickSemantic(tokens2, getPath(defaultMode, ["surface", "card"]), getPath(surfaceAliases, ["card"])));
1029
1034
  addBase(toVariableName(prefix, "surface", "input"), pickSemantic(tokens2, getPath(defaultMode, ["surface", "input"]), getPath(surfaceAliases, ["input"])));
1030
1035
  addBase(toVariableName(prefix, "surface", "overlay"), pickSemantic(tokens2, getPath(defaultMode, ["surface", "overlay"]), getPath(surfaceAliases, ["overlay"])));
1036
+ addBase(toVariableName(prefix, "surface", "alternate"), pickSemantic(tokens2, getPath(defaultMode, ["surface", "alternate"])));
1031
1037
  addBase(toVariableName(prefix, "surface", "hero"), pickSemantic(tokens2, getPath(defaultMode, ["surface", "hero"]), getPath(surfaceAliases, ["hero"])));
1032
1038
  addBase(toVariableName(prefix, "text", "on", "page", "default"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onPage", "default"]), getPath(textAliases, ["onPage", "default"])));
1033
1039
  addBase(toVariableName(prefix, "text", "on", "page", "muted"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onPage", "muted"]), getPath(textAliases, ["onPage", "muted"])));
1034
1040
  addBase(toVariableName(prefix, "text", "on", "page", "subtle"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onPage", "subtle"]), getPath(textAliases, ["onPage", "subtle"])));
1035
1041
  addBase(toVariableName(prefix, "text", "on", "page", "meta"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onPage", "meta"]), getPath(textAliases, ["onPage", "meta"])));
1042
+ addBase(toVariableName(prefix, "text", "on", "page", "brand"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onPage", "brand"]), getPath(textAliases, ["onPage", "brand"])));
1036
1043
  addBase(toVariableName(prefix, "text", "on", "surface", "default"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onSurface", "default"]), getPath(textAliases, ["onSurface", "default"])));
1037
1044
  addBase(toVariableName(prefix, "text", "on", "surface", "muted"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onSurface", "muted"]), getPath(textAliases, ["onSurface", "muted"])));
1038
1045
  addBase(toVariableName(prefix, "text", "on", "surface", "subtle"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onSurface", "subtle"]), getPath(textAliases, ["onSurface", "subtle"])));
1039
1046
  addBase(toVariableName(prefix, "text", "on", "surface", "meta"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onSurface", "meta"]), getPath(textAliases, ["onSurface", "meta"])));
1047
+ addBase(toVariableName(prefix, "text", "on", "surface", "brand"), pickSemantic(tokens2, getPath(defaultMode, ["text", "onSurface", "brand"]), getPath(textAliases, ["onSurface", "brand"])));
1040
1048
  addBase(toVariableName(prefix, "component", "card", "text"), pickSemantic(tokens2, getPath(defaultMode, ["component", "card", "text"]), getPath(componentAliases, ["card", "text"])));
1041
1049
  addBase(toVariableName(prefix, "component", "card", "text-muted"), pickSemantic(tokens2, getPath(defaultMode, ["component", "card", "textMuted"]), getPath(componentAliases, ["card", "textMuted"])));
1042
1050
  addBase(toVariableName(prefix, "component", "input", "text"), pickSemantic(tokens2, getPath(defaultMode, ["component", "input", "text"]), getPath(componentAliases, ["input", "text"])));
@@ -1080,6 +1088,10 @@ var generateCssVariables = (tokens2, options = {}) => {
1080
1088
  toVariableName(prefix, "surface", "overlay"),
1081
1089
  pickSemantic(tokens2, getPath(darkMode, ["surface", "overlay"]), getPath(defaultMode, ["surface", "overlay"]), getPath(surfaceAliases, ["overlay"]))
1082
1090
  );
1091
+ addDark(
1092
+ toVariableName(prefix, "surface", "alternate"),
1093
+ pickSemantic(tokens2, getPath(darkMode, ["surface", "alternate"]), getPath(defaultMode, ["surface", "alternate"]))
1094
+ );
1083
1095
  addDark(
1084
1096
  toVariableName(prefix, "surface", "hero"),
1085
1097
  pickSemantic(tokens2, getPath(darkMode, ["surface", "hero"]), getPath(defaultMode, ["surface", "hero"]), getPath(surfaceAliases, ["hero"]))
@@ -1120,6 +1132,15 @@ var generateCssVariables = (tokens2, options = {}) => {
1120
1132
  getPath(textAliases, ["onPage", "meta"])
1121
1133
  )
1122
1134
  );
1135
+ addDark(
1136
+ toVariableName(prefix, "text", "on", "page", "brand"),
1137
+ pickSemantic(
1138
+ tokens2,
1139
+ getPath(darkMode, ["text", "onPage", "brand"]),
1140
+ getPath(defaultMode, ["text", "onPage", "brand"]),
1141
+ getPath(textAliases, ["onPage", "brand"])
1142
+ )
1143
+ );
1123
1144
  addDark(
1124
1145
  toVariableName(prefix, "text", "on", "surface", "default"),
1125
1146
  pickSemantic(
@@ -1156,6 +1177,15 @@ var generateCssVariables = (tokens2, options = {}) => {
1156
1177
  getPath(textAliases, ["onSurface", "meta"])
1157
1178
  )
1158
1179
  );
1180
+ addDark(
1181
+ toVariableName(prefix, "text", "on", "surface", "brand"),
1182
+ pickSemantic(
1183
+ tokens2,
1184
+ getPath(darkMode, ["text", "onSurface", "brand"]),
1185
+ getPath(defaultMode, ["text", "onSurface", "brand"]),
1186
+ getPath(textAliases, ["onSurface", "brand"])
1187
+ )
1188
+ );
1159
1189
  addDark(
1160
1190
  toVariableName(prefix, "component", "card", "text"),
1161
1191
  pickSemantic(