mirror of
https://github.com/HoshinoSuzumi/rayine-ui.git
synced 2025-04-16 12:48:50 +08:00
🚚 Runtime libs
This commit is contained in:
parent
ff817e25e6
commit
dad948469f
@ -1,5 +1,6 @@
|
||||
export default defineNuxtConfig({
|
||||
modules: ['../src/module'],
|
||||
myModule: {},
|
||||
compatibilityDate: "2024-11-18",
|
||||
devtools: { enabled: true },
|
||||
})
|
||||
modules: ["../src/module"],
|
||||
rayui: {},
|
||||
});
|
||||
|
@ -1,19 +1,35 @@
|
||||
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
|
||||
import { defineNuxtModule, addPlugin, createResolver } from "@nuxt/kit";
|
||||
import type { config } from "process";
|
||||
import type { Strategy, DeepPartial } from "./runtime/types/utils";
|
||||
|
||||
export type RayUI = {
|
||||
primary?: string;
|
||||
gray?: string;
|
||||
strategy?: Strategy;
|
||||
colors?: string[];
|
||||
[key: string]: any;
|
||||
} & DeepPartial<typeof config>;
|
||||
|
||||
declare module "@nuxt/schema" {
|
||||
interface AppConfigInput {
|
||||
rayui?: RayUI;
|
||||
}
|
||||
}
|
||||
|
||||
// Module options TypeScript interface definition
|
||||
export interface ModuleOptions {}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
meta: {
|
||||
name: 'my-module',
|
||||
configKey: 'myModule',
|
||||
name: "rayine/ui",
|
||||
configKey: "rayui",
|
||||
},
|
||||
// Default configuration options of the Nuxt module
|
||||
defaults: {},
|
||||
setup(_options, _nuxt) {
|
||||
const resolver = createResolver(import.meta.url)
|
||||
const resolver = createResolver(import.meta.url);
|
||||
|
||||
// Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
|
||||
addPlugin(resolver.resolve('./runtime/plugin'))
|
||||
addPlugin(resolver.resolve("./runtime/plugin"));
|
||||
},
|
||||
})
|
||||
});
|
||||
|
68
src/runtime/components/elements/Button.vue
Normal file
68
src/runtime/components/elements/Button.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<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>
|
||||
|
||||
<template>
|
||||
<button :class="buttonClass" v-bind="{ ...attrs }">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
11
src/runtime/components/forms/Input.vue
Normal file
11
src/runtime/components/forms/Input.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input placeholder="test from rayine" class="rounded-lg border border-neutral-200 px-2 py-1" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
10
src/runtime/components/icons/CircleError.vue
Normal file
10
src/runtime/components/icons/CircleError.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">
|
||||
<g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="M0 0h24v24H0z"></path>
|
||||
<path fill="currentColor"
|
||||
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-6.489 5.8a1 1 0 0 0-1.218 1.567L10.585 12l-1.292 1.293l-.083.094a1 1 0 0 0 1.497 1.32L12 13.415l1.293 1.292l.094.083a1 1 0 0 0 1.32-1.497L13.415 12l1.292-1.293l.083-.094a1 1 0 0 0-1.497-1.32L12 10.585l-1.293-1.292l-.094-.083z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
7
src/runtime/components/icons/CircleInfo.vue
Normal file
7
src/runtime/components/icons/CircleInfo.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" fillRule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2s10 4.477 10 10Zm-10 5.75a.75.75 0 0 0 .75-.75v-6a.75.75 0 0 0-1.5 0v6c0 .414.336.75.75.75ZM12 7a1 1 0 1 1 0 2a1 1 0 0 1 0-2Z"
|
||||
clipRule="evenodd"></path>
|
||||
</svg>
|
||||
</template>
|
10
src/runtime/components/icons/CircleSuccess.vue
Normal file
10
src/runtime/components/icons/CircleSuccess.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">
|
||||
<g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="M0 0h24v24H0z"></path>
|
||||
<path fill="currentColor"
|
||||
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-1.293 5.953a1 1 0 0 0-1.32-.083l-.094.083L11 12.585l-1.293-1.292l-.094-.083a1 1 0 0 0-1.403 1.403l.083.094l2 2l.094.083a1 1 0 0 0 1.226 0l.094-.083l4-4l.083-.094a1 1 0 0 0-.083-1.32z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
10
src/runtime/components/icons/CircleWarning.vue
Normal file
10
src/runtime/components/icons/CircleWarning.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">
|
||||
<g fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="M0 0h24v24H0z"></path>
|
||||
<path fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1-19.995.324L2 12l.004-.28C2.152 6.327 6.57 2 12 2zm.01 13l-.127.007a1 1 0 0 0 0 1.986L12 17l.127-.007a1 1 0 0 0 0-1.986L12.01 15zM12 7a1 1 0 0 0-.993.883L11 8v4l.007.117a1 1 0 0 0 1.986 0L13 12V8l-.007-.117A1 1 0 0 0 12 7z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
11
src/runtime/components/icons/Spinner.vue
Normal file
11
src/runtime/components/icons/Spinner.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"></path>
|
||||
<path fill="currentColor"
|
||||
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z">
|
||||
<animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate"
|
||||
values="0 12 12;360 12 12"></animateTransform>
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
62
src/runtime/components/overlays/Message.vue
Normal file
62
src/runtime/components/overlays/Message.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<script lang="ts" setup>
|
||||
import type { MessageProviderApi, Message } from '../../types/message';
|
||||
|
||||
const providerApi = inject<MessageProviderApi>('ray-message-provider')
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
require: true,
|
||||
type: Object,
|
||||
},
|
||||
})
|
||||
|
||||
const message = ref<Message>(props.message as Message)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
providerApi?.destroy(message.value.id)
|
||||
}, message.value?.duration || 3000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="message" :class="{
|
||||
[message.type]: message.type
|
||||
}">
|
||||
<IconCircleSuccess v-if="message.type === 'success'" class="text-xl" />
|
||||
<IconCircleWarning v-if="message.type === 'warning'" class="text-xl" />
|
||||
<IconCircleError v-if="message.type === 'error'" class="text-xl" />
|
||||
<IconCircleInfo v-if="message.type === 'info'" class="text-xl" />
|
||||
<span>
|
||||
{{ message.content }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message {
|
||||
min-width: 80px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, .2);
|
||||
@apply h-fit px-2 py-1.5 border bg-white border-gray-300 rounded-md text-gray-500 text-xs font-sans flex items-center gap-1.5 first-of-type:mt-2.5 mt-2.5 font-bold pointer-events-auto;
|
||||
}
|
||||
|
||||
.message.info {
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, .2);
|
||||
@apply !text-blue-500 !border-blue-400 !bg-blue-50 dark:!text-blue-300 dark:!border-blue-600 dark:!bg-blue-900;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, .2);
|
||||
@apply !text-emerald-500 !border-emerald-400 !bg-emerald-50 dark:!text-emerald-300 dark:!border-emerald-600 dark:!bg-emerald-900;
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
box-shadow: 0 4px 12px rgba(249, 115, 22, .2);
|
||||
@apply !text-orange-500 !border-orange-400 !bg-orange-50 dark:!text-orange-300 dark:!border-orange-600 dark:!bg-orange-900;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
box-shadow: 0 4px 12px rgba(244, 63, 94, .2);
|
||||
@apply !text-rose-500 !border-rose-400 !bg-rose-50 dark:!text-rose-300 dark:!border-rose-600 dark:!bg-rose-900;
|
||||
}
|
||||
</style>
|
95
src/runtime/components/overlays/MessageProvider.vue
Normal file
95
src/runtime/components/overlays/MessageProvider.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Message, MessageType } from '../../types/message';
|
||||
|
||||
const props = defineProps({
|
||||
max: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
})
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
const messageList = ref<Message[]>([])
|
||||
|
||||
const createMessage = (content: string, type: MessageType, duration: number = 3000) => {
|
||||
const { max } = props
|
||||
messageList.value.push({
|
||||
id: (Date.now() + Math.random() * 100).toString(32).toUpperCase(),
|
||||
content,
|
||||
type,
|
||||
duration,
|
||||
})
|
||||
if (messageList.value.length > max) {
|
||||
messageList.value.shift()
|
||||
}
|
||||
}
|
||||
|
||||
const providerApi = {
|
||||
destroy: (id: string) => {
|
||||
if (!messageList.value.find(message => message.id === id)) return
|
||||
messageList.value.splice(messageList.value.findIndex(message => message.id === id), 1)
|
||||
},
|
||||
}
|
||||
|
||||
const api = {
|
||||
info: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'info', duration)
|
||||
},
|
||||
success: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'success', duration)
|
||||
},
|
||||
warning: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'warning', duration)
|
||||
},
|
||||
error: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'error', duration)
|
||||
},
|
||||
}
|
||||
|
||||
nuxtApp.vueApp.provide('ray-message-provider', providerApi)
|
||||
nuxtApp.vueApp.provide('ray-message', api)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot></slot>
|
||||
<teleport to="body">
|
||||
<div id="message-provider">
|
||||
<div class="message-wrapper">
|
||||
<TransitionGroup name="message">
|
||||
<RayMessage v-for="(message, k) in messageList" :key="message.id" :message="message" />
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#message-provider .message-wrapper {
|
||||
@apply z-[50000] fixed inset-0 flex flex-col items-center pointer-events-none;
|
||||
}
|
||||
|
||||
.message-move,
|
||||
.message-leave-active {
|
||||
transition: all .8s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
.message-enter-active {
|
||||
transition: all .8s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
filter: blur(2px);
|
||||
opacity: 0;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
filter: blur(6px);
|
||||
opacity: 0;
|
||||
transform: translateY(-20%);
|
||||
}
|
||||
|
||||
.message-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
9
src/runtime/composables/useMessage.ts
Normal file
9
src/runtime/composables/useMessage.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { MessageApi } from "../types/message";
|
||||
|
||||
export const useMessage = () => {
|
||||
const message = inject<MessageApi>("ray-message");
|
||||
if (!message) {
|
||||
throw new Error("No outer message-provider found!");
|
||||
}
|
||||
return message;
|
||||
};
|
29
src/runtime/composables/useUI.ts
Normal file
29
src/runtime/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,
|
||||
};
|
||||
};
|
87
src/runtime/plugins/colors.ts
Normal file
87
src/runtime/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);
|
||||
});
|
23
src/runtime/types/button.d.ts
vendored
Normal file
23
src/runtime/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;
|
||||
}
|
20
src/runtime/types/message.d.ts
vendored
Normal file
20
src/runtime/types/message.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
export type MessageType = "success" | "warning" | "error" | "info";
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
content: string;
|
||||
type: MessageType;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface MessageApi {
|
||||
info: (content: string, duration?: number) => void;
|
||||
success: (content: string, duration?: number) => void;
|
||||
warning: (content: string, duration?: number) => void;
|
||||
error: (content: string, duration?: number) => void;
|
||||
destroyAll: () => void;
|
||||
}
|
||||
|
||||
export interface MessageProviderApi {
|
||||
destroy: (id: string) => void;
|
||||
}
|
50
src/runtime/types/utils.d.ts
vendored
Normal file
50
src/runtime/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
src/runtime/ui.config/elements/button.ts
Normal file
47
src/runtime/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
src/runtime/ui.config/index.ts
Normal file
2
src/runtime/ui.config/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// elements
|
||||
export { default as button } from './elements/button'
|
167
src/runtime/utils/colors.ts
Normal file
167
src/runtime/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
src/runtime/utils/index.ts
Normal file
28
src/runtime/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
src/runtime/utils/objectUtils.ts
Normal file
37
src/runtime/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