Skip to content

Commit

Permalink
feat(colors): add onColors support (#3911)
Browse files Browse the repository at this point in the history
  • Loading branch information
m0ksem authored Sep 28, 2023
1 parent d9c6ca3 commit ea7c198
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 24 deletions.
3 changes: 2 additions & 1 deletion packages/docs/page-config/styles/colors/code/scheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ createVuestic({
presets: {
light: {
primary: '#f0f0f0',
myCoolColor: '#f00f0f',
myCoolColor: '#ff00ff',
onMyCoolColor: '#ffffff',
}
}
},
Expand Down
48 changes: 48 additions & 0 deletions packages/docs/page-config/styles/colors/components/Threshold.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
const threshold = ref(170)
const customColor = ref('#666E75')
</script>

<template>
<VaConfig
:colors="{
threshold,
variables: {
customColor,
},
}"
>
<div class="flex gap-2 mb-2 flex-wrap sm:flex-nowrap">
<div class="w-full sm:w-1/3 py-2 px-3 h-14 rounded-md" :style="{ background: 'var(--va-primary)', color: 'var(--va-on-primary)' }">
Primary
</div>
<div class="w-full sm:w-1/3 py-2 px-3 rounded-md" :style="{ background: 'var(--va-warning)', color: 'var(--va-on-warning)' }">
Warning
</div>
<div class="w-full sm:w-1/3 py-2 px-3 rounded-md" :style="{ background: 'var(--va-success)', color: 'var(--va-on-success)' }">
Success
</div>
</div>
<div>
<div class="py-2 px-3 rounded-md flex gap-4 items-center" :style="{ background: 'var(--va-custom-color)', color: 'var(--va-on-custom-color)' }">
CustomColor
<VaColorInput v-model="customColor" />
</div>
</div>
</VaConfig>
<VaSlider
v-model="threshold"
label="threshold"
:max="255"
:min="0"
class="my-4"
>
<template #append>
<va-counter
v-model.number="threshold"
class="w-[140px]"
type="number"
/>
</template>
</VaSlider>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ const primaryColor = ref(palette[0]);
},
}"
>
<div class="mb-2">
<VaAvatar>M.N.</VaAvatar>
Maksim N.
</div>
<VaButton>Play</VaButton>
<VaButton preset="secondary">Follow</VaButton>
</VaConfig>
Expand Down
14 changes: 10 additions & 4 deletions packages/docs/page-config/styles/colors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ export default definePageConfig({
block.paragraph("Vuestic UI library offers a color palette consisting of 14 colors. These colors are divided into accent colors, background colors, text colors and utility colors. By default, there are two color presets available: light and dark."),
block.component("PaletteGrid", { hideCode: true }),

block.subtitle("Auto text color detection"),
block.paragraph("With auto text color detection you don't need to worry about text readability. Vuestic UI library will automatically detect the best text color for you."),
block.paragraph("We use `threshold` property to detect if text should be dark or light. It used as a lightness value. If lightness value is less than `threshold` we assume it is dark background and text color must be light. Otherwise, text color must be dark."),
block.component("Threshold"),
block.paragraph('In case `threshold` is not enough for you, you can set `onPrimary`, `onSuccess`, `onBackgroundPrimary` and other colors in colors config.'),

block.subtitle("Customization"),
block.paragraph("There is the flexibility to modify or expand upon the existing color set, as well as define own custom color presets. The color scheme can be applied globally using a [Colors Config](https"),
block.paragraph("There is the flexibility to modify or expand upon the existing color set, as well as define own custom color presets. The color scheme can be applied globally using a [Colors Config](/services/colors-config)"),
block.code("scheme"),
block.link("Read more about Colors Config", "/services/colors-config"),

Expand All @@ -19,9 +25,9 @@ export default definePageConfig({
block.component("Components", { hideTemplate: true }),
block.example("ThemeSwitcher", { hideTitle: true }),

// block.subtitle("Making a local theme switcher"),
// block.paragraph("Color preset can be provided using [VaConfig](https://vuestic.dev/en/ui-elements/config) for specific part of application:"),
// block.example("LocalThemeSwitcher", { hideTitle: true }),
block.subtitle("Making a local theme switcher"),
block.paragraph("Color preset can be provided using [VaConfig](https://vuestic.dev/en/ui-elements/config) for specific part of application:"),
block.example("LocalThemeSwitcher", { hideTitle: true }),

block.subtitle("CSS Variables"),
block.paragraph("In case you need custom component that will follow Vuestic theme, you can use CSS variables. CSS variables are reactive and can be also scoped using [VaConfig](https://vuestic.dev/en/ui-elements/config)."),
Expand Down
8 changes: 5 additions & 3 deletions packages/ui/src/composables/tests/createTestComposable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineComponent, h } from 'vue'
import { mount } from '@vue/test-utils'
import { createVuestic } from '../../main'
import { GlobalConfig, PartialGlobalConfig, createVuestic } from '../../main'

export type Composable = (...args: any[]) => any

Expand All @@ -15,7 +15,7 @@ export type Composable = (...args: any[]) => any
* @param composables a composable or an array of composables
* @returns { composableWrapper } a composableWrapper object containing the values returned by the composables
*/
export function createTestComposable (composables: Composable | Composable[]) {
export function createTestComposable (composables: Composable | Composable[], globalConfig?: PartialGlobalConfig) {
const App = defineComponent({
setup () {
if (!Array.isArray(composables)) {
Expand All @@ -42,7 +42,9 @@ export function createTestComposable (composables: Composable | Composable[]) {

const appWrapper = mount(App, {
global: {
plugins: [createVuestic()],
plugins: [createVuestic({
config: globalConfig,
})],
},
})

Expand Down
36 changes: 29 additions & 7 deletions packages/ui/src/composables/tests/useColor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,9 @@ describe('applyPreset function', () => {
it(
'Given a non-existing preset name, the current preset name is changed and the warn function is called once',
() => {
// Since this test's goal is to trigger a warning
// by passing the wrong value to a function, we mock
// useReactiveComputed to prevent it from throwing an unwanted error
vi.mock('../useReactiveComputed', () => ({
useReactiveComputed: vi.fn(),
}))
applyPreset('foo')
expect(globalConfig.value.colors.currentPresetName).toBe('foo')
expect(warn).toHaveBeenCalledTimes(1)
vi.doUnmock('../useReactiveComputed')
vi.clearAllMocks()
},
)
Expand Down Expand Up @@ -52,3 +45,32 @@ describe('currentPresetName computed', () => {
expect(globalConfig.value.colors.currentPresetName).toBe('dark')
})
})

describe('getColor("onColor")', () => {
it('Given a color name, getColor("onColor") returns the corresponding color name with the "on" prefix', () => {
const {
composableWrapper: { getColor },
} = createTestComposable([useColors])

expect(getColor('onPrimary')).toBe('#FFFFFF')
expect(getColor('onWarning')).toBe('#262824')
expect(getColor('onNonExisting')).toBe('#154EC1') // Fallback to primary
expect(getColor('onNonExisting', '#FF00FF')).toBe('#FF00FF')
})
})

describe('getTextColor("onColor")', () => {
it('Given a color name, getColor("onColor") returns the corresponding color name with the "on" prefix', () => {
const {
composableWrapper: { getTextColor },
} = createTestComposable([useColors], {
colors: {
variables: {
onPrimary: '#FF00FF',
},
},
})

expect(getTextColor('primary')).toBe('#FF00FF')
})
})
19 changes: 15 additions & 4 deletions packages/ui/src/composables/useColors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ColorVariables, CssColor } from '../services/color'
import { computed } from 'vue'
import { capitalize, computed } from 'vue'
import { useGlobalConfig } from '../services/global-config/global-config'
import { warn } from '../utils/console'
import { useCache } from './useCache'
Expand Down Expand Up @@ -44,7 +44,6 @@ export const useColors = () => {
}

const { globalConfig } = gc

const colors = useReactiveComputed<ColorVariables>({
get: () => globalConfig.value.colors!.presets[globalConfig.value.colors!.currentPresetName],
set: (v: ColorVariables) => { setColors(v) },
Expand Down Expand Up @@ -72,7 +71,7 @@ export const useColors = () => {
/**
* Most default color - fallback when nothing else is found.
*/
defaultColor = getColors().primary
defaultColor = colors.primary
}

if (prop === 'transparent') {
Expand All @@ -83,7 +82,13 @@ export const useColors = () => {
return prop
}

const colors = getColors()
if (prop?.startsWith('on')) {
const colorName = prop.slice(2)

if (colors[normalizeColorName(colorName)]) {
return getColor(getTextColor(getColor(colorName)), undefined, preferVariables)
}
}

if (!prop) {
prop = getColor(defaultColor)
Expand Down Expand Up @@ -149,6 +154,11 @@ export const useColors = () => {
})

const getTextColor = (color: ColorInput, darkColor?: string, lightColor?: string) => {
const onColorName = `on${capitalize(String(color))}`
if (colors[onColorName]) {
return colors[onColorName]
}

darkColor = darkColor || computedDarkColor.value
lightColor = lightColor || computedLightColor.value
return getColorLightnessFromCache(color) > globalConfig.value.colors.threshold ? darkColor : lightColor
Expand All @@ -162,6 +172,7 @@ export const useColors = () => {

const applyPreset = (presetName: string) => {
globalConfig.value.colors!.currentPresetName = presetName

if (!globalConfig.value.colors!.presets[presetName]) {
return warn(`Preset ${presetName} does not exist`)
}
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/composables/useReactiveComputed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const useReactiveComputed = <T extends object>(obj: WritableComputedOptio

const proxy = new Proxy(objectRef, {
get (target, p: string, receiver) {
if (typeof objectRef.value !== 'object') {
return undefined
}
return unref(Reflect.get(objectRef.value, p, receiver))
},
set (target, p, value) {
Expand All @@ -19,9 +22,15 @@ export const useReactiveComputed = <T extends object>(obj: WritableComputedOptio
return Reflect.deleteProperty(objectRef.value, p)
},
has (target, p) {
if (typeof objectRef.value !== 'object') {
return false
}
return Reflect.has(objectRef.value, p)
},
ownKeys () {
if (typeof objectRef.value !== 'object') {
return []
}
return Object.keys(objectRef.value)
},
getOwnPropertyDescriptor () {
Expand Down
9 changes: 8 additions & 1 deletion packages/ui/src/services/color/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ export type EssentialVariables = {
transparent: CssColor,
}

export type ColorVariables = { [colorName: string]: CssColor } & EssentialVariables
type Capitalize<S extends string> = S extends `${infer First}${infer Rest}`
? `${Uppercase<First>}${Rest}`
: S
type OnColors = `on${Capitalize<keyof EssentialVariables>}`

export type ColorVariables = { [colorName: string]: CssColor } & EssentialVariables & {
[key in OnColors]: CssColor
}

export type ColorConfig = {
variables: ColorVariables,
Expand Down

0 comments on commit ea7c198

Please sign in to comment.