@payfit/unity-themes 2.24.1 → 2.25.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/dist/css/unity.css +1653 -543
- package/dist/esm/components/unity-theme-provider.d.ts +25 -0
- package/dist/esm/components/unity-theme-provider.js +34 -0
- package/dist/esm/components/unity-theme-provider.test.d.ts +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +8 -5
- package/dist/esm/scripts/actions/compose-multi-theme.d.ts +15 -0
- package/dist/esm/scripts/transforms/tailwind-color-token.d.ts +4 -3
- package/dist/esm/scripts/transforms/tailwind-grid-token.d.ts +2 -1
- package/dist/esm/scripts/transforms/tailwind-spacing-token.d.ts +1 -1
- package/dist/esm/scripts/transforms/tailwind-text-token.d.ts +1 -1
- package/dist/esm/scripts/transforms/tailwind-typography-token.d.ts +1 -4
- package/dist/esm/scripts/utils/prefix-transform.d.ts +4 -0
- package/dist/esm/utils/cn.d.ts +4 -3
- package/package.json +3 -2
- package/src/components/unity-theme-provider.stories.tsx +532 -0
- package/src/components/unity-theme-provider.test.tsx +150 -0
- package/src/components/unity-theme-provider.tsx +72 -0
- package/src/index.ts +8 -0
- package/src/scripts/actions/compose-multi-theme.ts +59 -0
- package/src/scripts/build.ts +261 -55
- package/src/scripts/formats/unity-theme.test.ts +180 -253
- package/src/scripts/formats/unity-theme.ts +27 -64
- package/src/scripts/transforms/tailwind-color-token.test.ts +18 -0
- package/src/scripts/transforms/tailwind-color-token.ts +7 -3
- package/src/scripts/transforms/tailwind-grid-token.test.ts +22 -0
- package/src/scripts/transforms/tailwind-grid-token.ts +7 -3
- package/src/scripts/transforms/tailwind-spacing-token.test.ts +9 -0
- package/src/scripts/transforms/tailwind-spacing-token.ts +15 -2
- package/src/scripts/transforms/tailwind-text-token.test.ts +18 -0
- package/src/scripts/transforms/tailwind-text-token.ts +15 -2
- package/src/scripts/transforms/tailwind-typography-token.test.ts +8 -2
- package/src/scripts/transforms/tailwind-typography-token.ts +5 -1
- package/src/scripts/utils/prefix-transform.test.ts +137 -0
- package/src/scripts/utils/prefix-transform.ts +16 -0
- package/src/utils/cn.ts +2 -2
- package/tokens/common/aspect-ratios.json +11 -0
- package/tokens/common/breakpoints.json +18 -0
- package/tokens/{text.json → common/font-sizes.json} +0 -28
- package/tokens/common/font-weights.json +18 -0
- package/tokens/{radii.json → common/radii.json} +0 -15
- package/tokens/{spacings.json → common/spacings.json} +0 -25
- package/tokens/legacy/radii.json +21 -0
- package/tokens/legacy/text.json +14 -0
- package/tokens/rebrand/colors.json +1400 -0
- package/tokens/rebrand/radii.json +21 -0
- package/tokens/rebrand/shadows.json +81 -0
- package/tokens/rebrand/text.json +14 -0
- package/tokens/rebrand/typography.json +329 -0
- package/dist/esm/scripts/formats/generators/header-generator.d.ts +0 -2
- package/src/scripts/formats/generators/header-generator.ts +0 -32
- /package/tokens/{animations.json → common/animations.json} +0 -0
- /package/tokens/{layout.json → common/layout.json} +0 -0
- /package/tokens/{sizes.json → common/sizes.json} +0 -0
- /package/tokens/{colors.json → legacy/colors.json} +0 -0
- /package/tokens/{shadows.json → legacy/shadows.json} +0 -0
- /package/tokens/{typography.json → legacy/typography.json} +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import React, { useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { PropsWithChildren } from 'react'
|
|
4
|
+
|
|
5
|
+
import type { UnityTheme } from './unity-theme-provider'
|
|
6
|
+
|
|
7
|
+
import preview from '../../.storybook/preview'
|
|
8
|
+
import { cn } from '../utils/cn'
|
|
9
|
+
import { UnityThemeProvider, useUnityTheme } from './unity-theme-provider'
|
|
10
|
+
|
|
11
|
+
const meta = preview.meta({
|
|
12
|
+
component: UnityThemeProvider,
|
|
13
|
+
title: '3 - Component Reference/UnityThemeProvider',
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component: `\`UnityThemeProvider\` manages the active Unity theme for your application by setting the \`data-uy-theme\` attribute on a target DOM element (defaults to \`<html>\`). All Unity CSS custom properties (\`--uy-*\`) resolve based on this attribute, so every \`uy:\` utility class updates automatically when the theme changes.
|
|
18
|
+
|
|
19
|
+
Wrap your app (or a subtree) with this provider and use the \`useUnityTheme()\` hook to read or switch the active theme at runtime. You can scope a theme to a specific element by passing a React ref or CSS selector to the \`target\` prop.
|
|
20
|
+
|
|
21
|
+
Supported themes: \`legacy\` (default) and \`rebrand\`.`,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
layout: 'padded',
|
|
25
|
+
},
|
|
26
|
+
tags: ['autodocs'],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function ThemedCard({
|
|
34
|
+
label,
|
|
35
|
+
children,
|
|
36
|
+
}: PropsWithChildren<{ label?: string }>) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="uy:rounded-md uy:border uy:border-border-neutral uy:bg-surface-neutral uy:p-300 uy:text-content-neutral uy:shadow-raising uy:typography-body uy:flex uy:flex-col uy:items-start uy:gap-100 uy:max-w-80">
|
|
39
|
+
<h2 className="uy:typography-h1 uy:text-content-primary uy:mb-100">
|
|
40
|
+
{label ?? 'Themed card'}
|
|
41
|
+
</h2>
|
|
42
|
+
{children ?? (
|
|
43
|
+
<p>
|
|
44
|
+
This card uses semantic tokens. Its appearance changes when the active
|
|
45
|
+
theme is switched between <code>legacy</code> and <code>rebrand</code>
|
|
46
|
+
.
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
49
|
+
<footer className="uy:pt-150">
|
|
50
|
+
<button className="uy:transition-all uy:cursor-pointer uy:px-300 uy:py-100 uy:rounded-md uy:bg-surface-primary uy:text-content-inverted uy:typography-action uy:grow-0 uy:hover:bg-surface-primary-hover">
|
|
51
|
+
Themed button
|
|
52
|
+
</button>
|
|
53
|
+
</footer>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ThemeSwitcher() {
|
|
59
|
+
const { theme, setTheme } = useUnityTheme()
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="uy:inline-flex uy:gap-100 uy:items-center">
|
|
63
|
+
<span id="theme-switcher-label" className="uy:typography-body">
|
|
64
|
+
Switch theme:
|
|
65
|
+
</span>
|
|
66
|
+
<div
|
|
67
|
+
role="group"
|
|
68
|
+
aria-labelledby="theme-switcher-label"
|
|
69
|
+
className="uy:inline-flex uy:gap-50 uy:w-fit uy:rounded-sm uy:bg-surface-neutral-lowest uy:p-50 uy:overflow-hidden"
|
|
70
|
+
>
|
|
71
|
+
{(['legacy', 'rebrand'] as UnityTheme[]).map(t => (
|
|
72
|
+
<button
|
|
73
|
+
key={t}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
setTheme(t)
|
|
76
|
+
}}
|
|
77
|
+
className={cn(
|
|
78
|
+
'uy:rounded-sm uy:typography-action uy:cursor-pointer uy:transition-colors uy:py-75 uy:px-150',
|
|
79
|
+
'uy:text-content-neutral-enabled uy:hover:bg-surface-neutral-hover',
|
|
80
|
+
'uy:focus-visible:outline-2 uy:focus-visible:outline-offset-2 uy:focus-visible:outline-utility-focus-ring',
|
|
81
|
+
{
|
|
82
|
+
'uy:bg-surface-neutral uy:shadow-floating': theme === t,
|
|
83
|
+
'uy:bg-transparent': theme !== t,
|
|
84
|
+
},
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{t}
|
|
88
|
+
</button>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Stories
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* You can set up theming by wrapping your application with `UnityThemeProvider` and passing the desired theme via the `theme` prop.
|
|
101
|
+
* The provider sets `data-uy-theme` on `<html>` by default, which activates the corresponding set of `--uy-*` CSS custom properties so all `uy:` utility classes resolve to the correct palette.
|
|
102
|
+
*/
|
|
103
|
+
export const BasicSetup = meta.story({
|
|
104
|
+
name: 'Basic Setup',
|
|
105
|
+
parameters: {
|
|
106
|
+
docs: {
|
|
107
|
+
source: {
|
|
108
|
+
code: `
|
|
109
|
+
import { UnityThemeProvider } from '@payfit/unity-themes'
|
|
110
|
+
|
|
111
|
+
function ThemedCard() {
|
|
112
|
+
return (
|
|
113
|
+
<div className="uy:rounded-md uy:border uy:border-border-neutral uy:bg-surface-neutral uy:p-300 uy:text-content-neutral uy:shadow-raising uy:typography-body uy:flex uy:flex-col uy:items-start uy:gap-100 uy:max-w-80">
|
|
114
|
+
<h2 className="uy:typography-h1 uy:text-content-primary uy:mb-100">
|
|
115
|
+
Legacy theme
|
|
116
|
+
</h2>
|
|
117
|
+
<p>
|
|
118
|
+
This card uses semantic tokens. Its appearance changes when the active
|
|
119
|
+
theme is switched between <code>legacy</code> and <code>rebrand</code>.
|
|
120
|
+
</p>
|
|
121
|
+
<footer className="uy:pt-150">
|
|
122
|
+
<button className="uy:transition-all uy:cursor-pointer uy:px-300 uy:py-100 uy:rounded-md uy:bg-surface-primary uy:text-content-inverted uy:typography-action uy:grow-0 uy:hover:bg-surface-primary-hover">
|
|
123
|
+
Themed button
|
|
124
|
+
</button>
|
|
125
|
+
</footer>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function App() {
|
|
131
|
+
return (
|
|
132
|
+
<UnityThemeProvider theme="legacy">
|
|
133
|
+
<ThemedCard />
|
|
134
|
+
</UnityThemeProvider>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
`.trim(),
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
render() {
|
|
142
|
+
return (
|
|
143
|
+
<UnityThemeProvider theme="legacy">
|
|
144
|
+
<div className="uy:flex uy:flex-col uy:gap-200">
|
|
145
|
+
<ThemedCard label="Legacy theme" />
|
|
146
|
+
</div>
|
|
147
|
+
</UnityThemeProvider>
|
|
148
|
+
)
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* You can switch the active theme at runtime by calling `setTheme()` from the `useUnityTheme()` hook.
|
|
154
|
+
* The provider updates the `data-uy-theme` attribute on the target element, causing all `--uy-*` CSS custom properties to re-resolve automatically without prop drilling or page reloads.
|
|
155
|
+
*/
|
|
156
|
+
export const RuntimeThemeSwitching = meta.story({
|
|
157
|
+
name: 'Runtime Theme Switching',
|
|
158
|
+
parameters: {
|
|
159
|
+
docs: {
|
|
160
|
+
source: {
|
|
161
|
+
code: `
|
|
162
|
+
import { UnityThemeProvider, useUnityTheme } from '@payfit/unity-themes'
|
|
163
|
+
import { cn } from '@payfit/unity-themes'
|
|
164
|
+
import type { UnityTheme } from '@payfit/unity-themes'
|
|
165
|
+
|
|
166
|
+
const themes: UnityTheme[] = ['legacy', 'rebrand']
|
|
167
|
+
|
|
168
|
+
function ThemeSwitcher() {
|
|
169
|
+
const { theme, setTheme } = useUnityTheme()
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className="uy:inline-flex uy:gap-100 uy:items-center">
|
|
173
|
+
<span id="theme-switcher-label" className="uy:typography-body">Switch theme:</span>
|
|
174
|
+
<div role="group" aria-labelledby="theme-switcher-label" className="uy:inline-flex uy:gap-50 uy:w-fit uy:rounded-sm uy:bg-surface-neutral-lowest uy:p-50 uy:overflow-hidden">
|
|
175
|
+
{themes.map(t => (
|
|
176
|
+
<button
|
|
177
|
+
key={t}
|
|
178
|
+
onClick={() => setTheme(t)}
|
|
179
|
+
className={
|
|
180
|
+
cn(
|
|
181
|
+
'uy:rounded-sm uy:typography-action uy:cursor-pointer uy:transition-colors uy:py-75 uy:px-150',
|
|
182
|
+
'uy:text-content-neutral-enabled uy:hover:bg-surface-neutral-hover',
|
|
183
|
+
'uy:focus-visible:outline-2 uy:focus-visible:outline-offset-2 uy:focus-visible:outline-utility-focus-ring',
|
|
184
|
+
{
|
|
185
|
+
'uy:bg-surface-neutral uy:shadow-floating': theme === t,
|
|
186
|
+
'uy:bg-transparent': theme !== t,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
{t}
|
|
192
|
+
</button>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ThemedCard() {
|
|
200
|
+
return (
|
|
201
|
+
<div className="uy:rounded-md uy:border uy:border-border-neutral uy:bg-surface-neutral uy:p-300 uy:text-content-neutral uy:shadow-raising uy:typography-body uy:flex uy:flex-col uy:items-start uy:gap-100 uy:max-w-80 uy:[&>code]:inline">
|
|
202
|
+
<h2 className="uy:typography-h1 uy:text-content-primary uy:mb-100">
|
|
203
|
+
Themed card
|
|
204
|
+
</h2>
|
|
205
|
+
<p>
|
|
206
|
+
This card uses semantic tokens. Its appearance changes when the active
|
|
207
|
+
theme is switched between <code>legacy</code> and <code>rebrand</code>.
|
|
208
|
+
</p>
|
|
209
|
+
<footer className="uy:pt-150">
|
|
210
|
+
<button className="uy:transition-all uy:cursor-pointer uy:px-300 uy:py-100 uy:rounded-md uy:bg-surface-primary uy:text-content-inverted uy:typography-action uy:grow-0 uy:hover:bg-surface-primary-hover">
|
|
211
|
+
Themed button
|
|
212
|
+
</button>
|
|
213
|
+
</footer>
|
|
214
|
+
</div>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function App() {
|
|
219
|
+
return (
|
|
220
|
+
<UnityThemeProvider theme="legacy">
|
|
221
|
+
<div className="uy:flex uy:flex-col uy:gap-200">
|
|
222
|
+
<ThemeSwitcher />
|
|
223
|
+
<ThemedCard />
|
|
224
|
+
</div>
|
|
225
|
+
</UnityThemeProvider>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
`.trim(),
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
render() {
|
|
233
|
+
return (
|
|
234
|
+
<UnityThemeProvider theme="legacy">
|
|
235
|
+
<div className="uy:flex uy:flex-col uy:gap-200">
|
|
236
|
+
<ThemeSwitcher />
|
|
237
|
+
<ThemedCard />
|
|
238
|
+
</div>
|
|
239
|
+
</UnityThemeProvider>
|
|
240
|
+
)
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* You can scope a theme to a specific DOM element by passing a React ref or CSS selector string to the `target` prop.
|
|
246
|
+
* This sets `data-uy-theme` on that element instead of `<html>`.
|
|
247
|
+
*
|
|
248
|
+
* You can specify 3 possible value types:
|
|
249
|
+
* - **`undefined` or not set:** targets `document.documentElement` (usually the `<html>` tag)
|
|
250
|
+
* - **A React ref:** targets the element that the ref points to (remember to use `useRef` and assign it via the `ref` attribute to the element).
|
|
251
|
+
* - **A string with a valid CSS selector:** the provider will use `document.querySelector()` to find the element and attach the attribute.
|
|
252
|
+
*
|
|
253
|
+
* This will allow for different parts of the page to use different themes simultaneously. e.g: a rebrand preview panel embedded in a legacy app.
|
|
254
|
+
*
|
|
255
|
+
* > ⚠️ **Warning:** Nested `UnityThemeProvider`s _do not share any state between them_, which can lead to theme discrepancies, particularly when changing it at runtime. If you use scoped themes, it is your responsibility to keep them in sync (if they need to).
|
|
256
|
+
*/
|
|
257
|
+
export const ScopedTarget = meta.story({
|
|
258
|
+
name: 'Scoped Target',
|
|
259
|
+
parameters: {
|
|
260
|
+
docs: {
|
|
261
|
+
source: {
|
|
262
|
+
code: `
|
|
263
|
+
import { useRef } from 'react'
|
|
264
|
+
import { UnityThemeProvider } from '@payfit/unity-themes'
|
|
265
|
+
|
|
266
|
+
function ThemedCard({ label, children }: PropsWithChildren<{ label: string }>) {
|
|
267
|
+
return (
|
|
268
|
+
<div className="uy:rounded-md uy:border uy:border-border-neutral uy:bg-surface-neutral uy:p-300 uy:text-content-neutral uy:shadow-raising uy:typography-body uy:flex uy:flex-col uy:items-start uy:gap-100 uy:max-w-80">
|
|
269
|
+
<h2 className="uy:typography-h1 uy:text-content-primary uy:mb-100">
|
|
270
|
+
{label}
|
|
271
|
+
</h2>
|
|
272
|
+
{children}
|
|
273
|
+
<footer className="uy:pt-150">
|
|
274
|
+
<button className="uy:transition-all uy:cursor-pointer uy:px-300 uy:py-100 uy:rounded-md uy:bg-surface-primary uy:text-content-inverted uy:typography-action uy:grow-0 uy:hover:bg-surface-primary-hover">
|
|
275
|
+
Themed button
|
|
276
|
+
</button>
|
|
277
|
+
</footer>
|
|
278
|
+
</div>
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function ScopedThemeDemo() {
|
|
283
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<UnityThemeProvider theme="legacy">
|
|
287
|
+
<div className="uy:flex uy:flex-col uy:gap-300">
|
|
288
|
+
<p className="uy:typography-body">
|
|
289
|
+
The theme below is scoped to its own container via a <code>ref</code>.
|
|
290
|
+
It does <strong>not</strong> change the global <code><html></code> attribute.
|
|
291
|
+
</p>
|
|
292
|
+
|
|
293
|
+
<ThemeSwitcher />
|
|
294
|
+
|
|
295
|
+
<div
|
|
296
|
+
ref={ref}
|
|
297
|
+
className="uy:rounded-md uy:border uy:border-border-neutral-low uy:p-300"
|
|
298
|
+
>
|
|
299
|
+
<UnityThemeProvider theme="rebrand" target={ref}>
|
|
300
|
+
<ThemedCard label="Scoped Themed Card">
|
|
301
|
+
<p>This card is inside a nested <code>UnityThemeProvider</code> with the <code>rebrand</code> theme. It will always pick up this theme despite the set theme in the root page element</p>
|
|
302
|
+
</ThemedCard>
|
|
303
|
+
</UnityThemeProvider>
|
|
304
|
+
</div>
|
|
305
|
+
<div className="uy:rounded-md uy:border uy:border-border-neutral-low uy:p-300">
|
|
306
|
+
<ThemedCard label="Global Themed Card">
|
|
307
|
+
<p>This card will take whichever theme the <code>html</code> element has set up</p>
|
|
308
|
+
</ThemedCard>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</UnityThemeProvider>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
`.trim(),
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
render() {
|
|
319
|
+
function ScopedDemo() {
|
|
320
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
321
|
+
return (
|
|
322
|
+
<UnityThemeProvider theme="legacy">
|
|
323
|
+
<div className="uy:flex uy:flex-col uy:gap-300">
|
|
324
|
+
<p className="uy:typography-body">
|
|
325
|
+
The theme below is scoped to its own container via a{' '}
|
|
326
|
+
<code>ref</code>. It does <strong>not</strong> change the global{' '}
|
|
327
|
+
<code><html></code> attribute.
|
|
328
|
+
</p>
|
|
329
|
+
|
|
330
|
+
<ThemeSwitcher />
|
|
331
|
+
|
|
332
|
+
<div
|
|
333
|
+
ref={ref}
|
|
334
|
+
className="uy:rounded-md uy:border uy:border-border-neutral-low uy:p-300"
|
|
335
|
+
>
|
|
336
|
+
<UnityThemeProvider theme="rebrand" target={ref}>
|
|
337
|
+
<ThemedCard label="Scoped Themed Card">
|
|
338
|
+
<p>
|
|
339
|
+
This card is inside a nested <code>UnityThemeProvider</code>
|
|
340
|
+
with the <code>rebrand</code> theme. It will always pick up
|
|
341
|
+
this theme despite the set theme in the root page element
|
|
342
|
+
</p>
|
|
343
|
+
</ThemedCard>
|
|
344
|
+
</UnityThemeProvider>
|
|
345
|
+
</div>
|
|
346
|
+
<div className="uy:rounded-md uy:border uy:border-border-neutral-low uy:p-300">
|
|
347
|
+
<ThemedCard label="Global Themed Card">
|
|
348
|
+
<p>
|
|
349
|
+
This card will take whichever theme the <code>html</code>{' '}
|
|
350
|
+
element has set up
|
|
351
|
+
</p>
|
|
352
|
+
</ThemedCard>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
</UnityThemeProvider>
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return <ScopedDemo />
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* The provider works by setting a `data-uy-theme` attribute on its target element. Unity's CSS defines two token blocks — `:root` for legacy and `[data-uy-theme="rebrand"]` for rebrand — so toggling the attribute instantly swaps all `--uy-*` custom properties.
|
|
365
|
+
* Open DevTools and inspect the `<html>` element to see the attribute change as you toggle between themes.
|
|
366
|
+
*/
|
|
367
|
+
export const HowItWorks = meta.story({
|
|
368
|
+
name: 'How It Works',
|
|
369
|
+
parameters: {
|
|
370
|
+
docs: {
|
|
371
|
+
source: {
|
|
372
|
+
code: `
|
|
373
|
+
import { UnityThemeProvider, useUnityTheme } from '@payfit/unity-themes'
|
|
374
|
+
|
|
375
|
+
function ThemedCard() {
|
|
376
|
+
return (
|
|
377
|
+
<div className="uy:rounded-md uy:border uy:border-border-neutral uy:bg-surface-neutral uy:p-300 uy:text-content-neutral uy:shadow-raising uy:typography-body uy:flex uy:flex-col uy:items-start uy:gap-100 uy:max-w-80">
|
|
378
|
+
<h2 className="uy:typography-h1 uy:text-content-primary uy:mb-100">
|
|
379
|
+
Themed card
|
|
380
|
+
</h2>
|
|
381
|
+
<p>
|
|
382
|
+
This card uses semantic tokens. Its appearance changes when the active
|
|
383
|
+
theme is switched between <code>legacy</code> and <code>rebrand</code>.
|
|
384
|
+
</p>
|
|
385
|
+
<footer className="uy:pt-150">
|
|
386
|
+
<button className="uy:transition-all uy:cursor-pointer uy:px-300 uy:py-100 uy:rounded-md uy:bg-surface-primary uy:text-content-inverted uy:typography-action uy:grow-0 uy:hover:bg-surface-primary-hover">
|
|
387
|
+
Themed button
|
|
388
|
+
</button>
|
|
389
|
+
</footer>
|
|
390
|
+
</div>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function Demo() {
|
|
395
|
+
const { theme } = useUnityTheme()
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<div className="uy:flex uy:flex-col uy:gap-300">
|
|
399
|
+
<div className="uy:rounded-md uy:bg-surface-neutral-lowest uy:p-300 uy:typography-body-small">
|
|
400
|
+
Applied Code: <code><html data-uy-theme="{theme}"></code>
|
|
401
|
+
</div>
|
|
402
|
+
|
|
403
|
+
<ThemeSwitcher />
|
|
404
|
+
|
|
405
|
+
<ThemedCard />
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function App() {
|
|
411
|
+
return (
|
|
412
|
+
<UnityThemeProvider theme="legacy">
|
|
413
|
+
<Demo />
|
|
414
|
+
</UnityThemeProvider>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
`.trim(),
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
render() {
|
|
422
|
+
function Demo() {
|
|
423
|
+
const { theme } = useUnityTheme()
|
|
424
|
+
return (
|
|
425
|
+
<div className="uy:flex uy:flex-col uy:gap-300">
|
|
426
|
+
<div className="uy:rounded-md uy:bg-surface-neutral-lowest uy:p-300 uy:typography-body-small">
|
|
427
|
+
Applied Code: <code><html data-uy-theme="{theme}"></code>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<ThemeSwitcher />
|
|
431
|
+
|
|
432
|
+
<ThemedCard />
|
|
433
|
+
</div>
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return (
|
|
438
|
+
<UnityThemeProvider theme="legacy">
|
|
439
|
+
<Demo />
|
|
440
|
+
</UnityThemeProvider>
|
|
441
|
+
)
|
|
442
|
+
},
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* You can apply styles conditionally per theme using the `theme-legacy:` and `theme-rebrand:` Tailwind custom variant prefixes.
|
|
447
|
+
* These variants match elements inside the corresponding `data-uy-theme` context, letting you handle one-off visual differences between themes directly in utility classes without writing raw CSS.
|
|
448
|
+
*
|
|
449
|
+
* For example, `uy:theme-legacy:bg-red-l4 uy:theme-rebrand:bg-blue-l9` renders a red background in legacy and blue in rebrand.
|
|
450
|
+
*/
|
|
451
|
+
export const ThemeSpecificStyles = meta.story({
|
|
452
|
+
name: 'Targeting Specific Themes in Styles',
|
|
453
|
+
parameters: {
|
|
454
|
+
docs: {
|
|
455
|
+
source: {
|
|
456
|
+
code: `
|
|
457
|
+
import { UnityThemeProvider } from '@payfit/unity-themes'
|
|
458
|
+
|
|
459
|
+
function Demo() {
|
|
460
|
+
return (
|
|
461
|
+
<div className="uy:flex uy:flex-col uy:gap-300">
|
|
462
|
+
<ThemeSwitcher />
|
|
463
|
+
|
|
464
|
+
<div className="uy:flex uy:gap-200">
|
|
465
|
+
<div className="uy:flex-1 uy:rounded-md uy:p-300 uy:typography-body-strong uy:text-grayscale-l1 uy:theme-legacy:bg-yellow-l6 uy:theme-rebrand:bg-orange-l7 uy:theme-legacy:text-content-neutral uy:theme-rebrand:text-content-inverted">
|
|
466
|
+
<code>theme-legacy:bg-yellow-l6</code>
|
|
467
|
+
<br />
|
|
468
|
+
<code>theme-rebrand:bg-orange-l7</code>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<div className="uy:flex-1 uy:rounded-md uy:border-2 uy:p-300 uy:typography-body-strong uy:theme-legacy:border-blue-l7 uy:theme-legacy:text-blue-l9 uy:theme-rebrand:border-plum-l7 uy:theme-rebrand:text-plum-l9">
|
|
472
|
+
<code>theme-legacy:border-blue-l7</code>
|
|
473
|
+
<br />
|
|
474
|
+
<code>theme-rebrand:border-plum-l7</code>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<p className="uy:typography-body-small uy:text-content-neutral-low">
|
|
479
|
+
Toggle the theme to see different colors applied via
|
|
480
|
+
theme-legacy: and theme-rebrand: variant prefixes.
|
|
481
|
+
</p>
|
|
482
|
+
</div>
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function App() {
|
|
487
|
+
return (
|
|
488
|
+
<UnityThemeProvider theme="legacy">
|
|
489
|
+
<Demo />
|
|
490
|
+
</UnityThemeProvider>
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
`.trim(),
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
render() {
|
|
498
|
+
function Demo() {
|
|
499
|
+
return (
|
|
500
|
+
<div className="uy:flex uy:flex-col uy:gap-300">
|
|
501
|
+
<ThemeSwitcher />
|
|
502
|
+
|
|
503
|
+
<div className="uy:flex uy:gap-200">
|
|
504
|
+
<div className="uy:flex-1 uy:rounded-md uy:p-300 uy:typography-body-strong uy:text-grayscale-l1 uy:theme-legacy:bg-yellow-l6 uy:theme-rebrand:bg-orange-l7 uy:theme-legacy:text-content-neutral uy:theme-rebrand:text-content-inverted">
|
|
505
|
+
<code>theme-legacy:bg-yellow-l6</code>
|
|
506
|
+
<br />
|
|
507
|
+
<code>theme-rebrand:bg-orange-l7</code>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div className="uy:flex-1 uy:rounded-md uy:border-2 uy:p-300 uy:typography-body-strong uy:theme-legacy:border-blue-l7 uy:theme-legacy:text-blue-l9 uy:theme-rebrand:border-plum-l7 uy:theme-rebrand:text-plum-l9">
|
|
511
|
+
<code>theme-legacy:border-blue-l7</code>
|
|
512
|
+
<br />
|
|
513
|
+
<code>theme-rebrand:border-plum-l7</code>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
<p className="uy:typography-body-small uy:text-content-neutral-low">
|
|
518
|
+
Toggle the theme to see different colors applied via{' '}
|
|
519
|
+
<code>theme-legacy:</code> and <code>theme-rebrand:</code> variant
|
|
520
|
+
prefixes.
|
|
521
|
+
</p>
|
|
522
|
+
</div>
|
|
523
|
+
)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<UnityThemeProvider theme="legacy">
|
|
528
|
+
<Demo />
|
|
529
|
+
</UnityThemeProvider>
|
|
530
|
+
)
|
|
531
|
+
},
|
|
532
|
+
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { PropsWithChildren } from 'react'
|
|
4
|
+
|
|
5
|
+
import { act, render, renderHook } from '@testing-library/react'
|
|
6
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { UnityThemeProvider, useUnityTheme } from './unity-theme-provider'
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
delete document.documentElement.dataset.uyTheme
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('UnityThemeProvider', () => {
|
|
15
|
+
it('sets data-uy-theme on the html element when mounted', () => {
|
|
16
|
+
render(
|
|
17
|
+
<UnityThemeProvider theme="rebrand">
|
|
18
|
+
<div>child</div>
|
|
19
|
+
</UnityThemeProvider>,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(document.documentElement.dataset.uyTheme).toBe('rebrand')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('defaults to legacy when no theme prop is provided', () => {
|
|
26
|
+
render(
|
|
27
|
+
<UnityThemeProvider>
|
|
28
|
+
<div>child</div>
|
|
29
|
+
</UnityThemeProvider>,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
expect(document.documentElement.dataset.uyTheme).toBe('legacy')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('removes data-uy-theme on unmount', () => {
|
|
36
|
+
const { unmount } = render(
|
|
37
|
+
<UnityThemeProvider theme="rebrand">
|
|
38
|
+
<div>child</div>
|
|
39
|
+
</UnityThemeProvider>,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expect(document.documentElement.dataset.uyTheme).toBe('rebrand')
|
|
43
|
+
|
|
44
|
+
unmount()
|
|
45
|
+
|
|
46
|
+
expect(document.documentElement.dataset.uyTheme).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('renders children unchanged', () => {
|
|
50
|
+
const { getByText } = render(
|
|
51
|
+
<UnityThemeProvider>
|
|
52
|
+
<span>hello world</span>
|
|
53
|
+
</UnityThemeProvider>,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect(getByText('hello world')).toBeTruthy()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('target', () => {
|
|
60
|
+
it('sets data-uy-theme on a custom element via CSS selector', () => {
|
|
61
|
+
const container = document.createElement('div')
|
|
62
|
+
container.id = 'theme-root'
|
|
63
|
+
document.body.appendChild(container)
|
|
64
|
+
|
|
65
|
+
render(
|
|
66
|
+
<UnityThemeProvider theme="rebrand" target="#theme-root">
|
|
67
|
+
<div>child</div>
|
|
68
|
+
</UnityThemeProvider>,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(container.dataset.uyTheme).toBe('rebrand')
|
|
72
|
+
expect(document.documentElement.dataset.uyTheme).toBeUndefined()
|
|
73
|
+
|
|
74
|
+
document.body.removeChild(container)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('sets data-uy-theme on a custom element via ref', () => {
|
|
78
|
+
const targetEl = document.createElement('section')
|
|
79
|
+
document.body.appendChild(targetEl)
|
|
80
|
+
|
|
81
|
+
function TestComponent() {
|
|
82
|
+
const ref = useRef<HTMLElement>(targetEl)
|
|
83
|
+
return (
|
|
84
|
+
<UnityThemeProvider theme="rebrand" target={ref}>
|
|
85
|
+
<div>child</div>
|
|
86
|
+
</UnityThemeProvider>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
render(<TestComponent />)
|
|
91
|
+
|
|
92
|
+
expect(targetEl.dataset.uyTheme).toBe('rebrand')
|
|
93
|
+
expect(document.documentElement.dataset.uyTheme).toBeUndefined()
|
|
94
|
+
|
|
95
|
+
document.body.removeChild(targetEl)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('useUnityTheme', () => {
|
|
101
|
+
it('returns the theme value from the provider', () => {
|
|
102
|
+
const { result } = renderHook(() => useUnityTheme(), {
|
|
103
|
+
wrapper: ({ children }) => (
|
|
104
|
+
<UnityThemeProvider theme="rebrand">{children}</UnityThemeProvider>
|
|
105
|
+
),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(result.current.theme).toBe('rebrand')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns "legacy" when used outside a provider', () => {
|
|
112
|
+
const { result } = renderHook(() => useUnityTheme())
|
|
113
|
+
|
|
114
|
+
expect(result.current.theme).toBe('legacy')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does not throw when used outside a provider', () => {
|
|
118
|
+
expect(() => renderHook(() => useUnityTheme())).not.toThrow()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('exposes setTheme to change the theme at runtime', () => {
|
|
122
|
+
const wrapper = ({ children }: PropsWithChildren) => (
|
|
123
|
+
<UnityThemeProvider theme="legacy">{children}</UnityThemeProvider>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const { result } = renderHook(() => useUnityTheme(), { wrapper })
|
|
127
|
+
|
|
128
|
+
expect(result.current.theme).toBe('legacy')
|
|
129
|
+
expect(document.documentElement.dataset.uyTheme).toBe('legacy')
|
|
130
|
+
|
|
131
|
+
act(() => {
|
|
132
|
+
result.current.setTheme('rebrand')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(result.current.theme).toBe('rebrand')
|
|
136
|
+
expect(document.documentElement.dataset.uyTheme).toBe('rebrand')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('setTheme is a no-op outside a provider', () => {
|
|
140
|
+
const { result } = renderHook(() => useUnityTheme())
|
|
141
|
+
|
|
142
|
+
expect(() => {
|
|
143
|
+
act(() => {
|
|
144
|
+
result.current.setTheme('rebrand')
|
|
145
|
+
})
|
|
146
|
+
}).not.toThrow()
|
|
147
|
+
|
|
148
|
+
expect(result.current.theme).toBe('legacy')
|
|
149
|
+
})
|
|
150
|
+
})
|