@evolve.ui/theme-system 1.1.0 → 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 (99) 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 +5 -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 +3 -0
  62. package/src/tailwind/animation.ts +30 -0
  63. package/src/tailwind/borderWidth.ts +14 -0
  64. package/src/tailwind/plugin.ts +230 -239
  65. package/src/tailwind/reducedMotion.ts +164 -0
  66. package/src/tailwind/text.ts +4 -2
  67. package/src/themeValues.snapshot.ts +6735 -0
  68. package/src/themes/EverydayHarbor.ts +147 -0
  69. package/src/themes/allThemes.ts +2 -0
  70. package/src/themes/brandSources.ts +37 -0
  71. package/src/themes/brands.ts +41 -15
  72. package/src/themes/contrastFloors.ts +26 -0
  73. package/src/themes/inkRamps.ts +43 -0
  74. package/src/themes/runtime/buildTheme.ts +6 -19
  75. package/src/themes/runtime/everydayHarbor.ts +3 -0
  76. package/src/themes/semanticColorTokenSteps.ts +5 -2
  77. package/src/themes/themeValues.ts +374 -0
  78. package/src/themes/wcag.ts +48 -0
  79. package/tailwind/plugin.cjs +5 -1
  80. package/tailwind/plugin.d.cts +458 -1
  81. package/tailwind/plugin.d.cts.map +1 -1
  82. package/tailwind/plugin.d.mts +458 -1
  83. package/tailwind/plugin.d.mts.map +1 -1
  84. package/tailwind/plugin.mjs +2 -2
  85. package/themes/runtime/default.cjs +1 -1
  86. package/themes/runtime/default.mjs +1 -1
  87. package/themes/themeType.d.cts +1 -1
  88. package/themes/themeType.d.mts +1 -1
  89. package/allThemes-CFS_9-tk.cjs.map +0 -1
  90. package/allThemes-OV-ayn4C.mjs.map +0 -1
  91. package/brands-n30rolHQ.d.cts +0 -33
  92. package/brands-n30rolHQ.d.cts.map +0 -1
  93. package/brands-n30rolHQ.d.mts +0 -33
  94. package/brands-n30rolHQ.d.mts.map +0 -1
  95. package/default-B2n4PnsJ.mjs.map +0 -1
  96. package/default-b-tgfyxR.cjs.map +0 -1
  97. package/plugin-D4eCyUJe.mjs.map +0 -1
  98. package/plugin-DPtvp4lp.cjs.map +0 -1
  99. package/src/themes/generate.ts +0 -46
@@ -0,0 +1,32 @@
1
+ import type { EffectRule } from './rules';
2
+
3
+ /**
4
+ * Recipes emitted into every effect brand file before that brand's own bindings.
5
+ * Materials come first so `hui-fx-glass hui-fx-featured` composes in source order.
6
+ * CTA ink is a channel remap, so a component's own `color` utilities still apply when the guard matches.
7
+ */
8
+ import { effectClasses } from './classes';
9
+ import { fx } from './tokens';
10
+
11
+ const NOT_INTERACTIVE = ':disabled, [aria-disabled="true"]';
12
+
13
+ /** Opt-in glass remaps the same text colours as the catalog hooks, so `hui-fx-glass` text reads the same everywhere. */
14
+ const GLASS_TEXT = { '--color-text-secondary': fx.textMutedRgb, '--color-text-success': fx.textSuccessRgb } as const;
15
+
16
+ export const vocabularyRules: readonly EffectRule[] = [
17
+ { on: effectClasses.glass, material: 'surface', radius: 'surface-radius', vars: GLASS_TEXT },
18
+ { on: effectClasses.glassRaised, material: 'raised', radius: 'surface-radius', vars: GLASS_TEXT },
19
+ { on: effectClasses.featured, shadow: 'featured' },
20
+ { on: effectClasses.halo, shadow: 'halo' },
21
+ { on: effectClasses.glow, shadow: 'glow' },
22
+ {
23
+ on: effectClasses.cta,
24
+ unless: NOT_INTERACTIVE,
25
+ paint: { background: fx.ctaFill },
26
+ vars: { '--color-text-inverse': fx.ctaInkRgb, '--color-icon-inverse': fx.ctaInkRgb },
27
+ },
28
+ { on: effectClasses.cta, when: 'hover', unless: NOT_INTERACTIVE, paint: { background: fx.ctaFillHover } },
29
+ { on: effectClasses.accentFont, paint: { fontFamily: fx.fontAccent } },
30
+ { on: effectClasses.serif, vars: { '--effect-font-accent': fx.fontSerif } },
31
+ { on: effectClasses.backdrop, paint: { background: fx.backdrop } },
32
+ ];
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  export { themePlugin } from './tailwind/plugin';
3
3
 
