diff --git a/dprint.json b/dprint.json index c03c892533..cd0e1c6a2f 100644 --- a/dprint.json +++ b/dprint.json @@ -7,5 +7,8 @@ "**/node_modules", "!packages/ui-themes/src/themes/newThemeTokens/" ], - "plugins": ["https://plugins.dprint.dev/typescript-0.95.11.wasm"] + "plugins": [ + "https://plugins.dprint.dev/typescript-0.95.11.wasm", + "https://plugins.dprint.dev/g-plane/malva-v0.16.0.wasm" + ] } diff --git a/packages/ui-scripts/lib/build/buildThemes/buildCSSVariables.ts b/packages/ui-scripts/lib/build/buildThemes/buildCSSVariables.ts new file mode 100644 index 0000000000..393a645e6a --- /dev/null +++ b/packages/ui-scripts/lib/build/buildThemes/buildCSSVariables.ts @@ -0,0 +1,81 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { promises as fs } from 'fs' +import path from 'path' +import { pathToFileURL } from 'url' + +const importDefault = async (file: string) => + (await import(pathToFileURL(file).href)).default + +const loadTheme = async (themePath: string) => { + const [primitives, semantics, sharedTokens] = await Promise.all([ + importDefault(path.join(themePath, 'primitives.ts')), + importDefault(path.join(themePath, 'semantics.ts')), + importDefault(path.join(themePath, 'sharedTokens.ts')) + ]) + return sharedTokens(semantics(primitives)) +} + +const flattenObj = (obj: Record) => { + const result: Record = {} + + for (const i in obj) { + if (typeof obj[i] === 'object' && !Array.isArray(obj[i])) { + const temp = flattenObj(obj[i]) + for (const j in temp) { + result[i + '-' + j] = temp[j] + } + } else { + result[i] = obj[i] + } + } + return result +} + +const buildCSSVariables = async (targetPath: string) => { + const root = path.resolve(targetPath) + const entries = await fs.readdir(root, { withFileTypes: true }) + + const themeDirs = entries + .filter((e) => e.isDirectory() && e.name !== 'componentTypes') + .map((e) => e.name) + + const sharedTokensByThemes: Record = {} + + for (const theme of themeDirs) { + const resolved = await loadTheme(path.join(root, theme)) + + const flatResult = flattenObj(resolved) + const cssVariables = Object.keys(flatResult).reduce( + (res, key) => `${res}--${key}:${flatResult[key]};`, + '' + ) + sharedTokensByThemes[theme] = cssVariables + } + + return sharedTokensByThemes +} + +export default buildCSSVariables diff --git a/packages/ui-scripts/lib/build/buildThemes/setupThemes.ts b/packages/ui-scripts/lib/build/buildThemes/setupThemes.ts index 26e7dd76ae..24c845d1c8 100644 --- a/packages/ui-scripts/lib/build/buildThemes/setupThemes.ts +++ b/packages/ui-scripts/lib/build/buildThemes/setupThemes.ts @@ -33,6 +33,7 @@ import generateComponent, { generateComponentType } from './generateComponents.ts' import { resolveBin, runCommandAsync } from '@instructure/command-utils' +import buildCSSVariables from './buildCSSVariables.ts' // transform to an object for easier handling export const transformThemes = (themes: any, input: any) => @@ -193,12 +194,12 @@ const setupThemes = async (targetPath: string, input: any): Promise => { sharedTokensTypes = componentTypes const componentFileContent = ` - import { SharedTokens } from '../commonTypes' + import type { SharedTokens } from '../commonTypes' import type { Semantics } from "./semantics" const ${fullComponentName} = (semantic: Semantics): ${capitalize( - fullComponentName - )} => ({${componentThemeVars}}) + fullComponentName + )} => ({${componentThemeVars}}) export default ${fullComponentName} ` @@ -368,6 +369,38 @@ const setupThemes = async (targetPath: string, input: any): Promise => { } ` await createFile(`${targetPath}/index.ts`, exportIndexFileContent) + + // generate themes as css variables + const cssVarObject = await buildCSSVariables(targetPath) + + const cssThemes = Object.keys(cssVarObject) + for (let i = 0; i < cssThemes.length; i++) { + const cssTheme = cssThemes[i] + await createFile( + `${targetPath}/themesAsCSSVariables/${cssTheme}.css`, + `.${cssTheme}{ + ${cssVarObject[cssTheme]} + }` + ) + } + + const cssThemesWithMediaQueries = ` + :root { + color-scheme: light; + ${cssVarObject['light']} + } + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + ${cssVarObject['dark']} + } + } + ` + await createFile( + `${targetPath}/themesAsCSSVariables/cssThemesWithMediaQueries.css`, + `${cssThemesWithMediaQueries}` + ) + try { const dprintBin = resolveBin('dprint') const { stdout, stderr } = await runCommandAsync(dprintBin, [ diff --git a/packages/ui-themes/package.json b/packages/ui-themes/package.json index 3b520480db..3e5825bc71 100644 --- a/packages/ui-themes/package.json +++ b/packages/ui-themes/package.json @@ -35,7 +35,9 @@ "publishConfig": { "access": "public" }, - "sideEffects": false, + "sideEffects": [ + "*.css" + ], "exports": { "./lib/*": "./lib/*", "./es/*": "./es/*",