Merge pull request #1 from HoshinoSuzumi/migrate-module

Migrate module
This commit is contained in:
Timothy Yin 2024-11-18 17:45:34 +08:00 committed by GitHub
commit 0e864ec019
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 2597 additions and 2181 deletions

45
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npx nypm@latest i
- name: Lint
run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npx nypm@latest i
- name: Playground prepare
run: npm run dev:prepare
- name: Test
run: npm run test

65
.gitignore vendored
View File

@ -1,21 +1,56 @@
# Dependencies
node_modules node_modules
*.log
.nuxt # Logs
nuxt.d.ts *.log*
.output
.data # Temp directories
.env .temp
package-lock.json .tmp
framework .cache
dist
.DS_Store
# Yarn # Yarn
.yarn/cache **/.yarn/cache
.yarn/*state* **/.yarn/*state*
# Local History # Generated dirs
.history dist
# Nuxt
.nuxt
.output
.data
.vercel_build_output
.build-*
.netlify
# Env
.env
# Testing
reports
coverage
*.lcov
.nyc_output
# VSCode # VSCode
.vscode/ .vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Intellij idea
*.iml
.idea
# OSX
.DS_Store
.AppleDouble
.LSOverride
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

View File

@ -1 +0,0 @@
typescript.includeWorkspace = true

View File

@ -1,2 +0,0 @@
export default defineAppConfig({
})

View File

@ -1,29 +0,0 @@
<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>

View File

@ -1,81 +0,0 @@
<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">
<LazyShiki class="text-sm" :lang="lang" :code="codeSlotContent" />
</div>
</template>
</div>
</template>
<style scoped></style>

View File

@ -1,21 +0,0 @@
<script lang="ts" setup>
// const appConfig = useAppConfig();
// appConfig.rayui.primary = 'red';
</script>
<template>
<header class="w-full flex justify-between items-center py-2 border-b border-b-neutral-100 dark:border-b-neutral-800">
<div class="text-neutral-900 dark:text-neutral-100">
<h1 class="font-medium text-xl">RayineSoft<sup class="text-sm"> &copy;</sup></h1>
<h2 class="font-normal text-xs">Common Components</h2>
</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>
</template>
<style scoped></style>

View File

@ -1,10 +0,0 @@
<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>

View File

@ -1,10 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#f5de19" d="M2 2h28v28H2z"></path><path d="M20.809 23.875a2.87 2.87 0 0 0 2.6 1.6c1.09 0 1.787-.545 1.787-1.3c0-.9-.716-1.222-1.916-1.747l-.658-.282c-1.9-.809-3.16-1.822-3.16-3.964c0-1.973 1.5-3.476 3.853-3.476a3.89 3.89 0 0 1 3.742 2.107L25 18.128A1.79 1.79 0 0 0 23.311 17a1.145 1.145 0 0 0-1.259 1.128c0 .789.489 1.109 1.618 1.6l.658.282c2.236.959 3.5 1.936 3.5 4.133c0 2.369-1.861 3.667-4.36 3.667a5.06 5.06 0 0 1-4.795-2.691Zm-9.295.228c.413.733.789 1.353 1.693 1.353c.864 0 1.41-.338 1.41-1.653v-8.947h2.631v8.982c0 2.724-1.6 3.964-3.929 3.964a4.085 4.085 0 0 1-3.947-2.4Z"></path></svg>
</template>
<script>
export default {
name: 'VscodeIconsFileTypeJsOfficial'
}
</script>

View File

@ -1,10 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><rect width="28" height="28" x="2" y="2" fill="#3178c6" rx="1.312"></rect><path fill="#fff" fillRule="evenodd" d="M18.245 23.759v3.068a6.5 6.5 0 0 0 1.764.575a11.6 11.6 0 0 0 2.146.192a10 10 0 0 0 2.088-.211a5.1 5.1 0 0 0 1.735-.7a3.54 3.54 0 0 0 1.181-1.266a4.47 4.47 0 0 0 .186-3.394a3.4 3.4 0 0 0-.717-1.117a5.2 5.2 0 0 0-1.123-.877a12 12 0 0 0-1.477-.734q-.6-.249-1.08-.484a5.5 5.5 0 0 1-.813-.479a2.1 2.1 0 0 1-.516-.518a1.1 1.1 0 0 1-.181-.618a1.04 1.04 0 0 1 .162-.571a1.4 1.4 0 0 1 .459-.436a2.4 2.4 0 0 1 .726-.283a4.2 4.2 0 0 1 .956-.1a6 6 0 0 1 .808.058a6 6 0 0 1 .856.177a6 6 0 0 1 .836.3a4.7 4.7 0 0 1 .751.422V13.9a7.5 7.5 0 0 0-1.525-.4a12.4 12.4 0 0 0-1.9-.129a8.8 8.8 0 0 0-2.064.235a5.2 5.2 0 0 0-1.716.733a3.66 3.66 0 0 0-1.171 1.271a3.73 3.73 0 0 0-.431 1.845a3.6 3.6 0 0 0 .789 2.34a6 6 0 0 0 2.395 1.639q.63.26 1.175.509a6.5 6.5 0 0 1 .942.517a2.5 2.5 0 0 1 .626.585a1.2 1.2 0 0 1 .23.719a1.1 1.1 0 0 1-.144.552a1.3 1.3 0 0 1-.435.441a2.4 2.4 0 0 1-.726.292a4.4 4.4 0 0 1-1.018.105a5.8 5.8 0 0 1-1.969-.35a5.9 5.9 0 0 1-1.805-1.045m-5.154-7.638h4v-2.527H5.938v2.527H9.92v11.254h3.171Z"></path></svg>
</template>
<script>
export default {
name: 'VscodeIconsFileTypeTypescriptOfficial'
}
</script>

View File

@ -1,10 +0,0 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 32 32"><path fill="#41b883" d="M24.4 3.925H30l-14 24.15L2 3.925h10.71l3.29 5.6l3.22-5.6Z"></path><path fill="#41b883" d="m2 3.925l14 24.15l14-24.15h-5.6L16 18.415L7.53 3.925Z"></path><path fill="#35495e" d="M7.53 3.925L16 18.485l8.4-14.56h-5.18L16 9.525l-3.29-5.6Z"></path></svg>
</template>
<script>
export default {
name: 'VscodeIconsFileTypeVue'
}
</script>

View File

@ -1,38 +0,0 @@
<script lang="ts" setup>
useSeoMeta({
title: 'RayineSoft Common Components'
})
</script>
<template>
<div class="max-w-4xl mx-auto px-4">
<TitleBar />
<main class="pt-4">
<slot></slot>
</main>
</div>
</template>
<style>
body {
@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) {
.shiki,
.shiki span,
code.shiki {
color: var(--shiki-dark) !important;
background-color: rgba(0, 0, 0, 0) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
</style>

View File

@ -1,49 +0,0 @@
import defaultTheme from "tailwindcss/defaultTheme";
export default defineNuxtConfig({
extends: [".."],
vite: {
build: {
rollupOptions: {
external: ["shiki/wasm"],
},
},
},
modules: ["@nuxt/eslint", "@nuxt/fonts", "nuxt-shiki"],
tailwindcss: {
config: {
theme: {
extend: {
fontFamily: {
sans: ["Rubik", '"Noto Sans SC"', ...defaultTheme.fontFamily.sans],
},
},
},
},
},
shiki: {
bundledLangs: [
"js",
"ts",
"json",
"html",
"css",
"yaml",
"vue",
"vue-html",
"sh",
],
bundledThemes: [
"light-plus",
"dark-plus",
"material-theme",
"material-theme-lighter",
],
highlightOptions: {
themes: {
light: "material-theme-lighter",
dark: "material-theme",
},
},
},
});

View File

@ -1,116 +0,0 @@
<script lang="ts" setup>
const message = useMessage();
</script>
<template>
<div class="flex flex-col items-start gap-16 pb-20">
<section>
<DocContentBlock title="Button" accent-title />
<DocContentBlock title="Variants" />
<DocExampleBlock lang="vue-html">
<div class="flex items-center gap-2">
<RayButton>Solid</RayButton>
<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>
<DocContentBlock title="Colors" />
<DocExampleBlock lang="vue-html">
<div class="flex items-center gap-2">
<RayButton color="amber">amber</RayButton>
<RayButton color="violet" variant="outline" @click="message.success('I like this color!')">violet</RayButton>
<RayButton color="red" variant="soft">red</RayButton>
<RayButton color="emerald" variant="ghost">emerald</RayButton>
<RayButton color="cyan" variant="link">cyan</RayButton>
</div>
<template #code>
{{ `
<template>
<RayButton color="amber">amber</RayButton>
<RayButton color="violet" variant="outline">violet</RayButton>
<RayButton color="red" variant="soft">red</RayButton>
<RayButton color="emerald" variant="ghost">emerald</RayButton>
</template>` }}
</template>
</DocExampleBlock>
<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>
</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>
</template>
<style scoped>
section {
@apply w-full flex flex-col gap-4;
}
</style>

View File

@ -1,59 +0,0 @@
<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&nbsp;&nbsp;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&nbsp;&nbsp;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>

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"eslint.experimental.useFlatConfig": true
}

27
CHANGELOG.md Normal file
View File

@ -0,0 +1,27 @@
# Changelog
## v1.0.6
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.5...v1.0.6)
## v1.0.5
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.4...v1.0.5)
## v1.0.4
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.3...v1.0.4)
## v1.0.3
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.2...v1.0.3)
## v1.0.2
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.1...v1.0.2)
## v1.0.1
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.1-beta.1...v1.0.1)

119
README.md
View File

@ -1,73 +1,84 @@
# Nuxt Layer Starter <!--
Get your module up and running quickly.
Create Nuxt extendable layer with this GitHub template. Find and replace all on all files (CMD+SHIFT+F):
- Name: My Module
- Package name: my-module
- Description: My new Nuxt module
-->
## Setup # My Module
Make sure to install the dependencies: [![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
[![Nuxt][nuxt-src]][nuxt-href]
My new Nuxt module for doing amazing things.
- [✨ &nbsp;Release Notes](/CHANGELOG.md)
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/my-module?file=playground%2Fapp.vue) -->
<!-- - [📖 &nbsp;Documentation](https://example.com) -->
## Features
<!-- Highlight some of the features your module provide here -->
- ⛰ &nbsp;Foo
- 🚠 &nbsp;Bar
- 🌲 &nbsp;Baz
## Quick Setup
Install the module to your Nuxt application with one command:
```bash ```bash
pnpm install npx nuxi module add my-module
``` ```
## Working on your layer That's it! You can now use My Module in your Nuxt app ✨
Your layer is at the root of this repository, it is exactly like a regular Nuxt project, except you can publish it on NPM.
The `.playground` directory should help you on trying your layer during development. ## Contribution
Running `pnpm dev` will prepare and boot `.playground` directory, which imports your layer itself. <details>
<summary>Local development</summary>
## Distributing your layer
Your Nuxt layer is shaped exactly the same as any other Nuxt project, except you can publish it on NPM.
To do so, you only have to check if `files` in `package.json` are valid, then run:
```bash ```bash
npm publish --access public # Install dependencies
npm install
# Generate type stubs
npm run dev:prepare
# Develop with the playground
npm run dev
# Build the playground
npm run dev:build
# Run ESLint
npm run lint
# Run Vitest
npm run test
npm run test:watch
# Release new version
npm run release
``` ```
Once done, your users will only have to run: </details>
```bash
npm install --save your-layer
```
Then add the dependency to their `extends` in `nuxt.config`: <!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=020420&colorB=00DC82
[npm-version-href]: https://npmjs.com/package/my-module
```ts [npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=020420&colorB=00DC82
defineNuxtConfig({ [npm-downloads-href]: https://npm.chart.dev/my-module
extends: 'your-layer'
})
```
## Development Server [license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=020420&colorB=00DC82
[license-href]: https://npmjs.com/package/my-module
Start the development server on http://localhost:3000 [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
[nuxt-href]: https://nuxt.com
```bash
pnpm dev
```
## Production
Build the application for production:
```bash
pnpm build
```
Or statically generate it with:
```bash
pnpm generate
```
Locally preview production build:
```bash
pnpm preview
```
Checkout the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@ -1,24 +0,0 @@
import type { DeepPartial, Strategy } from "./types/utils";
import type * as config from "./ui.config";
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 {
rayui?: RayUI;
}
}

10
app.vue
View File

@ -1,10 +0,0 @@
<script>
</script>
<template>
<RayMessageProvider>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</RayMessageProvider>
</template>

View File

@ -1,11 +0,0 @@
<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>

View File

@ -1,10 +0,0 @@
<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>

View File

@ -1,10 +0,0 @@
<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>

View File

@ -1,10 +0,0 @@
<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>

View File

@ -1,11 +0,0 @@
<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>

View File

@ -1,9 +0,0 @@
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;
};

View File

@ -1,29 +0,0 @@
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,
};
};

View File

@ -1,3 +0,0 @@
import withNuxt from './.playground/.nuxt/eslint.config.mjs'
export default withNuxt()

29
eslint.config.mjs Normal file
View File

@ -0,0 +1,29 @@
// @ts-check
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
// Run `npx @eslint/config-inspector` to inspect the resolved config interactively
export default createConfigForNuxt({
features: {
// Rules for module authors
tooling: true,
// Rules for formatting
stylistic: true,
},
dirs: {
src: ['./playground'],
},
}).overrideRules({
'@typescript-eslint/no-unused-expressions': [
'error',
{ allowShortCircuit: true },
],
'vue/multi-word-component-names': 'off',
'vue/max-attributes-per-line': ['error', { singleline: 5 }],
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'regexp/no-super-linear-backtracking': 'off',
})

View File

@ -1,68 +0,0 @@
import { addTemplate, useNuxt } from "@nuxt/kit";
import { setColors } from "./utils/colors";
import { generateSafelist } from "./utils/colors";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
compatibilityDate: "2024-11-15",
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: [
{
path: "./components",
prefix: "Ray",
pathPrefix: false,
},
{
path: "./components/icons",
prefix: "Icon",
pathPrefix: false,
},
],
});

View File

@ -1,29 +1,54 @@
{ {
"name": "rayine-layer", "name": "rayine-ui",
"version": "1.0.7-beta.1",
"description": "RayineSoft UI Components",
"repository": "HoshinoSuzumi/rayine-ui",
"license": "MIT",
"type": "module", "type": "module",
"version": "0.1.4-beta.3", "exports": {
"main": "./nuxt.config.ts", ".": {
"types": "./dist/types.d.ts",
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
}
},
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
"files": [
"dist"
],
"scripts": { "scripts": {
"dev": "nuxi dev .playground", "prepack": "nuxt-module-build build",
"dev:prepare": "nuxt prepare .playground", "dev": "nuxi dev playground",
"build": "nuxt build .playground", "dev:build": "nuxi build playground",
"generate": "nuxt generate .playground", "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
"preview": "nuxt preview .playground", "release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
"lint": "eslint ." "lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest watch",
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
},
"dependencies": {
"@nuxt/kit": "^3.14.159",
"@nuxtjs/tailwindcss": "^6.12.2",
"defu": "^6.1.4",
"pathe": "^1.1.2",
"scule": "^1.3.0",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.15"
}, },
"devDependencies": { "devDependencies": {
"@nuxt/eslint": "latest", "@nuxt/devtools": "^1.6.0",
"@nuxt/fonts": "^0.10.2", "@nuxt/eslint-config": "^0.7.0",
"eslint": "^9.14.0", "@nuxt/module-builder": "^0.8.4",
"@nuxt/schema": "^3.14.159",
"@nuxt/test-utils": "^3.14.4",
"@types/node": "latest",
"changelogen": "^0.5.7",
"eslint": "^9.15.0",
"nuxt": "^3.14.159", "nuxt": "^3.14.159",
"typescript": "^5.6.3", "typescript": "latest",
"vue": "latest" "vitest": "^2.1.5",
}, "vue-tsc": "^2.1.10"
"packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a",
"dependencies": {
"defu": "^6.1.4",
"tailwind-merge": "^2.5.4",
"@nuxtjs/tailwindcss": "^6.12.2",
"nuxt-shiki": "^0.3.0"
} }
} }

6
playground/app.config.ts Normal file
View File

@ -0,0 +1,6 @@
export default defineAppConfig({
rayui: {
primary: 'indigo',
gray: 'neutral',
},
})

11
playground/app.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
<div>
<h1 class="text-primary">
Nuxt module playground!
</h1>
<RayButton>button</RayButton>
</div>
</template>

View File

@ -0,0 +1,6 @@
export default defineNuxtConfig({
modules: ['../src/module'],
devtools: { enabled: true },
compatibilityDate: '2024-11-18',
rayui: {},
})

13
playground/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"private": true,
"name": "my-module-playground",
"type": "module",
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi generate"
},
"dependencies": {
"nuxt": "^3.14.159"
}
}

View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

3
playground/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}

2461
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

96
src/module.ts Normal file
View File

@ -0,0 +1,96 @@
import type { config } from 'node:process'
import { createRequire } from 'node:module'
import {
defineNuxtModule,
createResolver,
addPlugin,
addComponentsDir,
addImportsDir,
} from '@nuxt/kit'
import { name, version } from '../package.json'
import { installTailwind } from './tailwind'
import type { Strategy, DeepPartial } from './runtime/types/utils'
import { createTemplates } from './template'
const _require = createRequire(import.meta.url)
const defaultColors = _require('tailwindcss/colors.js')
delete defaultColors.lightBlue
delete defaultColors.warmGray
delete defaultColors.trueGray
delete defaultColors.coolGray
delete defaultColors.blueGray
type RayUI = {
primary?: string
gray?: string
strategy?: Strategy
colors?: string[]
[key: string]: any
} & DeepPartial<typeof config, string | number | boolean>
declare module '@nuxt/schema' {
interface AppConfigInput {
rayui?: RayUI
}
}
export interface ModuleOptions {
prefix?: string
safeColors?: string[]
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name,
version,
configKey: 'rayui',
compatibility: {
nuxt: '>=3.0.0',
},
},
defaults: {
prefix: 'Ray',
safeColors: ['primary'],
},
async setup(_options, _nuxt) {
const { resolve } = createResolver(import.meta.url)
const runtimePath = resolve('./runtime')
_nuxt.options.build.transpile.push(runtimePath)
_nuxt.options.alias['#rayui'] = runtimePath
createTemplates(_nuxt)
// Modules
installTailwind(_options, _nuxt, resolve)
// Plugins
addPlugin({
src: resolve(runtimePath, 'plugins', 'colors'),
})
// Components
addComponentsDir({
path: resolve(runtimePath, 'components', 'elements'),
prefix: _options.prefix,
global: false,
watch: false,
})
addComponentsDir({
path: resolve(runtimePath, 'components', 'forms'),
prefix: _options.prefix,
global: false,
watch: false,
})
addComponentsDir({
path: resolve(runtimePath, 'components', 'overlays'),
prefix: _options.prefix,
global: false,
watch: false,
})
// Composables
addImportsDir(resolve(runtimePath, 'composables'))
},
})

View File

@ -1,28 +1,29 @@
<script lang="ts" setup> <script lang="ts" setup>
import { twJoin, twMerge } from 'tailwind-merge'; import { twJoin, twMerge } from 'tailwind-merge'
import { computed, toRef, type PropType } from 'vue'
import { button } from '../../ui.config' import { button } from '../../ui.config'
import type { DeepPartial, Strategy } from '../../types/utils'; import type { DeepPartial, Strategy } from '../../types/utils'
import type { PropType } from 'vue'; import type { ButtonColor, ButtonSize, ButtonVariant } from '../../types/button'
import type { ButtonColor, ButtonSize, ButtonVariant } from '../../types/button'; import { useRayUI } from '#build/imports'
const config = button; const config = button
const props = defineProps({ const props = defineProps({
class: { class: {
type: String, type: String,
default: '' default: '',
}, },
padded: { padded: {
type: Boolean, type: Boolean,
default: true default: true,
}, },
square: { square: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
block: { block: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
size: { size: {
type: String as PropType<ButtonSize>, type: String as PropType<ButtonSize>,
@ -34,15 +35,15 @@ const props = defineProps({
}, },
variant: { variant: {
type: String as PropType<ButtonVariant>, type: String as PropType<ButtonVariant>,
default: () => button.default.variant default: () => button.default.variant,
}, },
ui: { ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>, type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}) default: () => ({}),
} },
}) })
const { ui, attrs } = useUI('button', toRef(props, 'ui'), config) const { ui, attrs } = useRayUI('button', toRef(props, 'ui'), config)
const buttonClass = computed(() => { const buttonClass = computed(() => {
// @ts-ignore // @ts-ignore
@ -60,8 +61,11 @@ const buttonClass = computed(() => {
</script> </script>
<template> <template>
<button :class="buttonClass" v-bind="{ ...attrs }"> <button
<slot></slot> :class="buttonClass"
v-bind="{ ...attrs }"
>
<slot />
</button> </button>
</template> </template>

View File

@ -0,0 +1,14 @@
<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>

View File

@ -0,0 +1,21 @@
<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
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"
/>
</g>
</svg>
</template>

View File

@ -1,7 +1,15 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <svg
<path fill="currentColor" fillRule="evenodd" 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" 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> clipRule="evenodd"
/>
</svg> </svg>
</template> </template>

View File

@ -0,0 +1,21 @@
<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
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"
/>
</g>
</svg>
</template>

View File

@ -0,0 +1,21 @@
<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
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"
/>
</g>
</svg>
</template>

View File

@ -0,0 +1,26 @@
<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
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"
/>
</path>
</svg>
</template>

View File

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MessageProviderApi, Message } from '../../types/message'; import { inject, ref, onMounted } from 'vue'
import type { MessageProviderApi, Message } from '../../types/message'
const providerApi = inject<MessageProviderApi>('ray-message-provider') const providerApi = inject<MessageProviderApi>('ray-message-provider')
@ -20,13 +21,28 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="message" :class="{ <div
[message.type]: message.type class="message"
}"> :class="{
<IconCircleSuccess v-if="message.type === 'success'" class="text-xl" /> [message.type]: message.type,
<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" /> <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> <span>
{{ message.content }} {{ message.content }}
</span> </span>

View File

@ -1,5 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Message, MessageType } from '../../types/message'; import { ref } from 'vue'
import type { Message, MessageType } from '../../types/message'
import { useNuxtApp } from '#app'
const props = defineProps({ const props = defineProps({
max: { max: {
@ -51,12 +53,16 @@ nuxtApp.vueApp.provide('ray-message', api)
</script> </script>
<template> <template>
<slot></slot> <slot />
<teleport to="body"> <teleport to="body">
<div id="message-provider"> <div id="message-provider">
<div class="message-wrapper"> <div class="message-wrapper">
<TransitionGroup name="message"> <TransitionGroup name="message">
<RayMessage v-for="(message, k) in messageList" :key="message.id" :message="message" /> <RayMessage
v-for="(message) in messageList"
:key="message.id"
:message="message"
/>
</TransitionGroup> </TransitionGroup>
</div> </div>
</div> </div>

View File

@ -0,0 +1,10 @@
import { inject } from 'vue'
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
}

View File

@ -0,0 +1,33 @@
import { type Ref, useAttrs, computed, toValue } from 'vue'
import type { DeepPartial, Strategy } from '../types/utils'
import { mergeUiConfig } from '../utils'
import { omit, getValueByPath } from '../utils/objectUtils'
import { useAppConfig } from '#app'
export const useRayUI = <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,
}
}

View File

@ -1,87 +1,89 @@
import { computed } from "vue"; import { computed } from 'vue'
import { defineNuxtPlugin, useAppConfig, useNuxtApp, useHead } from "#imports"; import { defineNuxtPlugin } from 'nuxt/app'
import colors from "tailwindcss/colors"; import { getValueByPath } from '../utils/objectUtils'
import { useAppConfig, useNuxtApp, useHead } from '#imports'
import colors from '#tailwind-config/theme/colors'
const rgbHexPattern = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; const rgbHexPattern = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i
function hexToRgb(hex: string) { function hexToRgb(hex: string) {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
hex = hex.replace(shorthandRegex, function (_, r, g, b) { hex = hex.replace(shorthandRegex, function (_, r, g, b) {
return r + r + g + g + b + b; return r + r + g + g + b + b
}); })
const result = rgbHexPattern.exec(hex); const result = rgbHexPattern.exec(hex)
return result return result
? `${Number.parseInt(result[1], 16)} ${Number.parseInt( ? `${Number.parseInt(result[1], 16)} ${Number.parseInt(
result[2], result[2],
16 16,
)} ${Number.parseInt(result[3], 16)}` )} ${Number.parseInt(result[3], 16)}`
: null; : null
} }
function parseConfigValue(value: string) { function parseConfigValue(value: string) {
return rgbHexPattern.test(value) ? hexToRgb(value) : value; return rgbHexPattern.test(value) ? hexToRgb(value) : value
} }
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const appConfig = useAppConfig(); const appConfig = useAppConfig()
const nuxtApp = useNuxtApp(); const nuxtApp = useNuxtApp()
const root = computed(() => { const root = computed(() => {
const primary: Record<string, string> | undefined = getValueByPath( const primary: Record<string, string> | undefined = getValueByPath(
colors, colors,
appConfig.rayui.primary appConfig.rayui.primary,
); )
const gray: Record<string, string> | undefined = getValueByPath( const gray: Record<string, string> | undefined = getValueByPath(
colors, colors,
appConfig.rayui.gray appConfig.rayui.gray,
); )
return `:root { return `:root {
${Object.entries(primary || colors.indigo) ${Object.entries(primary || colors.indigo)
.map(([key, value]) => `--color-primary-${key}: ${parseConfigValue(value)};`) .map(([key, value]) => `--color-primary-${key}: ${parseConfigValue(value)};`)
.join("\n")} .join('\n')}
--color-primary-DEFAULT: var(--color-primary-500); --color-primary-DEFAULT: var(--color-primary-500);
${Object.entries(gray || colors.neutral) ${Object.entries(gray || colors.neutral)
.map(([key, value]) => `--color-gray-${key}: ${parseConfigValue(value)};`) .map(([key, value]) => `--color-gray-${key}: ${parseConfigValue(value)};`)
.join("\n")} .join('\n')}
} }
.dark { .dark {
--color-primary-DEFAULT: var(--color-primary-400); --color-primary-DEFAULT: var(--color-primary-400);
} }
`; `
}); })
const headData: any = { const headData: any = {
style: [ style: [
{ {
innerHTML: () => root.value, innerHTML: () => root.value,
tagPriority: -2, tagPriority: -2,
id: "ray-colors", id: 'ray-colors',
}, },
], ],
}; }
if ( if (
import.meta.client && import.meta.client
nuxtApp.isHydrating && && nuxtApp.isHydrating
!nuxtApp.payload.serverRendered && !nuxtApp.payload.serverRendered
) { ) {
const style = document.createElement("style"); const style = document.createElement('style')
style.innerHTML = root.value; style.innerHTML = root.value
style.setAttribute("data-ray-colors", ""); style.setAttribute('data-ray-colors', '')
document.head.appendChild(style); document.head.appendChild(style)
headData.script = [ headData.script = [
{ {
innerHTML: innerHTML:
"document.head.removeChild(document.querySelector('[data-ray-colors]'))", 'document.head.removeChild(document.querySelector(\'[data-ray-colors]\'))',
}, },
]; ]
} }
useHead(headData); useHead(headData)
}); })

View File

@ -0,0 +1,3 @@
{
"extends": "../../../.nuxt/tsconfig.server.json",
}

23
src/runtime/types/button.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
import type { AppConfig } from 'nuxt/schema'
import type { button } from '../ui.config'
import type { ExtractDeepObject, NestedKeyOf, ExtractDeepKey } from './utils'
import type colors from '#ray-colors'
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
}

View File

@ -0,0 +1,4 @@
export * from './button'
export * from './message'
export * from './utils'

20
src/runtime/types/message.d.ts vendored Normal file
View 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
}

View File

@ -1,7 +1,7 @@
export type Strategy = "override" | "merge"; export type Strategy = 'override' | 'merge'
export interface TightMap<O = any> { export interface TightMap<O = any> {
[key: string]: TightMap | O; [key: string]: TightMap | O
} }
export type DeepPartial<T, O = any> = { export type DeepPartial<T, O = any> = {
@ -11,25 +11,25 @@ export type DeepPartial<T, O = any> = {
? string ? string
: T[P]; : T[P];
} & { } & {
[key: string]: O | TightMap<O>; [key: string]: O | TightMap<O>
}; }
export type NestedKeyOf<ObjectType extends Record<string, any>> = { export type NestedKeyOf<ObjectType extends Record<string, any>> = {
[Key in keyof ObjectType]: ObjectType[Key] extends Record<string, any> [Key in keyof ObjectType]: ObjectType[Key] extends Record<string, any>
? NestedKeyOf<ObjectType[Key]> ? NestedKeyOf<ObjectType[Key]>
: Key; : Key;
}[keyof ObjectType]; }[keyof ObjectType]
type DeepKey<T, Keys extends string[]> = Keys extends [ type DeepKey<T, Keys extends string[]> = Keys extends [
infer First, infer First,
...infer Rest ...infer Rest,
] ]
? First extends keyof T ? First extends keyof T
? Rest extends string[] ? Rest extends string[]
? DeepKey<T[First], Rest> ? DeepKey<T[First], Rest>
: never : never
: never : never
: T; : T
export type ExtractDeepKey<T, Path extends string[]> = DeepKey< export type ExtractDeepKey<T, Path extends string[]> = DeepKey<
T, T,
@ -38,7 +38,7 @@ export type ExtractDeepKey<T, Path extends string[]> = DeepKey<
? Result extends Record<string, any> ? Result extends Record<string, any>
? keyof Result ? keyof Result
: never : never
: never; : never
export type ExtractDeepObject<T, Path extends string[]> = DeepKey< export type ExtractDeepObject<T, Path extends string[]> = DeepKey<
T, T,
@ -47,4 +47,4 @@ export type ExtractDeepObject<T, Path extends string[]> = DeepKey<
? Result extends Record<string, any> ? Result extends Record<string, any>
? Result ? Result
: never : never
: never; : never

View File

@ -1,47 +1,47 @@
export default { 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", 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", rounded: 'rounded-lg',
font: "font-medium", font: 'font-medium',
block: "w-full flex justify-center items-center", block: 'w-full flex justify-center items-center',
inline: "inline-flex items-center", inline: 'inline-flex items-center',
size: { size: {
"2xs": "text-xs", '2xs': 'text-xs',
xs: "text-xs", 'xs': 'text-xs',
sm: "text-sm", 'sm': 'text-sm',
md: "text-sm", 'md': 'text-sm',
lg: "text-sm", 'lg': 'text-sm',
xl: "text-base", 'xl': 'text-base',
}, },
padding: { padding: {
"2xs": "px-2 py-1", '2xs': 'px-2 py-1',
xs: "px-2.5 py-1.5", 'xs': 'px-2.5 py-1.5',
sm: "px-2.5 py-1.5", 'sm': 'px-2.5 py-1.5',
md: "px-3 py-2", 'md': 'px-3 py-2',
lg: "px-3.5 py-2.5", 'lg': 'px-3.5 py-2.5',
xl: "px-3.5 py-2.5", 'xl': 'px-3.5 py-2.5',
}, },
square: { square: {
"2xs": "p-1", '2xs': 'p-1',
xs: "p-1.5", 'xs': 'p-1.5',
sm: "p-1.5", 'sm': 'p-1.5',
md: "p-2", 'md': 'p-2',
lg: "p-2.5", 'lg': 'p-2.5',
xl: "p-2.5", 'xl': 'p-2.5',
}, },
color: {}, color: {},
variant: { variant: {
solid: 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", '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: 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", '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", 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: 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", '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", 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: { default: {
size: "sm", size: 'sm',
color: "primary", color: 'primary',
variant: "solid", variant: 'solid',
}, },
}; }

215
src/runtime/utils/colors.ts Normal file
View File

@ -0,0 +1,215 @@
import type { Config as TwConfig } from 'tailwindcss'
import defaultColors from 'tailwindcss/colors.js'
import { camelCase, upperFirst } from 'scule'
import { omit } from './objectUtils'
const colorsToRegex = (colors: string[]): string => colors.join('|')
type ColorConfig = Exclude<NonNullable<TwConfig['theme']>['colors'], Function>
export const excludeColors = (
colors: ColorConfig | typeof defaultColors,
): string[] => {
const colorEntries = Object.entries(omit(colors as Record<string, any>, []))
return colorEntries
.filter(([, value]) => typeof value === 'object')
.map(([key]) => key)
}
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 excludeColors(_globalColors)
}
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[]) => {
const safelist = Object.keys(safelistForComponent).flatMap(component =>
safelistForComponent[component](colorsToRegex(colors)),
)
return [...safelist]
}
type SafelistFn = Exclude<
NonNullable<Extract<TwConfig['content'], { extract?: unknown }>['extract']>,
Record<string, unknown>
>
export const customSafelistExtractor = (
prefix: string,
content: string,
colors: string[],
safelistColors: string[],
): ReturnType<SafelistFn> => {
const classes: string[] = []
const regex
= /<([A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)*)\s+(?![^>]*:color\b)[^>]*\bcolor=["']([^"']+)["'][^>]*>/g
const matches = content.matchAll(regex)
const components = Object.keys(safelistForComponent).map(
component =>
`${prefix}${component.charAt(0).toUpperCase() + component.slice(1)}`,
)
for (const match of matches) {
const [, component, color] = match
const camelComponent = upperFirst(camelCase(component))
if (!colors.includes(color) || safelistColors.includes(color)) {
continue
}
let name = camelComponent as string
if (!components.includes(name)) {
continue
}
name = name.replace(prefix, '').toLowerCase()
const matchClasses = safelistForComponent[name](color)?.flatMap((group) => {
return typeof group === 'string'
? ''
: ['', ...(group.variants || [])].flatMap((variant) => {
const matches = group.pattern.source.match(/\(([^)]+)\)/g)
return (
matches
?.map((match) => {
const colorOptions = match
.substring(1, match.length - 1)
.split('|')
return colorOptions.map((color) => {
const classesExtracted = group.pattern.source
.replace(match, color)
.replace('^', '')
.replace('$', '')
if (variant) {
return `${variant}:${classesExtracted}`
}
return classesExtracted
})
})
.flat() || []
)
})
})
classes.push(...(matchClasses as string[]))
}
return classes
}

View File

@ -0,0 +1,30 @@
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
}
export * from './objectUtils'

View File

@ -0,0 +1,38 @@
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) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
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
}

104
src/tailwind.ts Normal file
View File

@ -0,0 +1,104 @@
import { useNuxt, createResolver, addTemplate, installModule, tryResolveModule } from '@nuxt/kit'
import type { ModuleOptions } from '@nuxt/schema'
import defu from 'defu'
import { join } from 'pathe'
import { setColors } from './runtime/utils/colors'
export const installTailwind = (
moduleOptions: ModuleOptions,
nuxt = useNuxt(),
resolve = createResolver(import.meta.url).resolve,
) => {
const runtimePath = resolve('./runtime')
nuxt.hook('tailwindcss:config', (tailwindConfig) => {
tailwindConfig.theme = tailwindConfig.theme || {}
tailwindConfig.theme.extend = tailwindConfig.theme.extend || {}
tailwindConfig.theme.extend.colors
= tailwindConfig.theme.extend.colors || {}
const colors = setColors(tailwindConfig.theme)
nuxt.options.appConfig.rayui = {
primary: 'indigo',
gray: 'neutral',
strategy: 'merge',
colors,
}
})
const configTemplate = addTemplate({
filename: 'ray-tailwind.config.cjs',
write: true,
getContents: ({ nuxt }) => `
const { defaultExtractor: createDefaultExtractor } = require('tailwindcss/lib/lib/defaultExtractor.js')
const { customSafelistExtractor, generateSafelist } = require(${JSON.stringify(
resolve(runtimePath, 'utils', 'colors'),
)})
const defaultExtractor = createDefaultExtractor({ tailwindConfig: { separator: ':' } })
module.exports = {
content: {
files: [
${JSON.stringify(
resolve(runtimePath, 'components/**/*.{vue,mjs,ts}'),
)},
${JSON.stringify(
resolve(runtimePath, 'ui.config/**/*.{mjs,js,ts}'),
)}
],
},
transform: {
vue: (content) => {
return content.replaceAll(/(?:\\r\\n|\\r|\\n)/g, ' ')
}
},
extract: {
vue: (content) => {
return [
...defaultExtractor(content),
...customSafelistExtractor(${JSON.stringify(
moduleOptions.prefix,
)}, content, ${JSON.stringify(
nuxt.options.appConfig.rayui.colors,
)}, ${JSON.stringify(moduleOptions.safeColors)})
]
}
},
safelist: generateSafelist(${JSON.stringify(
moduleOptions.safeColors || [],
)}, ${JSON.stringify(nuxt.options.appConfig.rayui.colors)}),
}
`,
})
const { configPath: userTwConfigPath = [], ...twModuleConfig }
= nuxt.options.tailwindcss ?? {}
const twConfigPaths = [
configTemplate.dst,
join(nuxt.options.rootDir, 'tailwind.config'),
]
if (typeof userTwConfigPath === 'string') {
twConfigPaths.push(userTwConfigPath)
}
else {
twConfigPaths.push(...userTwConfigPath)
}
return installModule(
'@nuxtjs/tailwindcss',
defu(
{
exposeConfig: true,
config: {
darkMode: 'class' as const,
},
configPath: twConfigPaths,
},
twModuleConfig,
),
)
}

24
src/template.ts Normal file
View File

@ -0,0 +1,24 @@
import { addTemplate, useNuxt } from '@nuxt/kit'
export const createTemplates = (nuxt = useNuxt()) => {
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 })
})
}

View File

@ -1,11 +0,0 @@
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;

15
test/basic.test.ts Normal file
View File

@ -0,0 +1,15 @@
import { fileURLToPath } from 'node:url'
import { describe, it, expect } from 'vitest'
import { setup, $fetch } from '@nuxt/test-utils/e2e'
describe('ssr', async () => {
await setup({
rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)),
})
it('renders the index page', async () => {
// Get response to a server-rendered page with `$fetch`.
const html = await $fetch('/')
expect(html).toContain('<div>basic</div>')
})
})

6
test/fixtures/basic/app.vue vendored Normal file
View File

@ -0,0 +1,6 @@
<template>
<div>basic</div>
</template>
<script setup>
</script>

7
test/fixtures/basic/nuxt.config.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import MyModule from '../../../src/module'
export default defineNuxtConfig({
modules: [
MyModule,
],
})

5
test/fixtures/basic/package.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"private": true,
"name": "basic",
"type": "module"
}

View File

@ -1,3 +1,8 @@
{ {
"extends": "./.playground/.nuxt/tsconfig.json" "extends": "./.nuxt/tsconfig.json",
"exclude": [
"dist",
"node_modules",
"playground",
]
} }

20
types/Message.d.ts vendored
View File

@ -1,20 +0,0 @@
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;
}

23
types/button.d.ts vendored
View File

@ -1,23 +0,0 @@
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;
}

View File

@ -1,167 +0,0 @@
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}`)
) || []
);
});
};

View File

@ -1,28 +0,0 @@
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;
};

View File

@ -1,37 +0,0 @@
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;
};