diff options
-rw-r--r-- | components.d.ts | 5 | ||||
-rw-r--r-- | src/config.ts | 4 | ||||
-rw-r--r-- | src/router.ts | 2 | ||||
-rw-r--r-- | src/ui/c-button/c-button.demo.vue | 29 | ||||
-rw-r--r-- | src/ui/c-button/c-button.theme.ts | 292 | ||||
-rw-r--r-- | src/ui/c-button/c-button.vue | 5 | ||||
-rw-r--r-- | src/ui/c-card/c-card.demo.vue | 13 | ||||
-rw-r--r-- | src/ui/c-link/c-link.demo.vue | 12 | ||||
-rw-r--r-- | src/ui/c-link/c-link.vue | 10 | ||||
-rw-r--r-- | src/ui/color/color.models.test.ts | 40 | ||||
-rw-r--r-- | src/ui/color/color.models.ts | 33 | ||||
-rw-r--r-- | src/ui/demo/demo-wrapper.vue | 33 | ||||
-rw-r--r-- | src/ui/demo/demo.routes.ts | 25 | ||||
-rw-r--r-- | src/ui/theme/themes.ts | 23 |
14 files changed, 286 insertions, 240 deletions
diff --git a/components.d.ts b/components.d.ts index ae1710f..2750ce6 100644 --- a/components.d.ts +++ b/components.d.ts @@ -21,16 +21,21 @@ declare module '@vue/runtime-core' { Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default'] CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default'] CButton: typeof import('./src/ui/c-button/c-button.vue')['default'] + 'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default'] CCard: typeof import('./src/ui/c-card/c-card.vue')['default'] + 'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] + 'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default'] CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] + 'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default'] + DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default'] DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default'] diff --git a/src/config.ts b/src/config.ts index c0545f6..2558d03 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,9 +23,9 @@ export const config = figue({ env: { doc: 'Application current env', format: 'enum', - values: ['production', 'development', 'test'], + values: ['production', 'development', 'preview', 'test'], default: 'development', - env: 'MODE', + env: 'VITE_VERCEL_ENV', }, }, plausible: { diff --git a/src/router.ts b/src/router.ts index f16b418..00c20c7 100644 --- a/src/router.ts +++ b/src/router.ts @@ -4,6 +4,7 @@ import HomePage from './pages/Home.page.vue'; import NotFound from './pages/404.page.vue'; import { tools } from './tools'; import { config } from './config'; +import { routes as demoRoutes } from './ui/demo/demo.routes'; const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({ path, @@ -32,6 +33,7 @@ const router = createRouter({ }, ...toolsRoutes, ...toolsRedirectRoutes, + ...(config.app.env === 'development' ? demoRoutes : []), { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }, ], }); diff --git a/src/ui/c-button/c-button.demo.vue b/src/ui/c-button/c-button.demo.vue new file mode 100644 index 0000000..988659f --- /dev/null +++ b/src/ui/c-button/c-button.demo.vue @@ -0,0 +1,29 @@ +<template> + <div v-for="buttonVariant of buttonVariants" :key="buttonVariant"> + <h2>{{ _.capitalize(buttonVariant) }}</h2> + + <c-button v-for="buttonType of buttonTypes" :key="buttonType" :variant="buttonVariant" :type="buttonType" mx-1> + Button + </c-button> + + <c-button + v-for="buttonType of buttonTypes" + :key="buttonType" + :variant="buttonVariant" + :type="buttonType" + circle + mx-1 + > + A + </c-button> + </div> +</template> + +<script lang="ts" setup> +import _ from 'lodash'; + +const buttonVariants = ['basic', 'text']; +const buttonTypes = ['default', 'primary']; +</script> + +<style lang="less" scoped></style> diff --git a/src/ui/c-button/c-button.theme.ts b/src/ui/c-button/c-button.theme.ts index 2f25e6f..87ad89f 100644 --- a/src/ui/c-button/c-button.theme.ts +++ b/src/ui/c-button/c-button.theme.ts @@ -1,240 +1,76 @@ +import { darken, lighten } from '../color/color.models'; import { defineThemes } from '../theme/theme.models'; import { appThemes } from '../theme/themes'; -export const { useTheme } = defineThemes({ - dark: { - basic: { - default: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: 'rgba(255, 255, 255, 0.08)', - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: 'rgba(255, 255, 255, 0.12)', - }, - - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: 'rgba(255, 255, 255, 0.24)', - }, - - outline: { - color: appThemes.dark.primary.color, - }, - }, - - primary: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.primary.color, - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.primary.colorHover, - }, - - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.primary.colorPressed, - }, - - outline: { - color: appThemes.dark.primary.color, - }, - }, - - warning: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.warning.color, - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.warning.colorHover, - }, - - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.warning.colorPressed, - }, - - outline: { - color: appThemes.dark.warning.color, - }, - }, - }, - - text: { - default: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: 'transparent', - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: 'rgba(255, 255, 255, 0.12)', - }, - - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: 'rgba(255, 255, 255, 0.82)', - }, - - outline: { - color: appThemes.dark.primary.color, - }, - }, - - primary: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.primary.color, - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.primary.colorHover, - }, - - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.primary.colorPressed, - }, - - outline: { - color: appThemes.dark.primary.color, - }, - }, - - warning: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.warning.color, - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.warning.colorHover, - }, +const createState = ({ + textColor, + backgroundColor, + hoverBackground, + hoveredTextColor = textColor, + pressedBackground, + pressedTextColor = textColor, +}: { + textColor: string; + backgroundColor: string; + hoverBackground: string; + hoveredTextColor?: string; + pressedBackground: string; + pressedTextColor?: string; +}) => ({ + textColor, + backgroundColor, + hover: { textColor: hoveredTextColor, backgroundColor: hoverBackground }, + pressed: { textColor: pressedTextColor, backgroundColor: pressedBackground }, +}); - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.dark.warning.colorPressed, - }, +const createTheme = ({ style }: { style: 'light' | 'dark' }) => { + const theme = appThemes[style]; - outline: { - color: appThemes.dark.warning.color, - }, - }, - }, - }, - light: { + return { basic: { - default: { - textColor: appThemes.light.text.baseColor, - backgroundColor: 'rgba(46, 51, 56, 0.05)', - - hover: { - textColor: appThemes.light.text.baseColor, - backgroundColor: 'rgba(46, 51, 56, 0.09)', - }, - - pressed: { - textColor: appThemes.light.text.baseColor, - backgroundColor: 'rgba(46, 51, 56, 0.22)', - }, - - outline: { - color: appThemes.light.primary.color, - }, - }, - - primary: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.light.primary.color, - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.light.primary.colorHover, - }, - - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.light.primary.colorPressed, - }, - - outline: { - color: appThemes.light.primary.color, - }, - }, - - warning: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.light.warning.color, - - hover: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.light.warning.colorHover, - }, - - pressed: { - textColor: appThemes.dark.text.baseColor, - backgroundColor: appThemes.light.warning.colorPressed, - }, - - outline: { - color: appThemes.light.warning.color, - }, - }, + default: createState({ + textColor: theme.text.baseColor, + backgroundColor: theme.default.color, + hoverBackground: theme.default.colorHover, + pressedBackground: theme.default.colorPressed, + }), + primary: createState({ + textColor: theme.primary.color, + backgroundColor: theme.primary.colorFaded, + hoverBackground: lighten(theme.primary.colorFaded, 30), + pressedBackground: darken(theme.primary.colorFaded, 30), + }), + warning: createState({ + textColor: theme.text.baseColor, + backgroundColor: theme.warning.color, + hoverBackground: theme.warning.colorHover, + pressedBackground: theme.warning.colorPressed, + }), }, text: { - default: { - textColor: appThemes.light.text.baseColor, + default: createState({ + textColor: theme.text.baseColor, backgroundColor: 'transparent', - - hover: { - textColor: appThemes.light.text.baseColor, - backgroundColor: 'rgba(46, 51, 56, 0.09)', - }, - - pressed: { - textColor: appThemes.light.text.baseColor, - backgroundColor: 'rgba(46, 51, 56, 0.13)', - }, - - outline: { - color: appThemes.light.primary.color, - }, - }, - primary: { - textColor: appThemes.light.primary.color, - backgroundColor: 'transparent', - - hover: { - textColor: appThemes.light.primary.colorHover, - backgroundColor: 'rgba(46, 51, 56, 0.09)', - }, - - pressed: { - textColor: appThemes.light.primary.colorPressed, - backgroundColor: 'rgba(46, 51, 56, 0.13)', - }, - - outline: { - color: appThemes.light.primary.color, - }, - }, - warning: { - textColor: appThemes.light.warning.color, + hoverBackground: theme.default.colorHover, + pressedBackground: theme.default.colorPressed, + }), + primary: createState({ + textColor: theme.primary.color, backgroundColor: 'transparent', - - hover: { - textColor: appThemes.light.warning.colorHover, - backgroundColor: 'rgba(46, 51, 56, 0.09)', - }, - - pressed: { - textColor: appThemes.light.warning.colorPressed, - backgroundColor: 'rgba(46, 51, 56, 0.13)', - }, - - outline: { - color: appThemes.light.warning.color, - }, - }, + hoverBackground: theme.primary.colorFaded, + pressedBackground: darken(theme.primary.colorFaded, 30), + }), + warning: createState({ + textColor: theme.text.baseColor, + backgroundColor: theme.warning.color, + hoverBackground: theme.warning.colorHover, + pressedBackground: theme.warning.colorPressed, + }), }, - }, + }; +}; + +export const { useTheme } = defineThemes({ + dark: createTheme({ style: 'dark' }), + light: createTheme({ style: 'light' }), }); diff --git a/src/ui/c-button/c-button.vue b/src/ui/c-button/c-button.vue index f52a069..2cbc4fd 100644 --- a/src/ui/c-button/c-button.vue +++ b/src/ui/c-button/c-button.vue @@ -14,6 +14,7 @@ <script lang="ts" setup> import type { RouteLocationRaw } from 'vue-router'; import { useTheme } from './c-button.theme'; +import { useAppTheme } from '../theme/themes'; const props = withDefaults( defineProps<{ @@ -56,11 +57,11 @@ const tag = computed(() => { } return 'button'; }); +const appTheme = useAppTheme(); </script> <style lang="less" scoped> .c-button { - margin: 0; line-height: 1; font-family: inherit; font-size: inherit; @@ -103,7 +104,7 @@ const tag = computed(() => { } &:focus { - outline: 2px solid v-bind('variantTheme.outline.color'); + outline: 1px solid v-bind('appTheme.primary.color'); } &.disabled { diff --git a/src/ui/c-card/c-card.demo.vue b/src/ui/c-card/c-card.demo.vue new file mode 100644 index 0000000..6d81ee6 --- /dev/null +++ b/src/ui/c-card/c-card.demo.vue @@ -0,0 +1,13 @@ +<template> + <div> + <h2>Default</h2> + + <c-card title="Title"> + Lorem ipsum, dolor sit amet consectetur adipisicing elit. Repudiandae ipsa reiciendis facilis officia nulla. + Laboriosam cumque molestias excepturi doloribus nulla nemo quod ratione rerum possimus. Excepturi nihil possimus + error itaque. + </c-card> + </div> +</template> + +<script lang="ts" setup></script> diff --git a/src/ui/c-link/c-link.demo.vue b/src/ui/c-link/c-link.demo.vue new file mode 100644 index 0000000..a655f11 --- /dev/null +++ b/src/ui/c-link/c-link.demo.vue @@ -0,0 +1,12 @@ +<template> + <div> + <h2>Default</h2> + <c-link mx-1> Link </c-link> + </div> +</template> + +<script lang="ts" setup> +import CLink from './c-link.vue'; +</script> + +<style lang="less" scoped></style> diff --git a/src/ui/c-link/c-link.vue b/src/ui/c-link/c-link.vue index df10120..a7d1b83 100644 --- a/src/ui/c-link/c-link.vue +++ b/src/ui/c-link/c-link.vue @@ -16,7 +16,15 @@ const props = defineProps<{ const { href, to } = toRefs(props); const theme = useTheme(); -const tag = computed(() => (href?.value ? 'a' : RouterLink)); +const tag = computed(() => { + if (href?.value) { + return 'a'; + } + if (to?.value) { + return RouterLink; + } + return 'span'; +}); </script> <style lang="less" scoped> diff --git a/src/ui/color/color.models.test.ts b/src/ui/color/color.models.test.ts new file mode 100644 index 0000000..dc59fa8 --- /dev/null +++ b/src/ui/color/color.models.test.ts @@ -0,0 +1,40 @@ +import { describe, test, expect } from 'vitest'; +import { darken, lighten, setOpacity } from './color.models'; + +describe('color models', () => { + describe('lighten', () => { + test('lightens a color', () => { + expect(lighten('#000000', 10)).toBe('#0a0a0a'); + expect(lighten('#000000', 20)).toBe('#141414'); + expect(lighten('#ffffff', 30)).toBe('#ffffff'); + }); + + test('lightens a color with alpha', () => { + expect(lighten('#00000080', 10)).toBe('#0a0a0a80'); + expect(lighten('#00000080', 20)).toBe('#14141480'); + expect(lighten('#ffffff80', 30)).toBe('#ffffff80'); + }); + }); + + describe('darken', () => { + test('darkens a color', () => { + expect(darken('#ffffff', 10)).toBe('#f5f5f5'); + expect(darken('#ffffff', 20)).toBe('#ebebeb'); + expect(darken('#000000', 30)).toBe('#000000'); + }); + + test('darkens a color with alpha', () => { + expect(darken('#ffffff80', 10)).toBe('#f5f5f580'); + }); + }); + + describe('setOpacity', () => { + test('sets the opacity of a color', () => { + expect(setOpacity('#000000', 0.5)).toBe('#00000080'); + }); + + test('sets the opacity of a color with alpha', () => { + expect(setOpacity('#00000000', 0.5)).toBe('#00000080'); + }); + }); +}); diff --git a/src/ui/color/color.models.ts b/src/ui/color/color.models.ts new file mode 100644 index 0000000..5b4c79b --- /dev/null +++ b/src/ui/color/color.models.ts @@ -0,0 +1,33 @@ +export { lighten, darken, setOpacity }; + +const clampHex = (value: number) => Math.max(0, Math.min(255, Math.round(value))); + +function lighten(color: string, amount: number): string { + const alpha = color.length === 9 ? color.slice(7) : ''; + const num = parseInt(color.slice(1, 7), 16); + + const r = clampHex(((num >> 16) & 255) + amount); + const g = clampHex(((num >> 8) & 255) + amount); + const b = clampHex((num & 255) + amount); + + return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}${alpha}`; +} + +function darken(color: string, amount: number): string { + return lighten(color, -amount); +} + +function setOpacity(color: string, opacity: number): string { + const alpha = clampHex(Math.round(opacity * 255)) + .toString(16) + .padStart(2, '0'); + + if (color.length === 7) { + return `${color}${alpha}`; + } + + if (color.length === 9) { + return `${color.slice(0, 7)}${alpha}`; + } + throw new Error('Invalid hex color'); +} diff --git a/src/ui/demo/demo-wrapper.vue b/src/ui/demo/demo-wrapper.vue new file mode 100644 index 0000000..cc16a00 --- /dev/null +++ b/src/ui/demo/demo-wrapper.vue @@ -0,0 +1,33 @@ +<template> + <div mt-2 w-full p-8> + <h1>c-lib components</h1> + + <div flex> + <div w-30 b-r b-gray b-op-10 b-r-solid pr-4> + <c-button + v-for="{ name } of demoRoutes" + :key="name" + variant="text" + :to="{ name }" + w-full + important:justify-start + :type="route.name === name ? 'primary' : 'default'" + > + {{ name }} + </c-button> + </div> + + <div flex-1 pl-4> + <router-view /> + </div> + </div> + </div> +</template> + +<script lang="ts" setup> +import { demoRoutes } from './demo.routes'; + +const route = useRoute(); +</script> + +<style lang="less" scoped></style> diff --git a/src/ui/demo/demo.routes.ts b/src/ui/demo/demo.routes.ts new file mode 100644 index 0000000..0e9a9e4 --- /dev/null +++ b/src/ui/demo/demo.routes.ts @@ -0,0 +1,25 @@ +import type { RouteRecordRaw } from 'vue-router'; + +const demoPages = import.meta.glob('../*/*.demo.vue'); + +export const demoRoutes = Object.keys(demoPages).map((path) => { + const [, , fileName] = path.split('/'); + const name = fileName.split('.').shift(); + + console.log(path); + + return { + path: name, + name, + component: () => import(/* @vite-ignore */ path), + } as RouteRecordRaw; +}); + +export const routes = [ + { + path: '/c-lib', + name: 'c-lib', + children: demoRoutes, + component: () => import('./demo-wrapper.vue'), + }, +]; diff --git a/src/ui/theme/themes.ts b/src/ui/theme/themes.ts index eca63db..18b2c8d 100644 --- a/src/ui/theme/themes.ts +++ b/src/ui/theme/themes.ts @@ -3,16 +3,20 @@ import { defineThemes } from './theme.models'; export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({ light: { text: { - baseColor: 'rgb(51, 54, 57)', - mutedColor: 'rgba(118, 124, 130)', + baseColor: '#333639', + mutedColor: '#767c82', + }, + default: { + color: 'rgba(46, 51, 56, 0.05)', + colorHover: 'rgba(46, 51, 56, 0.09)', + colorPressed: 'rgba(46, 51, 56, 0.22)', }, - primary: { color: '#18a058', colorHover: '#1ea54c', colorPressed: '#0C7A43', + colorFaded: '#18a0582f', }, - warning: { color: '#f59e0b', colorHover: '#f59e0b', @@ -33,14 +37,19 @@ export const { themes: appThemes, useTheme: useAppTheme } = defineThemes({ }, dark: { text: { - baseColor: 'rgba(255, 255, 255, 0.82)', - mutedColor: 'rgba(255, 255, 255, 0.5)', + baseColor: '#ffffffd1', + mutedColor: '#ffffff80', + }, + default: { + color: 'rgba(255, 255, 255, 0.08)', + colorHover: 'rgba(255, 255, 255, 0.12)', + colorPressed: 'rgba(255, 255, 255, 0.24)', }, - primary: { color: '#1ea54c', colorHover: '#36AD6A', colorPressed: '#0C7A43', + colorFaded: '#18a0582f', }, warning: { color: '#f59e0b', |