├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── test ├── utils │ ├── index.ts │ └── app-config.ts └── components │ ├── base │ ├── BaseText.spec.ts │ ├── BaseHeading.spec.ts │ ├── BaseProgressCircle.spec.ts │ ├── BaseAvatar.spec.ts │ └── BaseCard.spec.ts │ └── form │ └── BaseSelect.spec.ts ├── .playground ├── tsconfig.json ├── pages │ ├── index.vue │ └── tests │ │ ├── base │ │ ├── theme-toggle.vue │ │ ├── focus-loop.vue │ │ ├── placeholder.vue │ │ ├── tooltip.vue │ │ ├── placeload.vue │ │ ├── avatar-group.vue │ │ ├── pagination.vue │ │ ├── accordion.vue │ │ ├── message.vue │ │ ├── breadcrumb.vue │ │ ├── list.vue │ │ ├── snack.vue │ │ └── button-close.vue │ │ ├── authority │ │ ├── icons.vue │ │ └── typography.vue │ │ ├── form │ │ ├── input-file.vue │ │ └── switch.vue │ │ └── tests │ │ └── listbox.vue ├── tailwind.config.ts ├── app.config.ts ├── app.vue ├── package.json ├── components │ ├── NuiPreviewContainer.vue │ ├── NuiPreview.vue │ ├── NuiLogo.vue │ └── NuiLogoText.vue ├── nuxt.config.ts ├── layouts │ └── default.vue └── pnpm-lock.yaml ├── tsconfig.json ├── .nuxtrc ├── commitlint.config.cjs ├── components ├── base │ ├── BaseDropdownDivider.vue │ ├── BasePlaceload.vue │ ├── BaseButtonGroup.vue │ ├── BaseProse.vue │ ├── BaseList.vue │ ├── BaseLink.vue │ ├── BaseListItem.vue │ ├── BaseThemeSwitch.vue │ ├── BaseKbd.vue │ ├── BaseThemeToggle.vue │ ├── BaseCard.vue │ ├── BaseButtonClose.vue │ ├── BaseText.vue │ ├── BaseHeading.vue │ ├── BaseParagraph.vue │ ├── BaseTag.vue │ ├── BasePlaceholderPage.vue │ ├── BaseButtonAction.vue │ ├── BaseAvatarGroup.vue │ ├── BaseSnack.vue │ ├── BaseIconBox.vue │ ├── BaseButtonIcon.vue │ ├── BaseProgress.vue │ ├── BaseTabs.vue │ ├── BaseProgressCircle.vue │ ├── BaseDropdownItem.vue │ ├── BaseTabSlider.vue │ └── BaseMessage.vue ├── icon │ ├── IconCheckCircle.vue │ ├── IconMoon.vue │ ├── IconMinus.vue │ ├── IconChevronDown.vue │ ├── IconIndeterminate.vue │ ├── IconPlus.vue │ ├── IconClose.vue │ ├── IconCheck.vue │ └── IconSun.vue └── form │ ├── BaseInputHelpText.vue │ ├── BaseRadioHeadless.vue │ ├── BaseCheckboxHeadless.vue │ ├── BaseListboxItem.vue │ ├── BaseTreeSelectItem.vue │ ├── BaseSwitchThin.vue │ ├── BaseSwitchBall.vue │ ├── BaseRadio.vue │ ├── BaseFullscreenDropfile.vue │ └── BaseInputFileHeadless.vue ├── .npmrc ├── tailwind.config.ts ├── vitest.config.ts ├── .editorconfig ├── codecov.yml ├── eslint.config.js ├── .versionrc.json ├── composables ├── default-property.ts ├── window-scroll.ts ├── input-id.ts ├── file-preview.ts ├── mark.ts ├── buttons.ts └── scrollspy.ts ├── .gitignore ├── nuxt.config.ts ├── LICENSE.md ├── scripts └── copy-meta.ts ├── package.json ├── README.md └── CONTRIBUTING.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: cssninjaStudio -------------------------------------------------------------------------------- /test/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app-config' 2 | -------------------------------------------------------------------------------- /.playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.playground/.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | typescript.includeWorkspace=true 2 | typescript.shim=false 3 | typescript.strict=true -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /components/base/BaseDropdownDivider.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | # shamefully-hoist=true 3 | use-lockfile-v6=true 4 | resolution-mode=highest 5 | -------------------------------------------------------------------------------- /components/base/BasePlaceload.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/base/BaseButtonGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { withShurikenUI } from '@shuriken-ui/tailwind' 2 | 3 | export default withShurikenUI({ content: [] }) 4 | -------------------------------------------------------------------------------- /.playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineVitestConfig } from '@nuxt/test-utils/config' 2 | 3 | export default defineVitestConfig({ 4 | test: { 5 | environment: 'nuxt', 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /.playground/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { withShurikenUI } from '@shuriken-ui/tailwind' 2 | 3 | export default withShurikenUI({ 4 | content: [], 5 | theme: { 6 | extend: { 7 | shurikenUi: {}, 8 | }, 9 | }, 10 | }) 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /components/icon/IconCheckCircle.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /components/icon/IconMoon.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /components/icon/IconMinus.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/icon/IconChevronDown.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/icon/IconIndeterminate.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/icon/IconPlus.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: " diff, flags, files" 3 | behavior: default 4 | require_changes: false 5 | require_base: false 6 | require_head: true 7 | hide_project_coverage: false 8 | coverage: 9 | status: 10 | project: 11 | default: 12 | target: auto 13 | threshold: 0% 14 | base: auto -------------------------------------------------------------------------------- /components/icon/IconClose.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /.playground/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | nuxtIcon: {}, 3 | nui: { 4 | BaseButton: { 5 | variant: 'solid', 6 | rounded: 'sm', 7 | color: 'default', 8 | size: 'md', 9 | }, 10 | BaseMessage: { 11 | color: 'default', 12 | rounded: 'sm', 13 | }, 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /test/utils/app-config.ts: -------------------------------------------------------------------------------- 1 | import defaultConfig from '../../app.config' 2 | 3 | export function resetNuiAppConfig(name: string) { 4 | if (!(name in defaultConfig.nui)) { 5 | throw new Error(`resetNuiConfig: Unknown configuration with key "${name}"`) 6 | } 7 | 8 | const nui = useAppConfig().nui as any 9 | nui[name] = Object.assign({}, (defaultConfig.nui as any)[name]) 10 | } 11 | -------------------------------------------------------------------------------- /components/icon/IconCheck.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | { 5 | ignores: [ 6 | 'node_modules', 7 | 'dist', 8 | '.component-meta', 9 | '.vscode', 10 | '.github', 11 | '**/.nuxt/**', 12 | '**/.output/**', 13 | 'CHANGELOG.md', 14 | 'README.md', 15 | 'LICENSE.md', 16 | ], 17 | }, 18 | ) 19 | -------------------------------------------------------------------------------- /components/icon/IconSun.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | { "type": "feat", "section": "🚀 Features" }, 4 | { "type": "fix", "section": "🩹 Bug Fixes" }, 5 | { "type": "docs", "section": "📖 Documentation" }, 6 | { "type": "perf", "section": "🔥 Performance" }, 7 | { "type": "refactor", "section": "💅 Refactors" }, 8 | { "type": "test", "section": "⚙️ Tests" }, 9 | { "type": "ci", "hidden": true }, 10 | { "type": "chore", "hidden": true } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /composables/default-property.ts: -------------------------------------------------------------------------------- 1 | import type { AppConfig } from 'nuxt/schema' 2 | 3 | export function useNuiDefaultProperty< 4 | T extends Record, 5 | C extends keyof AppConfig['nui'], 6 | K extends keyof T, 7 | >(properties: T, component: C, property: K): Ref> { 8 | const config = useAppConfig().nui 9 | return computed(() => { 10 | return (properties?.[property] 11 | ?? config?.[component]?.[property]) as NonNullable 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /.playground/app.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /test/components/base/BaseText.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseText } from '#components' 2 | import { mount } from '@vue/test-utils' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | describe('component: BaseText', () => { 6 | describe('usage', () => { 7 | it('should show default slot', async () => { 8 | const component = mount(BaseText, { 9 | slots: { 10 | default: () => 'Default Slot', 11 | }, 12 | }) 13 | expect(component.text()).toMatchInlineSnapshot( 14 | '"Default Slot"', 15 | ) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /test/components/base/BaseHeading.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseHeading } from '#components' 2 | import { mount } from '@vue/test-utils' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | describe('component: BaseHeading', () => { 6 | describe('usage', () => { 7 | it('should show default slot', async () => { 8 | const component = mount(BaseHeading, { 9 | slots: { 10 | default: () => 'Default Slot', 11 | }, 12 | }) 13 | expect(component.text()).toMatchInlineSnapshot( 14 | '"Default Slot"', 15 | ) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /.playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "private": true, 5 | "devDependencies": { 6 | "@iconify-json/cib": "^1.2.1", 7 | "@iconify-json/clarity": "^1.2.1", 8 | "@iconify-json/fa": "^1.2.0", 9 | "@iconify-json/heroicons": "^1.2.1", 10 | "@iconify-json/ic": "^1.2.1", 11 | "@iconify-json/ion": "^1.2.1", 12 | "@iconify-json/lucide": "^1.2.11", 13 | "@iconify-json/mdi-light": "^1.2.1", 14 | "@iconify-json/ph": "^1.2.1", 15 | "@iconify-json/system-uicons": "^1.2.1", 16 | "@iconify-json/tabler": "^1.2.7" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.playground/components/NuiPreviewContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /composables/window-scroll.ts: -------------------------------------------------------------------------------- 1 | import { useEventListener } from '@vueuse/core' 2 | 3 | export function useNinjaWindowScroll() { 4 | const x = ref(0) 5 | const y = ref(0) 6 | 7 | if (import.meta.browser) { 8 | useEventListener( 9 | window, 10 | 'scroll', 11 | () => { 12 | x.value = window.scrollX 13 | y.value = window.scrollY 14 | }, 15 | { 16 | capture: false, 17 | passive: true, 18 | }, 19 | ) 20 | onMounted(() => { 21 | x.value = window.scrollX 22 | y.value = window.scrollY 23 | }) 24 | } 25 | 26 | return { x, y } 27 | } 28 | -------------------------------------------------------------------------------- /composables/input-id.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from 'vue' 2 | 3 | export function useNinjaId(id?: MaybeRefOrGetter) { 4 | const internal = ref(toValue(id)) 5 | 6 | watch( 7 | () => toValue(id), 8 | (value) => { 9 | internal.value 10 | = value || `nui-input-${Math.random().toString(36).slice(2)}` 11 | }, 12 | ) 13 | 14 | // only generate identifier on client to avoid hydration issues 15 | onMounted(() => { 16 | if (!internal.value) { 17 | internal.value = `nui-input-${Math.random().toString(36).slice(2)}` 18 | } 19 | }) 20 | 21 | return readonly(internal) 22 | } 23 | -------------------------------------------------------------------------------- /test/components/base/BaseProgressCircle.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseProgressCircle } from '#components' 2 | import { mount } from '@vue/test-utils' 3 | import { describe, expect, it } from 'vitest' 4 | 5 | describe('component: BaseProgressCircle', () => { 6 | describe('usage', () => { 7 | it('should show default slot', async () => { 8 | const component = mount(BaseProgressCircle, { 9 | props: { 10 | value: 12, 11 | max: 42, 12 | } 13 | }) 14 | const svg = component.get('svg') 15 | expect(svg.attributes('aria-valuenow')).toBe('12') 16 | expect(svg.attributes('aria-valuemax')).toBe('42') 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/theme-toggle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /components/base/BaseProse.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | .eslintcache 12 | 13 | # Yarn 14 | **/.yarn/cache 15 | **/.yarn/*state* 16 | 17 | # Generated dirs 18 | dist 19 | 20 | # Nuxt 21 | .nuxt 22 | .output 23 | .vercel_build_output 24 | .build-* 25 | .env 26 | .netlify 27 | 28 | # Env 29 | .env 30 | 31 | # Testing 32 | reports 33 | coverage 34 | *.lcov 35 | .nyc_output 36 | 37 | # VSCode 38 | .vscode 39 | 40 | # Intellij idea 41 | *.iml 42 | .idea 43 | 44 | # OSX 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | 54 | component-meta.mjs 55 | component-meta.cjs 56 | component-meta.d.ts -------------------------------------------------------------------------------- /.playground/components/NuiPreview.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { createResolver } from '@nuxt/kit' 2 | 3 | const { resolve } = createResolver(import.meta.url) 4 | 5 | export default defineNuxtConfig({ 6 | modules: [ 7 | '@vueuse/nuxt', 8 | '@nuxtjs/color-mode', 9 | '@nuxtjs/tailwindcss', 10 | '@nuxt/icon', 11 | ], 12 | 13 | colorMode: { 14 | classSuffix: '', 15 | }, 16 | 17 | components: [ 18 | { 19 | prefix: '', 20 | path: resolve('./components/base'), 21 | global: false, 22 | }, 23 | { 24 | prefix: '', 25 | path: resolve('./components/icon'), 26 | global: false, 27 | }, 28 | { 29 | prefix: '', 30 | path: resolve('./components/form'), 31 | global: false, 32 | }, 33 | ], 34 | 35 | devtools: { 36 | enabled: false, 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests and upload coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Run tests and collect coverage 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - run: corepack enable 16 | 17 | - name: Set up Node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: pnpm 22 | 23 | - name: Install dependencies 24 | run: pnpm install 25 | 26 | - name: Run tests 27 | run: pnpm vitest run --coverage 28 | 29 | - name: Upload results to Codecov 30 | uses: codecov/codecov-action@v4 31 | with: 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | -------------------------------------------------------------------------------- /composables/file-preview.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from 'vue' 2 | 3 | const previewMap = new WeakMap>() 4 | 5 | export function useNinjaFilePreview( 6 | _file: MaybeRefOrGetter, 7 | ) { 8 | const fileReference = toRef(_file) 9 | 10 | const preview = computed(() => { 11 | const file = fileReference.value 12 | if (!file) 13 | return '' 14 | if (previewMap.has(file)) 15 | return previewMap.get(file)?.value 16 | 17 | const reader = new FileReader() 18 | const source = ref('') 19 | 20 | const listener = () => { 21 | source.value = reader.result?.toString() ?? '' 22 | reader.removeEventListener('load', listener) 23 | } 24 | reader.addEventListener('load', listener) 25 | reader.readAsDataURL(file) 26 | previewMap.set(file, source) 27 | 28 | return previewMap.get(file)?.value 29 | }) 30 | 31 | return preview 32 | } 33 | -------------------------------------------------------------------------------- /composables/mark.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from 'vue' 2 | 3 | import { escapeHtml } from '@vue/shared' 4 | 5 | function escapeRegExp(literal: string): string { 6 | return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') 7 | } 8 | 9 | export function useNinjaMark( 10 | _text?: MaybeRefOrGetter, 11 | _search?: MaybeRefOrGetter, 12 | _classes?: MaybeRefOrGetter, 13 | ) { 14 | const text = toRef(_text) 15 | const classes = toRef(_classes) 16 | const search = toRef(_search) 17 | 18 | return computed(() => { 19 | const txt = unref(text) 20 | const srch = unref(search) 21 | if (!txt) { 22 | return '' 23 | } 24 | 25 | if (!srch) { 26 | return escapeHtml(txt) 27 | } 28 | 29 | const regex = new RegExp(escapeRegExp(srch), 'gi') 30 | 31 | return txt.replace(regex, (part) => { 32 | return `${escapeHtml(part)}` 33 | }) 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /.playground/pages/tests/authority/icons.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 31 | -------------------------------------------------------------------------------- /components/base/BaseList.vue: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Css Ninja 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /components/base/BaseLink.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/focus-loop.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 39 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/placeholder.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | -------------------------------------------------------------------------------- /components/base/BaseListItem.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 54 | -------------------------------------------------------------------------------- /.playground/components/NuiLogo.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /test/components/base/BaseAvatar.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseAvatar } from '#components' 2 | import { mount } from '@vue/test-utils' 3 | import { beforeEach, describe, expect, it } from 'vitest' 4 | import { resetNuiAppConfig } from '../../utils' 5 | 6 | describe('component: BaseAvatar', () => { 7 | describe('rendering', () => { 8 | beforeEach(() => resetNuiAppConfig('BaseAvatar')) 9 | 10 | it('should render with custom app.config', async () => { 11 | // useAppConfig().nui.BaseAvatar!.color = 'primary' 12 | useAppConfig().nui.BaseAvatar!.size = 'xl' 13 | useAppConfig().nui.BaseAvatar!.rounded = 'lg' 14 | 15 | const component = mount(BaseAvatar) 16 | 17 | const wrapper = component.get('.nui-avatar') 18 | // expect(wrapper.classes('nui-avatar-muted')).toBeTruthy() 19 | expect(wrapper.classes('nui-avatar-xl')).toBeTruthy() 20 | expect(wrapper.classes('nui-avatar-rounded-lg')).toBeTruthy() 21 | }) 22 | 23 | it('should render with default app.config', async () => { 24 | const component = mount(BaseAvatar) 25 | 26 | const wrapper = component.get('.nui-avatar') 27 | // expect(wrapper.classes('nui-avatar-muted')).toBeTruthy() 28 | expect(wrapper.classes('nui-avatar-sm')).toBeTruthy() 29 | expect(wrapper.classes('nui-avatar-rounded-full')).toBeTruthy() 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /components/form/BaseInputHelpText.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 53 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/tooltip.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 47 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/placeload.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 47 | -------------------------------------------------------------------------------- /.playground/pages/tests/form/input-file.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 58 | -------------------------------------------------------------------------------- /.playground/pages/tests/tests/listbox.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 58 | -------------------------------------------------------------------------------- /components/base/BaseThemeSwitch.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 66 | 67 | 73 | -------------------------------------------------------------------------------- /scripts/copy-meta.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'node:fs' 2 | import { copyFile, writeFile } from 'node:fs/promises' 3 | import process from 'node:process' 4 | import { fileURLToPath } from 'node:url' 5 | import { dirname, join } from 'pathe' 6 | 7 | const unusedKeys = new Set([ 8 | 'declarations', 9 | 'fullPath', 10 | 'shortPath', 11 | 'filePath', 12 | 'schema', 13 | 'priority', 14 | 'preload', 15 | 'prefetch', 16 | 'global', 17 | 'export', 18 | 'mode', 19 | ]) 20 | 21 | function filterKeys(key: string, value: unknown) { 22 | if (unusedKeys.has(key)) { 23 | return undefined 24 | } 25 | 26 | return value 27 | } 28 | 29 | async function main() { 30 | const directory = dirname(fileURLToPath(import.meta.url)) 31 | 32 | const inputSource = join(directory, '../.playground/.nuxt/component-meta.mjs') 33 | const outImport = join(directory, '../component-meta.mjs') 34 | const outRequire = join(directory, '../component-meta.cjs') 35 | 36 | const inputDts = join(directory, '../.playground/.nuxt/component-meta.d.ts') 37 | const outDts = join(directory, '../component-meta.d.ts') 38 | 39 | if (!existsSync(inputSource) || !existsSync(inputDts)) { 40 | console.error( 41 | 'component-meta.mjs file does not exists in playground, run "npx nuxt build .playground" to fix', 42 | ) 43 | 44 | process.exit(1) 45 | } 46 | 47 | const components = await import(inputSource).then(m => m.default || m) 48 | 49 | await Promise.all([ 50 | copyFile(inputDts, outDts), 51 | writeFile( 52 | outImport, 53 | `export default ${JSON.stringify(components, filterKeys, 2)}`, 54 | ), 55 | writeFile( 56 | outRequire, 57 | `module.exports = ${JSON.stringify(components, filterKeys, 2)}`, 58 | ), 59 | ]) 60 | } 61 | 62 | main().catch((error) => { 63 | console.error(error) 64 | 65 | process.exit(1) 66 | }) 67 | -------------------------------------------------------------------------------- /components/form/BaseRadioHeadless.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 82 | -------------------------------------------------------------------------------- /test/components/base/BaseCard.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseCard } from '#components' 2 | import { mount } from '@vue/test-utils' 3 | import { beforeEach, describe, expect, it } from 'vitest' 4 | import { resetNuiAppConfig } from '../../utils' 5 | 6 | describe('component: BaseCard', () => { 7 | describe('usage', () => { 8 | it('should show default slot', async () => { 9 | const component = mount(BaseCard, { 10 | slots: { 11 | default: () => 'Default Slot', 12 | }, 13 | }) 14 | expect(component.text()).toMatchInlineSnapshot( 15 | '"Default Slot"', 16 | ) 17 | }) 18 | }) 19 | describe('rendering', () => { 20 | beforeEach(() => { 21 | resetNuiAppConfig('BaseCard') 22 | }) 23 | 24 | it('should render with custom app.config', async () => { 25 | useAppConfig().nui.BaseCard!.color = 'primary' 26 | useAppConfig().nui.BaseCard!.rounded = 'lg' 27 | 28 | const component = mount(BaseCard) 29 | 30 | const wrapper = component.get('.nui-card') 31 | expect(wrapper.classes('nui-card-primary')).toBeTruthy() 32 | expect(wrapper.classes('nui-card-rounded-lg')).toBeTruthy() 33 | }) 34 | 35 | it('should render with default app.config', async () => { 36 | const component = mount(BaseCard) 37 | 38 | const wrapper = component.get('.nui-card') 39 | expect(wrapper.classes('nui-card-default')).toBeTruthy() 40 | expect(wrapper.classes('nui-card-rounded-sm')).toBeTruthy() 41 | }) 42 | 43 | it('should render with props', async () => { 44 | const component = mount(BaseCard, { 45 | props: { 46 | color: 'success', 47 | rounded: 'md', 48 | }, 49 | }) 50 | 51 | const wrapper = component.get('.nui-card') 52 | expect(wrapper.classes('nui-card-success')).toBeTruthy() 53 | expect(wrapper.classes('nui-card-rounded-md')).toBeTruthy() 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /composables/buttons.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationRaw } from 'vue-router' 2 | 3 | export interface BaseButtonProperties { 4 | type?: 'button' | 'submit' | 'reset' 5 | to?: RouteLocationRaw 6 | href?: string 7 | disabled?: boolean 8 | rel?: string 9 | target?: string 10 | } 11 | 12 | export function useNinjaButton(properties: BaseButtonProperties, { 13 | // @todo: make this configurable (design tokens) 14 | externalDefaultRelationship = 'noopener noreferrer', 15 | externalDefaultTarget = '_blank', 16 | } = {}) { 17 | const NuxtLink = defineNuxtLink({}) 18 | 19 | const is = computed(() => 20 | properties.to ? NuxtLink : properties.href ? 'a' : 'button', 21 | ) 22 | const type = computed(() => { 23 | if (is.value === 'button') { 24 | return properties.type || 'button' 25 | } 26 | }) 27 | const external = computed(() => { 28 | if (typeof properties.to === 'string' && properties.to.startsWith('http')) { 29 | return true 30 | } 31 | else if ( 32 | typeof properties.to === 'object' 33 | && 'path' in properties.to 34 | && properties.to.path?.startsWith('http') 35 | ) { 36 | return true 37 | } 38 | 39 | return false 40 | }) 41 | const relationship = computed(() => { 42 | if (!external.value) { 43 | return properties.rel 44 | } 45 | 46 | return properties.rel ?? externalDefaultRelationship 47 | }) 48 | const target = computed(() => { 49 | if (!external.value) { 50 | return properties.target 51 | } 52 | 53 | return properties.target ?? externalDefaultTarget 54 | }) 55 | 56 | const attributes = computed(() => ({ 57 | to: properties.disabled ? undefined : properties.to, 58 | href: properties.disabled ? undefined : properties.href, 59 | disabled: properties.disabled, 60 | type: type.value, 61 | rel: relationship.value, 62 | target: target.value, 63 | })) 64 | 65 | return { 66 | attributes, 67 | is, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /components/base/BaseKbd.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 84 | -------------------------------------------------------------------------------- /components/base/BaseThemeToggle.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 74 | 75 | 81 | -------------------------------------------------------------------------------- /components/base/BaseCard.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 86 | -------------------------------------------------------------------------------- /components/form/BaseCheckboxHeadless.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 89 | -------------------------------------------------------------------------------- /components/form/BaseListboxItem.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 87 | -------------------------------------------------------------------------------- /components/base/BaseButtonClose.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 91 | -------------------------------------------------------------------------------- /components/base/BaseText.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 96 | -------------------------------------------------------------------------------- /.playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable ts/ban-ts-comment */ 2 | 3 | import { fileURLToPath } from 'node:url' 4 | import { addTemplate } from '@nuxt/kit' 5 | import { dirname, join } from 'pathe' 6 | 7 | export default defineNuxtConfig({ 8 | compatibilityDate: '2024-11-02', 9 | extends: '..', 10 | modules: ['nuxt-component-meta', 'unplugin-fonts/nuxt'], 11 | unfonts: { 12 | google: { 13 | families: ['Roboto Flex', 'Inter', 'Karla'], 14 | }, 15 | }, 16 | sourcemap: true, 17 | tailwindcss: { 18 | // config: { 19 | // content: [], 20 | // theme: { 21 | // fontFamily: { 22 | // heading: ['Inter', 'sans-serif'], 23 | // sans: ['Inter', 'sans-serif'], 24 | // alt: ['Karla', 'sans-serif'], 25 | // mono: [ 26 | // 'ui-monospace', 27 | // 'SFMono-Regular', 28 | // 'Menlo', 29 | // 'Monaco', 30 | // 'Consolas', 31 | // '"Liberation Mono"', 32 | // '"Courier New"', 33 | // 'monospace', 34 | // ], 35 | // }, 36 | // extend: { 37 | // colors: { 38 | // primary: colors?.violet, 39 | // muted: colors?.stone, 40 | // }, 41 | // }, 42 | // }, 43 | // }, 44 | }, 45 | componentMeta: { 46 | globalsOnly: false, 47 | debug: 2, 48 | exclude: [ 49 | (component: any) => { 50 | const componentsPath = join( 51 | dirname(fileURLToPath(import.meta.url)), 52 | '../components', 53 | ) 54 | const isExternal = !component.filePath?.startsWith?.(componentsPath) 55 | const isIcon = component?.kebabName?.startsWith('icon-') 56 | 57 | return isExternal || isIcon 58 | }, 59 | ], 60 | checkerOptions: { 61 | schema: { 62 | ignore: ['KeyboardEvent'], 63 | }, 64 | }, 65 | }, 66 | hooks: { 67 | // @ts-ignore 68 | 'tailwindcss:resolvedConfig': function (config) { 69 | addTemplate({ 70 | filename: 'tailwind.config.ts', // gets prepended by .nuxt/ 71 | getContents: () => `export default ${JSON.stringify(config, null, 2)}`, 72 | write: true, 73 | }) 74 | }, 75 | }, 76 | vite: { 77 | optimizeDeps: { 78 | include: ['@headlessui/vue', '@headlessui-float/vue'], 79 | }, 80 | }, 81 | }) 82 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/avatar-group.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 71 | -------------------------------------------------------------------------------- /composables/scrollspy.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeRefOrGetter } from 'vue' 2 | 3 | /** 4 | * Scrollspy allows you to watch visible headings in a specific page. 5 | * Useful for table of contents live style updates. 6 | */ 7 | export function useNinjaScrollspy(_options?: MaybeRefOrGetter, _selectors?: MaybeRefOrGetter) { 8 | const options = toRef(_options) 9 | const selectors = toRef(_selectors) 10 | 11 | const observer = shallowRef() 12 | const intersectingIds = shallowRef([]) 13 | const activeIds = shallowRef([]) 14 | const route = useRoute() 15 | let timer: any 16 | 17 | const observerCallback = (entries: IntersectionObserverEntry[]) => { 18 | for (const entry of entries) { 19 | const id = entry.target.id 20 | 21 | if (entry.isIntersecting) { 22 | intersectingIds.value.push(id) 23 | } 24 | else { 25 | intersectingIds.value = intersectingIds.value.filter(t => t !== id) 26 | } 27 | } 28 | } 29 | 30 | const updateElements = (elements: Element[]) => { 31 | observer.value?.disconnect() 32 | for (const element of elements) { 33 | observer.value?.observe(element) 34 | } 35 | } 36 | 37 | watch(intersectingIds, (value, oldValue) => { 38 | activeIds.value = value.length === 0 ? oldValue : value 39 | }) 40 | 41 | // Create intersection observer 42 | onBeforeMount(() => { 43 | observer.value = new IntersectionObserver(observerCallback, options.value) 44 | }) 45 | 46 | // Watch for selectors 47 | if (import.meta.browser) { 48 | watch( 49 | [() => route.path, selectors], 50 | () => { 51 | if (selectors.value?.length) { 52 | if (timer) { 53 | clearTimeout(timer) 54 | } 55 | 56 | timer = setTimeout(() => { 57 | if (selectors.value?.length) { 58 | const element = document.querySelectorAll( 59 | selectors.value.join(', '), 60 | ) 61 | updateElements([...element]) 62 | } 63 | }, 300) 64 | } 65 | }, 66 | { 67 | immediate: true, 68 | }, 69 | ) 70 | } 71 | 72 | // Destroy it 73 | onBeforeUnmount(() => { 74 | if (timer) { 75 | clearTimeout(timer) 76 | } 77 | 78 | observer.value?.disconnect() 79 | }) 80 | 81 | return { 82 | visibleIds: intersectingIds, 83 | activeIds, 84 | updateElements, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/pagination.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 91 | -------------------------------------------------------------------------------- /components/base/BaseHeading.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 105 | -------------------------------------------------------------------------------- /components/base/BaseParagraph.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 105 | -------------------------------------------------------------------------------- /components/base/BaseTag.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 119 | -------------------------------------------------------------------------------- /components/base/BasePlaceholderPage.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 113 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/accordion.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 75 | -------------------------------------------------------------------------------- /components/form/BaseTreeSelectItem.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 121 | -------------------------------------------------------------------------------- /components/base/BaseButtonAction.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 121 | -------------------------------------------------------------------------------- /components/base/BaseAvatarGroup.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 120 | -------------------------------------------------------------------------------- /components/base/BaseSnack.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 132 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/message.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 95 | -------------------------------------------------------------------------------- /components/base/BaseIconBox.vue: -------------------------------------------------------------------------------- 1 | 129 | 130 | 135 | -------------------------------------------------------------------------------- /components/form/BaseSwitchThin.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 142 | -------------------------------------------------------------------------------- /components/form/BaseSwitchBall.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 148 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shuriken-ui/nuxt", 3 | "type": "module", 4 | "version": "3.6.2", 5 | "packageManager": "pnpm@9.12.3", 6 | "author": "Css Ninja (https://cssninja.io)", 7 | "license": "MIT", 8 | "homepage": "https://github.com/shuriken-ui/nuxt", 9 | "repository": "shuriken-ui/nuxt", 10 | "bugs": "https://github.com/shuriken-ui/nuxt/issues", 11 | "keywords": [ 12 | "nuxt", 13 | "nuxt3", 14 | "ui", 15 | "framework", 16 | "library", 17 | "components", 18 | "tailwind", 19 | "tailwindcss", 20 | "design-system", 21 | "module" 22 | ], 23 | "exports": { 24 | ".": "./nuxt.config.ts", 25 | "./component-meta": { 26 | "types": "./.component-meta/component-meta.d.ts", 27 | "import": "./.component-meta/component-meta.mjs", 28 | "require": "./.component-meta/component-meta.cjs" 29 | } 30 | }, 31 | "files": [ 32 | ".component-meta", 33 | "app.config.ts", 34 | "assets", 35 | "components", 36 | "composables", 37 | "nuxt.config.ts", 38 | "nuxt.schema.ts" 39 | ], 40 | "scripts": { 41 | "dev": "nuxt dev .playground --open", 42 | "lint": "eslint . --cache", 43 | "lint:fix": "eslint . --cache --fix", 44 | "coverage": "vitest run --coverage", 45 | "test": "run-p test:*", 46 | "test:unit": "vitest test", 47 | "test:tsc": "vue-tsc --noEmit", 48 | "test:lint": "run-s lint:eslint lint:prettier", 49 | "release": "run-s test release:*", 50 | "release:standard-version": "standard-version", 51 | "release:publish": "git push --follow-tags origin main && npm publish", 52 | "prepare": "simple-git-hooks && nuxt prepare .playground", 53 | "prepack": "nuxt-component-meta --no-schema --outputDir='../.component-meta' .playground" 54 | }, 55 | "dependencies": { 56 | "@headlessui-float/vue": "^0.15.0", 57 | "@headlessui/vue": "^1.7.23", 58 | "@iconify/vue": "^4.1.2", 59 | "@nuxt/icon": "^1.6.1", 60 | "@nuxtjs/color-mode": "^3.5.2", 61 | "@nuxtjs/tailwindcss": "^6.12.2", 62 | "@shuriken-ui/tailwind": "^3.1.3", 63 | "@vueuse/nuxt": "^11.2.0", 64 | "autoprefixer": "^10.4.20" 65 | }, 66 | "devDependencies": { 67 | "@antfu/eslint-config": "^3.8.0", 68 | "@commitlint/cli": "^19.5.0", 69 | "@commitlint/config-conventional": "^19.5.0", 70 | "@nuxt/kit": "^3.13.2", 71 | "@nuxt/test-utils": "^3.14.4", 72 | "@types/node": "22.8.6", 73 | "@vitest/coverage-v8": "2.1.4", 74 | "@vitest/ui": "2.1.4", 75 | "@vue/shared": "^3.5.12", 76 | "@vue/test-utils": "^2.4.6", 77 | "@vueuse/core": "^11.2.0", 78 | "commitlint": "^19.5.0", 79 | "eslint": "9.14.0", 80 | "happy-dom": "^15.8.0", 81 | "jiti": "^2.4.0", 82 | "lint-staged": "^15.2.10", 83 | "npm-run-all": "^4.1.5", 84 | "nuxt": "^3.13.2", 85 | "nuxt-component-meta": "^0.9.0", 86 | "pathe": "^1.1.2", 87 | "playwright-core": "^1.48.2", 88 | "simple-git-hooks": "^2.11.0", 89 | "standard-version": "^9.5.0", 90 | "tailwindcss": "^3.4.14", 91 | "typescript": "^5.6.3", 92 | "unplugin-fonts": "^1.1.1", 93 | "vitest": "^2.1.4", 94 | "vue": "^3.5.12", 95 | "vue-router": "^4.4.5", 96 | "vue-tsc": "^2.1.10" 97 | }, 98 | "pnpm": { 99 | "peerDependencyRules": { 100 | "ignoreMissing": [ 101 | "webpack", 102 | "tailwindcss", 103 | "postcss", 104 | "vite", 105 | "vue" 106 | ] 107 | }, 108 | "overrides": { 109 | "vue-component-meta": "^2.1.10" 110 | } 111 | }, 112 | "simple-git-hooks": { 113 | "pre-commit": "pnpm lint-staged", 114 | "commit-msg": "pnpm commitlint -e -V " 115 | }, 116 | "lint-staged": { 117 | "*.vue": [ 118 | "eslint --fix" 119 | ], 120 | "*.{js,json}": [ 121 | "eslint --fix" 122 | ], 123 | "*.ts?(x)": [ 124 | "eslint --fix" 125 | ] 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /components/form/BaseRadio.vue: -------------------------------------------------------------------------------- 1 | 115 | 116 | 154 | -------------------------------------------------------------------------------- /components/base/BaseButtonIcon.vue: -------------------------------------------------------------------------------- 1 | 135 | 136 | 142 | -------------------------------------------------------------------------------- /components/base/BaseProgress.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 152 | -------------------------------------------------------------------------------- /.playground/pages/tests/authority/typography.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 103 | -------------------------------------------------------------------------------- /components/form/BaseFullscreenDropfile.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 133 | -------------------------------------------------------------------------------- /.playground/pages/tests/form/switch.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 132 | -------------------------------------------------------------------------------- /.playground/components/NuiLogoText.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /.playground/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | Shuriken UI logo 6 | 7 |

8 | 9 | 10 |

11 | v3.shurikenui.com | 12 | by cssninja.io 13 |

14 | 15 | > [!IMPORTANT] 16 | > Looking for Shuriken UI v4, with tailwind v4 support? Check https://github.com/shuriken-ui/shuriken-ui 17 | 18 | --- 19 | 20 | ## Shuriken UI - Nuxt 21 | 22 | Shuriken UI is a free and open-source Tailwind CSS UI Kit. It is a collection of components and templates that you can use to build your next Tailwind CSS project. 23 | 24 | This repository contains the Nuxt version (a [layer](https://nuxt.com/docs/getting-started/layers)) of Shuriken UI with ready to use components (form inputs, buttons, cards, etc.) that you can use to build your project. 25 | 26 | ## Installation 27 | 28 | ```bash 29 | pnpm install -D @shuriken-ui/nuxt 30 | ``` 31 | 32 | > **Note**: This also installs the [Shuriken UI Tailwind CSS](https://github.com/shuriken-ui/tailwind) package and required nuxt modules: 33 | > 34 | > - [@nuxtjs/tailwindcss](https://github.com/nuxt-modules/tailwindcss) 35 | > - [@nuxtjs/color-mode](https://github.com/nuxt-modules/color-mode) 36 | > - [@vueuse/nuxt](https://github.com/vueuse/vueuse/tree/main/packages/nuxt) 37 | > - [nuxt-icon](https://github.com/nuxt-modules/icon) 38 | 39 | ## Usage 40 | 41 | 42 | ```ts 43 | // nuxt.config.ts 44 | export default defineNuxtConfig({ 45 | extends: [ 46 | '@shuriken-ui/nuxt' 47 | ] 48 | }) 49 | ``` 50 | 51 | > **Note**: This is a [layer](https://nuxt.com/docs/getting-started/layers) and not a module, so you must extend your nuxt config with it. 52 | 53 | ## Configuration 54 | 55 | ### Nuxt `app.config.ts` 56 | 57 | This is the [app configuration](https://nuxt.com/docs/getting-started/configuration#app-configuration) for Shuriken UI components 58 | 59 | > **Note**: It's not a module configuration, so you must define it in `app.config.ts`, not in `nuxt.config.ts`. 60 | 61 | ```ts 62 | export default defineAppConfig({ 63 | /** 64 | * Shuriken UI layer configuration 65 | */ 66 | nui: { 67 | /** 68 | * Set default properties for BaseButton component 69 | */ 70 | BaseButton: { 71 | variant: 'pastel', 72 | rounded: 'md', 73 | }, 74 | 75 | // ... 76 | }, 77 | }) 78 | ``` 79 | 80 | 81 | ### Tailwind `tailwind.config.ts` 82 | 83 | ```ts 84 | import { withShurikenUI } from '@shuriken-ui/tailwind' 85 | import colors from 'tailwindcss/colors' 86 | 87 | /** 88 | * Shuriken UI tailwind configuration 89 | */ 90 | export default withShurikenUI({ 91 | content: [], 92 | theme: { 93 | /** 94 | * Customize fonts 95 | * 96 | * You must load them yourself 97 | * (ex: with unplugin-fonts) 98 | */ 99 | fontFamily: { 100 | sans: ['Roboto Flex', 'sans-serif'], 101 | heading: ['Inter', 'sans-serif'], 102 | alt: ['Karla', 'sans-serif'], 103 | mono: ['ui-monospace', 'monospace'], 104 | }, 105 | extend: { 106 | /** 107 | * Customize colors 108 | * 109 | * Use tailwind predefined colors, 110 | * or generate your own with tools like https://tailwindshades.com 111 | */ 112 | colors: { 113 | // Define only the ones you want to override 114 | muted: colors.slate, 115 | primary: colors.violet, 116 | info: colors.sky, 117 | success: colors.teal, 118 | warning: colors.amber, 119 | danger: colors.rose, 120 | }, 121 | 122 | /** 123 | * Customize Shuriken UI components 124 | * 125 | * @see https://github.com/shuriken-ui/tailwind 126 | */ 127 | nui: { 128 | // ... 129 | } 130 | }, 131 | }, 132 | }) 133 | ``` 134 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/list.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 121 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/snack.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 120 | -------------------------------------------------------------------------------- /.playground/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@iconify-json/cib': 12 | specifier: ^1.2.1 13 | version: 1.2.1 14 | '@iconify-json/clarity': 15 | specifier: ^1.2.1 16 | version: 1.2.1 17 | '@iconify-json/fa': 18 | specifier: ^1.2.0 19 | version: 1.2.0 20 | '@iconify-json/heroicons': 21 | specifier: ^1.2.1 22 | version: 1.2.1 23 | '@iconify-json/ic': 24 | specifier: ^1.2.1 25 | version: 1.2.1 26 | '@iconify-json/ion': 27 | specifier: ^1.2.1 28 | version: 1.2.1 29 | '@iconify-json/lucide': 30 | specifier: ^1.2.11 31 | version: 1.2.11 32 | '@iconify-json/mdi-light': 33 | specifier: ^1.2.1 34 | version: 1.2.1 35 | '@iconify-json/ph': 36 | specifier: ^1.2.1 37 | version: 1.2.1 38 | '@iconify-json/system-uicons': 39 | specifier: ^1.2.1 40 | version: 1.2.1 41 | '@iconify-json/tabler': 42 | specifier: ^1.2.7 43 | version: 1.2.7 44 | 45 | packages: 46 | 47 | '@iconify-json/cib@1.2.1': 48 | resolution: {integrity: sha512-3GaouBPiNevaodcZ3eJi0kxzZYFQxTkR847SNbW+ezGJSQsdO6hsvZQl0HKfla56zFXLaL4Ne/zybT3EtMjwrA==} 49 | 50 | '@iconify-json/clarity@1.2.1': 51 | resolution: {integrity: sha512-xUjFG3QU/Kqcs2O7/wHAj+xLlfJvzPY0bE5C5AaspwmOnau5R+rcTCAKtDZHUUvhSq+9t7GAJ5bUO5nkzmttrA==} 52 | 53 | '@iconify-json/fa@1.2.0': 54 | resolution: {integrity: sha512-D49fT0bDtnoJIEE2586VDL6Yuti6BBYWA90GIeZabJ6KZ8WXylw+EhQfz6weMDr0HrRDFV/mzwS8gpWADBprMA==} 55 | 56 | '@iconify-json/heroicons@1.2.1': 57 | resolution: {integrity: sha512-TkKfS5U27kE5MXmSGLzPoz95BP5VA9xEJXwJFwmPMVLX+xyWq0OkoiWTUXB0uAoQODpb8BaRpzSydItrq9fIRA==} 58 | 59 | '@iconify-json/ic@1.2.1': 60 | resolution: {integrity: sha512-UjL/bjJP/T5EV881+hTzcfTKVo0KEUjhnMiJcLtPzNgPtU2KZZmRx8BSKKR61H4CN/5FTEbyawGyG0aEt3SwGQ==} 61 | 62 | '@iconify-json/ion@1.2.1': 63 | resolution: {integrity: sha512-33jiBdtP8+uBV0KRz8Z9g54QFujzssiT9W6wdNuY61VQM1s3cvoAX89yiczRs4/6b2xJE5vqqvsEJAQPqfY5JA==} 64 | 65 | '@iconify-json/lucide@1.2.11': 66 | resolution: {integrity: sha512-dqpbV7+g1qqxtZOHCZKwdKhtYYqEUjFhYiOg/+PcADbjtapoL+bwa1Brn12gAHq5r2K7Mf29xRHOTmZ3UHHOrw==} 67 | 68 | '@iconify-json/mdi-light@1.2.1': 69 | resolution: {integrity: sha512-kTs9efbpF5GDcevR9PWCDJQrV1Vfr6RS8KJyGIPM6zxd8AGGAtNCxyACrJ+zeq6K99ZmcTIPr8tlymmPFAxwnw==} 70 | 71 | '@iconify-json/ph@1.2.1': 72 | resolution: {integrity: sha512-x0DNfwWrS18dbsBYOq3XGiZnGz4CgRyC+YSl/TZvMQiKhIUl1woWqUbMYqqfMNUBzjyk7ulvaRovpRsIlqIf8g==} 73 | 74 | '@iconify-json/system-uicons@1.2.1': 75 | resolution: {integrity: sha512-P3CEr7GewtwcAWX/RZeRZw4Gkq+WS+w5iTh+0jDR30A2bEEhm98R1N0Pas3VUJSU/b/3EfsA6cAX18wrjcqwkw==} 76 | 77 | '@iconify-json/tabler@1.2.7': 78 | resolution: {integrity: sha512-q6FbDeC5caOC7i7/dcJOv7PdovHWItd84hCvsnlD/mzsrl5Nhol6eSQOMRv1bIpyxykGEiSDbOsVK5f23j/aFg==} 79 | 80 | '@iconify/types@2.0.0': 81 | resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} 82 | 83 | snapshots: 84 | 85 | '@iconify-json/cib@1.2.1': 86 | dependencies: 87 | '@iconify/types': 2.0.0 88 | 89 | '@iconify-json/clarity@1.2.1': 90 | dependencies: 91 | '@iconify/types': 2.0.0 92 | 93 | '@iconify-json/fa@1.2.0': 94 | dependencies: 95 | '@iconify/types': 2.0.0 96 | 97 | '@iconify-json/heroicons@1.2.1': 98 | dependencies: 99 | '@iconify/types': 2.0.0 100 | 101 | '@iconify-json/ic@1.2.1': 102 | dependencies: 103 | '@iconify/types': 2.0.0 104 | 105 | '@iconify-json/ion@1.2.1': 106 | dependencies: 107 | '@iconify/types': 2.0.0 108 | 109 | '@iconify-json/lucide@1.2.11': 110 | dependencies: 111 | '@iconify/types': 2.0.0 112 | 113 | '@iconify-json/mdi-light@1.2.1': 114 | dependencies: 115 | '@iconify/types': 2.0.0 116 | 117 | '@iconify-json/ph@1.2.1': 118 | dependencies: 119 | '@iconify/types': 2.0.0 120 | 121 | '@iconify-json/system-uicons@1.2.1': 122 | dependencies: 123 | '@iconify/types': 2.0.0 124 | 125 | '@iconify-json/tabler@1.2.7': 126 | dependencies: 127 | '@iconify/types': 2.0.0 128 | 129 | '@iconify/types@2.0.0': {} 130 | -------------------------------------------------------------------------------- /components/base/BaseTabs.vue: -------------------------------------------------------------------------------- 1 | 119 | 120 | 169 | -------------------------------------------------------------------------------- /components/form/BaseInputFileHeadless.vue: -------------------------------------------------------------------------------- 1 | 162 | 163 | 185 | -------------------------------------------------------------------------------- /test/components/form/BaseSelect.spec.ts: -------------------------------------------------------------------------------- 1 | import { BaseSelect } from '#components' 2 | import { mount } from '@vue/test-utils' 3 | import { beforeEach, describe, expect, it } from 'vitest' 4 | import { resetNuiAppConfig } from '../../utils' 5 | 6 | const TestComponent = { 7 | template: ` 8 | 9 | 10 | 11 | 12 | `, 13 | // forward v-model from BaseSelect 14 | props: ['modelValue'], 15 | emits: ['update:modelValue'], 16 | components: { 17 | BaseSelect, 18 | }, 19 | setup(props: any, { emit }: any) { 20 | const modelValue = computed({ 21 | get: () => props.modelValue, 22 | set: value => emit('update:modelValue', value), 23 | }) 24 | return { 25 | modelValue, 26 | } 27 | }, 28 | } 29 | 30 | describe('component: BaseSelect', () => { 31 | describe('usage', () => { 32 | it('should handle v-model', async () => { 33 | const component = mount(TestComponent, { 34 | props: { 35 | 'modelValue': '41', 36 | 'onUpdate:modelValue': (e: any) => component.setProps({ modelValue: e }), 37 | }, 38 | }) 39 | 40 | await component.get('.nui-select').setValue('42') 41 | expect(component.props('modelValue')).toBe('42') 42 | }) 43 | 44 | it('should handle v-model, direct', async () => { 45 | const component = mount(BaseSelect, { 46 | props: { 47 | 'modelValue': '21', 48 | 'onUpdate:modelValue': (e: any) => component.setProps({ modelValue: e }), 49 | }, 50 | slots: { 51 | default: [ 52 | ``, 53 | ``, 54 | ].join(''), 55 | }, 56 | }) 57 | 58 | await component.get('.nui-select').setValue('22') 59 | expect(component.props('modelValue')).toBe('22') 60 | }) 61 | }) 62 | 63 | describe('rendering', () => { 64 | beforeEach(() => { 65 | resetNuiAppConfig('BaseSelect') 66 | }) 67 | 68 | it('should render with custom app.config', async () => { 69 | useAppConfig().nui.BaseSelect!.size = 'sm' 70 | useAppConfig().nui.BaseSelect!.rounded = 'lg' 71 | 72 | const component = mount(BaseSelect) 73 | 74 | const wrapper = component.get('.nui-select-wrapper') 75 | expect(wrapper.classes('nui-select-sm')).toBeTruthy() 76 | expect(wrapper.classes('nui-select-rounded-lg')).toBeTruthy() 77 | }) 78 | 79 | it('should render with default app.config', async () => { 80 | const component = mount(BaseSelect) 81 | 82 | const wrapper = component.get('.nui-select-wrapper') 83 | expect(wrapper.classes('nui-select-md')).toBeTruthy() 84 | expect(wrapper.classes('nui-select-rounded-sm')).toBeTruthy() 85 | }) 86 | 87 | it('should render with props', async () => { 88 | const component = mount(BaseSelect, { 89 | props: { 90 | size: 'lg', 91 | rounded: 'md', 92 | }, 93 | }) 94 | 95 | const wrapper = component.get('.nui-select-wrapper') 96 | expect(wrapper.classes('nui-select-lg')).toBeTruthy() 97 | expect(wrapper.classes('nui-select-rounded-md')).toBeTruthy() 98 | }) 99 | 100 | it('should render error', async () => { 101 | const component = mount(BaseSelect, { 102 | props: { 103 | error: 'test', 104 | }, 105 | }) 106 | 107 | const wrapper = component.get('.nui-select-wrapper') 108 | const help = component.get('.nui-input-help-text') 109 | expect(wrapper.classes('nui-select-error')).toBeTruthy() 110 | expect(help.classes('text-danger-500')).toBeTruthy() 111 | expect(help.text()).toBe('test') 112 | }) 113 | 114 | it('should render disabled state', async () => { 115 | const component = mount(BaseSelect, { 116 | props: { 117 | disabled: true, 118 | }, 119 | }) 120 | 121 | const select = component.get('.nui-select') 122 | expect(select.attributes('disabled')).toBeDefined() 123 | }) 124 | 125 | it('should render with placeholder', async () => { 126 | const component = mount(BaseSelect, { 127 | props: { 128 | placeholder: 'Select an option', 129 | }, 130 | }) 131 | 132 | const option = component.get('option') 133 | expect(option.text()).toBe('Select an option') 134 | expect(option.attributes('disabled')).toBeDefined() 135 | expect(option.attributes('hidden')).toBeDefined() 136 | }) 137 | }) 138 | }) 139 | -------------------------------------------------------------------------------- /components/base/BaseProgressCircle.vue: -------------------------------------------------------------------------------- 1 | 112 | 113 | 150 | 151 | 179 | -------------------------------------------------------------------------------- /components/base/BaseDropdownItem.vue: -------------------------------------------------------------------------------- 1 | 142 | 143 | 183 | -------------------------------------------------------------------------------- /components/base/BaseTabSlider.vue: -------------------------------------------------------------------------------- 1 | 145 | 146 | 183 | -------------------------------------------------------------------------------- /.playground/pages/tests/base/button-close.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 113 | -------------------------------------------------------------------------------- /components/base/BaseMessage.vue: -------------------------------------------------------------------------------- 1 | 163 | 164 | 201 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Shuriken UI - Nuxt 2 | 3 | Thanks for taking the time to contribute! 🎉 4 | 5 | In this guide you will find all the information you need to contribute to the project. 6 | 7 | ## What is Shuriken UI Nuxt? 8 | 9 | Shuriken UI Nuxt is part of the Shuriken UI project by [Css Ninja](https://github.com/cssninjaStudio). It goal is to provide a set of components to build a apps with Nuxt.js using the Shuriken UI design system. 10 | 11 | ```mermaid 12 | graph TD; 13 | A("@shuriken-ui/tailwind")-->B("@shuriken-ui/nuxt"); 14 | A("@shuriken-ui/tailwind")-.->C(...); 15 | A("@shuriken-ui/tailwind")-->D("@shuriken-ui/next"); 16 | ``` 17 | 18 | ## New contributor guide 19 | 20 | Components in Shuriken UI Nuxt are built using Shuriken UI Tailwind which declare some components (prefixed with `nui-`) in tailwind plugins. This allow to use the same components in different frameworks and let tailwind jit to include only used styles. 21 | 22 | If you find a bug or want to add new features, makes sure to check if it can be fixed or added in Shuriken UI Tailwind first. 23 | 24 | Also, note that we use `release/next` branch to develop new features. If you want to contribute, make sure to create a new branch from `release/next` and to submit your pull request to `release/next` branch. (see [Git conventions](#git-conventions)) 25 | 26 | If you have any doubt or questions, feel free to [open a discussion](https://github.com/shuriken-ui/nuxt/discussions). 27 | 28 | ## Setup the project 29 | 30 | We use [pnpm](https://pnpm.io/) to manage our dependencies. Make sure to install it first. 31 | 32 | ```bash 33 | corepack enable 34 | corepack prepare pnpm@latest --activate 35 | ``` 36 | 37 | > **Note** 38 | > Corepack is installed with Node.js from v16.9.x. 39 | > If your version is below, install it with: `npm install -g corepack` 40 | 41 | Then install the dependencies: 42 | 43 | ```bash 44 | pnpm install 45 | ``` 46 | 47 | Now you can start the playground which is a Nuxt.js app using Shuriken UI Nuxt. 48 | 49 | ```bash 50 | pnpm dev 51 | ``` 52 | 53 | Everything is ready, you can start coding! 🎉 54 | 55 | Don't forget to run the tests after your changes to make sure everything is working as expected. 56 | 57 | ```bash 58 | pnpm test 59 | ``` 60 | 61 | ### Link Shuriken UI Tailwind 62 | 63 | If you need to update Shuriken UI Tailwind, you can link it to the playground by editing `nuxt.config.ts` at the root of the project: 64 | 65 | ```diff 66 | import { createResolver } from '@nuxt/kit' 67 | -import { withShurikenUI } from '@shuriken-ui/tailwind' 68 | +import { withShurikenUI } from '../path/to/shuriken-ui/tailwind/src' 69 | 70 | const { resolve } = createResolver(import.meta.url) 71 | 72 | export default defineNuxtConfig({ 73 | ``` 74 | 75 | > **Warning** 76 | > Make sure to remove the link before committing your changes. 77 | 78 | ## Submitting your changes 79 | 80 | Once you are done with your changes, you can submit a pull request, also known as a PR. 81 | 82 | - Make sure the tests pass locally by running `pnpm test`, this will run typescript check and the linter. 83 | - Make sure to create a draft PR if it's not ready to be merged, see [how to change stage of a PR](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request) 84 | - Make sure to add a description of your changes, if relevant, add screenshots or gifs to illustrate your changes. 85 | - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. 86 | - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. Once you submit your PR, a Shuriken UI maintainer will review your proposal. We may ask questions or request additional information. 87 | - We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. 88 | 89 | ## Git conventions 90 | 91 | We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to make our commits more readable and to generate our changelog. 92 | 93 | Make sure to prefix your commit with one of the following types: 94 | `fix:`, `feat:`, `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:` 95 | 96 | Use the imperative, present tense: “change” _not “changed” nor “changes”_. 97 | 98 | ```mermaid 99 | gitGraph 100 | commit 101 | commit tag: "vX.X.X" 102 | branch release/next 103 | commit 104 | branch feat/your-feature 105 | commit id: "feat(BaseButton): what you added" 106 | commit id: "refactor(BaseButton): clean up props" 107 | checkout release/next 108 | commit 109 | merge feat/your-feature 110 | commit tag: "vX.X.X-beta.x" 111 | ``` 112 | --------------------------------------------------------------------------------