Compare commits

...

115 Commits

Author SHA1 Message Date
d24140f673 feat(message): add icon suppoer to message component 2024-11-28 14:07:18 +08:00
e20572648d chore(release): v1.3.9 2024-11-27 22:30:03 +08:00
acee77ae7b
Update README.md 2024-11-27 22:28:17 +08:00
236e08ad6b
Update README.md 2024-11-27 22:27:04 +08:00
40ccfa0975 chore: add logos 2024-11-27 22:25:15 +08:00
ec054a98fd chore(docs): rename config to theme 2024-11-27 22:22:38 +08:00
55e9b5c09a chore(docs): adjust prop description color 2024-11-27 22:20:22 +08:00
1ed637cece chore: refactor 2024-11-27 22:08:15 +08:00
8fefe70937 docs: add props 2024-11-27 21:53:44 +08:00
a8e47c6bff docs(mark): add config section 2024-11-27 19:42:28 +08:00
b30a52cfa0 chore(release): v1.3.8 2024-11-27 18:23:07 +08:00
c3147c2fd9
Merge pull request #7 from HoshinoSuzumi/6-feat-new-component-mark
feat(mark): new component `RayMark`
2024-11-27 17:37:13 +08:00
ddff1ca9c0 feat(mark): new component RayMark 2024-11-27 17:29:38 +08:00
83f8593391 📝 docs(button): default loading set to false in Icon section 2024-11-27 17:29:13 +08:00
70d1af3d2f docs(interactive): add slot code rendering 2024-11-27 17:28:26 +08:00
049739db91
Update issue templates 2024-11-27 12:47:51 +08:00
17212d7982
Create LICENSE 2024-11-27 12:46:33 +08:00
69139c76b3 docs: update home page 2024-11-27 12:35:51 +08:00
8123f5918a chore(release): v1.3.7 2024-11-27 01:29:02 +08:00
4c1df313ce
Merge pull request #3 from HoshinoSuzumi/feat-button-icons-support
 feat(button): add `icon` and `loadingIcon` prop support
2024-11-27 01:27:38 +08:00
2996866b9a feat(button): add icon and loadingIcon prop support 2024-11-27 01:26:04 +08:00
1720987c4d
Update README.md 2024-11-26 22:00:12 +08:00
fcae8047d3 chore(release): v1.3.6 2024-11-26 19:41:21 +08:00
28975d633c feat(icon): the wrapper for @nuxt/icon 2024-11-26 19:39:40 +08:00
5e0e6bd1a6 chore(icon): add @nuxt/icon support 2024-11-26 17:42:34 +08:00
817611c731 🔥 perf(textarea): remove unused debug output 2024-11-26 17:29:59 +08:00
11018ba713 🐛 fix(textarea): disabled prop does not assigned to the textarea element 2024-11-26 17:27:58 +08:00
f52c45069f chore(release): v1.3.5 2024-11-26 14:10:09 +08:00
00a7c05aec feat(textarea): new component textarea 2024-11-26 14:09:11 +08:00
77cc38e552 🩹 perf(toggle): rename type.d 2024-11-25 23:50:41 +08:00
10b2c128d0 chore(release): v1.3.4 2024-11-25 22:38:55 +08:00
2729812a3c feat(toggle): new component 2024-11-25 22:35:39 +08:00
177c4cfd38 chore(release): v1.3.3 2024-11-24 21:01:43 +08:00
1f1647c4bc feat(kbd): add kbd component 2024-11-24 21:00:14 +08:00
ab67a97ac9 📦 build: add build.config.ts 2024-11-24 19:27:39 +08:00
f4d1d36f5b 📝 docs: fix since label appearance in darkmode 2024-11-24 17:48:02 +08:00
3061d73bc5 📝 docs: add since label for component docs 2024-11-24 17:40:55 +08:00
247f0c13af chore(release): v1.3.3-beta.1 2024-11-24 17:04:42 +08:00
34c6641643 🎨 chore(lint): lint code 2024-11-24 17:00:33 +08:00
4ae71dd0e7 🚑 fix(build): update typescript version in package.json 2024-11-24 16:59:28 +08:00
80c94ac457
chore: input.d.ts 2024-11-24 06:22:09 +08:00
e9b9b070f7
perf(input): update InputType in input.d.ts 2024-11-24 06:21:05 +08:00
abd99b2e6d 📝 docs(input): add input component doc 2024-11-24 05:16:01 +08:00
565a4b5e4f feat{input}: new input component
Added input component and merge the common size, padding etc. values to ui.config/standard.ts
2024-11-24 05:15:38 +08:00
07b317f23b chore(release): v1.3.2 2024-11-24 00:35:50 +08:00
3112d0013e 📦 build: add release-it 2024-11-24 00:33:48 +08:00
0ebd88d8c0 📝 docs: add new logo and ico 2024-11-23 23:59:41 +08:00
15b24f42dc chore(release): v1.3.1 2024-11-23 19:39:19 +08:00
d685aae4d0 🐛 fix(docs): hydration mismatch 2024-11-23 19:38:39 +08:00
fdb0919268 chore(release): v1.3.0 2024-11-23 18:46:26 +08:00
69cbab8bb4 🎨 chore: lint code 2024-11-23 17:58:55 +08:00
0e070c8909 feat(Button): add match and invert color mode
refactor(docs): update docs
2024-11-23 17:58:06 +08:00
15aa2315ae ♻️ refactor(button): export defineComponent instead of setup 2024-11-23 14:58:12 +08:00
f68fa2f936 📝 docs(message): fix typo 2024-11-22 22:56:42 +08:00
421de5a89a chore: show the version of rayine-ui in the header 2024-11-22 22:47:06 +08:00
e06d10ec2c chore(release): v1.2.0 2024-11-22 22:17:56 +08:00
09a5faa361 🎨 chore: reformat code 2024-11-22 22:17:15 +08:00
d9ad42dfa1 📝 docs(message,button): update docs 2024-11-22 22:16:28 +08:00
222d2e8f1d 💥 refactor(message): BREAKING redesign component props 2024-11-22 22:16:08 +08:00
440613047a feat(message): new component messages and message 2024-11-22 16:53:46 +08:00
f95e068bbe 🚧 wip(messages) 2024-11-22 12:34:48 +08:00
6d95d9f9a8 🐛 fix(docs): wrong import path 2024-11-22 01:40:27 +08:00
e62f7590d0 🎨 fix(docs): interactive colors rendering and lint code 2024-11-22 01:35:58 +08:00
36818cefa2 🩹 fix(docs): some colors can not be rendered 2024-11-22 01:30:27 +08:00
ce5214607b 💄 docs(ui): adjust the readability of page title 2024-11-22 01:24:23 +08:00
355843b054 📝 docs(interactive): add interactive props 2024-11-22 01:23:09 +08:00
d3bd236e67 chore(release): v1.1.0 2024-11-21 18:42:38 +08:00
ed419eff25 feat(button): add disabled and loading state 2024-11-21 18:42:05 +08:00
d913f91644
Update README.md 2024-11-21 11:00:14 +08:00
d1ebd83109
Update README.md 2024-11-21 10:58:29 +08:00
ce278dea76
Update README.md 2024-11-21 10:56:18 +08:00
5cd3de4629
Update ci.yml 2024-11-20 17:42:30 +08:00
32c7b0be33
Update ci.yml 2024-11-20 17:40:16 +08:00
9937aa412b
Update ci.yml 2024-11-20 17:35:28 +08:00
2c9228ee35 Merge branch 'main' of https://github.com/HoshinoSuzumi/rayine-ui 2024-11-20 14:41:11 +08:00
15e88012ea 💚 ci use pnpm 2024-11-20 14:40:58 +08:00
2b9e77689b
docs: fix typo 2024-11-20 10:27:29 +08:00
0d04a4581a chore(release): v1.0.7 2024-11-20 04:45:33 +08:00
339cb8642d 🔧 chore: update tsconfig to exclude docs directory 2024-11-20 04:45:04 +08:00
6e22ccc4c0 lint code 2024-11-20 04:39:14 +08:00
8cbef084da 📝 docs: installation guide 2024-11-20 04:34:31 +08:00
a7d52de649 📝 add docs 2024-11-20 04:23:15 +08:00
a6ee301d5b docs framework: tocs, contents, highlighter 2024-11-19 20:53:54 +08:00
ab9fe5f242 🚧 docs wip 2024-11-19 12:57:02 +08:00
64abbba614 chore: release v1.0.7-beta.2 2024-11-19 00:02:07 +08:00
d3a4d54938 Add tailwind typo 2024-11-19 00:01:48 +08:00
42370a0692 📝 Doc(frame): initial 2024-11-18 19:40:23 +08:00
0e864ec019
Merge pull request #1 from HoshinoSuzumi/migrate-module
Migrate module
2024-11-18 17:45:34 +08:00
81dbf4911c chore: release v1.0.7-beta.1 2024-11-18 17:44:15 +08:00
c4b755adee Fix message 2024-11-18 17:43:29 +08:00
22a6b205f8 chore 2024-11-18 17:34:45 +08:00
0edeb24315 chore(release): v1.0.6 2024-11-18 16:39:30 +08:00
afa73ec91c test2 2024-11-18 16:38:47 +08:00
1c10598871 test 2024-11-18 16:23:41 +08:00
7d0be6224d chore(release): v1.0.5 2024-11-18 15:54:05 +08:00
62008676ae 🎨 format code 2024-11-18 15:53:42 +08:00
0cf7db80fe 🐛 tw template 2024-11-18 15:52:55 +08:00
0cef7ba436 🐛 tailwind template 2024-11-18 15:47:26 +08:00
7906e2f19c chore(release): v1.0.4 2024-11-18 15:26:33 +08:00
d07fc5b301 🚨 lint 2024-11-18 15:26:09 +08:00
c2e97c1d17 ♻️ Rename useUI 2024-11-18 15:25:30 +08:00
ce36a35873 chore(release): v1.0.3 2024-11-18 15:20:38 +08:00
b052a8a8e3 🎨 Eslint 2024-11-18 15:20:14 +08:00
eeebcc2ee3 chore(release): v1.0.2 2024-11-18 15:19:19 +08:00
5949272d34 chore(release): v1.0.1 2024-11-18 15:06:35 +08:00
319e28ce33 chore: release v1.0.1-beta.1 2024-11-18 14:18:15 +08:00
54422ae5fe Add tailwindcss 2024-11-18 14:00:41 +08:00
2995a60a0a chore: exports 2024-11-18 05:48:16 +00:00
b911671f19 🚨 Fix defineNuxtPlugin importing 2024-11-18 13:32:04 +08:00
a542bc82b9 🐛 Fix safelist 2024-11-18 13:19:26 +08:00
d4a601105d 🚚 framework 2024-11-18 12:45:36 +08:00
dad948469f 🚚 Runtime libs 2024-11-18 03:56:02 +08:00
ff817e25e6 ♻️ Create module project 2024-11-18 03:49:04 +08:00
106f36b5bd chore: release v0.1.4-beta.3 2024-11-18 03:20:49 +08:00
4fd484e688 ⬆️ Update pnpm-lock 2024-11-18 03:20:44 +08:00
144 changed files with 9914 additions and 2123 deletions

