♻️ Create module project

This commit is contained in:
Timothy Yin 2024-11-18 03:49:04 +08:00
parent 106f36b5bd
commit ff817e25e6
58 changed files with 1700 additions and 3136 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
*.log
.nuxt
nuxt.d.ts
.output
.data
.env
package-lock.json
framework
dist
.DS_Store
# Logs
*.log*
# Temp directories
.temp
.tmp
.cache
# Yarn
.yarn/cache
.yarn/*state*
**/.yarn/cache
**/.yarn/*state*
# Local History
.history
# Generated dirs
dist
# Nuxt
.nuxt
.output
.data
.vercel_build_output
.build-*
.netlify
# Env
.env
# Testing
reports
coverage
*.lcov
.nyc_output
# 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
}

125
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
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>
```bash
# 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
```
## Distributing your layer
</details>
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:
<!-- 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
```bash
npm publish --access public
```
[npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=020420&colorB=00DC82
[npm-downloads-href]: https://npm.chart.dev/my-module
Once done, your users will only have to run:
[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
```bash
npm install --save your-layer
```
Then add the dependency to their `extends` in `nuxt.config`:
```ts
defineNuxtConfig({
extends: 'your-layer'
})
```
## Development Server
Start the development server on http://localhost:3000
```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.
[nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
[nuxt-href]: https://nuxt.com

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

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

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

View File

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

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()

20
eslint.config.mjs Normal file
View File

@ -0,0 +1,20 @@
// @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',
],
},
})
.append(
// your custom flat config here...
)

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,48 @@
{
"name": "rayine-layer",
"name": "my-module",
"version": "1.0.0",
"description": "My new Nuxt module",
"repository": "your-org/my-module",
"license": "MIT",
"type": "module",
"version": "0.1.4-beta.3",
"main": "./nuxt.config.ts",
"exports": {
".": {
"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": {
"dev": "nuxi dev .playground",
"dev:prepare": "nuxt prepare .playground",
"build": "nuxt build .playground",
"generate": "nuxt generate .playground",
"preview": "nuxt preview .playground",
"lint": "eslint ."
"prepack": "nuxt-module-build build",
"dev": "nuxi dev playground",
"dev:build": "nuxi build playground",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
"release": "npm run lint && npm run test && npm run prepack && changelogen --release && npm publish && git push --follow-tags",
"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"
},
"devDependencies": {
"@nuxt/eslint": "latest",
"@nuxt/fonts": "^0.10.2",
"eslint": "^9.14.0",
"@nuxt/devtools": "^1.6.0",
"@nuxt/eslint-config": "^0.7.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",
"typescript": "^5.6.3",
"vue": "latest"
},
"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"
"typescript": "latest",
"vitest": "^2.1.5",
"vue-tsc": "^2.1.10"
}
}

8
playground/app.vue Normal file
View File

@ -0,0 +1,8 @@
<template>
<div>
Nuxt module playground!
</div>
</template>
<script setup>
</script>

View File

@ -0,0 +1,5 @@
export default defineNuxtConfig({
modules: ['../src/module'],
myModule: {},
devtools: { enabled: true },
})

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"
}

View File

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

3083
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

19
src/module.ts Normal file
View File

@ -0,0 +1,19 @@
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
// Module options TypeScript interface definition
export interface ModuleOptions {}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'my-module',
configKey: 'myModule',
},
// Default configuration options of the Nuxt module
defaults: {},
setup(_options, _nuxt) {
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'))
},
})

5
src/runtime/plugin.ts Normal file
View File

@ -0,0 +1,5 @@
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin((_nuxtApp) => {
console.log('Plugin injected by my-module!')
})

View File

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

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;
}

50
types/utils.d.ts vendored
View File

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

View File

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

View File

@ -1,2 +0,0 @@
// elements
export { default as button } from './elements/button'

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;
};