♻️ 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
}

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
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.
## 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:
<details>
<summary>Local development</summary>
```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
defineNuxtConfig({
extends: 'your-layer'
})
```
[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
## 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
```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;
};