@evolve.ui/theme-system 1.0.1 → 1.2.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.
Files changed (119) hide show
  1. package/{allThemes-CFS_9-tk.cjs → allThemes-3d8nhymo.cjs} +12 -2
  2. package/allThemes-3d8nhymo.cjs.map +1 -0
  3. package/{allThemes-OV-ayn4C.mjs → allThemes-Abn6KDm7.mjs} +7 -3
  4. package/allThemes-Abn6KDm7.mjs.map +1 -0
  5. package/allThemes.cjs +1 -1
  6. package/allThemes.d.cts +1 -0
  7. package/allThemes.d.cts.map +1 -1
  8. package/allThemes.d.mts +1 -0
  9. package/allThemes.d.mts.map +1 -1
  10. package/allThemes.mjs +1 -1
  11. package/brands-CE0wIkKX.d.cts +58 -0
  12. package/brands-CE0wIkKX.d.cts.map +1 -0
  13. package/brands-CE0wIkKX.d.mts +58 -0
  14. package/brands-CE0wIkKX.d.mts.map +1 -0
  15. package/brands.cjs +16 -13
  16. package/brands.cjs.map +1 -1
  17. package/brands.d.cts +2 -2
  18. package/brands.d.mts +2 -2
  19. package/brands.mjs +16 -13
  20. package/brands.mjs.map +1 -1
  21. package/css/EverydayHarbor.css +377 -0
  22. package/css/EverydayHarbor.effects.css +650 -0
  23. package/{default-b-tgfyxR.cjs → default-B6VWF0Js.cjs} +237 -36
  24. package/default-B6VWF0Js.cjs.map +1 -0
  25. package/{default-B2n4PnsJ.mjs → default-BogJDhKE.mjs} +238 -37
  26. package/default-BogJDhKE.mjs.map +1 -0
  27. package/effects.cjs +26 -0
  28. package/effects.cjs.map +1 -0
  29. package/effects.d.cts +22 -0
  30. package/effects.d.cts.map +1 -0
  31. package/effects.d.mts +22 -0
  32. package/effects.d.mts.map +1 -0
  33. package/effects.mjs +22 -0
  34. package/effects.mjs.map +1 -0
  35. package/evolve-ui.entries.json +51 -0
  36. package/index.cjs +12 -5
  37. package/index.cjs.map +1 -1
  38. package/index.d.cts +108 -4
  39. package/index.d.cts.map +1 -1
  40. package/index.d.mts +108 -4
  41. package/index.d.mts.map +1 -1
  42. package/index.mjs +9 -6
  43. package/index.mjs.map +1 -1
  44. package/package.json +5 -1
  45. package/{plugin-DPtvp4lp.cjs → plugin-B3PF4RMI.cjs} +299 -110
  46. package/plugin-B3PF4RMI.cjs.map +1 -0
  47. package/{plugin-D4eCyUJe.mjs → plugin-D6aLCeNB.mjs} +258 -111
  48. package/plugin-D6aLCeNB.mjs.map +1 -0
  49. package/src/effects/__fixtures__/fixtureLayer.ts +88 -0
  50. package/src/effects/brands/EverydayHarbor.ts +392 -0
  51. package/src/effects/classes.ts +19 -0
  52. package/src/effects/emit.ts +314 -0
  53. package/src/effects/hooks.ts +493 -0
  54. package/src/effects/index.ts +2 -0
  55. package/src/effects/layers.ts +18 -0
  56. package/src/effects/rules.ts +90 -0
  57. package/src/effects/rules.typecheck.ts +50 -0
  58. package/src/effects/tokens.ts +148 -0
  59. package/src/effects/validate.ts +683 -0
  60. package/src/effects/vocabulary.ts +32 -0
  61. package/src/index.ts +40 -0
  62. package/src/tailwind/animation.ts +30 -0
  63. package/src/tailwind/borderRadius.ts +32 -0
  64. package/src/tailwind/borderWidth.ts +14 -0
  65. package/src/tailwind/boxShadow.ts +19 -0
  66. package/src/tailwind/colors.ts +332 -0
  67. package/src/tailwind/cssVars.ts +301 -0
  68. package/src/tailwind/fontFamily.ts +14 -0
  69. package/src/tailwind/plugin.ts +299 -0
  70. package/src/tailwind/reducedMotion.ts +164 -0
  71. package/src/tailwind/screens.ts +22 -0
  72. package/src/tailwind/spacing.ts +20 -0
  73. package/src/tailwind/text.ts +172 -0
  74. package/src/tailwind/width.ts +14 -0
  75. package/src/themeValues.snapshot.ts +6735 -0
  76. package/src/themes/BasketballTraining.ts +166 -0
  77. package/src/themes/Default.ts +165 -0
  78. package/src/themes/EverydayHarbor.ts +147 -0
  79. package/src/themes/ProfessionalBrand.ts +166 -0
  80. package/src/themes/allThemes.ts +14 -0
  81. package/src/themes/brandSources.ts +37 -0
  82. package/src/themes/brands.ts +63 -0
  83. package/src/themes/common.ts +65 -0
  84. package/src/themes/contrastFloors.ts +26 -0
  85. package/src/themes/generateCSSVars.ts +123 -0
  86. package/src/themes/inkRamps.ts +43 -0
  87. package/src/themes/runtime/basketballTraining.ts +3 -0
  88. package/src/themes/runtime/buildTheme.ts +411 -0
  89. package/src/themes/runtime/default.ts +3 -0
  90. package/src/themes/runtime/everydayHarbor.ts +3 -0
  91. package/src/themes/runtime/professionalBrand.ts +3 -0
  92. package/src/themes/semanticColorTokenSteps.ts +23 -0
  93. package/src/themes/themeType.ts +169 -0
  94. package/src/themes/themeValues.ts +374 -0
  95. package/src/themes/wcag.ts +48 -0
  96. package/src/themes/writeToCSSFile.ts +11 -0
  97. package/src/utils/cn.ts +35 -0
  98. package/src/utils/color.ts +82 -0
  99. package/src/utils/wrapVar.ts +24 -0
  100. package/tailwind/plugin.cjs +5 -1
  101. package/tailwind/plugin.d.cts +458 -1
  102. package/tailwind/plugin.d.cts.map +1 -1
  103. package/tailwind/plugin.d.mts +458 -1
  104. package/tailwind/plugin.d.mts.map +1 -1
  105. package/tailwind/plugin.mjs +2 -2
  106. package/themes/runtime/default.cjs +1 -1
  107. package/themes/runtime/default.mjs +1 -1
  108. package/themes/themeType.d.cts +1 -1
  109. package/themes/themeType.d.mts +1 -1
  110. package/allThemes-CFS_9-tk.cjs.map +0 -1
  111. package/allThemes-OV-ayn4C.mjs.map +0 -1
  112. package/brands-n30rolHQ.d.cts +0 -33
  113. package/brands-n30rolHQ.d.cts.map +0 -1
  114. package/brands-n30rolHQ.d.mts +0 -33
  115. package/brands-n30rolHQ.d.mts.map +0 -1
  116. package/default-B2n4PnsJ.mjs.map +0 -1
  117. package/default-b-tgfyxR.cjs.map +0 -1
  118. package/plugin-D4eCyUJe.mjs.map +0 -1
  119. package/plugin-DPtvp4lp.cjs.map +0 -1
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Build-time effect sheet. Called from tsdown `onSuccess` for each registered brand.
3
+ * The build writes `*.effects.css` only for brands in the registry.
4
+ *
5
+ * Order: layer statement, token block, two identical fallback remaps, then `@layer hui-effects`
6
+ * (vocabulary rules, then the brand's rules). No palette block and no `:root`.
7
+ * Pseudo-element declarations are enforced: Tailwind preflight's unlayered `::before, ::after`
8
+ * reset would otherwise drop a layered-normal edge or dashed frame.
9
+ */
10
+ import { mkdirSync, writeFileSync } from 'node:fs';
11
+ import path from 'node:path';
12
+
13
+ import { brandsToCSS } from '../themes/brands';
14
+ import { effectClasses } from './classes';
15
+ import { effectHooks, freePseudoSlot, hookSelector, type EffectHookId, type PseudoSlot } from './hooks';
16
+ import { effectLayers } from './layers';
17
+ import { materials, type Decoration, type EffectLayer, type EffectRule, type PaintProperty } from './rules';
18
+ import { effectTokens, type EffectTokenName, type EffectTokenSpec } from './tokens';
19
+ import { validateEffectLayer, validateRules } from './validate';
20
+ import { vocabularyRules } from './vocabulary';
21
+
22
+ export const EFFECT_LAYER = 'hui-effects';
23
+
24
+ export const BACKDROP_SUPPORT = '(-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))';
25
+
26
+ /** Reduced transparency, increased contrast, or a forced palette. Both fallbacks share one body. */
27
+ export const TRANSPARENCY_OFF =
28
+ '(prefers-reduced-transparency: reduce), (prefers-contrast: more), (forced-colors: active)';
29
+
30
+ /** Keeps Tailwind `ring-*` when an effect replaces `box-shadow`. */
31
+ export const RING_PREFIX = 'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000)';
32
+
33
+ const EDGE_DECLARATIONS = [
34
+ ['content', "''"],
35
+ ['position', 'absolute'],
36
+ ['inset', '0'],
37
+ ['border-radius', 'inherit'],
38
+ ['padding', '1px'],
39
+ ['background', 'var(--effect-edge)'],
40
+ ['-webkit-mask', 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)'],
41
+ ['mask', 'linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)'],
42
+ ['-webkit-mask-composite', 'xor'],
43
+ ['mask-composite', 'exclude'],
44
+ ['pointer-events', 'none'],
45
+ ] as const;
46
+
47
+ const PAINT_CSS: Record<PaintProperty, string> = {
48
+ backdropFilter: 'backdrop-filter',
49
+ background: 'background',
50
+ backgroundColor: 'background-color',
51
+ backgroundImage: 'background-image',
52
+ color: 'color',
53
+ fontFamily: 'font-family',
54
+ };
55
+
56
+ const PAINT_ORDER: readonly PaintProperty[] = [
57
+ 'background',
58
+ 'backgroundColor',
59
+ 'backgroundImage',
60
+ 'backdropFilter',
61
+ 'color',
62
+ 'fontFamily',
63
+ ];
64
+
65
+ type Declaration = readonly [property: string, value: string, important: boolean];
66
+
67
+ export type GenerateEffectSheetInput = {
68
+ readonly className: string;
69
+ readonly siblings: readonly string[];
70
+ readonly layer: EffectLayer;
71
+ };
72
+
73
+ export type EmitEffectSheetsInput = {
74
+ readonly outDir: string;
75
+ readonly layers?: Readonly<Record<string, EffectLayer>>;
76
+ readonly classNames?: Readonly<Record<string, string>>;
77
+ };
78
+
79
+ const isOptInClass = (value: string): boolean => (Object.values(effectClasses) as readonly string[]).includes(value);
80
+
81
+ const hasOwnHook = (on: string): boolean => Object.prototype.hasOwnProperty.call(effectHooks, on);
82
+
83
+ const onIsHook = (on: string): on is EffectHookId => hasOwnHook(on) && !isOptInClass(on);
84
+
85
+ const ruleBlock = (selector: string, declarations: readonly Declaration[]): string => {
86
+ const body = declarations
87
+ .map(([property, value, important]) => ` ${property}: ${value}${important ? ' !important' : ''};`)
88
+ .join('\n');
89
+ return ` ${selector} {\n${body}\n }`;
90
+ };
91
+
92
+ const tokenValue = (tokens: EffectLayer['tokens'], key: EffectTokenName, spec: EffectTokenSpec): string => {
93
+ if (Object.prototype.hasOwnProperty.call(tokens, key)) {
94
+ const value = Reflect.get(tokens, key) as unknown;
95
+ if (typeof value === 'string') {
96
+ return value;
97
+ }
98
+ }
99
+ if (spec.default !== undefined) {
100
+ return spec.default;
101
+ }
102
+ throw new Error(`Missing effect token ${key}`);
103
+ };
104
+
105
+ const tokenBlock = (className: string, tokens: EffectLayer['tokens']): string => {
106
+ const lines = (Object.entries(effectTokens) as [EffectTokenName, EffectTokenSpec][]).map(
107
+ ([key, spec]) => ` --effect-${key}: ${tokenValue(tokens, key, spec)};`
108
+ );
109
+ return `.${className} {\n${lines.join('\n')}\n}`;
110
+ };
111
+
112
+ const fallbackBody = (className: string): string => {
113
+ const lines = (Object.entries(effectTokens) as [EffectTokenName, EffectTokenSpec][]).flatMap(([key, spec]) => {
114
+ if (spec.opaque === undefined) {
115
+ return [];
116
+ }
117
+ const value = spec.opaque === 'none' ? 'none' : `var(--effect-${spec.opaque})`;
118
+ return [` --effect-${key}: ${value};`];
119
+ });
120
+ return ` .${className} {\n${lines.join('\n')}\n }`;
121
+ };
122
+
123
+ /** `Object.values(brandsToCSS)` minus `self`, sorted. */
124
+ export const siblingBrandClasses = (self: string): string[] =>
125
+ Object.values(brandsToCSS)
126
+ .filter((className) => className !== self)
127
+ .sort();
128
+
129
+ /**
130
+ * `:where(.brand-x <compound>:not(.hui-fx-none)[:not(.other)…:not(.brand-x :is(.other) *)])`.
131
+ * The island guard is omitted when there are no siblings. Pseudo-elements stay outside `:where`.
132
+ * A brand island is one level deep; an element under `.brand-x .brand-other .brand-x` gets no effect rules (it fails safe).
133
+ */
134
+ export const scopeFor =
135
+ (className: string, siblings: readonly string[]) =>
136
+ (compound: string): string => {
137
+ // The island root is excluded as well as its subtree: a locked brand never defines `--effect-*`, so an element
138
+ // carrying another brand's class would otherwise inherit this brand's tokens. One `:not()` per class, because
139
+ // jsdom's selector engine misreads `:not(.x :is(...))` (Chromium agrees with this form).
140
+ const island =
141
+ siblings.length === 0
142
+ ? ''
143
+ : `${siblings.map((sibling) => `:not(.${sibling})`).join('')}:not(.${className} :is(${siblings
144
+ .map((sibling) => `.${sibling}`)
145
+ .join(', ')}) *)`;
146
+ return `:where(.${className} ${compound}:not(.${effectClasses.none})${island})`;
147
+ };
148
+
149
+ export const composeShadow = (token: string): string => `${RING_PREFIX}, var(--effect-${token})`;
150
+
151
+ /** First pseudo slot the target does not already paint. Opt-in classes start at `before`. */
152
+ export const edgeSlotFor = (on: string): PseudoSlot => {
153
+ const taken = isOptInClass(on) || !hasOwnHook(on) ? [] : effectHooks[on as EffectHookId].taken;
154
+ const slot = freePseudoSlot(taken);
155
+ if (slot === undefined) {
156
+ throw new Error(`No free pseudo slot for ${on}`);
157
+ }
158
+ return slot;
159
+ };
160
+
161
+ const decorationDeclarations = (decoration: Decoration): Declaration[] => {
162
+ const declarations: Declaration[] = [
163
+ ['content', "''", true],
164
+ ['position', 'absolute', true],
165
+ ['pointer-events', 'none', true],
166
+ ];
167
+ if (decoration.inset !== undefined) {
168
+ declarations.push(['inset', decoration.inset, true]);
169
+ }
170
+ if (decoration.borderRadius !== undefined) {
171
+ declarations.push(['border-radius', decoration.borderRadius, true]);
172
+ }
173
+ if (decoration.borderWidth !== undefined) {
174
+ declarations.push(['border-width', decoration.borderWidth, true]);
175
+ }
176
+ if (decoration.borderStyle !== undefined) {
177
+ declarations.push(['border-style', decoration.borderStyle, true]);
178
+ }
179
+ if (decoration.borderColor !== undefined) {
180
+ declarations.push(['border-color', decoration.borderColor, true]);
181
+ }
182
+ if (decoration.background !== undefined) {
183
+ declarations.push(['background', decoration.background, true]);
184
+ }
185
+ if (decoration.boxShadow !== undefined) {
186
+ declarations.push(['box-shadow', decoration.boxShadow, true]);
187
+ }
188
+ return declarations;
189
+ };
190
+
191
+ /** One rule becomes an enforced block, a default block, an edge block, and decoration blocks. */
192
+ export const emitRule = (rule: EffectRule, scope: (compound: string) => string): string => {
193
+ const optIn = isOptInClass(rule.on);
194
+ const compound = `${optIn ? `.${rule.on}` : hookSelector(rule.on as EffectHookId)}${
195
+ rule.unless !== undefined ? `:not(${rule.unless})` : ''
196
+ }${rule.when !== undefined ? `:${rule.when}` : ''}`;
197
+ const scoped = (pseudo: '' | '::before' | '::after'): string => `${scope(compound)}${pseudo}`;
198
+ const blocks: string[] = [];
199
+
200
+ const enforced: Declaration[] = [];
201
+ if (rule.material !== undefined) {
202
+ const material = materials[rule.material];
203
+ enforced.push(['background', `var(--effect-${material.fill})`, true]);
204
+ enforced.push(['-webkit-backdrop-filter', `var(--effect-${material.filter})`, true]);
205
+ enforced.push(['backdrop-filter', `var(--effect-${material.filter})`, true]);
206
+ if (material.shadow !== null) {
207
+ enforced.push(['box-shadow', composeShadow(material.shadow), true]);
208
+ }
209
+ }
210
+ if (rule.shadow !== undefined) {
211
+ enforced.push(['box-shadow', composeShadow(rule.shadow), true]);
212
+ }
213
+ if (rule.paint !== undefined) {
214
+ for (const property of PAINT_ORDER) {
215
+ const value = rule.paint[property];
216
+ if (value === undefined) {
217
+ continue;
218
+ }
219
+ if (property === 'backdropFilter') {
220
+ enforced.push(['-webkit-backdrop-filter', value, true]);
221
+ }
222
+ enforced.push([PAINT_CSS[property], value, true]);
223
+ }
224
+ }
225
+ if (enforced.length > 0) {
226
+ blocks.push(ruleBlock(scoped(''), enforced));
227
+ }
228
+
229
+ const defaults: Declaration[] = [];
230
+ if (rule.vars !== undefined) {
231
+ for (const [key, value] of Object.entries(rule.vars)) {
232
+ if (value !== undefined) {
233
+ defaults.push([key, value, false]);
234
+ }
235
+ }
236
+ }
237
+ if (rule.radius !== undefined) {
238
+ defaults.push(['border-radius', `var(--effect-${rule.radius})`, false]);
239
+ }
240
+ const paintsEdge = rule.material !== undefined && materials[rule.material].edge && rule.when === undefined;
241
+ const paintsDecoration = rule.before !== undefined || rule.after !== undefined;
242
+ const unpositionedHook = onIsHook(rule.on) && !effectHooks[rule.on].positioned;
243
+ if ((optIn || unpositionedHook) && (paintsEdge || paintsDecoration)) {
244
+ defaults.push(['position', 'relative', false]);
245
+ }
246
+ if (defaults.length > 0) {
247
+ blocks.push(ruleBlock(scoped(''), defaults));
248
+ }
249
+
250
+ if (paintsEdge) {
251
+ const slot = edgeSlotFor(rule.on);
252
+ blocks.push(
253
+ ruleBlock(
254
+ scoped(slot === 'before' ? '::before' : '::after'),
255
+ EDGE_DECLARATIONS.map(([property, value]) => [property, value, true])
256
+ )
257
+ );
258
+ }
259
+ if (rule.before !== undefined) {
260
+ blocks.push(ruleBlock(scoped('::before'), decorationDeclarations(rule.before)));
261
+ }
262
+ if (rule.after !== undefined) {
263
+ blocks.push(ruleBlock(scoped('::after'), decorationDeclarations(rule.after)));
264
+ }
265
+ return blocks.join('\n\n');
266
+ };
267
+
268
+ export const generateEffectSheet = ({ className, layer, siblings }: GenerateEffectSheetInput): string => {
269
+ validateEffectLayer(layer);
270
+ validateRules(vocabularyRules, 'vocabulary');
271
+ const body = fallbackBody(className);
272
+ const scope = scopeFor(className, siblings);
273
+ const layerBlock = `@layer ${EFFECT_LAYER} {\n${[...vocabularyRules, ...layer.rules]
274
+ .map((rule) => emitRule(rule, scope))
275
+ .filter((block) => block !== '')
276
+ .join('\n\n')}\n}`;
277
+ return `${[
278
+ `@layer ${EFFECT_LAYER};`,
279
+ tokenBlock(className, layer.tokens),
280
+ `@supports not (${BACKDROP_SUPPORT}) {\n${body}\n}`,
281
+ `@media ${TRANSPARENCY_OFF} {\n${body}\n}`,
282
+ layerBlock,
283
+ ].join('\n\n')}\n`;
284
+ };
285
+
286
+ /**
287
+ * Writes `<brand>.effects.css` for each registry entry. An empty registry writes nothing.
288
+ * `layers` and `classNames` are overrides for tests; the build calls this with `outDir` only.
289
+ */
290
+ export const emitEffectSheets = ({
291
+ classNames,
292
+ layers = effectLayers as Readonly<Record<string, EffectLayer>>,
293
+ outDir,
294
+ }: EmitEffectSheetsInput): string[] => {
295
+ const entries = Object.entries(layers).sort(([left], [right]) => left.localeCompare(right));
296
+ if (entries.length === 0) {
297
+ return [];
298
+ }
299
+ mkdirSync(outDir, { recursive: true });
300
+ const written: string[] = [];
301
+ for (const [brand, layer] of entries) {
302
+ const className = classNames?.[brand] ?? (brandsToCSS as Record<string, string>)[brand];
303
+ if (className === undefined) {
304
+ throw new Error(`No brand class for effect layer ${brand}`);
305
+ }
306
+ const fileName = `${brand}.effects.css`;
307
+ writeFileSync(
308
+ path.join(outDir, fileName),
309
+ generateEffectSheet({ className, layer, siblings: siblingBrandClasses(className) })
310
+ );
311
+ written.push(fileName);
312
+ }
313
+ return written;
314
+ };