4
4
  // Tailwind configuration modules
5
+ export * from './tailwind/animation';
6
+ export * from './tailwind/reducedMotion';
5
7
  export * from './tailwind/colors';
6
8
  export * from './tailwind/screens';
7
9
  export * from './tailwind/spacing';
@@ -32,6 +34,7 @@ export * from './themes/runtime/buildTheme';
32
34
  export * from './themes/runtime/default';
33
35
  export * from './themes/runtime/basketballTraining';
34
36
  export * from './themes/runtime/professionalBrand';
37
+ export * from './themes/runtime/everydayHarbor';
35
38
 
36
39
  // Class name utility
37
40
  export { cn } from './utils/cn';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Animation registry for the theme system
3
+ *
4
+ * The `animate-*` utilities this library registers, extracted from `plugin.ts` so the
5
+ * reduced-motion policy can be checked against them (see `./reducedMotion`). These sit under
6
+ * `theme.extend`, so Tailwind's own defaults (`spin`, `ping`, `pulse`, `bounce`, `none`) survive
7
+ * alongside them — the resolved registry is this object layered over `defaultTheme.animation`,
8
+ * and the policy must cover both.
9
+ *
10
+ * Keyframes stay in `plugin.ts` next to the `addBase` block that emits some of them.
11
+ */
12
+ export const animation = {
13
+ fadeIn: 'fadeIn .2s ease-in-out',
14
+ fadeInOpacity: 'fadeInOpacity 0.5s ease-in-out',
15
+ fadeInScaled: 'fadeInScaled .2s ease-in',
16
+ fadeOut: 'fadeOut .2s ease-in-out',
17
+ loading: 'loading 1.4s infinite ease-in-out',
18
+ 'progress-completion': 'shrink 0.15s ease-in-out both, grow 0.15s ease-in-out 0.15s both',
19
+ 'progress-icon-fade-in': 'fadeInOpacity 0.15s ease-in-out 0.15s both',
20
+ 'progress-wave':
21
+ 'progressWave 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, progressWaveRotate 1568ms linear infinite both',
22
+ saveButtonPulse: 'saveButtonPulse 500ms cubic-bezier(0.34, 1.56, 0.64, 1)',
23
+ saveButtonWiggle: 'saveButtonWiggle 500ms ease-in-out',
24
+ saveHeartColorFlash: 'saveHeartColorFlash 500ms cubic-bezier(0.45, 0, 0.55, 1)',
25
+ saveHeartPop: 'saveHeartPop 480ms cubic-bezier(0.34, 1.56, 0.64, 1)',
26
+ saveHeartTwirlBloom: 'saveHeartTwirlBloom 500ms cubic-bezier(0.45, 0, 0.55, 1)',
27
+ saveLabelShimmer: 'saveLabelShimmer 500ms ease-in-out',
28
+ slideDown: 'slideDown 0.3s ease-in forwards',
29
+ slideUp: 'slideUp 0.3s ease-out forwards',
30
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Border width scale the theme ships.
3
+ *
4
+ * These match the widths Tailwind uses when a plugin does not replace `borderWidth`.
5
+ * They sit on the theme object so the value snapshot locks the same scale.
6
+ */
7
+
8
+ export const borderWidth = {
9
+ DEFAULT: '1px',
10
+ 0: '0px',
11
+ 2: '2px',
12
+ 4: '4px',
13
+ 8: '8px',
14
+ };
@@ -11,78 +11,111 @@
11
11
 
12
12
  import plugin from 'tailwindcss/plugin';
13
13
 
14
+ import { animation } from './animation';
14
15
  import { borderRadius } from './borderRadius';
16
+ import { borderWidth } from './borderWidth';
15
17
  import { boxShadow } from './boxShadow';
16
18
  import { colors } from './colors';
17
19
  import { fontFamily } from './fontFamily';
20
+ import { buildReducedMotionBase } from './reducedMotion';
18
21
  import { screens } from './screens';
19
22
  import { spacing } from './spacing';
20
23
  import { textComponents } from './text';
21
24
  import { width } from './width';
22
25
 
23
- const autofillStyles = {
26
+ export const autofillStyles = {
24
27
  '-webkit-box-shadow': '0 0 0 12px rgb(var(--color-background-primary)) inset !important',
25
28
  'box-shadow': '0 0 0 12px rgb(var(--color-background-primary)) inset !important',
26
29
  };
27
30
 
28
- export const themePlugin = plugin(
29
- ({ addBase, addComponents, matchUtilities }) => {
30
- // https://tailwindcss.com/docs/plugins#css-in-js-syntax
31
- addComponents(textComponents);
32
- addComponents({
33
- '.hui-autofill-transparent': {
34
- '&:-webkit-autofill': autofillStyles,
35
- '&:-webkit-autofill:active': autofillStyles,
36
- '&:-webkit-autofill:focus': autofillStyles,
37
- '&:-webkit-autofill:hover': autofillStyles,
31
+ export const tailwindTheme = {
32
+ borderRadius,
33
+ borderWidth,
34
+ boxShadow,
35
+ colors,
36
+ extend: {
37
+ animation,
38
+ keyframes: {
39
+ fadeIn: {
40
+ from: {
41
+ opacity: '0',
42
+ },
43
+ to: {
44
+ opacity: '1',
45
+ },
38
46
  },
39
- '.hui-focus-visible-outline': {
40
- '@apply focus-visible:outline-focus-primary focus-visible:outline focus-visible:outline-4': {},
47
+ fadeInOpacity: {
48
+ from: {
49
+ opacity: '0',
50
+ visibility: 'hidden',
51
+ },
52
+ to: {
53
+ opacity: '1',
54
+ visibility: 'visible',
55
+ },
41
56
  },
42
- '.hui-focus-visible-outline-inverse': {
43
- '@apply focus-visible:outline-focus-inverse focus-visible:outline focus-visible:outline-4': {},
57
+ fadeInScaled: {
58
+ from: {
59
+ opacity: '0',
60
+ transform: 'scale(0.7)',
61
+ },
62
+ to: {
63
+ opacity: '1',
64
+ transform: 'scale(1)',
65
+ },
44
66
  },
45
- '.scrollbar-default': {
46
- /* Safari and Chrome */
47
- '&::-webkit-scrollbar': {
48
- display: 'block',
67
+ fadeOut: {
68
+ from: {
69
+ opacity: '1',
70
+ transform: 'scale(1)',
71
+ },
72
+ to: {
73
+ opacity: '0',
74
+ transform: 'scale(0.7)',
49
75
  },
50
- /* IE and Edge */
51
- '-ms-overflow-style': 'auto',
52
- /* Firefox */
53
- 'scrollbar-width': 'auto',
54
76
  },
55
- '.scrollbar-hide': {
56
- /* Safari and Chrome */
57
- '&::-webkit-scrollbar': {
58
- display: 'none',
77
+ grow: {
78
+ from: { transform: 'scale(0.5)' },
79
+ to: { transform: 'scale(1)' },
80
+ },
81
+ loading: {
82
+ '0%, 100%, 80%': { transform: 'scale(0)' },
83
+ '40%': { transform: 'scale(1)' },
84
+ },
85
+ progressWave: {
86
+ '0%': {
87
+ strokeDasharray: 'var(--progress-wave-start)',
88
+ strokeDashoffset: '0',
89
+ },
90
+ '100%': {
91
+ strokeDasharray: 'var(--progress-wave-end)',
92
+ strokeDashoffset: 'var(--progress-wave-full)',
93
+ },
94
+ '50%': {
95
+ strokeDasharray: 'var(--progress-wave-mid)',
96
+ strokeDashoffset: 'var(--progress-wave-offset)',
59
97
  },
60
- /* IE and Edge */
61
- '-ms-overflow-style': 'none',
62
- /* Firefox */
63
- 'scrollbar-width': 'none',
64
98
  },
65
- });
66
-
67
- // Save/Favorite button keyframes — emitted unconditionally via addBase so they are
68
- // available in all consumer builds regardless of Tailwind content scanning.
69
- // The corresponding animate-* utilities are registered in theme.extend.animation below.
70
- addBase({
71
- '@keyframes saveButtonWiggle': {
99
+ progressWaveRotate: {
100
+ '0%': { transform: 'rotate(-90deg)' },
101
+ '100%': { transform: 'rotate(270deg)' },
102
+ },
103
+ saveButtonPulse: {
104
+ '0%': { transform: 'scale(1)' },
105
+ '35%': { transform: 'scale(1.08)' },
106
+ '65%': { transform: 'scale(0.97)' },
107
+ '100%': { transform: 'scale(1)' },
108
+ },
109
+ saveButtonWiggle: {
72
110
  '0%, 100%': { transform: 'rotate(0deg) scale(1)' },
73
- '10%': { transform: 'rotate(-6deg) scale(1.03)' },
74
- '25%': { transform: 'rotate(5deg) scale(1.03)' },
75
- '40%': { transform: 'rotate(-4deg) scale(1.02)' },
76
- '55%': { transform: 'rotate(3deg) scale(1.02)' },
77
- '70%': { transform: 'rotate(-1.5deg) scale(1.01)' },
111
+ '10%': { transform: 'rotate(-4deg) scale(1.02)' },
112
+ '25%': { transform: 'rotate(3.5deg) scale(1.02)' },
113
+ '40%': { transform: 'rotate(-2.5deg) scale(1.015)' },
114
+ '55%': { transform: 'rotate(2deg) scale(1.015)' },
115
+ '70%': { transform: 'rotate(-1deg) scale(1.01)' },
78
116
  '85%': { transform: 'rotate(0.5deg) scale(1)' },
79
117
  },
80
- '@keyframes saveHeartTwirlBloom': {
81
- '0%': { transform: 'rotateY(0deg) scale(1)' },
82
- '50%': { transform: 'rotateY(180deg) scale(1.35)' },
83
- '100%': { transform: 'rotateY(360deg) scale(1)' },
84
- },
85
- '@keyframes saveHeartColorFlash': {
118
+ saveHeartColorFlash: {
86
119
  '0%': {
87
120
  fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
88
121
  },
@@ -96,49 +129,161 @@ export const themePlugin = plugin(
96
129
  fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
97
130
  },
98
131
  },
99
- '@keyframes saveLabelShimmer': {
100
- '0%': { backgroundPosition: '200% center' },
101
- '100%': { backgroundPosition: '-200% center' },
102
- },
103
- '@keyframes saveButtonPulse': {
104
- '0%': { transform: 'scale(1)' },
105
- '35%': { transform: 'scale(1.08)' },
106
- '65%': { transform: 'scale(0.97)' },
107
- '100%': { transform: 'scale(1)' },
108
- },
109
- '@keyframes saveHeartPop': {
132
+ saveHeartPop: {
110
133
  '0%': { transform: 'scale(1)' },
111
134
  '20%': { transform: 'scale(0.6)' },
112
135
  '60%': { transform: 'scale(1.35)' },
113
136
  '100%': { transform: 'scale(1)' },
114
137
  },
115
- // Save/Favorite button animated variant — shimmer and color-flash helpers.
116
- // Placed in addBase (not addComponents) so Vite's PostCSS pipeline emits
117
- // them unconditionally. addComponents is subject to caching discrepancies
118
- // in this monorepo's Storybook build that prevent new rules from landing.
119
- '.hui-fb-shimmer': {
120
- lineHeight: 'normal',
121
- backgroundImage:
122
- 'linear-gradient(90deg, currentColor 0%, currentColor 20%, rgb(var(--hui-save-color-critical-200, var(--color-critical-200))) 33%, rgb(var(--hui-save-color-critical-500, var(--color-critical-500))) 50%, rgb(var(--hui-save-color-tertiary-400, var(--color-tertiary-400))) 67%, currentColor 80%, currentColor 100%)',
123
- backgroundSize: '200% 100%',
124
- backgroundClip: 'text',
125
- '-webkit-background-clip': 'text',
126
- '-webkit-text-fill-color': 'transparent',
127
- '@media (prefers-reduced-motion: reduce)': {
128
- backgroundImage: 'none !important',
129
- '-webkit-text-fill-color': 'currentColor !important',
130
- },
138
+ saveHeartTwirlBloom: {
139
+ '0%': { transform: 'rotateY(0deg) scale(1)' },
140
+ '30%': { transform: 'rotateY(54deg) scale(1.15)' },
141
+ '100%': { transform: 'rotateY(180deg) scale(1)' },
131
142
  },
132
- '.hui-fb-color-flash .hui-fb-icon-frame svg path': {
133
- animation: 'saveHeartColorFlash 1100ms cubic-bezier(0.45, 0, 0.55, 1) 1',
134
- animationFillMode: 'forwards',
143
+ saveLabelShimmer: {
144
+ '0%': { backgroundPosition: '200% center' },
145
+ '100%': { backgroundPosition: '-200% center' },
135
146
  },
136
- '@media (prefers-reduced-motion: reduce)': {
137
- '.hui-fb-color-flash .hui-fb-icon-frame svg path': {
138
- animation: 'none !important',
139
- },
147
+ shrink: {
148
+ from: { transform: 'scale(1)' },
149
+ to: { transform: 'scale(0.5)' },
140
150
  },
141
- });
151
+ slideDown: {
152
+ '0%': { opacity: '1', transform: 'translateY(0)' },
153
+ '100%': { opacity: '0', transform: 'translateY(100%)' },
154
+ },
155
+ slideUp: {
156
+ '0%': { opacity: '0', transform: 'translateY(100%)' },
157
+ '100%': { opacity: '1', transform: 'translateY(0)' },
158
+ },
159
+ },
160
+
161
+ maxWidth: width,
162
+ minWidth: width,
163
+ spacing,
164
+ width,
165
+ },
166
+ fontFamily,
167
+ screens,
168
+ };
169
+
170
+ export const pluginComponentStyles = {
171
+ '.hui-autofill-transparent': {
172
+ '&:-webkit-autofill': autofillStyles,
173
+ '&:-webkit-autofill:active': autofillStyles,
174
+ '&:-webkit-autofill:focus': autofillStyles,
175
+ '&:-webkit-autofill:hover': autofillStyles,
176
+ },
177
+ '.hui-focus-visible-outline': {
178
+ '@apply focus-visible:outline-focus-primary focus-visible:outline focus-visible:outline-4': {},
179
+ },
180
+ '.hui-focus-visible-outline-inverse': {
181
+ '@apply focus-visible:outline-focus-inverse focus-visible:outline focus-visible:outline-4': {},
182
+ },
183
+ '.scrollbar-default': {
184
+ /* Safari and Chrome */
185
+ '&::-webkit-scrollbar': {
186
+ display: 'block',
187
+ },
188
+ /* IE and Edge */
189
+ '-ms-overflow-style': 'auto',
190
+ /* Firefox */
191
+ 'scrollbar-width': 'auto',
192
+ },
193
+ '.scrollbar-hide': {
194
+ /* Safari and Chrome */
195
+ '&::-webkit-scrollbar': {
196
+ display: 'none',
197
+ },
198
+ /* IE and Edge */
199
+ '-ms-overflow-style': 'none',
200
+ /* Firefox */
201
+ 'scrollbar-width': 'none',
202
+ },
203
+ };
204
+
205
+ // Save/Favorite button keyframes — emitted unconditionally via addBase so they are
206
+ // available in all consumer builds regardless of Tailwind content scanning.
207
+ // The corresponding animate-* utilities are registered in theme.extend.animation.
208
+ export const pluginBaseStyles = {
209
+ '@keyframes saveButtonWiggle': {
210
+ '0%, 100%': { transform: 'rotate(0deg) scale(1)' },
211
+ '10%': { transform: 'rotate(-4deg) scale(1.02)' },
212
+ '25%': { transform: 'rotate(3.5deg) scale(1.02)' },
213
+ '40%': { transform: 'rotate(-2.5deg) scale(1.015)' },
214
+ '55%': { transform: 'rotate(2deg) scale(1.015)' },
215
+ '70%': { transform: 'rotate(-1deg) scale(1.01)' },
216
+ '85%': { transform: 'rotate(0.5deg) scale(1)' },
217
+ },
218
+ '@keyframes saveHeartTwirlBloom': {
219
+ '0%': { transform: 'rotateY(0deg) scale(1)' },
220
+ '30%': { transform: 'rotateY(54deg) scale(1.15)' },
221
+ '100%': { transform: 'rotateY(180deg) scale(1)' },
222
+ },
223
+ '@keyframes saveHeartColorFlash': {
224
+ '0%': {
225
+ fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
226
+ },
227
+ '40%': {
228
+ fill: 'rgb(var(--hui-save-color-critical-300, var(--color-critical-300)))',
229
+ },
230
+ '60%': {
231
+ fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
232
+ },
233
+ '100%': {
234
+ fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
235
+ },
236
+ },
237
+ '@keyframes saveLabelShimmer': {
238
+ '0%': { backgroundPosition: '200% center' },
239
+ '100%': { backgroundPosition: '-200% center' },
240
+ },
241
+ '@keyframes saveButtonPulse': {
242
+ '0%': { transform: 'scale(1)' },
243
+ '35%': { transform: 'scale(1.08)' },
244
+ '65%': { transform: 'scale(0.97)' },
245
+ '100%': { transform: 'scale(1)' },
246
+ },
247
+ '@keyframes saveHeartPop': {
248
+ '0%': { transform: 'scale(1)' },
249
+ '20%': { transform: 'scale(0.6)' },
250
+ '60%': { transform: 'scale(1.35)' },
251
+ '100%': { transform: 'scale(1)' },
252
+ },
253
+ // Save/Favorite button animated variant — shimmer and color-flash helpers.
254
+ // Placed in addBase (not addComponents) so Vite's PostCSS pipeline emits
255
+ // them unconditionally. addComponents is subject to caching discrepancies
256
+ // in this monorepo's Storybook build that prevent new rules from landing.
257
+ '.hui-fb-shimmer': {
258
+ lineHeight: 'normal',
259
+ backgroundImage:
260
+ 'linear-gradient(90deg, currentColor 0%, currentColor 20%, rgb(var(--hui-save-color-critical-200, var(--color-critical-200))) 33%, rgb(var(--hui-save-color-critical-500, var(--color-critical-500))) 50%, rgb(var(--hui-save-color-tertiary-400, var(--color-tertiary-400))) 67%, currentColor 80%, currentColor 100%)',
261
+ backgroundSize: '200% 100%',
262
+ backgroundClip: 'text',
263
+ '-webkit-background-clip': 'text',
264
+ '-webkit-text-fill-color': 'transparent',
265
+ '@media (prefers-reduced-motion: reduce)': {
266
+ backgroundImage: 'none !important',
267
+ '-webkit-text-fill-color': 'currentColor !important',
268
+ },
269
+ },
270
+ '.hui-fb-color-flash .hui-fb-icon-frame svg path': {
271
+ animation: 'saveHeartColorFlash 500ms cubic-bezier(0.45, 0, 0.55, 1) 1',
272
+ animationFillMode: 'forwards',
273
+ },
274
+ // Reduced-motion gating for the whole animation registry (DS-95). This replaces the
275
+ // per-selector `animation: none` block that used to live here — see
276
+ // `./reducedMotion` for why duration-collapse rather than removal, and why the
277
+ // `.hui-fb-shimmer` paint block above is a deliberate exception to it.
278
+ ...buildReducedMotionBase(),
279
+ };
280
+
281
+ export const themePlugin = plugin(
282
+ ({ addBase, addComponents, matchUtilities }) => {
283
+ // https://tailwindcss.com/docs/plugins#css-in-js-syntax
284
+ addComponents(textComponents);
285
+ addComponents(pluginComponentStyles);
286
+ addBase(pluginBaseStyles);
142
287
 
143
288
  matchUtilities({
144
289
  'grid-area': (value) => {
@@ -149,160 +294,6 @@ export const themePlugin = plugin(
149
294
  });
150
295
  },
151
296
  {
152
- theme: {
153
- borderRadius,
154
- boxShadow,
155
- colors,
156
- extend: {
157
- animation: {
158
- fadeIn: 'fadeIn .2s ease-in-out',
159
- fadeInOpacity: 'fadeInOpacity 0.5s ease-in-out',
160
- fadeInScaled: 'fadeInScaled .2s ease-in',
161
- fadeOut: 'fadeOut .2s ease-in-out',
162
- loading: 'loading 1.4s infinite ease-in-out',
163
- 'progress-completion': 'shrink 0.15s ease-in-out both, grow 0.15s ease-in-out 0.15s both',
164
- 'progress-icon-fade-in': 'fadeInOpacity 0.15s ease-in-out 0.15s both',
165
- 'progress-wave':
166
- 'progressWave 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, progressWaveRotate 1568ms linear infinite both',
167
- saveButtonPulse: 'saveButtonPulse 500ms cubic-bezier(0.34, 1.56, 0.64, 1)',
168
- saveButtonWiggle: 'saveButtonWiggle 1.3s cubic-bezier(0.36, 0, 0.66, -0.56)',
169
- saveHeartColorFlash: 'saveHeartColorFlash 1100ms cubic-bezier(0.45, 0, 0.55, 1)',
170
- saveHeartPop: 'saveHeartPop 480ms cubic-bezier(0.34, 1.56, 0.64, 1)',
171
- saveHeartTwirlBloom: 'saveHeartTwirlBloom 1100ms cubic-bezier(0.45, 0, 0.55, 1)',
172
- saveLabelShimmer: 'saveLabelShimmer 1100ms ease-in-out',
173
- slideDown: 'slideDown 0.3s ease-in forwards',
174
- slideUp: 'slideUp 0.3s ease-out forwards',
175
- },
176
- keyframes: {
177
- fadeIn: {
178
- from: {
179
- opacity: '0',
180
- },
181
- to: {
182
- opacity: '1',
183
- },
184
- },
185
- fadeInOpacity: {
186
- from: {
187
- opacity: '0',
188
- visibility: 'hidden',
189
- },
190
- to: {
191
- opacity: '1',
192
- visibility: 'visible',
193
- },
194
- },
195
- fadeInScaled: {
196
- from: {
197
- opacity: '0',
198
- transform: 'scale(0.7)',
199
- },
200
- to: {
201
- opacity: '1',
202
- transform: 'scale(1)',
203
- },
204
- },
205
- fadeOut: {
206
- from: {
207
- opacity: '1',
208
- transform: 'scale(1)',
209
- },
210
- to: {
211
- opacity: '0',
212
- transform: 'scale(0.7)',
213
- },
214
- },
215
- grow: {
216
- from: { transform: 'scale(0.5)' },
217
- to: { transform: 'scale(1)' },
218
- },
219
- loading: {
220
- '0%, 100%, 80%': { transform: 'scale(0)' },
221
- '40%': { transform: 'scale(1)' },
222
- },
223
- progressWave: {
224
- '0%': {
225
- strokeDasharray: 'var(--progress-wave-start)',
226
- strokeDashoffset: '0',
227
- },
228
- '100%': {
229
- strokeDasharray: 'var(--progress-wave-end)',
230
- strokeDashoffset: 'var(--progress-wave-full)',
231
- },
232
- '50%': {
233
- strokeDasharray: 'var(--progress-wave-mid)',
234
- strokeDashoffset: 'var(--progress-wave-offset)',
235
- },
236
- },
237
- progressWaveRotate: {
238
- '0%': { transform: 'rotate(-90deg)' },
239
- '100%': { transform: 'rotate(270deg)' },
240
- },
241
- saveButtonPulse: {
242
- '0%': { transform: 'scale(1)' },
243
- '35%': { transform: 'scale(1.08)' },
244
- '65%': { transform: 'scale(0.97)' },
245
- '100%': { transform: 'scale(1)' },
246
- },
247
- saveButtonWiggle: {
248
- '0%, 100%': { transform: 'rotate(0deg) scale(1)' },
249
- '10%': { transform: 'rotate(-6deg) scale(1.03)' },
250
- '25%': { transform: 'rotate(5deg) scale(1.03)' },
251
- '40%': { transform: 'rotate(-4deg) scale(1.02)' },
252
- '55%': { transform: 'rotate(3deg) scale(1.02)' },
253
- '70%': { transform: 'rotate(-1.5deg) scale(1.01)' },
254
- '85%': { transform: 'rotate(0.5deg) scale(1)' },
255
- },
256
- saveHeartColorFlash: {
257
- '0%': {
258
- fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
259
- },
260
- '40%': {
261
- fill: 'rgb(var(--hui-save-color-critical-300, var(--color-critical-300)))',
262
- },
263
- '60%': {
264
- fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
265
- },
266
- '100%': {
267
- fill: 'rgb(var(--hui-save-color-critical-500, var(--color-critical-500)))',
268
- },
269
- },
270
- saveHeartPop: {
271
- '0%': { transform: 'scale(1)' },
272
- '20%': { transform: 'scale(0.6)' },
273
- '60%': { transform: 'scale(1.35)' },
274
- '100%': { transform: 'scale(1)' },
275
- },
276
- saveHeartTwirlBloom: {
277
- '0%': { transform: 'rotateY(0deg) scale(1)' },
278
- '50%': { transform: 'rotateY(180deg) scale(1.35)' },
279
- '100%': { transform: 'rotateY(360deg) scale(1)' },
280
- },
281
- saveLabelShimmer: {
282
- '0%': { backgroundPosition: '200% center' },
283
- '100%': { backgroundPosition: '-200% center' },
284
- },
285
- shrink: {
286
- from: { transform: 'scale(1)' },
287
- to: { transform: 'scale(0.5)' },
288
- },
289
- slideDown: {
290
- '0%': { opacity: '1', transform: 'translateY(0)' },
291
- '100%': { opacity: '0', transform: 'translateY(100%)' },
292
- },
293
- slideUp: {
294
- '0%': { opacity: '0', transform: 'translateY(100%)' },
295
- '100%': { opacity: '1', transform: 'translateY(0)' },
296
- },
297
- },
298
-
299
- maxWidth: width,
300
- minWidth: width,
301
- spacing,
302
- width,
303
- },
304
- fontFamily,
305
- screens,
306
- },
297
+ theme: tailwindTheme,
307
298
  }
308
299
  );