10
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Bug report
about: Create a report to help us improve
title: 'Bug: '
labels: bug
assignees: ''
---

View File

@ -0,0 +1,10 @@
---
name: Feature request
about: Suggest an idea for this project
title: 'Feat: '
labels: enhancement
assignees: ''
---

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

@ -0,0 +1,55 @@
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: setup pnpm
uses: pnpm/action-setup@v4.0.0
with:
version: 9
- name: Install dependencies
run: pnpm i
- name: Lint
run: pnpm lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: setup pnpm
uses: pnpm/action-setup@v4.0.0
with:
version: 9
- name: Install dependencies
run: pnpm i
- name: Playground prepare
run: pnpm dev:prepare
- name: Test
run: pnpm 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,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>

24
.release-it.json Normal file
View File

@ -0,0 +1,24 @@
{
"plugins": {
"release-it-pnpm": {},
"@release-it/conventional-changelog": {
"preset": {
"name": "conventionalcommits"
},
"ignoreRecommendedBump": true,
"infile": "CHANGELOG.md",
"header": "# Changelog"
}
},
"git": {
"commitMessage": "chore(release): v${version}",
"tagName": "v${version}"
},
"hooks": {
"before:init": [
"pnpm lint",
"pnpm test",
"pnpm prepack"
]
}
}

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

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

155
CHANGELOG.md Normal file
View File

