diff --git a/README.md b/README.md index a52c4a0..8aa287a 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,21 @@ -# bun starter +# Omegui theme generator -## Getting Started +## Disclaimer -Click the [Use this template](https://github.com/wobsoriano/bun-lib-starter/generate) button to create a new repository with the contents starter. +This tool is still in early development and may not work as expected. Please report any issues you encounter. -OR +## Description -Run `bun create wobsoriano/bun-lib-starter ./my-lib`. +Omegui is a set of tools to make Tamagui easier to set up and customize. -## Setup +This is the theme generator, which allows you to create a theme for Tamagui's component library from a minimal set of colors. You can also use preset themes to get started quickly. -```bash -# install dependencies -bun install +## Upcoming -# test the app -bun test - -# build the app, available under dist -bun run build -``` +- Support for dark themes +- Better documentation +- Wrapper for Tamagui components to add color variants (e.g. primary, secondary, etc.) +- Support for theming border radius, shadows, animations and other properties (aiming for parity with daisyUI themes) ## License diff --git a/bun.lockb b/bun.lockb index 7b8645e..254592d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 3e1b342..f65caec 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,19 @@ "bun" ], "license": "MIT", - "homepage": "https://github.com/wobsoriano/pkg-name#readme", + "homepage": "TODO", "repository": { "type": "git", - "url": "git+https://github.com/wobsoriano/pkg-name.git" + "url": "TODO" }, - "bugs": "https://github.com/wobsoriano/pkg-name/issues", - "author": "Robert Soriano ", + "bugs": "TODO", + "author": "Marin Godechot ", "devDependencies": { "bun-plugin-dts": "^0.2.1", "@types/bun": "^1.0.0", "typescript": "^5.2.2" + }, + "dependencies": { + "colorjs.io": "^0.5.0" } -} \ No newline at end of file +} diff --git a/src/colorUtils.ts b/src/colorUtils.ts new file mode 100644 index 0000000..12218e3 --- /dev/null +++ b/src/colorUtils.ts @@ -0,0 +1,25 @@ +import Color from "colorjs.io"; + +// TODO: Restrict type of color utils to make sure they are in the right color space + +export const stringColorToOklch = (color: string): Color => { + const parsedColor = new Color(color); + parsedColor; + return parsedColor.to("oklch"); +}; + +export const colorToHex = (color: Color): string => { + return color.to("srgb").toString({ format: "hex" }); +}; + +export const colorToShade = (color: Color, shade: number): Color => { + const newColor = color.clone(); + newColor.l += shade; + return newColor; +}; + +export const hasEnoughContrast = (color1: Color, color2: Color): boolean => { + const contrast = color1.contrast(color2, "WCAG21"); + const hasEnoughContrast = contrast > 4.5; + return hasEnoughContrast; +}; diff --git a/src/index.ts b/src/index.ts index e1c8f2c..b48bb88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1 @@ -export const one = 1 -export const two = 2 +export { omeguiThemeGenerator } from "./themeGenerator"; diff --git a/src/presetThemes.ts b/src/presetThemes.ts new file mode 100644 index 0000000..4e3d213 --- /dev/null +++ b/src/presetThemes.ts @@ -0,0 +1,83 @@ +export const presetThemes = { + emerald: { + primary: "#66cc8a", + primaryContent: "#223D30", + secondary: "#377cfb", + secondaryContent: "#fff", + accent: "#f68067", + accentContent: "#000", + neutral: "#333c4d", + neutralContent: "#f9fafb", + background: "oklch(100% 0 0)", + foreground: "#333c4d", + info: "#00d3ee", + infoContent: "#ffffff", + success: "#39DA8A", + successContent: "#ffffff", + warning: "#ffaa00", + warningContent: "#ffffff", + error: "#f44336", + errorContent: "#ffffff", + }, + retro: { + primary: "#ef9995", + primaryContent: "#282425", + secondary: "#a4cbb4", + secondaryContent: "#282425", + accent: "#DC8850", + accentContent: "#282425", + neutral: "#2E282A", + neutralContent: "#EDE6D4", + background: "#ece3ca", + background2: "#e4d8b4", + background3: "#DBCA9A", + foreground: "#282425", + info: "#2563eb", + success: "#16a34a", + warning: "#d97706", + error: "oklch(65.72% 0.199 27.33)", + }, + cyberpunk: { + primary: "oklch(74.22% 0.209 6.35)", + secondary: "oklch(83.33% 0.184 204.72)", + accent: "oklch(71.86% 0.2176 310.43)", + neutral: "oklch(23.04% 0.065 269.31)", + neutralContent: "oklch(94.51% 0.179 104.32)", + background: "oklch(94.51% 0.179 104.32)", + }, + pastel: { + primary: "#d1c1d7", + secondary: "#f6cbd1", + accent: "#b4e9d6", + neutral: "#70acc7", + background: "oklch(100% 0 0)", + background2: "#f9fafb", + background3: "#d1d5db", + }, + nord: { + primary: "#5E81AC", + secondary: "#81A1C1", + accent: "#88C0D0", + neutral: "#4C566A", + neutralContent: "#D8DEE9", + background: "#ECEFF4", + background2: "#E5E9F0", + background3: "#D8DEE9", + foreground: "#2E3440", + info: "#B48EAD", + success: "#A3BE8C", + warning: "#EBCB8B", + error: "#BF616A", + }, + autumn: { + primary: "#8C0327", + secondary: "#D85251", + accent: "#D59B6A", + neutral: "#826A5C", + background: "#f1f1f1", + info: "#42ADBB", + success: "#499380", + warning: "#E97F14", + error: "oklch(53.07% 0.241 24.16)", + }, +}; diff --git a/src/themeGenerator.ts b/src/themeGenerator.ts new file mode 100644 index 0000000..fc8952a --- /dev/null +++ b/src/themeGenerator.ts @@ -0,0 +1,51 @@ +import { presetThemes } from "./presetThemes"; +import { InputTheme } from "./themeTypes"; +import { partialThemeToFullTheme, themeColorsToHexTheme } from "./themeUtils"; + +const inputThemeToTamaguiTheme = (inputTheme: InputTheme) => { + const convertedFullTheme = themeColorsToHexTheme( + partialThemeToFullTheme(inputTheme) + ); + return { + ...convertedFullTheme, + background: convertedFullTheme.background, + backgroundHover: convertedFullTheme.background3, + backgroundPress: convertedFullTheme.background3, + backgroundFocus: convertedFullTheme.background3, + borderColor: convertedFullTheme.background3, + borderColorPress: convertedFullTheme.background3, + borderColorFocus: convertedFullTheme.background3, + borderColorHover: convertedFullTheme.neutral, + color: convertedFullTheme.foreground, + colorHover: convertedFullTheme.foreground, + colorPress: convertedFullTheme.foreground, + colorFocus: convertedFullTheme.foreground, + shadowColor: "#363A3F1A", + shadowColorHover: "#363A3F26", + shadowColorPress: "#363A3F26", + shadowColorFocus: "#363A3F26", + placeholderColor: convertedFullTheme.neutral, + }; +}; + +type ThemeGeneratorInput = keyof typeof presetThemes | InputTheme; + +/** + * Generate a Tamagui theme from the provided input theme. + * @param light - The light theme, either a string corresponding to a preset theme or a custom theme object. + * @param dark - The dark theme, either a string corresponding to a preset theme or a custom theme object. + */ +export const omeguiThemeGenerator = ({ + light, + dark, +}: { + light: ThemeGeneratorInput; + dark: ThemeGeneratorInput; +}) => { + const lightTheme = typeof light === "string" ? presetThemes[light] : light; + const darkTheme = typeof dark === "string" ? presetThemes[dark] : dark; + return { + light: inputThemeToTamaguiTheme(lightTheme), + dark: inputThemeToTamaguiTheme(darkTheme), + }; +}; diff --git a/src/themeTypes.ts b/src/themeTypes.ts new file mode 100644 index 0000000..948bd53 --- /dev/null +++ b/src/themeTypes.ts @@ -0,0 +1,52 @@ +export type InputTheme = { + // colorScheme: 'light' | 'dark' + primary: string; + primaryContent?: string; + secondary: string; + secondaryContent?: string; + accent: string; + accentContent?: string; + neutral: string; + neutralContent?: string; + background?: string; + background2?: string; + background3?: string; + foreground?: string; + info?: string; + infoContent?: string; + success?: string; + successContent?: string; + warning?: string; + warningContent?: string; + error?: string; + errorContent?: string; +}; + +export type FullTheme = Required & { + primary2: string; + secondary2: string; + accent2: string; + neutral2: string; + info2: string; + success2: string; + warning2: string; + error2: string; + // tamagui + // background: string; + // backgroundHover: string; + // backgroundPress: string; + // backgroundFocus: string; + // borderColor: string; + // borderColorPress: string; + // borderColorFocus: string; + // borderColorHover: string; + // color: string; + // colorHover: string; + // colorPress: string; + // colorFocus: string; + // shadowColor: string; + // shadowColorHover: string; + // shadowColorPress: string; + // shadowColorFocus: string; + // placeholderColor: string; +}; diff --git a/src/themeUtils.ts b/src/themeUtils.ts new file mode 100644 index 0000000..b0d2240 --- /dev/null +++ b/src/themeUtils.ts @@ -0,0 +1,103 @@ +import type { FullTheme, InputTheme } from "./themeTypes"; +import { + colorToHex, + colorToShade, + hasEnoughContrast, + stringColorToOklch, +} from "./colorUtils"; + +export const themeColorsToHexTheme = (theme: InputTheme): any => { + return Object.keys(theme).reduce((acc, key) => { + return { + ...acc, + // @ts-ignore - TODO typescript magic to make this work + [key]: colorToHex(stringColorToOklch(theme[key])), + }; + }, {}); +}; + +const colorToContent = ( + color: string, + foreground: string, + background: string +): string => { + const goodContrast = hasEnoughContrast( + stringColorToOklch(color), + stringColorToOklch(foreground) + ); + if (goodContrast) { + return foreground; + } + return background; +}; + +// TODO check for darkness to set shade correctly +// TODO support dark themes +export const partialThemeToFullTheme = ( + partialTheme: InputTheme +): FullTheme => { + const background = partialTheme.background ?? "#FFF"; + const foreground = partialTheme.foreground ?? "#000"; + const info = partialTheme.info ?? "oklch(0.7206 0.191 231.6)"; + const success = partialTheme.success ?? "oklch(0.648 0.15 160)"; + const warning = partialTheme.warning ?? "oklch(0.8471 0.199 83.87)"; + const error = partialTheme.error ?? "oklch(0.7176 0.221 22.18)"; + return { + ...partialTheme, + background, + foreground, + neutralContent: + partialTheme.neutralContent ?? + colorToContent(partialTheme.neutral, foreground, background), + primaryContent: + partialTheme.primaryContent ?? + colorToContent(partialTheme.primary, foreground, background), + secondaryContent: + partialTheme.secondaryContent ?? + colorToContent(partialTheme.secondary, foreground, background), + accentContent: + partialTheme.accentContent ?? + colorToContent(partialTheme.accent, foreground, background), + background2: + partialTheme.background2 ?? + colorToShade(stringColorToOklch(background), -0.1).toString(), + background3: + partialTheme.background3 ?? + colorToShade(stringColorToOklch(background), -0.2).toString(), + primary2: colorToShade( + stringColorToOklch(partialTheme.primary), + -0.1 + ).toString(), + secondary2: colorToShade( + stringColorToOklch(partialTheme.secondary), + -0.1 + ).toString(), + accent2: colorToShade( + stringColorToOklch(partialTheme.accent), + -0.1 + ).toString(), + neutral2: colorToShade( + stringColorToOklch(partialTheme.neutral), + -0.1 + ).toString(), + info, + info2: colorToShade(stringColorToOklch(info), -0.1).toString(), + infoContent: + partialTheme.infoContent ?? colorToContent(info, foreground, background), + success, + success2: colorToShade(stringColorToOklch(success), -0.1).toString(), + successContent: + partialTheme.successContent ?? + colorToContent(success, foreground, background), + warning, + warning2: colorToShade(stringColorToOklch(warning), -0.1).toString(), + warningContent: + partialTheme.warningContent ?? + colorToContent(warning, foreground, background), + error, + error2: colorToShade(stringColorToOklch(error), -0.1).toString(), + errorContent: + partialTheme.errorContent ?? + colorToContent(error, foreground, background), + }; +}; diff --git a/test/colorUtils.test.ts b/test/colorUtils.test.ts new file mode 100644 index 0000000..91deee4 --- /dev/null +++ b/test/colorUtils.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from "bun:test"; +import Color from "colorjs.io"; +import { + stringColorToOklch, + colorToHex, + colorToShade, + hasEnoughContrast, +} from "../src/colorUtils"; + +describe("colorUtils", () => { + describe("stringColorToOklch", () => { + it("should convert string color to oklch color", () => { + const color = stringColorToOklch("#FF0000"); + expect(color.toString()).toBe("oklch(62.796% 0.25768 29.234)"); + }); + }); + + describe("colorToHex", () => { + it("should convert color to hex string", () => { + const color = new Color("oklch(62.796% 0.25768 29.234)"); + const hex = colorToHex(color); + expect(hex).toBe("#f00"); + }); + }); + + describe("colorToShade", () => { + it("should create a new color with the specified shade", () => { + const color = new Color("oklch(62.796% 0.25768 29.234)"); + const shadedColor = colorToShade(color, -0.2); + expect(shadedColor.toString()).toBe("oklch(42.796% 0.25768 29.234)"); + }); + }); + + describe("hasEnoughContrast", () => { + it("should return true if the colors have enough contrast", () => { + const color1 = new Color("#000000"); + const color2 = new Color("#FFFFFF"); + const hasEnough = hasEnoughContrast(color1, color2); + expect(hasEnough).toBe(true); + }); + + it("should return false if the colors do not have enough contrast", () => { + const color1 = new Color("#000000"); + const color2 = new Color("#333333"); + const hasEnough = hasEnoughContrast(color1, color2); + expect(hasEnough).toBe(false); + }); + }); +}); diff --git a/test/index.test.ts b/test/index.test.ts deleted file mode 100644 index ecd2e9e..0000000 --- a/test/index.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, it, expect } from 'bun:test' -import { one, two } from '../src' - -describe('should', () => { - it('export 1', () => { - expect(one).toBe(1) - }) - - it('export 2', () => { - expect(two).toBe(2) - }) -}) diff --git a/test/themeUtils.test.ts b/test/themeUtils.test.ts new file mode 100644 index 0000000..f6a752b --- /dev/null +++ b/test/themeUtils.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "bun:test"; +import { + themeColorsToHexTheme, + partialThemeToFullTheme, +} from "../src/themeUtils"; + +describe("themeUtils", () => { + describe("themeColorsToHexTheme", () => { + it("should convert theme colors to hex theme", () => { + const theme = { + primary: "oklch(62.796% 0.25768 29.234)", + secondary: "oklch(82.796% 0.25768 29.234)", + accent: "oklch(72.796% 0.25768 29.234)", + neutral: "oklch(22.796% 0.25768 29.234)", + }; + const hexTheme = themeColorsToHexTheme(theme); + expect(hexTheme).toEqual({ + primary: "#f00", + secondary: "#ffa899", + accent: "#ff6d5b", + neutral: "#400", + }); + }); + }); + + describe("partialThemeToFullTheme", () => { + it("should convert partial theme to full theme", () => { + const partialTheme = { + background: "#FFF", + foreground: "#000", + neutral: "#888", + primary: "#FF0000", + secondary: "#00FF00", + accent: "#0000FF", + }; + const fullTheme = partialThemeToFullTheme(partialTheme); + expect(fullTheme).toEqual({ + accent: "#0000FF", + accent2: "oklch(35.201% 0.31321 264.05)", + accentContent: "#FFF", + background: "#FFF", + background2: "oklch(90% 0 none)", + background3: "oklch(80% 0 none)", + error: "oklch(0.7176 0.221 22.18)", + error2: "oklch(61.76% 0.221 22.18)", + errorContent: "#000", + foreground: "#000", + info: "oklch(0.7206 0.191 231.6)", + info2: "oklch(62.06% 0.191 231.6)", + infoContent: "#000", + neutral: "#888", + neutral2: "oklch(52.675% 0 none)", + neutralContent: "#000", + primary: "#FF0000", + primary2: "oklch(52.796% 0.25768 29.234)", + primaryContent: "#000", + secondary: "#00FF00", + secondary2: "oklch(76.644% 0.29483 142.5)", + secondaryContent: "#000", + success: "oklch(0.648 0.15 160)", + success2: "oklch(54.8% 0.15 160)", + successContent: "#000", + warning: "oklch(0.8471 0.199 83.87)", + warning2: "oklch(74.71% 0.199 83.87)", + warningContent: "#000", + }); + }); + }); +});