diff --git a/src/App.tsx b/src/App.tsx index d1a560b7..658719cb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,12 +2,12 @@ import { Outlet } from "react-router-dom"; import Notifications from "./components/Notifications"; import { useThemeBuilderStore } from "./store"; import { useEffect } from "react"; +import { DefaultColorType } from "./utils/data.ts"; import { getNonColorCssProperties, getPaletteOutput, getSpeakingNames, -} from "./utils/outputs"; -import { DefaultColorType } from "./utils/data.ts"; +} from "./utils/outputs/web"; const App = () => { const { speakingNames, luminanceSteps, theme } = useThemeBuilderStore( diff --git a/src/utils/outputs/download.ts b/src/utils/outputs/download.ts index c85584fd..63a97e98 100644 --- a/src/utils/outputs/download.ts +++ b/src/utils/outputs/download.ts @@ -19,15 +19,20 @@ import { generateDensityEnumFile } from "./compose/density.ts"; import { getSketchColorsAsString } from "./sketch.ts"; import { getFontFaces } from "./web/fonts.ts"; import { kebabCase } from "../index.ts"; +import { generateCustomColorClass } from "./web/custom-color-class.ts"; +import { generateAndroidReadmeFile } from "./compose/readme.ts"; import { getCssPropertyAsString, getCssThemeProperties, getFullColorCss, getPaletteOutput, getSpeakingNames, -} from "./index.ts"; -import { generateCustomColorClass } from "./web/custom-color-class.ts"; -import { generateAndroidReadmeFile } from "./compose/readme.ts"; +} from "./web"; +import { + getSDColorPalette, + getSDSpeakingColors, +} from "./style-dictionary/colors.ts"; +import { getDBNonColorToken } from "./style-dictionary"; const download = (fileName: string, file: Blob) => { const element = document.createElement("a"); @@ -67,6 +72,33 @@ export const downloadTheme = async ( const zip = new JSZip(); zip.file(`${fileName}.json`, themeJsonString); + // Style dictionary + + const sdFolder: string = "StyleDictionary"; + zip.file( + `${sdFolder}/palette-colors.json`, + JSON.stringify(getSDColorPalette(allColors, luminanceSteps)), + ); + zip.file( + `${sdFolder}/speaking-colors.json`, + JSON.stringify(getSDSpeakingColors(speakingNames, allColors)), + ); + const token = getDBNonColorToken(theme); + const tokenProps = [ + "spacing", + "sizing", + "border", + "elevation", + "transition", + "font", + ]; + for (const prop of tokenProps) { + zip.file( + `${sdFolder}/${prop}s.json`, + JSON.stringify({ [prop]: token[prop] }), + ); + } + //Android const androidFolder: string = "Android"; const androidThemeFolder: string = `${androidFolder}/theme`; diff --git a/src/utils/outputs/index.ts b/src/utils/outputs/index.ts index a3d99234..8aa850c6 100644 --- a/src/utils/outputs/index.ts +++ b/src/utils/outputs/index.ts @@ -1,124 +1,34 @@ -import { - DefaultColorType, - HeisslufType, - SpeakingName, - ThemeType, -} from "../data.ts"; -import traverse from "traverse"; +import { DefaultColorType, HeisslufType } from "../data.ts"; import { getHeissluftColors } from "../generate-colors.ts"; export const prefix = "db"; -export const getCssPropertyAsString = ( - properties: any, - inRoot?: boolean, -): string => { - let resultString = ""; +export const nonRemProperties = ["opacity", "elevation", "transition", "font"]; - for (const [key, value] of Object.entries(properties)) { - resultString += `${key}: ${value};\n`; - } - - if (inRoot) { - return `:root{${resultString}}`; - } - - return resultString; -}; - -const nonRemProperties = ["opacity", "elevation", "transition", "font"]; - -const isFontFamily = (path: string[]): boolean => +export const isFontFamily = (path: string[]): boolean => (path[0] === "font" && path[1] === "family") || path[0] !== "font"; -export const getNonColorCssProperties = ( - theme: ThemeType, - asString?: boolean, -) => { - const resolvedProperties: any = {}; - traverse(theme).forEach(function (value) { - if ( - this.isLeaf && - this.path.length > 0 && - this.path[0] !== "colors" && - this.path[0] !== "additionalColors" && - this.path[0] !== "customColors" && - this.path[0] !== "branding" && - isFontFamily(this.path) && - !this.path.includes("_scale") - ) { - const key = `--${prefix}-${this.path - .map((path) => path.toLowerCase()) - .map((path) => { - if (path === "lineheight") { - return "line-height"; - } else if (path === "fontsize") { - return "font-size"; - } - return path; - }) - .join("-")}`; - - resolvedProperties[key] = - !nonRemProperties.includes(this.path[0]) && - (typeof value === "string" || value instanceof String) - ? `${value}rem` - : value; - - if (this.path.at(-1) === "fontSize") { - const lineHeightPath = [...this.path]; - lineHeightPath[lineHeightPath.length - 1] = "lineHeight"; - const fontSizeAsNumber = Number(value); - const lineHeightAsNumber = Number(traverse(theme).get(lineHeightPath)); - - const remainingIconPath = this.path - .filter((path) => path !== "typography" && path !== "fontSize") - .join("-"); - const fontSizing = fontSizeAsNumber * lineHeightAsNumber; - resolvedProperties[ - `--${prefix}-base-icon-weight-${remainingIconPath}` - ] = fontSizing * 16; - resolvedProperties[ - `--${prefix}-base-icon-font-size-${remainingIconPath}` - ] = `${fontSizing}rem`; - } - } - }); - - if (asString) { - return getCssPropertyAsString(resolvedProperties); - } - - return resolvedProperties; -}; - -export const getCssThemeProperties = (theme: ThemeType): string => { - const customTheme = getNonColorCssProperties(theme, true); - - return `:root{ - ${customTheme} - } - `; -}; - -export const getFullColorCss = ( - colorsPalette: string, - colorsSpeakingNames: string, -): string => { - return `:root{ - ${colorsPalette} - ${colorsSpeakingNames} - } - -[data-color-scheme="light"] { - color-scheme: light; -} - -[data-color-scheme="dark"] { - color-scheme: dark; -} - `; -}; +export const isDimensionProperty = (context: any) => + context.isLeaf && + context.path.length > 0 && + context.path[0] !== "colors" && + context.path[0] !== "additionalColors" && + context.path[0] !== "customColors" && + context.path[0] !== "branding" && + isFontFamily(context.path) && + !context.path.includes("_scale"); + +export const getTraverseKey = (path: string[], separator: string = "-") => + `${path + .map((p) => p.toLowerCase()) + .map((p) => { + return p === "lineheight" + ? "line-height" + : p === "fontsize" + ? "font-size" + : p; + }) + .join(separator)}`; export const getPalette = ( allColors: Record, @@ -142,90 +52,3 @@ export const getPalette = ( (previousValue, currentValue) => ({ ...previousValue, ...currentValue }), {}, ); - -export const getPaletteOutput = ( - allColors: Record, - luminanceSteps: number[], -): any => { - const palette: Record = getPalette( - allColors, - luminanceSteps, - ); - const result: any = {}; - - Object.entries(allColors).forEach(([unformattedName, color]) => { - const name = unformattedName.toLowerCase(); - - const hslType: HeisslufType[] = palette[unformattedName]; - hslType.forEach((hsl) => { - result[`--${prefix}-${name}-${hsl.index ?? hsl.name}`] = hsl.hex; - }); - - result[`--${prefix}-${name}-origin`] = color.origin; - result[`--${prefix}-${name}-origin-light-default`] = color.originLight; - result[`--${prefix}-${name}-origin-light-hovered`] = - color.originLightHovered; - result[`--${prefix}-${name}-origin-light-pressed`] = - color.originLightPressed; - result[`--${prefix}-${name}-on-origin-light-default`] = color.onOriginLight; - result[`--${prefix}-${name}-on-origin-light-hovered`] = - color.onOriginLightHovered; - result[`--${prefix}-${name}-on-origin-light-pressed`] = - color.onOriginLightPressed; - - result[`--${prefix}-${name}-origin-dark-default`] = color.originDark; - result[`--${prefix}-${name}-origin-dark-hovered`] = color.originDarkHovered; - result[`--${prefix}-${name}-origin-dark-pressed`] = color.originDarkPressed; - result[`--${prefix}-${name}-on-origin-dark-default`] = color.onOriginDark; - result[`--${prefix}-${name}-on-origin-dark-hovered`] = - color.onOriginDarkHovered; - result[`--${prefix}-${name}-on-origin-dark-pressed`] = - color.onOriginDarkPressed; - }); - - return result; -}; - -export const getSpeakingNames = ( - speakingNames: SpeakingName[], - allColors: Record, -): any => { - const result: any = {}; - Object.entries(allColors).forEach(([unformattedName]) => { - const name = unformattedName.toLowerCase(); - result[`--${prefix}-${name}-origin-default`] = - `light-dark(var(--${prefix}-${name}-origin-light-default),var(--${prefix}-${name}-origin-dark-default))`; - result[`--${prefix}-${name}-origin-hovered`] = - `light-dark(var(--${prefix}-${name}-origin-light-hovered),var(--${prefix}-${name}-origin-dark-hovered))`; - result[`--${prefix}-${name}-origin-pressed`] = - `light-dark(var(--${prefix}-${name}-origin-light-pressed),var(--${prefix}-${name}-origin-dark-pressed))`; - result[`--${prefix}-${name}-on-origin-default`] = - `light-dark(var(--${prefix}-${name}-on-origin-light-default),var(--${prefix}-${name}-on-origin-dark-default))`; - result[`--${prefix}-${name}-on-origin-hovered`] = - `light-dark(var(--${prefix}-${name}-on-origin-light-hovered),var(--${prefix}-${name}-on-origin-dark-hovered))`; - result[`--${prefix}-${name}-on-origin-pressed`] = - `light-dark(var(--${prefix}-${name}-on-origin-light-pressed),var(--${prefix}-${name}-on-origin-dark-pressed))`; - - speakingNames.forEach((speakingName) => { - if ( - speakingName.transparencyDark !== undefined || - speakingName.transparencyLight !== undefined - ) { - result[`--${prefix}-${name}-${speakingName.name}`] = - `light-dark(color-mix(in srgb, transparent ${ - speakingName.transparencyLight - }%, var(--${prefix}-${name}-${ - speakingName.light - })),color-mix(in srgb, transparent ${ - speakingName.transparencyDark - }%, var(--${prefix}-${name}-${speakingName.dark})))`; - } else { - result[`--${prefix}-${name}-${speakingName.name}`] = - `light-dark(var(--${prefix}-${name}-${ - speakingName.light - }),var(--${prefix}-${name}-${speakingName.dark}))`; - } - }); - }); - return result; -}; diff --git a/src/utils/outputs/style-dictionary/colors.ts b/src/utils/outputs/style-dictionary/colors.ts new file mode 100644 index 00000000..1f68fcd0 --- /dev/null +++ b/src/utils/outputs/style-dictionary/colors.ts @@ -0,0 +1,157 @@ +import { DefaultColorType, HeisslufType, SpeakingName } from "../../data.ts"; +import { getPalette } from "../index.ts"; +import { setObjectByPath } from "./index.ts"; + +export const getSDColorPalette = ( + allColors: Record, + luminanceSteps: number[], +): any => { + const palette: Record = getPalette( + allColors, + luminanceSteps, + ); + const colors: any = {}; + + Object.entries(allColors).forEach(([unformattedName, color]) => { + const name = unformattedName.toLowerCase(); + + const colorValues: any = {}; + + const hslType: HeisslufType[] = palette[unformattedName]; + hslType.forEach((hsl) => { + colorValues[`${hsl.index ?? hsl.name}`] = { value: hsl.hex }; + }); + + colorValues.origin = { + base: { + comment: "This is just to resolve the original origin color", + value: color.origin, + }, + }; + + colorValues.light = { + origin: { + default: { + value: color.originLight, + }, + hovered: { + value: color.originLightHovered, + }, + pressed: { + value: color.originLightPressed, + }, + }, + on: { + origin: { + default: { + value: color.onOriginLight, + }, + hovered: { + value: color.onOriginLightHovered, + }, + pressed: { + value: color.onOriginLightPressed, + }, + }, + }, + }; + + colorValues.dark = { + origin: { + default: { + value: color.originDark, + }, + hovered: { + value: color.originDarkHovered, + }, + pressed: { + value: color.originDarkPressed, + }, + }, + on: { + origin: { + default: { + value: color.onOriginDark, + }, + hovered: { + value: color.onOriginDarkHovered, + }, + pressed: { + value: color.onOriginDarkPressed, + }, + }, + }, + }; + + colors[name] = colorValues; + }); + + return { colors }; +}; + +export const getSDSpeakingColors = ( + speakingNames: SpeakingName[], + allColors: Record, +): any => { + const colors: any = { light: {}, dark: {} }; + const colorTheme = ["light", "dark"]; + for (const [unformattedName] of Object.entries(allColors)) { + const name = unformattedName.toLowerCase(); + + for (const theme of colorTheme) { + const isDark = theme === "dark"; + const themeObj: any = { + origin: { + default: { + value: `{colors.${name}.${theme}.origin.default.value}`, + }, + hovered: { + value: `{colors.${name}.${theme}.origin.hovered.value}`, + }, + pressed: { + value: `{colors.${name}.${theme}.origin.pressed.value}`, + }, + }, + on: { + origin: { + default: { + value: `{colors.${name}.${theme}.on.origin.default.value}`, + }, + hovered: { + value: `{colors.${name}.${theme}.on.origin.hovered.value}`, + }, + pressed: { + value: `{colors.${name}.${theme}.on.origin.pressed.value}`, + }, + }, + }, + }; + + for (const speakingName of speakingNames) { + const dotName = speakingName.name.replaceAll("-", "."); + + setObjectByPath( + themeObj, + `${dotName}.value`, + `{colors.${name}.${isDark ? speakingName.dark : speakingName.light}.value}`, + ); + + if ( + speakingName.transparencyDark !== undefined || + speakingName.transparencyLight !== undefined + ) { + setObjectByPath( + themeObj, + `${dotName}.transparent`, + isDark + ? speakingName.transparencyDark + : speakingName.transparencyLight, + ); + } + } + + colors[theme][name] = themeObj; + } + } + return { colors }; +}; diff --git a/src/utils/outputs/style-dictionary/index.ts b/src/utils/outputs/style-dictionary/index.ts new file mode 100644 index 00000000..6656e2e7 --- /dev/null +++ b/src/utils/outputs/style-dictionary/index.ts @@ -0,0 +1,49 @@ +import { ThemeType } from "../../data.ts"; +import traverse from "traverse"; +import { + getTraverseKey, + isDimensionProperty, + nonRemProperties, +} from "../index.ts"; + +export const setObjectByPath = ( + initObj: any, + path: string, + value: any, +): any => { + if (path == "") return value; + + const [k, next] = path.split({ + [Symbol.split](s) { + const i = s.indexOf("."); + return i == -1 ? [s, ""] : [s.slice(0, i), s.slice(i + 1)]; + }, + }); + + if (initObj !== undefined && typeof initObj !== "object") { + console.error(`cannot set property ${k} of ${typeof initObj}`); + } + + return Object.assign(initObj ?? {}, { + [k]: setObjectByPath(initObj?.[k], next, value), + }); +}; + +export const getDBNonColorToken = (theme: ThemeType) => { + const resolvedProperties: any = {}; + traverse(theme).forEach(function (value) { + if (isDimensionProperty(this)) { + const key = getTraverseKey(this.path, "."); + + const finalValue = + !nonRemProperties.includes(this.path[0]) && + (typeof value === "string" || value instanceof String) + ? `${value}rem` + : value; + + setObjectByPath(resolvedProperties, key, finalValue); + } + }); + + return resolvedProperties; +}; diff --git a/src/utils/outputs/web/index.ts b/src/utils/outputs/web/index.ts new file mode 100644 index 00000000..68053fe8 --- /dev/null +++ b/src/utils/outputs/web/index.ts @@ -0,0 +1,188 @@ +import { + DefaultColorType, + HeisslufType, + SpeakingName, + ThemeType, +} from "../../data.ts"; +import traverse from "traverse"; +import { + getPalette, + getTraverseKey, + isDimensionProperty, + nonRemProperties, + prefix, +} from "../index.ts"; + +export const getCssPropertyAsString = ( + properties: any, + inRoot?: boolean, +): string => { + let resultString = ""; + + for (const [key, value] of Object.entries(properties)) { + resultString += `${key}: ${value};\n`; + } + + if (inRoot) { + return `:root{${resultString}}`; + } + + return resultString; +}; + +export const getNonColorCssProperties = ( + theme: ThemeType, + asString?: boolean, +) => { + const resolvedProperties: any = {}; + traverse(theme).forEach(function (value) { + if (isDimensionProperty(this)) { + const key = `--${prefix}-${getTraverseKey(this.path)}`; + + resolvedProperties[key] = + !nonRemProperties.includes(this.path[0]) && + (typeof value === "string" || value instanceof String) + ? `${value}rem` + : value; + + if (this.path.at(-1) === "fontSize") { + const lineHeightPath = [...this.path]; + lineHeightPath[lineHeightPath.length - 1] = "lineHeight"; + const fontSizeAsNumber = Number(value); + const lineHeightAsNumber = Number(traverse(theme).get(lineHeightPath)); + + const remainingIconPath = this.path + .filter((path) => path !== "typography" && path !== "fontSize") + .join("-"); + const fontSizing = fontSizeAsNumber * lineHeightAsNumber; + resolvedProperties[ + `--${prefix}-base-icon-weight-${remainingIconPath}` + ] = fontSizing * 16; + resolvedProperties[ + `--${prefix}-base-icon-font-size-${remainingIconPath}` + ] = `${fontSizing}rem`; + } + } + }); + + if (asString) { + return getCssPropertyAsString(resolvedProperties); + } + + return resolvedProperties; +}; + +export const getCssThemeProperties = (theme: ThemeType): string => { + const customTheme = getNonColorCssProperties(theme, true); + + return `:root{ + ${customTheme} + } + `; +}; + +export const getFullColorCss = ( + colorsPalette: string, + colorsSpeakingNames: string, +): string => { + return `:root{ + ${colorsPalette} + ${colorsSpeakingNames} + } + +[data-color-scheme="light"] { + color-scheme: light; +} + +[data-color-scheme="dark"] { + color-scheme: dark; +} + `; +}; + +export const getPaletteOutput = ( + allColors: Record, + luminanceSteps: number[], +): any => { + const palette: Record = getPalette( + allColors, + luminanceSteps, + ); + const result: any = {}; + + Object.entries(allColors).forEach(([unformattedName, color]) => { + const name = unformattedName.toLowerCase(); + + const hslType: HeisslufType[] = palette[unformattedName]; + hslType.forEach((hsl) => { + result[`--${prefix}-${name}-${hsl.index ?? hsl.name}`] = hsl.hex; + }); + + result[`--${prefix}-${name}-origin`] = color.origin; + result[`--${prefix}-${name}-origin-light-default`] = color.originLight; + result[`--${prefix}-${name}-origin-light-hovered`] = + color.originLightHovered; + result[`--${prefix}-${name}-origin-light-pressed`] = + color.originLightPressed; + result[`--${prefix}-${name}-on-origin-light-default`] = color.onOriginLight; + result[`--${prefix}-${name}-on-origin-light-hovered`] = + color.onOriginLightHovered; + result[`--${prefix}-${name}-on-origin-light-pressed`] = + color.onOriginLightPressed; + + result[`--${prefix}-${name}-origin-dark-default`] = color.originDark; + result[`--${prefix}-${name}-origin-dark-hovered`] = color.originDarkHovered; + result[`--${prefix}-${name}-origin-dark-pressed`] = color.originDarkPressed; + result[`--${prefix}-${name}-on-origin-dark-default`] = color.onOriginDark; + result[`--${prefix}-${name}-on-origin-dark-hovered`] = + color.onOriginDarkHovered; + result[`--${prefix}-${name}-on-origin-dark-pressed`] = + color.onOriginDarkPressed; + }); + + return result; +}; + +export const getSpeakingNames = ( + speakingNames: SpeakingName[], + allColors: Record, +): any => { + const result: any = {}; + Object.entries(allColors).forEach(([unformattedName]) => { + const name = unformattedName.toLowerCase(); + result[`--${prefix}-${name}-origin-default`] = + `light-dark(var(--${prefix}-${name}-origin-light-default),var(--${prefix}-${name}-origin-dark-default))`; + result[`--${prefix}-${name}-origin-hovered`] = + `light-dark(var(--${prefix}-${name}-origin-light-hovered),var(--${prefix}-${name}-origin-dark-hovered))`; + result[`--${prefix}-${name}-origin-pressed`] = + `light-dark(var(--${prefix}-${name}-origin-light-pressed),var(--${prefix}-${name}-origin-dark-pressed))`; + result[`--${prefix}-${name}-on-origin-default`] = + `light-dark(var(--${prefix}-${name}-on-origin-light-default),var(--${prefix}-${name}-on-origin-dark-default))`; + result[`--${prefix}-${name}-on-origin-hovered`] = + `light-dark(var(--${prefix}-${name}-on-origin-light-hovered),var(--${prefix}-${name}-on-origin-dark-hovered))`; + result[`--${prefix}-${name}-on-origin-pressed`] = + `light-dark(var(--${prefix}-${name}-on-origin-light-pressed),var(--${prefix}-${name}-on-origin-dark-pressed))`; + + speakingNames.forEach((speakingName) => { + if ( + speakingName.transparencyDark !== undefined || + speakingName.transparencyLight !== undefined + ) { + result[`--${prefix}-${name}-${speakingName.name}`] = + `light-dark(color-mix(in srgb, transparent ${ + speakingName.transparencyLight + }%, var(--${prefix}-${name}-${ + speakingName.light + })),color-mix(in srgb, transparent ${ + speakingName.transparencyDark + }%, var(--${prefix}-${name}-${speakingName.dark})))`; + } else { + result[`--${prefix}-${name}-${speakingName.name}`] = + `light-dark(var(--${prefix}-${name}-${ + speakingName.light + }),var(--${prefix}-${name}-${speakingName.dark}))`; + } + }); + }); + return result; +};