mirror of
https://github.com/HoshinoSuzumi/rayine-ui.git
synced 2025-04-18 09:28:51 +08:00
✨ Feat(button): Framework and add button component
This commit is contained in:
parent
6daefcfc69
commit
6bd44814b5
29
.playground/components/DocContentBlock.vue
Normal file
29
.playground/components/DocContentBlock.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
accentTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
padTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 v-if="title" class="font-medium" :class="{ 'mt-8': padTop, 'text-primary text-2xl font-medium': accentTitle, 'text-xl font-normal': !accentTitle }">{{ title }}</h1>
|
||||||
|
<p v-if="description" class="text-sm text-justify">{{ description }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
81
.playground/components/DocExampleBlock.vue
Normal file
81
.playground/components/DocExampleBlock.vue
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import FileTypeVue from "./icon/VscodeIconsFileTypeVue.vue"
|
||||||
|
import FileTypeTypescript from "./icon/VscodeIconsFileTypeTypescriptOfficial.vue"
|
||||||
|
import FileTypeJavascript from "./icon/VscodeIconsFileTypeJsOfficial.vue"
|
||||||
|
import TablerTerminal from "./icon/TablerTerminal.vue";
|
||||||
|
|
||||||
|
const slots = defineSlots<{
|
||||||
|
default?: () => VNode[];
|
||||||
|
code?: () => VNode[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const IconComponents = {
|
||||||
|
'vue': FileTypeVue,
|
||||||
|
'vue-html': FileTypeVue,
|
||||||
|
'sh': TablerTerminal,
|
||||||
|
'ts': FileTypeTypescript,
|
||||||
|
'js': FileTypeJavascript,
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeSlotContent = computed(() => {
|
||||||
|
if (slots.code) {
|
||||||
|
const slotContent = slots.code();
|
||||||
|
let contentLines = slotContent
|
||||||
|
.map(vnode => vnode.children || '')
|
||||||
|
.join('')
|
||||||
|
.replace('\n', '') // remove first line break
|
||||||
|
.split('\n');
|
||||||
|
|
||||||
|
// calculate the minimum indent
|
||||||
|
const minIndent = contentLines.reduce((min, line) => {
|
||||||
|
const match = line.match(/^(\s*)\S/);
|
||||||
|
if (match) {
|
||||||
|
return Math.min(min, match[1].length);
|
||||||
|
}
|
||||||
|
return min;
|
||||||
|
}, Infinity);
|
||||||
|
|
||||||
|
// remove the minimum indent from each line
|
||||||
|
const stringContent = contentLines.map(line => line.slice(minIndent)).join('\n');
|
||||||
|
|
||||||
|
return stringContent;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
filename: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
type: String as PropType<keyof typeof IconComponents>,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg">
|
||||||
|
<div v-if="filename" class="p-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<component v-if="lang" :is="IconComponents[lang]" class="inline" />
|
||||||
|
<span class="text-sm text-neutral-500 dark:text-neutral-400">{{ filename }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="slots.default">
|
||||||
|
<div :class="['p-4 overflow-auto', $slots.code ? 'border-b border-neutral-200 dark:border-neutral-700' : '']">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="slots.code">
|
||||||
|
<div class="p-4 overflow-auto">
|
||||||
|
<Shiki class="text-sm" :lang="lang" :code="codeSlotContent" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
// const appConfig = useAppConfig();
|
||||||
|
// appConfig.rayui.primary = 'red';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -8,8 +9,13 @@
|
|||||||
<h1 class="font-medium text-xl">RayineSoft<sup class="text-sm"> ©</sup></h1>
|
<h1 class="font-medium text-xl">RayineSoft<sup class="text-sm"> ©</sup></h1>
|
||||||
<h2 class="font-normal text-xs">Common Components</h2>
|
<h2 class="font-normal text-xs">Common Components</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<NuxtLink to="/" class="text-neutral-400 dark:text-neutral-500"
|
||||||
|
active-class="!text-neutral-700 dark:!text-neutral-300">Docs</NuxtLink>
|
||||||
|
<NuxtLink to="/installation" class="text-neutral-400 dark:text-neutral-500"
|
||||||
|
active-class="!text-neutral-700 dark:!text-neutral-300">Installation</NuxtLink>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
</style>
|
|
||||||
|
10
.playground/components/icon/TablerTerminal.vue
Normal file
10
.playground/components/icon/TablerTerminal.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m5 7l5 5l-5 5m7 2h7"></path></svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TablerTerminal'
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,5 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'RayineSoft Common Components'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -16,10 +18,16 @@ body {
|
|||||||
@apply bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100;
|
@apply bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shiki,
|
||||||
|
.shiki span {
|
||||||
|
background-color: rgba(0, 0, 0, 0) !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
||||||
.shiki,
|
.shiki,
|
||||||
.shiki span {
|
.shiki span,
|
||||||
|
code.shiki {
|
||||||
color: var(--shiki-dark) !important;
|
color: var(--shiki-dark) !important;
|
||||||
background-color: rgba(0, 0, 0, 0) !important;
|
background-color: rgba(0, 0, 0, 0) !important;
|
||||||
font-style: var(--shiki-dark-font-style) !important;
|
font-style: var(--shiki-dark-font-style) !important;
|
||||||
|
@ -31,12 +31,18 @@ export default defineNuxtConfig({
|
|||||||
"yaml",
|
"yaml",
|
||||||
"vue",
|
"vue",
|
||||||
"vue-html",
|
"vue-html",
|
||||||
|
"sh",
|
||||||
|
],
|
||||||
|
bundledThemes: [
|
||||||
|
"light-plus",
|
||||||
|
"dark-plus",
|
||||||
|
"material-theme",
|
||||||
|
"material-theme-lighter",
|
||||||
],
|
],
|
||||||
bundledThemes: ["light-plus", "dark-plus"],
|
|
||||||
highlightOptions: {
|
highlightOptions: {
|
||||||
themes: {
|
themes: {
|
||||||
light: "light-plus",
|
light: "material-theme-lighter",
|
||||||
dark: "dark-plus",
|
dark: "material-theme",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3,56 +3,115 @@ const message = useMessage();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-start gap-12">
|
<div class="flex flex-col items-start gap-16 pb-20">
|
||||||
|
<section>
|
||||||
|
<DocContentBlock title="Button" accent-title />
|
||||||
|
|
||||||
<div class="w-full flex flex-col gap-4">
|
<DocContentBlock title="Variants" />
|
||||||
<div>
|
<DocExampleBlock lang="vue-html">
|
||||||
<h1 class="font-medium text-xl text-indigo-400">Message</h1>
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-sm">Message component like a toast</p>
|
<RayButton>Solid</RayButton>
|
||||||
</div>
|
<RayButton variant="outline">Outline</RayButton>
|
||||||
|
<RayButton variant="soft">Soft</RayButton>
|
||||||
|
<RayButton variant="ghost">Ghost</RayButton>
|
||||||
|
<RayButton variant="link">Link</RayButton>
|
||||||
|
</div>
|
||||||
|
<template #code>
|
||||||
|
{{ `
|
||||||
|
<template>
|
||||||
|
<RayButton>Solid</RayButton>
|
||||||
|
<RayButton variant="outline">Outline</RayButton>
|
||||||
|
<RayButton variant="soft">Soft</RayButton>
|
||||||
|
<RayButton variant="ghost">Ghost</RayButton>
|
||||||
|
<RayButton variant="link">Link</RayButton>
|
||||||
|
</template>` }}
|
||||||
|
</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
|
||||||
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg">
|
<DocContentBlock title="Colors" />
|
||||||
<div class="p-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
<DocExampleBlock lang="vue-html">
|
||||||
<span class="flex items-center gap-1">
|
<div class="flex items-center gap-2">
|
||||||
<IconVscodeIconsFileTypeVue class="inline" />
|
<RayButton color="amber">amber</RayButton>
|
||||||
<span class="text-sm text-neutral-500 dark:text-neutral-400">app.vue</span>
|
<RayButton color="violet" variant="outline">violet</RayButton>
|
||||||
</span>
|
<RayButton color="red" variant="soft">red</RayButton>
|
||||||
|
<RayButton color="emerald" variant="ghost">emerald</RayButton>
|
||||||
|
<RayButton color="cyan" variant="link">cyan</RayButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4">
|
<template #code>
|
||||||
<Shiki class="text-sm" lang="vue-html" :code="`<template>
|
{{ `
|
||||||
<RayMessageProvider>
|
<template>
|
||||||
<NuxtLayout>
|
<RayButton color="amber">amber</RayButton>
|
||||||
<NuxtPage />
|
<RayButton color="violet" variant="outline">violet</RayButton>
|
||||||
</NuxtLayout>
|
<RayButton color="red" variant="soft">red</RayButton>
|
||||||
</RayMessageProvider>
|
<RayButton color="emerald" variant="ghost">emerald</RayButton>
|
||||||
</template>`" />
|
</template>` }}
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</DocExampleBlock>
|
||||||
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg">
|
|
||||||
<div class="p-4 border-b border-neutral-200 dark:border-neutral-700">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button class="bg-indigo-400 text-white text-sm border border-indigo-400 px-2.5 py-1 rounded-lg"
|
|
||||||
@click="message.info('message info', 10000)">Info 10s</button>
|
|
||||||
<button class="bg-indigo-400 text-white text-sm border border-indigo-400 px-2.5 py-1 rounded-lg"
|
|
||||||
@click="message.success('message success')">Success</button>
|
|
||||||
<button class="bg-indigo-400 text-white text-sm border border-indigo-400 px-2.5 py-1 rounded-lg"
|
|
||||||
@click="message.warning('message warning')">Warning</button>
|
|
||||||
<button class="bg-indigo-400 text-white text-sm border border-indigo-400 px-2.5 py-1 rounded-lg"
|
|
||||||
@click="message.error('message error')">Error</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-4">
|
|
||||||
<Shiki class="text-sm" lang="ts" :code="`const message = useMessage();\n
|
|
||||||
message.info('message info', 10000);
|
|
||||||
message.success('message success');
|
|
||||||
message.warning('message warning');
|
|
||||||
message.error('message error');`" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<DocContentBlock title="Sizes" />
|
||||||
|
<DocExampleBlock lang="vue-html">
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<RayButton size="2xs">Button</RayButton>
|
||||||
|
<RayButton size="xs">Button</RayButton>
|
||||||
|
<RayButton size="sm">Button</RayButton>
|
||||||
|
<RayButton size="md">Button</RayButton>
|
||||||
|
<RayButton size="lg">Button</RayButton>
|
||||||
|
<RayButton size="xl">Button</RayButton>
|
||||||
|
</div>
|
||||||
|
<template #code>
|
||||||
|
{{ `
|
||||||
|
<template>
|
||||||
|
<RayButton size="2xs">Button</RayButton>
|
||||||
|
<RayButton size="xs">Button</RayButton>
|
||||||
|
<RayButton size="sm">Button</RayButton>
|
||||||
|
<RayButton size="md">Button</RayButton>
|
||||||
|
<RayButton size="lg">Button</RayButton>
|
||||||
|
<RayButton size="xl">Button</RayButton>
|
||||||
|
</template>` }}
|
||||||
|
</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<DocContentBlock title="Message" description="Message component like a toast" accent-title />
|
||||||
|
|
||||||
|
<DocExampleBlock lang="vue-html" filename="app.vue">
|
||||||
|
<template #code>
|
||||||
|
{{ `
|
||||||
|
<template>
|
||||||
|
<RayMessageProvider>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</RayMessageProvider>
|
||||||
|
</template>`}}
|
||||||
|
</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
|
||||||
|
<DocExampleBlock lang="ts">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<RayButton @click="message.info('message info', 10000)">Info 10s</RayButton>
|
||||||
|
<RayButton @click="message.success('message success')">Success</RayButton>
|
||||||
|
<RayButton @click="message.warning('message warning')">Warning</RayButton>
|
||||||
|
<RayButton @click="message.error('message error')">Error</RayButton>
|
||||||
|
<RayButton @click="console.log('test')" color="red">Test</RayButton>
|
||||||
|
</div>
|
||||||
|
<template #code>
|
||||||
|
{{ `
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
message.info('message info', 10000);
|
||||||
|
message.success('message success');
|
||||||
|
message.warning('message warning');
|
||||||
|
message.error('message error');` }}
|
||||||
|
</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
section {
|
||||||
|
@apply w-full flex flex-col gap-4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
59
.playground/pages/installation.vue
Normal file
59
.playground/pages/installation.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const configuration_code = `
|
||||||
|
export default defineAppConfig({
|
||||||
|
rayui: {
|
||||||
|
primary: "indigo", // primary color
|
||||||
|
gray: "neutral", // gray color
|
||||||
|
strategy: "merge", // merge or override
|
||||||
|
// components configuration...
|
||||||
|
// button: {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-start gap-12 pb-20">
|
||||||
|
<section>
|
||||||
|
<DocContentBlock title="Introduction"
|
||||||
|
description="This project aims to facilitate sharing a component library across multiple projects for my own use. Open-sourcing it is just a bonus. Therefore, I am under no obligation to meet your requirements, and breaking changes may occur at any time. Of course, pull requests are welcome."
|
||||||
|
accent-title />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<DocContentBlock title="Installation"
|
||||||
|
description="This project is a Nuxt Layer and can currently only be used with Nuxt. Get started using one of the following methods"
|
||||||
|
accent-title />
|
||||||
|
|
||||||
|
<DocContentBlock title="GitHub" />
|
||||||
|
<DocExampleBlock lang="ts" filename="nuxt.config.ts">
|
||||||
|
<template #code>
|
||||||
|
{{ `\nexport default defineNuxtConfig({\n extends: ["github:HoshinoSuzumi/rayine-layer"]\n}` }}
|
||||||
|
</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
|
||||||
|
<DocContentBlock title="NPM" />
|
||||||
|
<DocExampleBlock lang="sh" filename="terminal">
|
||||||
|
<template #code>npm install rayine-layer</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
<DocExampleBlock lang="ts" filename="nuxt.config.ts">
|
||||||
|
<template #code>
|
||||||
|
{{ `\nexport default defineNuxtConfig({\n extends: ["rayine-layer"]\n}` }}
|
||||||
|
</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<DocContentBlock title="Configuration" description="All components will be automatically injected" accent-title />
|
||||||
|
<DocExampleBlock lang="ts" filename="app.config.ts">
|
||||||
|
<template #code>
|
||||||
|
{{ configuration_code }}
|
||||||
|
</template>
|
||||||
|
</DocExampleBlock>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
section {
|
||||||
|
@apply w-full flex flex-col gap-4;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,14 +1,24 @@
|
|||||||
export default defineAppConfig({
|
import type { DeepPartial, Strategy } from "./types/utils";
|
||||||
myLayer: {
|
import type * as config from "./ui.config";
|
||||||
name: 'Hello from Nuxt layer'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
declare module '@nuxt/schema' {
|
export default defineAppConfig({
|
||||||
|
rayui: {
|
||||||
|
primary: "indigo",
|
||||||
|
gray: "neutral",
|
||||||
|
strategy: "merge",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RayUI = {
|
||||||
|
primary?: string;
|
||||||
|
gray?: string;
|
||||||
|
strategy?: Strategy;
|
||||||
|
colors?: string[];
|
||||||
|
[key: string]: any;
|
||||||
|
} & DeepPartial<typeof config>;
|
||||||
|
|
||||||
|
declare module "@nuxt/schema" {
|
||||||
interface AppConfigInput {
|
interface AppConfigInput {
|
||||||
myLayer?: {
|
rayui?: RayUI;
|
||||||
/** Project name */
|
|
||||||
name?: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,68 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { twJoin, twMerge } from 'tailwind-merge';
|
||||||
|
import { button } from '../../ui.config'
|
||||||
|
import type { DeepPartial, Strategy } from '../../types/utils';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import type { ButtonColor, ButtonSize, ButtonVariant } from '../../types/button';
|
||||||
|
|
||||||
|
const config = button;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
class: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
padded: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
square: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String as PropType<ButtonSize>,
|
||||||
|
default: () => button.default.size,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String as PropType<ButtonColor>,
|
||||||
|
default: () => button.default.color,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
type: String as PropType<ButtonVariant>,
|
||||||
|
default: () => button.default.variant
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { ui, attrs } = useUI('button', toRef(props, 'ui'), config)
|
||||||
|
|
||||||
|
const buttonClass = computed(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
const variant = ui.value.color?.[props.color as string]?.[props.variant as string] || ui.value.variant[props.variant]
|
||||||
|
return twMerge(twJoin(
|
||||||
|
ui.value.base,
|
||||||
|
ui.value.font,
|
||||||
|
ui.value.rounded,
|
||||||
|
ui.value.size[props.size],
|
||||||
|
props.padded && ui.value.padding[props.size],
|
||||||
|
variant?.replaceAll('{color}', props.color),
|
||||||
|
props.block ? ui.value.block : ui.value.inline,
|
||||||
|
), props.class)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button class="bg-cyan-200 rounded-lg border border-neutral-50 px-2 py-1">
|
<button :class="buttonClass" v-bind="{ ...attrs }">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MessageProviderApi, Message } from '../../types/Message';
|
import type { MessageProviderApi, Message } from '../../types/message';
|
||||||
|
|
||||||
const providerApi = inject<MessageProviderApi>('ray-message-provider')
|
const providerApi = inject<MessageProviderApi>('ray-message-provider')
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Message, MessageType } from '../../types/Message';
|
import type { Message, MessageType } from '../../types/message';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
max: {
|
max: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { MessageApi } from "../types/Message";
|
import type { MessageApi } from "../types/message";
|
||||||
|
|
||||||
export const useMessage = () => {
|
export const useMessage = () => {
|
||||||
const message = inject<MessageApi>("ray-message");
|
const message = inject<MessageApi>("ray-message");
|
||||||
|
29
composables/useUI.ts
Normal file
29
composables/useUI.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { DeepPartial, Strategy } from "../types/utils";
|
||||||
|
|
||||||
|
export const useUI = <T>(
|
||||||
|
key: string,
|
||||||
|
ui?: Ref<(DeepPartial<T> & { strategy?: Strategy }) | undefined>,
|
||||||
|
config?: T | Ref<T>
|
||||||
|
) => {
|
||||||
|
const _attrs = useAttrs();
|
||||||
|
const appConfig = useAppConfig();
|
||||||
|
|
||||||
|
const attrs = computed(() => omit(_attrs, ["class"]));
|
||||||
|
|
||||||
|
const _computedUiConfig = computed(() => {
|
||||||
|
const _ui = toValue(ui);
|
||||||
|
const _config = toValue(config);
|
||||||
|
|
||||||
|
return mergeUiConfig<T>(
|
||||||
|
_ui?.strategy || (appConfig.rayui?.strategy as Strategy),
|
||||||
|
_ui || {},
|
||||||
|
getValueByPath(appConfig.rayui, key, {}),
|
||||||
|
_config || {}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ui: _computedUiConfig,
|
||||||
|
attrs,
|
||||||
|
};
|
||||||
|
};
|
@ -1,8 +1,58 @@
|
|||||||
|
import { addTemplate, useNuxt } from "@nuxt/kit";
|
||||||
|
import { setColors } from "./utils/colors";
|
||||||
|
import { generateSafelist } from "./utils/colors";
|
||||||
|
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
compatibilityDate: "2024-11-15",
|
compatibilityDate: "2024-11-15",
|
||||||
modules: ["@nuxtjs/tailwindcss"],
|
modules: ["@nuxtjs/tailwindcss"],
|
||||||
|
hooks: {
|
||||||
|
"tailwindcss:config": (tailwindConfig) => {
|
||||||
|
const nuxt = useNuxt();
|
||||||
|
|
||||||
|
tailwindConfig.theme = tailwindConfig.theme || {};
|
||||||
|
tailwindConfig.theme.extend = tailwindConfig.theme.extend || {};
|
||||||
|
tailwindConfig.theme.extend.colors =
|
||||||
|
tailwindConfig.theme.extend.colors || {};
|
||||||
|
|
||||||
|
const colors = setColors(tailwindConfig.theme);
|
||||||
|
|
||||||
|
// generate safelist and inject it into tailwindConfig
|
||||||
|
const safelist = generateSafelist(
|
||||||
|
["primary", "amber", "violet", "red", "emerald", "cyan"],
|
||||||
|
colors
|
||||||
|
);
|
||||||
|
tailwindConfig.safelist = safelist;
|
||||||
|
|
||||||
|
// inject colors into appConfig
|
||||||
|
nuxt.options.appConfig.rayui = nuxt.options.appConfig.rayui || {};
|
||||||
|
nuxt.options.appConfig.rayui.colors = colors;
|
||||||
|
|
||||||
|
const template = addTemplate({
|
||||||
|
filename: "ray.colors.mjs",
|
||||||
|
getContents: () =>
|
||||||
|
`export default ${JSON.stringify(
|
||||||
|
nuxt.options.appConfig.rayui.colors
|
||||||
|
)};`,
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
const typesTemplate = addTemplate({
|
||||||
|
filename: "ray.colors.d.ts",
|
||||||
|
getContents: () =>
|
||||||
|
`declare module '#ray-colors' { const defaultExport: ${JSON.stringify(
|
||||||
|
nuxt.options.appConfig.rayui.colors
|
||||||
|
)}; export default defaultExport; }`,
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
nuxt.options.alias["#ray-colors"] = template.dst;
|
||||||
|
|
||||||
|
nuxt.hook("prepare:types", (opts) => {
|
||||||
|
opts.references.push({ path: typesTemplate.dst });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
path: "./components",
|
path: "./components",
|
||||||
|
@ -21,5 +21,9 @@
|
|||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
"vue": "latest"
|
"vue": "latest"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a"
|
"packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a",
|
||||||
|
"dependencies": {
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"tailwind-merge": "^2.5.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
87
plugins/colors.ts
Normal file
87
plugins/colors.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { computed } from "vue";
|
||||||
|
import { defineNuxtPlugin, useAppConfig, useNuxtApp, useHead } from "#imports";
|
||||||
|
import colors from "tailwindcss/colors";
|
||||||
|
|
||||||
|
const rgbHexPattern = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
|
||||||
|
|
||||||
|
function hexToRgb(hex: string) {
|
||||||
|
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
|
||||||
|
hex = hex.replace(shorthandRegex, function (_, r, g, b) {
|
||||||
|
return r + r + g + g + b + b;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = rgbHexPattern.exec(hex);
|
||||||
|
return result
|
||||||
|
? `${Number.parseInt(result[1], 16)} ${Number.parseInt(
|
||||||
|
result[2],
|
||||||
|
16
|
||||||
|
)} ${Number.parseInt(result[3], 16)}`
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConfigValue(value: string) {
|
||||||
|
return rgbHexPattern.test(value) ? hexToRgb(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const appConfig = useAppConfig();
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
|
|
||||||
|
const root = computed(() => {
|
||||||
|
const primary: Record<string, string> | undefined = getValueByPath(
|
||||||
|
colors,
|
||||||
|
appConfig.rayui.primary
|
||||||
|
);
|
||||||
|
const gray: Record<string, string> | undefined = getValueByPath(
|
||||||
|
colors,
|
||||||
|
appConfig.rayui.gray
|
||||||
|
);
|
||||||
|
|
||||||
|
return `:root {
|
||||||
|
${Object.entries(primary || colors.indigo)
|
||||||
|
.map(([key, value]) => `--color-primary-${key}: ${parseConfigValue(value)};`)
|
||||||
|
.join("\n")}
|
||||||
|
--color-primary-DEFAULT: var(--color-primary-500);
|
||||||
|
|
||||||
|
${Object.entries(gray || colors.neutral)
|
||||||
|
.map(([key, value]) => `--color-gray-${key}: ${parseConfigValue(value)};`)
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--color-primary-DEFAULT: var(--color-primary-400);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const headData: any = {
|
||||||
|
style: [
|
||||||
|
{
|
||||||
|
innerHTML: () => root.value,
|
||||||
|
tagPriority: -2,
|
||||||
|
id: "ray-colors",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
import.meta.client &&
|
||||||
|
nuxtApp.isHydrating &&
|
||||||
|
!nuxtApp.payload.serverRendered
|
||||||
|
) {
|
||||||
|
const style = document.createElement("style");
|
||||||
|
|
||||||
|
style.innerHTML = root.value;
|
||||||
|
style.setAttribute("data-ray-colors", "");
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
headData.script = [
|
||||||
|
{
|
||||||
|
innerHTML:
|
||||||
|
"document.head.removeChild(document.querySelector('[data-ray-colors]'))",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
useHead(headData);
|
||||||
|
});
|
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@ -7,6 +7,13 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
defu:
|
||||||
|
specifier: ^6.1.4
|
||||||
|
version: 6.1.4
|
||||||
|
tailwind-merge:
|
||||||
|
specifier: ^2.5.4
|
||||||
|
version: 2.5.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nuxt/eslint':
|
'@nuxt/eslint':
|
||||||
specifier: latest
|
specifier: latest
|
||||||
@ -3744,6 +3751,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: 1 || 2 || 2.0.1-compat || 3
|
tailwindcss: 1 || 2 || 2.0.1-compat || 3
|
||||||
|
|
||||||
|
tailwind-merge@2.5.4:
|
||||||
|
resolution: {integrity: sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==}
|
||||||
|
|
||||||
tailwindcss@3.4.15:
|
tailwindcss@3.4.15:
|
||||||
resolution: {integrity: sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==}
|
resolution: {integrity: sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@ -8533,6 +8543,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
tailwind-merge@2.5.4: {}
|
||||||
|
|
||||||
tailwindcss@3.4.15:
|
tailwindcss@3.4.15:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
|
11
tailwind.config.ts
Normal file
11
tailwind.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { type Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./components/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
"./ui.config/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
safelist: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
23
types/button.d.ts
vendored
Normal file
23
types/button.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import type { button } from "../ui.config";
|
||||||
|
import type colors from "#ray-colors";
|
||||||
|
import type { ExtractDeepObject, NestedKeyOf, ExtractDeepKey } from "./utils";
|
||||||
|
import type { AppConfig } from "nuxt/schema";
|
||||||
|
|
||||||
|
export type ButtonSize =
|
||||||
|
| keyof typeof button.size
|
||||||
|
| ExtractDeepKey<AppConfig, ["rayui", "button", "size"]>;
|
||||||
|
export type ButtonColor =
|
||||||
|
| keyof typeof button.color
|
||||||
|
| ExtractDeepKey<AppConfig, ["rayui", "button", "color"]>
|
||||||
|
| (typeof colors)[number];
|
||||||
|
export type ButtonVariant =
|
||||||
|
| keyof typeof button.variant
|
||||||
|
| ExtractDeepKey<AppConfig, ["rayui", "button", "variant"]>
|
||||||
|
| NestedKeyOf<typeof button.color>
|
||||||
|
| NestedKeyOf<ExtractDeepObject<AppConfig, ["rayui", "button", "color"]>>;
|
||||||
|
|
||||||
|
export interface Button {
|
||||||
|
size?: ButtonSize;
|
||||||
|
color?: ButtonColor;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
}
|
50
types/utils.d.ts
vendored
Normal file
50
types/utils.d.ts
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
export type Strategy = "override" | "merge";
|
||||||
|
|
||||||
|
export interface TightMap<O = any> {
|
||||||
|
[key: string]: TightMap | O;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeepPartial<T, O = any> = {
|
||||||
|
[P in keyof T]?: T[P] extends object
|
||||||
|
? DeepPartial<T[P], O>
|
||||||
|
: T[P] extends string
|
||||||
|
? string
|
||||||
|
: T[P];
|
||||||
|
} & {
|
||||||
|
[key: string]: O | TightMap<O>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NestedKeyOf<ObjectType extends Record<string, any>> = {
|
||||||
|
[Key in keyof ObjectType]: ObjectType[Key] extends Record<string, any>
|
||||||
|
? NestedKeyOf<ObjectType[Key]>
|
||||||
|
: Key;
|
||||||
|
}[keyof ObjectType];
|
||||||
|
|
||||||
|
type DeepKey<T, Keys extends string[]> = Keys extends [
|
||||||
|
infer First,
|
||||||
|
...infer Rest
|
||||||
|
]
|
||||||
|
? First extends keyof T
|
||||||
|
? Rest extends string[]
|
||||||
|
? DeepKey<T[First], Rest>
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export type ExtractDeepKey<T, Path extends string[]> = DeepKey<
|
||||||
|
T,
|
||||||
|
Path
|
||||||
|
> extends infer Result
|
||||||
|
? Result extends Record<string, any>
|
||||||
|
? keyof Result
|
||||||
|
: never
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export type ExtractDeepObject<T, Path extends string[]> = DeepKey<
|
||||||
|
T,
|
||||||
|
Path
|
||||||
|
> extends infer Result
|
||||||
|
? Result extends Record<string, any>
|
||||||
|
? Result
|
||||||
|
: never
|
||||||
|
: never;
|
47
ui.config/elements/button.ts
Normal file
47
ui.config/elements/button.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
export default {
|
||||||
|
base: "focus:outline-none focus-visible:outline-0 disabled:cursor-not-allowed disabled:opacity-75 aria-disabled:cursor-not-allowed aria-disabled:opacity-75 flex-shrink-0 transition",
|
||||||
|
rounded: "rounded-lg",
|
||||||
|
font: "font-medium",
|
||||||
|
block: "w-full flex justify-center items-center",
|
||||||
|
inline: "inline-flex items-center",
|
||||||
|
size: {
|
||||||
|
"2xs": "text-xs",
|
||||||
|
xs: "text-xs",
|
||||||
|
sm: "text-sm",
|
||||||
|
md: "text-sm",
|
||||||
|
lg: "text-sm",
|
||||||
|
xl: "text-base",
|
||||||
|
},
|
||||||
|
padding: {
|
||||||
|
"2xs": "px-2 py-1",
|
||||||
|
xs: "px-2.5 py-1.5",
|
||||||
|
sm: "px-2.5 py-1.5",
|
||||||
|
md: "px-3 py-2",
|
||||||
|
lg: "px-3.5 py-2.5",
|
||||||
|
xl: "px-3.5 py-2.5",
|
||||||
|
},
|
||||||
|
square: {
|
||||||
|
"2xs": "p-1",
|
||||||
|
xs: "p-1.5",
|
||||||
|
sm: "p-1.5",
|
||||||
|
md: "p-2",
|
||||||
|
lg: "p-2.5",
|
||||||
|
xl: "p-2.5",
|
||||||
|
},
|
||||||
|
color: {},
|
||||||
|
variant: {
|
||||||
|
solid:
|
||||||
|
"shadow-sm hover:shadow-md disabled:hover:shadow-sm active:shadow-none bg-{color}-500 disabled:bg-{color}-500 aria-disabled:bg-{color}-500 hover:bg-{color}-600 text-white active:bg-{color}-700 dark:active:bg-{color}-500 focus:ring focus:ring-{color}-300 focus:ring-opacity-50 dark:focus:ring-opacity-20",
|
||||||
|
outline:
|
||||||
|
"ring-1 ring-inset ring-current ring-{color}-500 text-{color}-500 dark:hover:text-{color}-400 dark:hover:text-{color}-500 hover:bg-{color}-100 dark:hover:bg-{color}-900 disabled:bg-transparent disabled:hover:bg-transparent aria-disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400",
|
||||||
|
soft: "text-{color}-500 dark:text-{color}-400 bg-{color}-50 hover:bg-{color}-100 disabled:bg-{color}-50 aria-disabled:bg-{color}-50 dark:bg-{color}-950 dark:hover:bg-{color}-900 dark:disabled:bg-{color}-950 dark:aria-disabled:bg-{color}-950 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400 transition-none",
|
||||||
|
ghost:
|
||||||
|
"text-{color}-500 dark:text-{color}-400 hover:bg-{color}-50 disabled:bg-transparent aria-disabled:bg-transparent dark:hover:bg-{color}-950 dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400",
|
||||||
|
link: "text-{color}-500 hover:text-{color}-600 disabled:text-{color}-500 aria-disabled:text-{color}-500 dark:text-{color}-400 dark:hover:text-{color}-500 dark:disabled:text-{color}-400 dark:aria-disabled:text-{color}-400 underline-offset-4 hover:underline focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-{color}-500 dark:focus-visible:ring-{color}-400",
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
size: "sm",
|
||||||
|
color: "primary",
|
||||||
|
variant: "solid",
|
||||||
|
},
|
||||||
|
};
|
2
ui.config/index.ts
Normal file
2
ui.config/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// elements
|
||||||
|
export { default as button } from './elements/button'
|
167
utils/colors.ts
Normal file
167
utils/colors.ts
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import type { Config as TwConfig } from "tailwindcss";
|
||||||
|
import defaultColors from "tailwindcss/colors";
|
||||||
|
import type { SafelistConfig } from "tailwindcss/types/config";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
delete defaultColors.lightBlue;
|
||||||
|
// @ts-ignore
|
||||||
|
delete defaultColors.warmGray;
|
||||||
|
// @ts-ignore
|
||||||
|
delete defaultColors.trueGray;
|
||||||
|
// @ts-ignore
|
||||||
|
delete defaultColors.coolGray;
|
||||||
|
// @ts-ignore
|
||||||
|
delete defaultColors.blueGray;
|
||||||
|
|
||||||
|
const colorsToRegex = (colors: string[]): string => colors.join("|");
|
||||||
|
|
||||||
|
type ColorConfig = Exclude<NonNullable<TwConfig["theme"]>["colors"], Function>;
|
||||||
|
|
||||||
|
export const setColors = (theme: TwConfig["theme"]) => {
|
||||||
|
const globalColors: ColorConfig = {
|
||||||
|
...(theme?.colors || defaultColors),
|
||||||
|
...theme?.extend?.colors,
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
globalColors.primary = theme.extend.colors.primary = {
|
||||||
|
50: "rgb(var(--color-primary-50) / <alpha-value>)",
|
||||||
|
100: "rgb(var(--color-primary-100) / <alpha-value>)",
|
||||||
|
200: "rgb(var(--color-primary-200) / <alpha-value>)",
|
||||||
|
300: "rgb(var(--color-primary-300) / <alpha-value>)",
|
||||||
|
400: "rgb(var(--color-primary-400) / <alpha-value>)",
|
||||||
|
500: "rgb(var(--color-primary-500) / <alpha-value>)",
|
||||||
|
600: "rgb(var(--color-primary-600) / <alpha-value>)",
|
||||||
|
700: "rgb(var(--color-primary-700) / <alpha-value>)",
|
||||||
|
800: "rgb(var(--color-primary-800) / <alpha-value>)",
|
||||||
|
900: "rgb(var(--color-primary-900) / <alpha-value>)",
|
||||||
|
950: "rgb(var(--color-primary-950) / <alpha-value>)",
|
||||||
|
DEFAULT: "rgb(var(--color-primary-DEFAULT) / <alpha-value>)",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (globalColors.gray) {
|
||||||
|
// @ts-ignore
|
||||||
|
globalColors.cool = theme.extend.colors.cool = defaultColors.gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
globalColors.gray = theme.extend.colors.gray = {
|
||||||
|
50: "rgb(var(--color-gray-50) / <alpha-value>)",
|
||||||
|
100: "rgb(var(--color-gray-100) / <alpha-value>)",
|
||||||
|
200: "rgb(var(--color-gray-200) / <alpha-value>)",
|
||||||
|
300: "rgb(var(--color-gray-300) / <alpha-value>)",
|
||||||
|
400: "rgb(var(--color-gray-400) / <alpha-value>)",
|
||||||
|
500: "rgb(var(--color-gray-500) / <alpha-value>)",
|
||||||
|
600: "rgb(var(--color-gray-600) / <alpha-value>)",
|
||||||
|
700: "rgb(var(--color-gray-700) / <alpha-value>)",
|
||||||
|
800: "rgb(var(--color-gray-800) / <alpha-value>)",
|
||||||
|
900: "rgb(var(--color-gray-900) / <alpha-value>)",
|
||||||
|
950: "rgb(var(--color-gray-950) / <alpha-value>)",
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.entries(globalColors)
|
||||||
|
.filter(([, value]) => typeof value === "object")
|
||||||
|
.map(([key]) => key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const safelistForComponent: Record<
|
||||||
|
string,
|
||||||
|
(colors: string) => TwConfig["safelist"]
|
||||||
|
> = {
|
||||||
|
button: (colorsToRegex) => [
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-50$`),
|
||||||
|
variants: ["hover", "disabled"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-100$`),
|
||||||
|
variants: ["hover"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-400$`),
|
||||||
|
variants: ["dark", "dark:disabled"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-500$`),
|
||||||
|
variants: ["disabled", "dark:hover", "dark:active"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-600$`),
|
||||||
|
variants: ["hover"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-700$`),
|
||||||
|
variants: ["active"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-900$`),
|
||||||
|
variants: ["dark:hover"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^bg-(${colorsToRegex})-950$`),
|
||||||
|
variants: ["dark", "dark:hover", "dark:disabled"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^text-(${colorsToRegex})-400$`),
|
||||||
|
variants: ["dark", "dark:hover", "dark:disabled"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^text-(${colorsToRegex})-500$`),
|
||||||
|
variants: ["dark:hover", "disabled"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^text-(${colorsToRegex})-600$`),
|
||||||
|
variants: ["hover"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^outline-(${colorsToRegex})-400$`),
|
||||||
|
variants: ["dark:focus-visible"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^outline-(${colorsToRegex})-500$`),
|
||||||
|
variants: ["focus-visible"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^ring-(${colorsToRegex})-300$`),
|
||||||
|
variants: ["focus", "dark:focus"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^ring-(${colorsToRegex})-400$`),
|
||||||
|
variants: ["dark:focus-visible"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: RegExp(`^ring-(${colorsToRegex})-500$`),
|
||||||
|
variants: ["focus-visible"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateSafelist = (
|
||||||
|
colors: string[],
|
||||||
|
globalColors: string[]
|
||||||
|
): string[] => {
|
||||||
|
const safelist = Object.keys(safelistForComponent)
|
||||||
|
.flatMap((component) =>
|
||||||
|
safelistForComponent[component](colorsToRegex(colors))
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(item): item is Exclude<SafelistConfig, string> => item !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const extractColorsFromPattern = (pattern: RegExp): string[] => {
|
||||||
|
const matches = pattern.source.match(/\(([^)]+)\)/);
|
||||||
|
if (!matches) return [];
|
||||||
|
return matches[1].split("|").map((color) =>
|
||||||
|
pattern.source.replace(matches[0], color).replace(/[\^\$]/g, "")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return safelist.flatMap((item) => {
|
||||||
|
const replacedStrings = extractColorsFromPattern(item.pattern);
|
||||||
|
return replacedStrings.concat(
|
||||||
|
item.variants?.flatMap((variant) =>
|
||||||
|
replacedStrings.map((str) => `${variant}:${str}`)
|
||||||
|
) || []
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
28
utils/index.ts
Normal file
28
utils/index.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { defu, createDefu } from "defu";
|
||||||
|
import { extendTailwindMerge } from "tailwind-merge";
|
||||||
|
import type { Strategy } from "../types/utils";
|
||||||
|
|
||||||
|
const custonTwMerge = extendTailwindMerge<string, string>({});
|
||||||
|
|
||||||
|
export const twMergeDefu = createDefu((obj, key, val, namespace) => {
|
||||||
|
if (namespace === "default" || namespace.startsWith("default.")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof obj[key] === "string" &&
|
||||||
|
typeof val === "string" &&
|
||||||
|
obj[key] &&
|
||||||
|
val
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
obj[key] = custonTwMerge<string, string>(obj[key], val);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mergeUiConfig = <T>(strategy: Strategy, ...configs: any): T => {
|
||||||
|
if (strategy === "merge") {
|
||||||
|
return twMergeDefu({}, ...configs) as T;
|
||||||
|
}
|
||||||
|
return defu({}, ...configs) as T;
|
||||||
|
};
|
37
utils/objectUtils.ts
Normal file
37
utils/objectUtils.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export const omit = <T extends Record<string, any>, K extends keyof T>(
|
||||||
|
object: T,
|
||||||
|
keysToOmit: K[] | any[]
|
||||||
|
): Pick<T, Exclude<keyof T, K>> => {
|
||||||
|
const result = { ...object };
|
||||||
|
|
||||||
|
for (const key of keysToOmit) {
|
||||||
|
delete result[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getValueByPath = (
|
||||||
|
obj: Record<string, any>,
|
||||||
|
path: string | (string | number)[],
|
||||||
|
defaultValue?: any
|
||||||
|
): any => {
|
||||||
|
if (typeof path === "string") {
|
||||||
|
path = path.split(".").map((key) => {
|
||||||
|
const num = Number(key);
|
||||||
|
return Number.isNaN(num) ? key : num;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = obj;
|
||||||
|
|
||||||
|
for (const key of path) {
|
||||||
|
if (result === undefined || result === null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result !== undefined ? result : defaultValue;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user