@ -0,0 +1,155 @@
# Changelog
## [1.3.9](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.8...v1.3.9) (2024-11-27)
## [1.3.8](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.7...v1.3.8) (2024-11-27)
## [1.3.7](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.6...v1.3.7) (2024-11-26)
### Features
* **button:** add `icon` and `loadingIcon` prop support ([2996866](https://github.com/HoshinoSuzumi/rayine-ui/commit/2996866b9adfed79fa64afa8ccd7c1fbfa88d059))
## [1.3.6](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.5...v1.3.6) (2024-11-26)
## [1.3.5](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.4...v1.3.5) (2024-11-26)
## [1.3.4](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.3...v1.3.4) (2024-11-25)
## [1.3.3](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.3-beta.1...v1.3.3) (2024-11-24)
## [1.3.3-beta.1](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.2...v1.3.3-beta.1) (2024-11-24)
### Performance Improvements
* **input:** update InputType in input.d.ts ([e9b9b07](https://github.com/HoshinoSuzumi/rayine-ui/commit/e9b9b070f75bd4b1c401801986e3208bf5b6aa0c))
## [1.3.2](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.1...v1.3.2) (2024-11-23)
## v1.3.1
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.3.0...v1.3.1)
### 🩹 Fixes
- **docs:** Hydration mismatch ([d685aae](https://github.com/HoshinoSuzumi/rayine-ui/commit/d685aae))
### ❤️ Contributors
- HoshinoSuzumi ([@HoshinoSuzumi](http://github.com/HoshinoSuzumi))
## v1.3.0
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.2.0...v1.3.0)
### 🚀 Enhancements
- **Button): add match and invert color mode refactor(docs:** Update docs ([0e070c8](https://github.com/HoshinoSuzumi/rayine-ui/commit/0e070c8))
### 💅 Refactors
- **button:** Export defineComponent instead of setup ([15aa231](https://github.com/HoshinoSuzumi/rayine-ui/commit/15aa231))
### 📖 Documentation
- **message:** Fix typo ([f68fa2f](https://github.com/HoshinoSuzumi/rayine-ui/commit/f68fa2f))
### 🏡 Chore
- Show the version of rayine-ui in the header ([421de5a](https://github.com/HoshinoSuzumi/rayine-ui/commit/421de5a))
- Lint code ([69cbab8](https://github.com/HoshinoSuzumi/rayine-ui/commit/69cbab8))
### ❤️ Contributors
- HoshinoSuzumi ([@HoshinoSuzumi](http://github.com/HoshinoSuzumi))
## v1.2.0
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.1.0...v1.2.0)
### 🚀 Enhancements
- **button:** Add disabled and loading state ([ed419ef](https://github.com/HoshinoSuzumi/rayine-ui/commit/ed419ef))
- **message:** New component `messages` and `message` ([4406130](https://github.com/HoshinoSuzumi/rayine-ui/commit/4406130))
### 🩹 Fixes
- **docs:** Some colors can not be rendered ([36818ce](https://github.com/HoshinoSuzumi/rayine-ui/commit/36818ce))
- **docs:** Interactive colors rendering and lint code ([e62f759](https://github.com/HoshinoSuzumi/rayine-ui/commit/e62f759))
- **docs:** Wrong import path ([6d95d9f](https://github.com/HoshinoSuzumi/rayine-ui/commit/6d95d9f))
### 💅 Refactors
- **message:** BREAKING redesign component props ([222d2e8](https://github.com/HoshinoSuzumi/rayine-ui/commit/222d2e8))
### 📖 Documentation
- **interactive:** Add interactive props ([355843b](https://github.com/HoshinoSuzumi/rayine-ui/commit/355843b))
- **ui:** Adjust the readability of page title ([ce52146](https://github.com/HoshinoSuzumi/rayine-ui/commit/ce52146))
- **message,button:** Update docs ([d9ad42d](https://github.com/HoshinoSuzumi/rayine-ui/commit/d9ad42d))
### 🏡 Chore
- **release:** V1.1.0 ([d3bd236](https://github.com/HoshinoSuzumi/rayine-ui/commit/d3bd236))
- Reformat code ([09a5faa](https://github.com/HoshinoSuzumi/rayine-ui/commit/09a5faa))
### ❤️ Contributors
- HoshinoSuzumi <master@uniiem.com>
## v1.1.0
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.7...v1.1.0)
### 🚀 Enhancements
- **button:** Add disabled and loading state ([ed419ef](https://github.com/HoshinoSuzumi/rayine-ui/commit/ed419ef))
### 📖 Documentation
- Fix typo ([2b9e776](https://github.com/HoshinoSuzumi/rayine-ui/commit/2b9e776))
### ❤️ Contributors
- HoshinoSuzumi ([@HoshinoSuzumi](http://github.com/HoshinoSuzumi))
- Timothy Yin ([@HoshinoSuzumi](http://github.com/HoshinoSuzumi))
## v1.0.7
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.7-beta.2...v1.0.7)
### 📖 Documentation
- Installation guide ([8cbef08](https://github.com/HoshinoSuzumi/rayine-ui/commit/8cbef08))
### 🏡 Chore
- Update tsconfig to exclude docs directory ([339cb86](https://github.com/HoshinoSuzumi/rayine-ui/commit/339cb86))
### ❤️ Contributors
- HoshinoSuzumi ([@HoshinoSuzumi](http://github.com/HoshinoSuzumi))
## v1.0.6
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.5...v1.0.6)
## v1.0.5
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.4...v1.0.5)
## v1.0.4
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.3...v1.0.4)
## v1.0.3
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.2...v1.0.3)
## v1.0.2
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.1...v1.0.2)
## v1.0.1
[compare changes](https://github.com/HoshinoSuzumi/rayine-ui/compare/v1.0.1-beta.1...v1.0.1)

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Timothy Yin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

115
README.md
View File

@ -1,73 +1,72 @@
# Nuxt Layer Starter
# Rayine UI
Create Nuxt extendable layer with this GitHub template.
![rayine-ui](https://socialify.git.ci/HoshinoSuzumi/rayine-ui/image?description=1&font=Rokkitt&issues=1&logo=https%3A%2F%2Frayui.uniiem.com%2Frayine_no_shadow.svg&name=1&owner=1&pattern=Brick%20Wall&pulls=1&stargazers=1&theme=Light)
## Setup
[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
Make sure to install the dependencies:
RayineUI is a multi-purpose customizable UI library.
- [✨ &nbsp;Release Notes](/CHANGELOG.md)
- [📖 &nbsp;Documentation](https://rayui.uniiem.com)
## Features
<!-- Highlight some of the features your module provide here -->
- ⚙️ &nbsp;Fully customizable components
- 🌪️ &nbsp;TailwindCSS inside
- 🔨 &nbsp;Full TypeScript support
## Quick Setup
Install the module to your Nuxt application with one command:
```bash
pnpm install
npx nuxi module add rayine-ui
```
## Working on your layer
That's it! You can now use Rayine UI 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/rayine-ui/latest.svg?style=flat&colorA=020420&colorB=00DC82
[npm-version-href]: https://npmjs.com/package/rayine-ui
```bash
npm publish --access public
```
[npm-downloads-src]: https://img.shields.io/npm/dm/rayine-ui.svg?style=flat&colorA=020420&colorB=00DC82
[npm-downloads-href]: https://npm.chart.dev/rayine-ui
Once done, your users will only have to run:
```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.
[license-src]: https://img.shields.io/npm/l/rayine-ui.svg?style=flat&colorA=020420&colorB=00DC82
[license-href]: https://npmjs.com/package/rayine-ui

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>

5
build.config.ts Normal file
View File

@ -0,0 +1,5 @@
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
externals: ['#ray-colors'],
})

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

24
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
docs/README.md Normal file
View File

@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

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

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

19
docs/app.vue Normal file
View File

@ -0,0 +1,19 @@
<script setup lang="ts">
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
provide('navigation', navigation)
</script>
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<RayMessages />
</template>
<style>
html {
scroll-behavior: smooth;
}
</style>

77
docs/components/Logo.vue Normal file
View File

@ -0,0 +1,77 @@
<script lang="ts" setup>
</script>
<template>
<svg
class="w-8 h-8"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 970 1008"
>
<g id="rayine">
<g id="r_leg">
<path
class="cls-1"
d="m921.3,858.91v121.45c0,15.27-12.38,27.64-27.64,27.64h-122.51c-7.33,0-14.36-2.91-19.55-8.1l-384.22-384.22,262.61-3.69c18.78,0,28.64-6.04,46.42-9.42l236.79,236.79c5.18,5.18,8.1,12.22,8.1,19.55Z"
/>
</g>
<path
id="r_head"
class="cls-2"
d="m921.3,323.84c0,142.4-102,260.99-236.93,286.69-17.79,3.38-36.14,5.16-54.91,5.16h-262.07v-208.38h207.85c20.44,0,39.18-7.36,53.69-19.57,18.2-15.3,29.77-38.24,29.77-63.89s-11.57-48.59-29.77-63.89c-14.51-12.22-33.25-19.57-53.69-19.57-.18,0-.35,0-.53.01h0s-207.32-.01-207.32-.01V32h262.07c161.18,0,291.84,130.66,291.84,291.84Z"
/>
<g id="cube_3">
<rect
class="cls-1"
x="48.7"
y="698.61"
width="249.85"
height="309.39"
rx="26"
ry="26"
/>
</g>
<rect
id="cube_2"
class="cls-3"
x="48.7"
y="407.3"
width="249.85"
height="208.38"
rx="26"
ry="26"
/>
<g id="cube_1">
<rect
class="cls-1"
x="48.7"
y="32"
width="249.85"
height="292.37"
rx="26"
ry="26"
/>
</g>
</g>
</svg>
</template>
<style scoped>
#cube_1,
#cube_3 {
@apply fill-zinc-600;
}
#cube_2 {
@apply fill-primary-400;
}
#r_head {
@apply fill-zinc-500 drop-shadow-2xl;
}
#r_leg {
@apply fill-zinc-600;
}
</style>

View File

@ -0,0 +1,37 @@
<script lang="ts" setup>
// const appConfig = useAppConfig();
// appConfig.rayui.primary = 'red';
const route = useRoute()
const runtimeConfig = useRuntimeConfig().public
</script>
<template>
<header
class="w-full flex justify-between items-center py-2 h-16 z-50 border-b sticky top-0 bg-white dark:bg-neutral-900 transition-colors"
:class="[route.path !== '/' ? 'border-b-neutral-200 dark:border-b-neutral-700' : 'border-b-transparent dark:border-b-transparent']"
>
<NuxtLink to="/" class="flex items-center gap-2 text-neutral-900 dark:text-neutral-100 group">
<Logo class="-mt-0.5" />
<h1 class="flex flex-col">
<span class="block font-medium text-xl leading-none">
RayineUI
<sup class="text-xs"><span class="text-primary font-medium">{{ runtimeConfig.version }}</span></sup>
</span>
<span class="block font-normal text-xs leading-none">
RayineSoft Components Lib
</span>
</h1>
</NuxtLink>
<div class="flex items-center gap-4">
<RayButton to="https://github.com/HoshinoSuzumi/rayine-ui" target="_blank" icon="tabler:brand-github" variant="ghost">
GitHub
</RayButton>
</div>
</header>
</template>
<style scoped>
#rayine .cls-1 {
@apply !bg-white dark:bg-neutral-900;
}
</style>

92
docs/components/Toc.vue Normal file
View File

@ -0,0 +1,92 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
interface TocItem {
id: string
depth: number
text: string
children?: TocItem[]
}
const props = defineProps<{
toc: TocItem[]
maxDepth?: number
}>()
const maxDepth = props.maxDepth ?? 3
const filteredToc = computed(() => {
const filterByDepth = (items: TocItem[]): TocItem[] => {
return items
.filter(item => item.depth <= maxDepth)
.map(item => ({
...item,
children: item.children ? filterByDepth(item.children) : undefined,
}))
}
return filterByDepth(props.toc)
})
const activeLinks = ref<string[]>([])
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
activeLinks.value.push(entry.target.id)
}
else {
activeLinks.value = activeLinks.value.filter(link => link !== entry.target.id)
}
})
},
// { rootMargin: '0px 0px -80% 0px' }
)
filteredToc.value.forEach((item) => {
const element = document.getElementById(item.id)
if (element) {
observer.observe(element)
}
item.children?.forEach((child) => {
const childElement = document.getElementById(child.id)
if (childElement) {
observer.observe(childElement)
}
})
})
})
</script>
<template>
<ul>
<template v-for="item in filteredToc" :key="item.id">
<li>
<NuxtLink :href="'#' + item.id" class="link" :class="{ active: activeLinks.includes(item.id) }">
{{ item.text }}
</NuxtLink>
<ul v-if="item.children && item.children.length" class="ml-4">
<template v-for="child in item.children" :key="child.id">
<li>
<NuxtLink :href="'#' + child.id" class="link" :class="{ active: activeLinks.includes(child.id) }">
{{ child.text }}
</NuxtLink>
</li>
</template>
</ul>
</li>
</template>
</ul>
</template>
<style scoped>
.link {
@apply text-xs text-neutral-400 dark:text-neutral-500 font-medium;
}
.link.active {
@apply text-primary;
}
</style>

View File

@ -0,0 +1,38 @@
<script lang="ts" setup>
import { camelCase, upperFirst } from 'scule'
import json5 from 'json5'
import * as config from '#rayui/themes'
const route = useRoute()
const props = defineProps({
slug: {
type: String,
default: null,
},
})
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const componentCamelName = camelCase(slug)
const componentName = `Ray${upperFirst(componentCamelName)}`
const defaults = config[componentCamelName as keyof typeof config]
const { data: defaultsRender } = await useAsyncData(`${componentName}-defaults`, () => {
return parseMarkdown(`
\`\`\`yaml
${json5.stringify(defaults, null, 2).replace(/,(\s+[}\]|])/g, '$1')}
\`\`\`
`)
})
</script>
<template>
<ContentRendererMarkdown :value="defaultsRender!" />
</template>
<style>
pre.shiki>code>span {
@apply text-wrap break-words;
}
</style>

View File

@ -0,0 +1,214 @@
<script lang="ts" setup>
import { camelCase, kebabCase, upperFirst } from 'scule'
const route = useRoute()
const appConfig = useAppConfig()
const { $prettier } = useNuxtApp()
const props = defineProps({
slug: {
type: String,
default: '',
},
props: {
type: Object,
default: () => ({}),
},
privateProps: {
type: Object,
default: () => ({}),
},
excludedProps: {
type: Array,
default: () => [],
},
slots: {
type: Object,
default: null,
},
options: {
type: Array as PropType<{ name: string, values: string[], restriction: 'expected' | 'included' | 'excluded' | 'only' }[]>,
default: () => [],
},
})
const componentName = props.slug || `Ray${upperFirst(camelCase(route.params.slug[route.params.slug.length - 1]))}`
const componentMeta = await fetchComponentMeta(componentName)
const privateProps = reactive({ ...props.privateProps })
const componentProps = reactive({ ...props.props })
const componentFullProps = computed(() => ({ ...componentProps, ...privateProps }))
const componentVModel = computed({
get: () => privateProps.modelValue,
set: (value) => {
privateProps.modelValue = value
},
})
const customizableOptions = (key: string, schema: { kind: string, type: string, schema: [] }) => {
let options: string[] = []
const optionItem = props?.options?.find(item => item?.name === key) || null
const types = schema?.type?.split('|')?.map(item => item.trim()?.replaceAll('"', '')) || []
const invalidTypes = ['string', 'number', 'boolean', 'array', 'object', 'Function', 'undefined']
const hasInvalidType = types?.every(type => invalidTypes.includes(type))
if (key.toLowerCase().endsWith('color')) {
options = [...appConfig.rayui.colors]
}
const schemaOptions = Object.values(schema?.schema || {})
if (key.toLowerCase() === 'size' && schemaOptions?.length) {
const baseSizeOrder = { xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }
schemaOptions.sort((a: string, b: string) => {
const [aBase, aNum] = [(a.match(/[a-z]+/i)?.[0].toLowerCase() || 'xs') as keyof typeof baseSizeOrder, Number.parseInt(a.match(/\d+/)?.[0] || '1')]
const [bBase, bNum] = [(b.match(/[a-z]+/i)?.[0].toLowerCase() || 'xs') as keyof typeof baseSizeOrder, Number.parseInt(b.match(/\d+/)?.[0] || '1')]
return aBase === bBase
? (aBase === 'xs' ? bNum - aNum : aNum - bNum)
: baseSizeOrder[aBase] - baseSizeOrder[bBase]
})
}
if (schemaOptions?.length > 0 && schema?.kind === 'enum' && !hasInvalidType && optionItem?.restriction !== 'only') {
options = schemaOptions.filter(option => typeof option === 'string' && option !== 'undefined').map((option: string) => option.replaceAll('"', ''))
}
if (optionItem?.restriction === 'only') {
options = optionItem.values
}
if (optionItem?.restriction === 'expected') {
options = options.filter(item => optionItem.values.includes(item))
}
if (optionItem?.restriction === 'included') {
options = [...options, ...optionItem.values]
}
if (optionItem?.restriction === 'excluded') {
options = options.filter(item => !optionItem.values.includes(item))
}
return options
}
const customizableProps = computed(() => Object.keys(componentProps).map((k) => {
if (props.excludedProps.includes(k)) return null
const prop = componentMeta?.meta?.props?.find((prop: any) => prop.name === k)
const schema = prop?.schema || {}
const options = customizableOptions(k, schema)
return {
name: k,
type: prop?.type || 'string',
label: k === 'modelValue' ? 'value' : camelCase(k),
options,
}
}).filter(prop => prop !== null))
const code = computed(() => {
let code = `\`\`\`html
<template>
<${componentName}`
for (const [k, v] of Object.entries(componentFullProps.value)) {
if (v === 'undefined' || v === null) {
continue
}
code += ` ${(typeof v === 'boolean' && (k === 'modelValue' || v !== true)) || typeof v === 'number' || typeof v === 'object' ? ':' : ''}${k === 'modelValue' ? 'model-value' : kebabCase(k)}${k !== 'modelValue' && typeof v === 'boolean' && !!v ? '' : `="${typeof v === 'object' ? renderObject(v) : v}"`}`
}
if (props.slots) {
code += `>
${Object.entries(props.slots).map(([key, value]) => {
return key === 'default'
? value
: `<template #${key}>
${value}
</template>`
}).join('\n ')}
</${componentName}>`
}
else {
code += ' />'
}
code += `\n</template>
\`\`\`
`
return code
})
const { data: codeRender } = await useAsyncData(`${componentName}-code-renderer-${JSON.stringify({ props: componentProps, slots: props.slots, code: code.value })}`, async () => {
let fortmattedCode = ''
try {
fortmattedCode = await $prettier.format(code.value, {
semi: false,
singleQuote: true,
})
}
catch (e) {
fortmattedCode = code.value
}
return parseMarkdown(fortmattedCode)
}, {
watch: [code],
})
</script>
<template>
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg not-prose my-2 overflow-hidden">
<div :class="['p-4 overflow-auto flex', !!codeRender ? 'border-b border-neutral-200 dark:border-neutral-700' : '']">
<component :is="componentName" v-model="componentVModel" v-bind="componentFullProps">
<ContentSlot v-if="$slots.default" :use="$slots.default" />
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
<ContentSlot :name="slot" unwrap="p" />
</template>
</component>
</div>
<div v-if="customizableProps.length > 0" class="border-b border-neutral-200 dark:border-neutral-700 flex">
<div
v-for="(prop, k) in customizableProps"
:key="k"
class="px-2 py-0.5 flex flex-col gap-0.5 border-r dark:border-neutral-700"
>
<label :for="`${prop.name}-prop`" class="text-sm text-neutral-400">{{ prop.name }}</label>
<input
v-if="prop.type.startsWith('boolean')"
:id="`${prop.name}-prop`"
v-model="componentProps[prop.name]"
type="checkbox"
class="mt-1 mb-2"
>
<select v-else-if="prop.options.length" :id="`${prop.name}-prop`" v-model="componentProps[prop.name]">
<option v-for="option in prop.options" :key="option" :value="option">
{{ option }}
</option>
</select>
<RayInput
v-else
:id="`${prop.name}-prop`"
:model-value="componentProps[prop.name]"
:type="prop.type.includes('number') ? 'number' : 'text'"
variant="plain"
:padded="false"
:ui="{ rounded: 'rounded-none' }"
:placeholder="prop.type"
autocomplete="off"
@update:model-value="val => componentProps[prop.name] = prop.type === 'number' ? Number(val) : val"
/>
</div>
</div>
<ContentRenderer v-if="codeRender" :value="codeRender" class="[&_.pre]:rounded-none [&_.pre]:border-none" />
</div>
</template>
<style scoped>
input,
select {
@apply text-sm outline-none border-none bg-transparent;
}
</style>

View File

@ -0,0 +1,50 @@
<script lang="ts" setup>
import type { ComponentMeta } from 'vue-component-meta'
import { camelCase, upperFirst } from 'scule'
const route = useRoute()
const props = defineProps({
slug: {
type: String,
default: null,
},
})
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const componentCamelName = camelCase(slug)
const componentName = `Ray${upperFirst(componentCamelName)}`
const meta = await fetchComponentMeta(componentName)
const metaProps: ComputedRef<ComponentMeta['props']> = computed(() => meta?.meta?.props || [])
</script>
<template>
<ProseTable>
<ProseThead>
<ProseTr>
<ProseTh>Prop</ProseTh>
<ProseTh>Default</ProseTh>
<ProseTh>Type</ProseTh>
</ProseTr>
</ProseThead>
<ProseTbody>
<ProseTr v-for="prop in metaProps" :key="prop.name">
<ProseTd>
{{ prop.name }}
</ProseTd>
<ProseTd>
<ProseCodeInline v-if="prop.default">
{{ prop.default }}
</ProseCodeInline>
</ProseTd>
<ProseTd>
<ProseCodeInline v-if="prop.type">
{{ prop.type }}
</ProseCodeInline>
<MDC v-if="prop.description" :value="prop.description" class="text-gray-500 dark:text-gray-400" />
</ProseTd>
</ProseTr>
</ProseTbody>
</ProseTable>
</template>

View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import { camelCase, upperFirst } from 'scule'
const route = useRoute()
const props = defineProps({
slug: {
type: String,
default: null,
},
})
const slug = props.slug || route.params.slug[route.params.slug.length - 1]
const componentCamelName = camelCase(slug)
const componentName = `Ray${upperFirst(componentCamelName)}`
const meta = await fetchComponentMeta(componentName)
</script>
<template>
<div class="flex flex-col not-prose font-mono divide-y divide-gray-100 dark:divide-gray-800">
<div v-for="(slot, k) in meta?.meta?.slots" :key="k" class="py-2">
<pre>{{ slot }}</pre>
</div>
</div>
</template>

View File

@ -0,0 +1,44 @@
<template>
<div class="bg-neutral-50 dark:bg-neutral-800/50 not-prose text-sm" data-prose-code>
<slot />
</div>
</template>
<script setup lang="ts">
defineProps({
code: {
type: String,
default: '',
},
language: {
type: String,
default: null,
},
filename: {
type: String,
default: null,
},
highlights: {
type: Array as () => number[],
default: () => [],
},
meta: {
type: String,
default: null,
},
})
</script>
<style>
pre {
@apply p-0 py-2;
}
pre code .line {
@apply block min-h-4 px-4;
}
pre code .line.highlight {
@apply !bg-gray-200/50 dark:!bg-gray-800;
}
</style>

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
const props = defineProps({
code: {
type: String,
default: '',
},
language: {
type: String,
default: null,
},
filename: {
type: String,
default: null,
},
highlights: {
type: Array as () => number[],
default: () => [],
},
meta: {
type: String,
default: null,
},
class: {
type: String,
default: null,
},
style: {
type: [String, Object],
default: null,
},
})
const iconNameLangMapping: Record<string, string> = {
'default': 'tabler:file',
'vue': 'vscode-icons:file-type-vue',
'vue-html': 'vscode-icons:file-type-vue',
'bash': 'tabler:terminal',
'sh': 'tabler:terminal',
'ts': 'vscode-icons:file-type-typescript-official',
'js': 'vscode-icons:file-type-js-official',
'json': 'vscode-icons:file-type-json',
}
const iconNameFilenameMapping: Record<string, string> = {
'nuxt.config.ts': 'vscode-icons:file-type-nuxt',
}
const resolvedIconName = computed(() => {
if (!props.language) {
return iconNameLangMapping['default']
}
if (props.filename.endsWith('.vue')) {
return iconNameLangMapping['vue']
}
if (iconNameFilenameMapping[props.filename]) {
return iconNameFilenameMapping[props.filename]
}
if (iconNameLangMapping[props.language]) {
return iconNameLangMapping[props.language]
}
return iconNameLangMapping['default']
})
</script>
<template>
<div data-prose-pre class="pre rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700">
<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">
<RayIcon v-if="resolvedIconName" :name="resolvedIconName" class="inline" />
<span class="text-sm text-neutral-500 dark:text-neutral-400">{{ filename }}</span>
</span>
</div>
<ProseCode
data-prose-precode
:code="code"
:language="language"
:filename="filename"
:highlights="highlights"
:meta="meta"
>
<pre data-prose-pre-inner-pre :class="$props.class" :style="style"><slot /></pre>
</ProseCode>
</div>
</template>
<style>
pre code .line {
display: block;
min-height: 1rem
}
</style>

View File

@ -0,0 +1,39 @@
interface ComponentMetaState {
[key: string]: any
}
const useComponentsMetaState = () =>
useState<ComponentMetaState>('components-meta', () => ({}))
export const fetchComponentMeta = async (name: string) => {
const state = useComponentsMetaState()
if (state.value[name]?.then) {
await state.value[name]
return state.value[name]
}
if (state.value[name]) {
return state.value[name]
}
if (import.meta.server) {
const event = useRequestEvent()
if (event && event.node && event.node.res) {
event.node.res.setHeader(
'x-nitro-prerender',
[
event.node.res.getHeader('x-nitro-prerender'),
`/api/component-meta/${name}.json`,
].filter(Boolean) as string[],
)
}
}
state.value[name] = $fetch(`/api/component-meta/${name}.json`).then(
(meta) => {
state.value[name] = meta
},
)
await state.value[name]
return state.value[name]
}

View File

@ -0,0 +1,15 @@
---
title: Introduction
description: Multi-purpose customizable components for RayineSoft projects
---
RayineUI is a multi-purpose customizable UI library for RayineSoft projects.
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.
## Features
- Fully customizable components
- TailwindCSS inside
- Full TypeScript support
- 200,000+ icons from [Iconify](https://iconify.design/)

View File

@ -0,0 +1,31 @@
## Setup
1. Install rayine-ui via npm or other package manager.
```bash [Terminal]
npm install rayine-ui
```
or...
```bash [Terminal]
npx nuxi@latest module add rayine-ui
```
2. Add to the modules in your project.
```ts [nuxt.config.ts]{2}
export default defineNuxtConfig({
modules: ['rayine-ui']
})
```
Now you can use the components in your project.
## TypeScript
*TBD*
## Options
*TBD*

View File

@ -0,0 +1 @@
title: Getting Started

View File

@ -0,0 +1,127 @@
---
description: Create a button component with different variants and colors
---
## Usage
Default button style
::ComponentPreview
Button
::
### Styles
Use the `variant` and `color` props to predefined styles and change the color of buttons.
::ComponentPreview
---
props:
variant: soft
color: violet
---
Button
::
#### Match
The color of the buttons will match the color theme.
::ComponentPreview
---
props:
color: match
excludedProps:
- color
---
Button
::
#### Invert
The color of the buttons will be the opposite of the color theme.
::ComponentPreview
---
props:
color: invert
excludedProps:
- color
---
Button
::
### Sizes
::ComponentPreview
---
props:
size: sm
---
Button
::
### Block
::ComponentPreview
---
props:
block: true
---
Button
::
### Label
::ComponentPreview
---
props:
label: Button
---
::
### Disabled
::ComponentPreview
---
props:
disabled: true
variant: solid
---
Button
::
### Icon
::ComponentPreview
---
props:
icon: tabler:adjustments
size: sm
loading: false
---
Settings
::
### Loading
::ComponentPreview
---
props:
loading: true
variant: solid
---
Button
::
## API
### Props
::ComponentProps
::
### Theme
::ComponentDefaults
::

View File

@ -0,0 +1,24 @@
---
description: Add icons to your app. Based on Iconify
---
## Usage
This component is a wrapper based on the `@nuxt/icon` library, which is based on Iconify. So you can use any icon name available on [icones.js.org](https://icones.js.org/).
::ComponentPreview
---
privateProps:
class: w-6 h-6
props:
name: tabler:brand-github
---
::
## Collections
It's recommended to install the icon collection you want to use locally. You can do this by running:
```bash [Terminal]
pnpm i @iconify-icons/[collection_name]
```

View File

@ -0,0 +1,155 @@
---
description: The input component is used to get user input
since: 1.3.2
---
## Usage
The basic usage.
:::ComponentPreview
---
privateProps:
placeholder: "Type something..."
---
:::
### Sizes
::ComponentPreview
---
privateProps:
placeholder: "Type something..."
props:
size: sm
---
::
### Colors
The `color` prop affects the color of the border.
::ComponentPreview
---
props:
color: primary
---
::
### Variants
::ComponentPreview
---
privateProps:
placeholder: "Search..."
props:
variant: outline
---
::
### Type
The `type` prop changes the type of the input. All the aviailable types can be found at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input).
::ComponentPreview
---
privateProps:
placeholder: "Type anything..."
props:
type: text
---
::
### Placeholder
The `placeholder` prop sets the placeholder text. It is shown when the input is empty.
::ComponentPreview
---
props:
placeholder: "Type anything..."
---
::
### Padded
Inputs can be with no padding.
::ComponentPreview
---
privateProps:
placeholder: "Search..."
variant: plain
props:
padded: false
---
::
### Disabled
Inputs can be disabled.
::ComponentPreview
---
privateProps:
placeholder: "Search..."
props:
disabled: true
---
::
### Model Modifiers
#### .trim
The `.trim` modifier trims the input value.
```vue [page]
<script lang="ts" setup>
const modal = ref<string>("");
</script>
<template>
<RayInput v-model.trim="modal" />
</template>
```
#### .number
The `.number` modifier converts the input value to a number. Non-numeric values are ignored.
```vue [page]
<script lang="ts" setup>
const modal = ref<number>(0);
</script>
<template>
<RayInput v-model.number="modal" />
</template>
```
#### .lazy
The `.lazy` modifier syncs the input value with the model only on `change` event.
```vue [page]
<script lang="ts" setup>
const modal = ref<string>("");
</script>
<template>
<RayInput v-model.lazy="modal" />
</template>
```
## API
### Props
::ComponentProps
::
### Theme
::ComponentDefaults
::

View File

@ -0,0 +1,57 @@
---
description: Display a keyboard keys such as shortcuts or hotkeys
since: 1.3.3
---
## Usage
Use the default slot to display the keyboard key.
::ComponentPreview
K
::
The `label` prop also can be used to do so.
::ComponentPreview
---
props:
label: K
---
::
### Sizes
The `size` prop changes the size of the `kbd`.
::ComponentPreview
---
props:
size: sm
---
K
::
### Shadow
Add a shadow to the `kbd`.
::ComponentPreview
---
props:
shadow: true
---
K
::
## API
### Props
::ComponentProps
::
### Theme
::ComponentDefaults
::

View File

@ -0,0 +1,95 @@
---
description: Display a indicator with or without counts on any component
---
## Usage
Use the default slot to add any component you want to display the indicator on.
::ComponentPreview
---
slots:
default: |
<RayButton icon="tabler:message" label="messages" color="invert" />
---
#default
:RayButton{icon="tabler:message" label="messages" color="invert"}
::
### Styles
You can change the color and size of the indicator by using the `color` and `size` props.
::ComponentPreview
---
props:
color: amber
size: sm
slots:
default: |
<RayButton icon="tabler:message" label="messages" color="invert" />
---
#default
:RayButton{icon="tabler:message" label="messages" color="invert"}
::
### Position
Use the `position` prop to change the position of the indicator.
::ComponentPreview
---
props:
position: top-right
slots:
default: |
<RayButton icon="tabler:message" label="messages" color="invert" />
---
#default
:RayButton{icon="tabler:message" label="messages" color="invert"}
::
### Count
Add a count to the indicator by using the `value` prop.
::ComponentPreview
---
props:
value: 5
slots:
default: |
<RayButton icon="tabler:message" label="messages" color="invert" />
---
#default
:RayButton{icon="tabler:message" label="messages" color="invert"}
::
#### Overflow
Set `max` prop to handle overflow situation.
::ComponentPreview
---
props:
value: 110
max: 99
slots:
default: |
<RayButton icon="tabler:message" label="messages" color="invert" />
---
#default
:RayButton{icon="tabler:message" label="messages" color="invert"}
::
## API
### Props
::ComponentProps
::
### Theme
::ComponentDefaults
::

View File

@ -0,0 +1,87 @@
---
description: The message component is used to display a message to the user
since: 1.2.0
---
## Usage
First add the `<RayMessages>` component to your `app.vue`.
```js [app.vue]{6}
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<RayMessages />
</template>
```
Then, use the `useMessage` composable to add messages to your app anywhere you want.
```vue [pages/index.vue]{2,5-8}
<script lang="ts" setup>
const message = useMessage()
const showMessage = () => {
message.add({
content: 'Hello RayineSoft',
type: 'success',
})
}
</script>
<template>
<RayButton label="Show Message" @click="showMessage" />
</template>
```
### Type
Multiple preset styles with icons and colors.
::ComponentPreview
---
props:
type: info
content: Hello RayineSoft
---
::
### Icon
Or you can use the `icon` prop to change the icon of the message.
::ComponentPreview
---
privateProps:
content: Thanks for activating
props:
icon: tabler:circle-key
---
::
### Color
Use the `color` prop to change the color of the message.
::ComponentPreview
---
props:
color: amber
content: Hello RayineSoft
---
::
## API
### Props
::ComponentProps
::
### Theme
::ComponentDefaults
::

View File

@ -0,0 +1,189 @@
---
description: Create a textarea component
since: 1.3.5
---
## Usage
The basic usage.
::ComponentPreview
---
privateProps:
placeholder: Description
---
::
### Sizes
::ComponentPreview
---
privateProps:
placeholder: Description
props:
size: sm
---
::
### Colors
::ComponentPreview
---
privateProps:
placeholder: Description
props:
color: primary
---
::
### Variants
::ComponentPreview
---
privateProps:
placeholder: Description
props:
variant: outline
---
::
### Placeholder
You can also set a placeholder.
::ComponentPreview
---
props:
placeholder: "Description here..."
---
::
### Padded
::ComponentPreview
---
privateProps:
placeholder: Description
variant: plain
props:
padded: false
---
::
### Rows
Set the number of rows of the textarea.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
rows: 4
---
::
### Resize
Enable the resize control.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
resize: true
---
::
### Auto Resize
The `autosize` prop enables the auto resizing of the textarea. The textarea will grow in height as the user types.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
autosize: true
---
::
The `maxrows` prop can be used to set the maximum number of rows the textarea can grow to.
::ComponentPreview
---
privateProps:
placeholder: Description
props:
autosize: true
maxrows: 8
---
::
### Disabled
::ComponentPreview
---
privateProps:
placeholder: Description
props:
disabled: true
---
::
### Model Modifiers
#### .trim
The `.trim` modifier trims the input value.
```vue [page]
<script lang="ts" setup>
const modal = ref<string>("");
</script>
<template>
<RayTextarea v-model.trim="modal" />
</template>
```
#### .number
The `.number` modifier converts the input value to a number. Non-numeric values are ignored.
```vue [page]
<script lang="ts" setup>
const modal = ref<number>(0);
</script>
<template>
<RayTextarea v-model.number="modal" />
</template>
```
#### .lazy
The `.lazy` modifier syncs the input value with the model only on `change` event.
```vue [page]
<script lang="ts" setup>
const modal = ref<string>("");
</script>
<template>
<RayTextarea v-model.lazy="modal" />
</template>
```
## API
### Props
::ComponentProps
::
### Theme
::ComponentDefaults
::

View File

@ -0,0 +1,76 @@
---
description: Get a dynamic switch component
since: 1.3.4
---
## Usage
Use the `v-model` directive to make it reactive.
::ComponentPreview
---
privateProps:
v-model: checked
---
::
### Colors
The `color` prop affects the background color of the toggle.
::ComponentPreview
---
privateProps:
modelValue: true
props:
color: primary
---
::
### Sizes
The default size of the toggle is `md`.
::ComponentPreview
---
props:
size: md
---
::
### Rounded
You can make the toggle rounded by setting the `rounded` prop to `true`.
::ComponentPreview
---
props:
rounded: true
size: md
---
::
### Disabled
Disable it.
::ComponentPreview
---
privateProps:
modelValue: true
props:
disabled: true
---
::
## API
### Props
::ComponentProps
::
### Theme
::ComponentDefaults
::

View File

@ -1,14 +1,14 @@
<script lang="ts" setup>
useSeoMeta({
title: 'RayineSoft Common Components'
title: 'RayineSoft Common Components',
})
</script>
<template>
<div class="max-w-4xl mx-auto px-4">
<div class="max-w-6xl mx-auto px-4">
<TitleBar />
<main class="pt-4">
<slot></slot>
<slot />
</main>
</div>
</template>
@ -18,11 +18,12 @@ body {
@apply bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100;
}
.shiki,
/* .shiki,
.shiki span {
background-color: rgba(0, 0, 0, 0) !important;
}
} */
/*
@media (prefers-color-scheme: dark) {
.shiki,
@ -34,5 +35,5 @@ body {
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
} */
</style>

87
docs/nuxt.config.ts Normal file
View File

@ -0,0 +1,87 @@
import { createResolver } from '@nuxt/kit'
import defaultTheme from 'tailwindcss/defaultTheme'
import colors from 'tailwindcss/colors'
import module from '../src/module'
import { excludeColors } from '../src/runtime/utils/colors'
import pkg from '../package.json'
const { resolve } = createResolver(import.meta.url)
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
modules: [
'@nuxt/content',
'@nuxt/fonts',
'@nuxtjs/color-mode',
module,
'nuxt-component-meta',
],
devtools: { enabled: true },
colorMode: {
preference: 'system',
classSuffix: '',
},
content: {
highlight: {
langs: ['postcss', 'mdc', 'html', 'vue', 'ts', 'js', 'bash', 'yml'],
},
},
mdc: {
highlight: {
theme: {
light: 'material-theme-lighter',
dark: 'material-theme',
},
themes: ['material-theme-lighter', 'material-theme'],
},
},
runtimeConfig: {
public: {
version: pkg.version,
},
},
routeRules: {
'/components': { redirect: '/components/button', prerender: false },
},
compatibilityDate: '2024-04-03',
typescript: {
includeWorkspace: true,
},
componentMeta: {
exclude: [
'@nuxt/content',
'@nuxt/icon',
'@nuxtjs/color-mode',
'@nuxtjs/mdc',
'nuxt/dist',
resolve('./components'),
],
metaFields: {
type: false,
props: true,
slots: true,
events: true,
exposed: false,
},
},
icon: {
clientBundle: {
scan: true,
},
},
rayui: {
globalComponents: true,
safeColors: [...excludeColors(colors)],
},
tailwindcss: {
config: {
theme: {
extend: {
fontFamily: {
sans: ['Rubik', '"Noto Sans SC"', ...defaultTheme.fontFamily.sans],
},
},
},
},
},
})

29
docs/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "rayine-ui-docs",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@iconify-json/vscode-icons": "^1.2.2",
"@nuxt/content": "^2.13.4",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/mdc": "^0.9.2",
"nuxt": "^3.14.159",
"nuxt-component-meta": "^0.9.0",
"nuxt-shiki": "^0.3.0",
"prettier": "^3.3.3",
"rayine-ui": "latest",
"ufo": "^1.5.4",
"vue": "latest",
"vue-router": "latest"
},
"devDependencies": {
"@nuxt/fonts": "^0.10.2"
}
}

166
docs/pages/[...slug].vue Normal file
View File

@ -0,0 +1,166 @@
<script lang="ts" setup>
import { withoutTrailingSlash } from 'ufo'
import { standard } from '#rayui/themes'
const route = useRoute()
const { data: page } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
if (!page.value) {
throw createError({
statusCode: 404, statusMessage: 'Page not found', fatal: true,
})
}
const hasToc = computed(() => page.value?.body?.toc && page.value?.body?.toc?.links.length !== 0)
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
return queryContent()
.where({
_extension: 'md',
navigation: {
$ne: false,
},
})
.only(['title', 'description', '_path'])
.findSurround(withoutTrailingSlash(route.path))
})
</script>
<template>
<div class="grid grid-cols-12 gap-4 pb-10">
<div class="hidden col-span-2 md:block">
<nav class="ml-1 overflow-hidden overflow-y-auto sticky top-[calc(64px+16px)]">
<ContentNavigation v-slot="{ navigation }">
<ul class="space-y-2">
<li v-for="link of navigation" :key="link._path">
<NuxtLink :to="link._path" class="text-sm text-neutral-600 dark:text-neutral-300 font-medium">
{{ link.title }}
</NuxtLink>
<ul v-if="link.children" class="pl-4 pt-2 space-y-1">
<li v-for="child in link.children" :key="child._path">
<NuxtLink
:to="child._path"
class="text-sm text-neutral-500 dark:text-neutral-400 flex items-center gap-1"
active-class="text-primary dark:text-primary font-medium"
>
<span>{{ child.title }}</span>
</NuxtLink>
</li>
</ul>
</li>
</ul>
</ContentNavigation>
</nav>
</div>
<div class="col-span-12" :class="[hasToc ? 'md:col-span-8' : 'md:col-span-10']">
<div>
<div class="flex justify-between items-center">
<h1 class="text-3xl text-primary font-medium">
{{ page?.title || 'untitled' }}
</h1>
<div
v-if="page?.since"
class="ring-1 ring-inset ring-primary-200 dark:ring-primary-900 text-primary-500 dark:text-primary-400 rounded-md bg-primary-50 dark:bg-primary-900 font-medium flex items-center gap-1"
:class="[standard.padding['sm'], standard.size['2xs']]"
>
<RayIcon name="tabler:git-merge" class="text-sm -mt-0.5" />
v{{ page.since }}
</div>
</div>
<p v-if="page?.description" class="text-lg text-neutral-500 dark:text-neutral-400 mt-2">
{{ page.description }}
</p>
</div>
<hr class="my-4 dark:border-neutral-700">
<div class="doc-body">
<ContentRenderer v-if="page?.body" :value="page" />
</div>
<div
class="w-full flex justify-between gap-4 mt-12 pt-12 border-t border-t-neutral-200 dark:border-t-neutral-700"
>
<div class="flex-1">
<NuxtLink v-if="surround?.[0]" :to="surround[0]._path" class="surround-btn">
<div>
<span class="tip">Previous</span>
<span class="title">{{ surround[0].title }}</span>
<span v-if="surround[0].description" class="description">{{ surround[0].description }}</span>
</div>
</NuxtLink>
</div>
<div class="flex-1">
<NuxtLink v-if="surround?.[1]" :to="surround[1]._path" class="surround-btn next">
<div>
<span class="tip">Next</span>
<span class="title">{{ surround[1].title }}</span>
<span v-if="surround[1].description" class="description">{{ surround[1].description }}</span>
</div>
</NuxtLink>
</div>
</div>
</div>
<div v-if="hasToc" class="hidden" :class="{ 'col-span-2 md:block': hasToc }">
<div
class="bg-neutral-50 dark:bg-neutral-800/50 rounded-lg px-4 py-3 overflow-hidden overflow-y-auto sticky top-[calc(64px+16px)]"
>
<span class="text-xs text-neutral-600 dark:text-neutral-300 font-medium inline-block mb-2">
Table of contents
</span>
<Toc :toc="page!.body!.toc!.links" />
</div>
</div>
</div>
</template>
<style>
.doc-body {
@apply prose prose-neutral dark:prose-invert max-w-none prose-headings:no-underline prose-p:text-justify;
hr {
@apply my-8 border-t border-neutral-200 dark:border-neutral-700;
}
h1 {
@apply text-3xl text-primary font-bold my-4 first:mt-0;
}
h2 a,
h3 a,
h4 a,
h5 a,
h6 a {
text-decoration: none;
}
}
.surround-btn {
@apply font-medium;
div {
@apply bg-neutral-100 dark:bg-neutral-800 rounded-lg px-8 py-6 w-full h-full flex flex-col gap-0 border border-transparent;
&:hover {
@apply border-primary;
}
}
&.next div {
@apply items-end text-right;
}
.tip {
@apply text-xs text-primary;
}
.title {
@apply text-base;
}
.description {
@apply pt-2 text-sm font-normal text-neutral-500 dark:text-neutral-400 line-clamp-2;
}
}
</style>

42
docs/pages/index.vue Normal file

File diff suppressed because one or more lines are too long

73
docs/plugins/prettier.ts Normal file
View File

@ -0,0 +1,73 @@
// ref: https://github.com/nuxt/ui/blob/f3632ddee511f0fccb24d4fc37403421e84ffdae/docs/plugins/prettier.ts
import type { Options } from 'prettier'
import { defu } from 'defu'
import PrettierWorker from '@/workers/prettier.js?worker&inline'
export interface SimplePrettier {
format: (source: string, options?: Options) => Promise<string>
}
function createPrettierWorkerApi(worker: Worker): SimplePrettier {
let counter = 0
const handlers: any = {}
worker.addEventListener('message', (event) => {
const { uid, message, error } = event.data
if (!handlers[uid]) {
return
}
const [resolve, reject] = handlers[uid]
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete handlers[uid]
if (error) {
reject(error)
}
else {
resolve(message)
}
})
function postMessage<T>(message: any) {
const uid = ++counter
return new Promise<T>((resolve, reject) => {
handlers[uid] = [resolve, reject]
worker.postMessage({ uid, message })
})
}
return {
format(source: string, options?: Options) {
return postMessage({ type: 'format', source, options })
},
}
}
export default defineNuxtPlugin(async () => {
let prettier: SimplePrettier
if (import.meta.server) {
const prettierModule = await import('prettier')
prettier = {
format(source, options = {}) {
return prettierModule.format(
source,
defu(options, {
parser: 'markdown',
}),
)
},
}
}
else {
const worker = new PrettierWorker()
prettier = createPrettierWorkerApi(worker)
}
return {
provide: {
prettier,
},
}
})

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
docs/public/rayine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

51
docs/public/rayine.svg Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 970 1008">
<defs>
<style>
.cls-1 {
fill: #3f3f46;
}
.cls-1, .cls-2, .cls-3 {
stroke-width: 0px;
}
.cls-2 {
fill: #71717a;
filter: url(#drop-shadow-1);
}
.cls-3 {
fill: #a5b4fc;
filter: url(#drop-shadow-2);
}
</style>
<filter id="drop-shadow-1" filterUnits="userSpaceOnUse">
<feOffset dx="0" dy="16"/>
<feGaussianBlur result="blur" stdDeviation="16"/>
<feFlood flood-color="#71717a" flood-opacity=".8"/>
<feComposite in2="blur" operator="in"/>
<feComposite in="SourceGraphic"/>
</filter>
<filter id="drop-shadow-2" filterUnits="userSpaceOnUse">
<feOffset dx="0" dy="16"/>
<feGaussianBlur result="blur-2" stdDeviation="16"/>
<feFlood flood-color="#a5b4fc" flood-opacity=".8"/>
<feComposite in2="blur-2" operator="in"/>
<feComposite in="SourceGraphic"/>
</filter>
</defs>
<g id="rayine">
<g id="r_leg">
<path class="cls-1" d="m921.3,858.91v121.45c0,15.27-12.38,27.64-27.64,27.64h-122.51c-7.33,0-14.36-2.91-19.55-8.1l-384.22-384.22,262.61-3.69c18.78,0,28.64-6.04,46.42-9.42l236.79,236.79c5.18,5.18,8.1,12.22,8.1,19.55Z"/>
</g>
<path id="r_head" class="cls-2" d="m921.3,323.84c0,142.4-102,260.99-236.93,286.69-17.79,3.38-36.14,5.16-54.91,5.16h-262.07v-208.38h207.85c20.44,0,39.18-7.36,53.69-19.57,18.2-15.3,29.77-38.24,29.77-63.89s-11.57-48.59-29.77-63.89c-14.51-12.22-33.25-19.57-53.69-19.57-.18,0-.35,0-.53.01h0s-207.32-.01-207.32-.01V32h262.07c161.18,0,291.84,130.66,291.84,291.84Z"/>
<g id="cube_3">
<rect class="cls-1" x="48.7" y="698.61" width="249.85" height="309.39" rx="26" ry="26"/>
</g>
<rect id="cube_2" class="cls-3" x="48.7" y="407.3" width="249.85" height="208.38" rx="26" ry="26"/>
<g id="cube_1">
<rect class="cls-1" x="48.7" y="32" width="249.85" height="292.37" rx="26" ry="26"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="rayine" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024">
<defs>
<style>
.cls-1 {
fill: #3f3f46;
}
.cls-1, .cls-2, .cls-3 {
stroke-width: 0px;
}
.cls-2 {
fill: #71717a;
}
.cls-3 {
fill: #a5b4fc;
}
</style>
</defs>
<g id="r_leg">
<path class="cls-1" d="m949.3,851.91v121.45c0,15.27-12.38,27.64-27.64,27.64h-122.51c-7.33,0-14.36-2.91-19.55-8.1l-384.22-384.22,262.61-3.69c18.78,0,28.64-6.04,46.42-9.42l236.79,236.79c5.18,5.18,8.1,12.22,8.1,19.55Z"/>
</g>
<path id="r_head" class="cls-2" d="m949.3,316.84c0,142.4-102,260.99-236.93,286.69-17.79,3.38-36.14,5.16-54.91,5.16h-262.07v-208.38h207.85c20.44,0,39.18-7.36,53.69-19.57,18.2-15.3,29.77-38.24,29.77-63.89s-11.57-48.59-29.77-63.89c-14.51-12.22-33.25-19.57-53.69-19.57-.18,0-.35,0-.53.01h0s-207.32-.01-207.32-.01V25h262.07c161.18,0,291.84,130.66,291.84,291.84Z"/>
<g id="cube_3">
<rect class="cls-1" x="76.7" y="691.61" width="249.85" height="309.39" rx="26" ry="26"/>
</g>
<rect id="cube_2" class="cls-3" x="76.7" y="400.3" width="249.85" height="208.38" rx="26" ry="26"/>
<g id="cube_1">
<rect class="cls-1" x="76.7" y="25" width="249.85" height="292.37" rx="26" ry="26"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
docs/public/robots.txt Normal file
View File

@ -0,0 +1 @@

View File

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

4
docs/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

17
docs/utils/index.ts Normal file
View File

@ -0,0 +1,17 @@
export const renderObject = (obj: any): string => {
if (Array.isArray(obj)) {
return `[${obj.map(renderObject).join(', ')}]`
}
if (typeof obj === 'object') {
return `{ ${Object.entries(obj)
.map(([key, value]) => `${key}: ${renderObject(value)}`)
.join(', ')} }`
}
if (typeof obj === 'string') {
return `'${obj}'`
}
return obj
}

34
docs/workers/prettier.js Normal file
View File

@ -0,0 +1,34 @@
// ref: https://github.com/nuxt/ui/blob/f3632ddee511f0fccb24d4fc37403421e84ffdae/docs/workers/prettier.js
/* eslint-disable no-undef */
self.onmessage = async function (event) {
self.postMessage({
uid: event.data.uid,
message: await handleMessage(event.data.message),
})
}
function handleMessage(message) {
switch (message.type) {
case 'format':
return handleFormatMessage(message)
}
}
async function handleFormatMessage(message) {
if (!globalThis.prettier) {
await Promise.all([
import('https://unpkg.com/prettier@3.3.3/standalone.js'),
import('https://unpkg.com/prettier@3.3.3/plugins/html.js'),
import('https://unpkg.com/prettier@3.3.3/plugins/markdown.js'),
])
}
const { options, source } = message
const formatted = await prettier.format(source, {
parser: 'markdown',
plugins: prettierPlugins,
...options,
})
return formatted
}

View File

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

29
eslint.config.mjs Normal file
View File

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

View File

@ -1,68 +0,0 @@
import { addTemplate, useNuxt } from "@nuxt/kit";
import { setColors } from "./utils/colors";
import { generateSafelist } from "./utils/colors";
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
compatibilityDate: "2024-11-15",
modules: ["@nuxtjs/tailwindcss"],
hooks: {
"tailwindcss:config": (tailwindConfig) => {
const nuxt = useNuxt();
tailwindConfig.theme = tailwindConfig.theme || {};
tailwindConfig.theme.extend = tailwindConfig.theme.extend || {};
tailwindConfig.theme.extend.colors =
tailwindConfig.theme.extend.colors || {};
const colors = setColors(tailwindConfig.theme);
// generate safelist and inject it into tailwindConfig
const safelist = generateSafelist(
["primary", "amber", "violet", "red", "emerald", "cyan"],
colors
);
tailwindConfig.safelist = safelist;
// inject colors into appConfig
nuxt.options.appConfig.rayui = nuxt.options.appConfig.rayui || {};
nuxt.options.appConfig.rayui.colors = colors;
const template = addTemplate({
filename: "ray.colors.mjs",
getContents: () =>
`export default ${JSON.stringify(
nuxt.options.appConfig.rayui.colors
)};`,
write: true,
});
const typesTemplate = addTemplate({
filename: "ray.colors.d.ts",
getContents: () =>
`declare module '#ray-colors' { const defaultExport: ${JSON.stringify(
nuxt.options.appConfig.rayui.colors
)}; export default defaultExport; }`,
write: true,
});
nuxt.options.alias["#ray-colors"] = template.dst;
nuxt.hook("prepare:types", (opts) => {
opts.references.push({ path: typesTemplate.dst });
});
},
},
components: [
{
path: "./components",
prefix: "Ray",
pathPrefix: false,
},
{
path: "./components/icons",
prefix: "Icon",
pathPrefix: false,
},
],
});

View File

@ -1,29 +1,68 @@
{
"name": "rayine-layer",
"name": "rayine-ui",
"version": "1.3.9",
"description": "RayineSoft UI Components",
"repository": "HoshinoSuzumi/rayine-ui",
"homepage": "https://rayui.uniiem.com",
"license": "MIT",
"type": "module",
"version": "0.1.4-beta.2",
"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 docs",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare docs",
"build:docs": "nuxi generate docs",
"play": "nuxi dev playground",
"release": "release-it",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest watch",
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
},
"dependencies": {
"@iconify-json/svg-spinners": "^1.2.1",
"@iconify-json/tabler": "^1.2.8",
"@nuxt/icon": "^1.8.2",
"@nuxt/kit": "^3.14.159",
"@nuxtjs/tailwindcss": "^6.12.2",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/typography": "^0.5.15",
"defu": "^6.1.4",
"pathe": "^1.1.2",
"scule": "^1.3.0",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.15"
},
"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",
"@release-it/conventional-changelog": "^9.0.3",
"@types/node": "latest",
"changelogen": "^0.5.7",
"eslint": "^9.15.0",
"nuxt": "^3.14.159",
"release-it": "^17.10.0",
"release-it-pnpm": "^4.6.3",
"typescript": "^5.6.3",
"vue": "latest"
"vitest": "^2.1.5",
"vue-tsc": "^2.1.10"
},
"packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a",
"dependencies": {
"defu": "^6.1.4",
"tailwind-merge": "^2.5.4",
"@nuxtjs/tailwindcss": "^6.12.2",
"nuxt-shiki": "^0.3.0"
"resolutions": {
"rayine-ui": "workspace:*",
"typescript": "5.6.3"
}
}
}

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

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

11
playground/app.vue Normal file
View File

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

View File

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

13
playground/package.json Normal file
View File

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

View File

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

3
playground/tsconfig.json Normal file
View File

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

5584
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
packages:
- './'
- 'playground'
- 'docs'

106
src/module.ts Normal file
View File

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

View File

@ -0,0 +1,126 @@
<script lang="ts">
import { twJoin, twMerge } from 'tailwind-merge'
import { computed, defineComponent, toRef, type PropType } from 'vue'
import { getNonUndefinedValuesFromObject } from '../../utils'
import { nuxtLinkProps } from '../../utils/link'
import { button } from '../../themes'
import type { ButtonColor, ButtonSize, ButtonVariant, DeepPartial, Strategy } from '../../types/index'
import { useRayUI } from '#build/imports'
const config = button
export default defineComponent({
inheritAttrs: false,
props: {
...nuxtLinkProps,
class: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
padded: {
type: Boolean,
default: true,
},
square: {
type: Boolean,
default: false,
},
block: {
type: Boolean,
default: false,
},
label: {
type: String,
default: '',
},
to: {
type: String,
default: '',
},
size: {
type: String as PropType<ButtonSize>,
default: () => config.default.size,
},
color: {
type: String as PropType<ButtonColor>,
default: () => config.default.color,
},
variant: {
type: String as PropType<ButtonVariant>,
default: () => config.default.variant,
},
loadingIcon: {
type: String,
default: () => config.default.loadingIcon,
},
icon: {
type: String,
default: null,
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
},
setup(props) {
const extProps = computed(() => getNonUndefinedValuesFromObject(props, nuxtLinkProps))
const { ui, attrs } = useRayUI('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.gap[props.size],
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)
})
const iconClass = computed(() => {
return twJoin(
ui.value.icon.base,
ui.value.icon.size[props.size],
)
})
const leadingIconName = computed(() => props.loading ? props.loadingIcon : props.icon)
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
extProps,
buttonClass,
iconClass,
leadingIconName,
}
},
})
</script>
<template>
<RayLink type="button" :disabled="disabled || loading" :class="buttonClass" v-bind="{ ...extProps, ...attrs }">
<slot name="leading" :disabled="disabled" :loading="loading">
<RayIcon v-if="leadingIconName" :name="leadingIconName" :class="iconClass" />
</slot>
<slot>
<span v-if="label">{{ label }}</span>
</slot>
</RayLink>
</template>
<style scoped></style>

View File

@ -0,0 +1,31 @@
<script lang="ts">
import { defineComponent, type PropType } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
required: true,
},
mode: {
type: String as PropType<'svg' | 'css'>,
required: false,
default: null,
},
size: {
type: [String, Number],
required: false,
default: null,
},
customize: {
type: Function,
required: false,
default: null,
},
},
})
</script>
<template>
<Icon v-bind="$props" />
</template>

View File

@ -0,0 +1,63 @@
<script lang="ts">
import { computed, defineComponent, toRef, type PropType } from 'vue'
import { twJoin, twMerge } from 'tailwind-merge'
import { kbd } from '../../themes'
import type { DeepPartial, KbdSize, Strategy } from '../../types'
import { useRayUI } from '#build/imports'
const config = kbd
export default defineComponent({
props: {
label: {
type: String,
default: null,
},
size: {
type: String as PropType<KbdSize>,
default: config.default.size,
},
shadow: {
type: Boolean,
default: false,
},
class: {
type: String,
default: '',
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
},
setup(props) {
const { ui, attrs } = useRayUI('kbd', toRef(props, 'ui'), config)
const kbdClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.background,
ui.value.rounded,
ui.value.font,
ui.value.padding,
ui.value.ring,
props.shadow && ui.value.shadow,
ui.value.size[props.size],
), props.class)
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
kbdClass,
}
},
})
</script>
<template>
<kbd :class="kbdClass" v-bind="attrs">
<slot>{{ label }}</slot>
</kbd>
</template>

View File

@ -0,0 +1,65 @@
<script lang="ts" setup>
import { nuxtLinkProps } from '../../utils/link'
const props = defineProps({
...nuxtLinkProps,
to: {
type: String,
default: '',
},
as: {
type: String,
default: 'button',
},
type: {
type: String,
default: 'button',
},
disabled: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: undefined,
},
activeClass: {
type: String,
default: undefined,
},
inactiveClass: {
type: String,
default: undefined,
},
})
</script>
<template>
<component
:is="as"
v-if="!to"
:type="type"
:class="active ? activeClass : inactiveClass"
:disabled="disabled"
v-bind="$attrs"
>
<slot v-bind="{ isActive: active }" />
</component>
<NuxtLink v-else v-slot="{ href, target, rel, navigate, isActive, isExternal }" v-bind="props" custom>
<a
v-bind="$attrs"
:href="!disabled ? href : undefined"
:aria-disabled="disabled ? 'true' : undefined"
:role="disabled ? 'link' : undefined"
:rel="rel"
:target="target"
:class="active !== undefined ? (active ? activeClass : inactiveClass) : { [activeClass]: isActive, [inactiveClass]: !isActive }"
:tabindex="!disabled ? undefined : -1"
@click="(e) => (!disabled && !isExternal) && navigate(e)"
>
<slot v-bind="{ isActive: active !== undefined ? active : isActive }" />
</a>
</NuxtLink>
</template>
<style scoped></style>

View File

@ -0,0 +1,90 @@
<script lang="ts">
import { computed, defineComponent, toRef, type PropType } from 'vue'
import { twJoin, twMerge } from 'tailwind-merge'
import { mark } from '../../themes'
import type { MarkColor, MarkPosition, MarkSize } from '../../types'
import { useRayUI } from '#build/imports'
const config = mark
export default defineComponent({
inheritAttrs: false,
props: {
value: {
type: [Number, String],
default: null,
},
max: {
type: Number,
default: null,
},
size: {
type: String as PropType<MarkSize>,
default: config.default.size,
},
color: {
type: String as PropType<MarkColor>,
default: config.default.color,
},
position: {
type: String as PropType<MarkPosition>,
default: config.default.position,
},
ui: {
type: Object as PropType<typeof config>,
default: () => ({}),
},
class: {
type: String,
default: '',
},
},
setup(props) {
const { ui, attrs } = useRayUI('mark', toRef(props, 'ui'), config)
const markClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.ring,
ui.value.position[props.position],
ui.value.background.replaceAll('{color}', props.color),
props.value ? ui.value.value.size[props.size] : ui.value.size[props.size],
props.value ? ui.value.value.translate[props.position] : ui.value.translate[props.position],
), props.class)
})
const isOverMax = computed(() => {
if (props.max === null) return false
if (typeof props.value === 'string') return false
return props.value > props.max
})
// consider string value
const countValue = computed(() => {
if (typeof props.value === 'string') return props.value
return isOverMax.value ? `${props.max}+` : props.value
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
markClass,
isOverMax,
countValue,
}
},
})
</script>
<template>
<div :class="ui.wrapper">
<span :class="markClass">
<Transition v-bind="ui.transition">
<span v-if="value" :key="countValue" class="leading-none">{{ countValue }}</span>
</Transition>
</span>
<slot />
</div>
</template>

View File

@ -0,0 +1,164 @@
<script lang="ts">
import { computed, defineComponent, toRef, type PropType } from 'vue'
import { twJoin, twMerge } from 'tailwind-merge'
import defu from 'defu'
import { input } from '../../themes'
import type { DeepPartial, InputColor, InputModelModifiers, InputSize, InputType, InputVariant, Strategy } from '../../types/index'
import { onMounted, ref, useRayUI } from '#build/imports'
const config = input
export default defineComponent({
props: {
modelValue: {
type: [String, Number] as PropType<string | number | null>,
default: '',
},
type: {
type: String as PropType<InputType>,
default: 'text',
},
autofocus: {
type: Boolean,
default: false,
},
autofocusDelay: {
type: Number,
default: 100,
},
required: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: null,
},
padded: {
type: Boolean,
default: true,
},
size: {
type: String as PropType<InputSize>,
default: () => config.default.size,
},
color: {
type: String as PropType<InputColor>,
default: () => config.default.color,
},
variant: {
type: String as PropType<InputVariant>,
default: () => config.default.variant,
},
class: {
type: String,
default: '',
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
modelModifiers: {
type: Object as PropType<InputModelModifiers>,
default: () => ({}),
},
},
emits: ['update:modelValue', 'change', 'blur'],
setup(props, { emit }) {
const { ui, attrs } = useRayUI('input', toRef(props, 'ui'), config)
const modelModifiers = ref(defu({}, props.modelModifiers, { lazy: false, number: false, trim: false }))
const input = ref<HTMLInputElement | null>(null)
const baseClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size],
props.padded && ui.value.padding[props.size],
ui.value.variant[props.variant].replaceAll('{color}', props.color),
), props.class)
})
const updateValue = (value: string) => {
if (modelModifiers.value.trim) {
value = value.trim()
}
if (modelModifiers.value.number || props.type === 'number') {
const n = Number.parseFloat(value)
value = (Number.isNaN(n) ? value : n) as any
}
emit('update:modelValue', value)
}
const onInput = (e: Event) => {
if (modelModifiers.value.lazy) return
updateValue((e.target as HTMLInputElement).value)
}
const onChange = (e: Event) => {
if (props.type === 'file') {
emit('change', (e.target as HTMLInputElement).files)
return
}
const value = (e.target as HTMLInputElement).value
emit('change', value)
if (modelModifiers.value.lazy) {
updateValue(value)
}
if (modelModifiers.value.trim) {
(e.target as HTMLInputElement).value = value.trim()
}
}
const onBlur = (e: Event) => {
emit('blur', e)
}
onMounted(() => {
if (props.autofocus) {
setTimeout(() => {
input.value?.focus()
}, props.autofocusDelay)
}
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
baseClass,
input,
onInput,
onChange,
onBlur,
}
},
})
</script>
<template>
<div :class="type === 'hidden' ? 'hidden' : ui.wrapper">
<input
ref="input"
:type="type"
:class="baseClass"
:disabled="disabled"
:placeholder="placeholder"
:required="required"
v-bind="type === 'file' ? attrs : { ...attrs, value: modelValue }"
@input="onInput"
@change="onChange"
@blur="onBlur"
>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,195 @@
<script lang="ts">
import { computed, defineComponent, onMounted, ref, toRef, watch, type PropType } from 'vue'
import { twMerge, twJoin } from 'tailwind-merge'
import defu from 'defu'
import { textarea } from '../../themes'
import type { DeepPartial, Strategy, TextareaColor, TextareaModelModifiers, TextareaSize, TextareaVariant } from '../../types'
import { useRayUI } from '#build/imports'
const config = textarea
export default defineComponent({
props: {
modelValue: {
type: [String, Number] as PropType<string | number | null>,
default: '',
},
size: {
type: String as PropType<TextareaSize>,
default: config.default.size,
},
variant: {
type: String as PropType<TextareaVariant>,
default: config.default.variant,
},
color: {
type: String as PropType<TextareaColor>,
default: config.default.color,
},
autofocus: {
type: Boolean,
default: false,
},
autofocusDelay: {
type: Number,
default: 100,
},
placeholder: {
type: String,
default: null,
},
rows: {
type: Number,
default: 3,
},
autosize: {
type: Boolean,
default: false,
},
maxrows: {
type: Number,
default: 0,
},
padded: {
type: Boolean,
default: true,
},
resize: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
class: {
type: String,
default: '',
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
modelModifiers: {
type: Object as PropType<TextareaModelModifiers>,
default: () => ({}),
},
},
emits: [
'update:modelValue',
'blur',
'change',
],
setup(props, { emit }) {
const { ui, attrs } = useRayUI('textarea', toRef(props, 'ui'), config)
const modelModifiers = ref(defu({}, props.modelModifiers, { lazy: false, number: false, trim: false }))
const textarea = ref<HTMLTextAreaElement | null>(null)
const baseClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
ui.value.rounded,
ui.value.placeholder,
ui.value.size[props.size],
props.padded && ui.value.padding[props.size],
ui.value.variant[props.variant].replaceAll('{color}', props.color),
!props.resize && 'resize-none',
), props.class)
})
const autoResize = () => {
if (!props.autosize) return
if (!textarea.value) return
textarea.value.rows = props.rows
const overflowBefore = textarea.value.style.overflow
textarea.value.style.overflow = 'hidden'
const style = window.getComputedStyle(textarea.value)
const padding = Number.parseInt(style.paddingTop) + Number.parseInt(style.paddingBottom)
const lineHeight = Number.parseInt(style.lineHeight)
const { scrollHeight, clientHeight } = textarea.value
const computedRows = Math.floor((scrollHeight - padding) / lineHeight)
if (computedRows > props.rows) {
textarea.value.rows = props.maxrows ? Math.min(computedRows, props.maxrows) : computedRows
}
textarea.value.style.overflow = overflowBefore
}
const updateValue = (value: string) => {
if (modelModifiers.value.trim) {
value = value.trim()
}
if (modelModifiers.value.number) {
const n = Number.parseFloat(value)
value = (Number.isNaN(n) ? value : n) as any
}
emit('update:modelValue', value)
}
const onInput = (e: Event) => {
autoResize()
if (modelModifiers.value.lazy) return
updateValue((e.target as HTMLInputElement).value)
}
const onChange = (e: Event) => {
const value = (e.target as HTMLInputElement).value
emit('change', value)
if (modelModifiers.value.lazy) {
updateValue(value)
}
if (modelModifiers.value.trim) {
(e.target as HTMLInputElement).value = value.trim()
}
}
const onBlur = (e: Event) => {
emit('blur', e)
}
watch(() => props.modelValue, () => {
autoResize()
})
onMounted(() => {
if (props.autofocus) {
setTimeout(() => {
textarea.value?.focus()
}, props.autofocusDelay)
}
autoResize()
})
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
textarea,
baseClass,
onInput,
onChange,
onBlur,
}
},
})
</script>
<template>
<div :class="ui.wrapper">
<textarea
ref="textarea"
:class="baseClass"
:rows="rows"
:placeholder="placeholder"
:disabled="disabled"
:value="modelValue"
v-bind="attrs"
@input="onInput"
@change="onChange"
@blur="onBlur"
/>
</div>
</template>

View File

@ -0,0 +1,100 @@
<script lang="ts">
import { computed, defineComponent, toRef, type PropType } from 'vue'
import { twJoin, twMerge } from 'tailwind-merge'
import { toggle } from '../../themes'
import type { DeepPartial, Strategy, ToggleColor, ToggleSize } from '../../types'
import { useRayUI } from '#build/imports'
const config = toggle
export default defineComponent({
props: {
modelValue: {
type: Boolean as PropType<boolean | null>,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
rounded: {
type: Boolean,
default: false,
},
size: {
type: String as PropType<ToggleSize>,
default: config.default.size,
},
color: {
type: String as PropType<ToggleColor>,
default: config.default.color,
},
class: {
type: String,
default: '',
},
ui: {
type: Object as PropType<DeepPartial<typeof config> & { strategy?: Strategy }>,
default: () => ({}),
},
},
emits: [
'update:modelValue',
'change',
],
setup(props, { emit }) {
const { ui, attrs } = useRayUI('toggle', toRef(props, 'ui'), config)
const checked = computed({
get: () => props.modelValue,
set: (value: boolean) => {
emit('update:modelValue', value)
emit('change', value)
},
})
const toggleClass = computed(() => {
return twMerge(twJoin(
ui.value.base,
props.rounded ? 'rounded-full' : ui.value.rounded,
ui.value.size[props.size],
ui.value.ring.replaceAll('{color}', props.color),
checked.value ? ui.value.active.replaceAll('{color}', props.color) : ui.value.inactive,
), props.class)
})
const bulletClass = computed(() => {
return twJoin(
ui.value.bullet.base,
props.rounded ? 'rounded-full' : ui.value.bullet.rounded,
ui.value.bullet.shadow,
ui.value.bullet.size[props.size],
!props.disabled && ui.value.bullet.translate[props.size],
checked.value ? ui.value.bullet.active[props.size] : ui.value.bullet.inactive,
)
})
const handleClick = () => {
if (!props.disabled) {
checked.value = !checked.value
}
}
return {
// eslint-disable-next-line vue/no-dupe-keys
ui,
attrs,
checked,
toggleClass,
bulletClass,
handleClick,
}
},
})
</script>
<template>
<button :class="toggleClass" :disabled="disabled" v-bind="attrs" @click="handleClick">
<span :class="bulletClass" />
</button>
</template>

View File

@ -0,0 +1,21 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-6.489 5.8a1 1 0 0 0-1.218 1.567L10.585 12l-1.292 1.293l-.083.094a1 1 0 0 0 1.497 1.32L12 13.415l1.293 1.292l.094.083a1 1 0 0 0 1.32-1.497L13.415 12l1.292-1.293l.083-.094a1 1 0 0 0-1.497-1.32L12 10.585l-1.293-1.292l-.094-.083z"
/>
</g>
</svg>
</template>

View File

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

View File

@ -0,0 +1,21 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-1.293 5.953a1 1 0 0 0-1.32-.083l-.094.083L11 12.585l-1.293-1.292l-.094-.083a1 1 0 0 0-1.403 1.403l.083.094l2 2l.094.083a1 1 0 0 0 1.226 0l.094-.083l4-4l.083-.094a1 1 0 0 0-.083-1.32z"
/>
</g>
</svg>
</template>

View File

@ -0,0 +1,21 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
>
<path d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1-19.995.324L2 12l.004-.28C2.152 6.327 6.57 2 12 2zm.01 13l-.127.007a1 1 0 0 0 0 1.986L12 17l.127-.007a1 1 0 0 0 0-1.986L12.01 15zM12 7a1 1 0 0 0-.993.883L11 8v4l.007.117a1 1 0 0 0 1.986 0L13 12V8l-.007-.117A1 1 0 0 0 12 7z"
/>
</g>
</svg>
</template>

View File

@ -0,0 +1,26 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/>
<path
fill="currentColor"
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"
>
<animateTransform
attributeName="transform"
dur="0.75s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"
/>
</path>
</svg>
</template>

Some files were not shown because too many files have changed in this diff Show More