mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: build out docker components (#1427)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced comprehensive, customizable Accordion, Dialog, DropdownMenu, and Select UI components with enhanced prop-driven and slot-based APIs. * Added grouped exports for UI primitives, simplifying imports and usage. * Added new Storybook stories demonstrating varied usage scenarios for Accordion, Dialog, DropdownMenu, and Select components. * **Refactor** * Replaced external UI dependencies with locally defined, typed components for Accordion, Dialog, DropdownMenu, and Select. * Streamlined component APIs by consolidating exports to main components and type exports, removing subcomponent exports. * Simplified dialog and dropdown menu implementations with explicit props, events, and slots. * Updated component styles and class bindings for improved appearance and interaction. * Refined select component into a fully featured, typed implementation supporting grouping and multiple selection. * Replaced custom dropdown menu implementation in user profile with the new DropdownMenu component. * Simplified internal prop forwarding using reactive utilities for dropdown menu and select subcomponents. * Improved dropdown menu stories with declarative props and slots, removing manual subcomponent composition. * Simplified notification filter UI by replacing nested select subcomponents with a declarative items prop. * **Bug Fixes** * Improved dropdown and select item handling, including disabled states, separators, and grouped options. * **Style** * Enhanced visual consistency and spacing in documentation and UI components. * Updated component classes for better appearance and usability. * **Chores** * Upgraded `@jsonforms` dependencies across all packages to version `^3.6.0`. * Improved test and mock setups for new component structures. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: mdatelle <mike@datelle.net> Co-authored-by: Eli Bosley <ekbosley@gmail.com>
This commit is contained in:
10
CLAUDE.md
10
CLAUDE.md
@@ -15,6 +15,7 @@ This is the Unraid API monorepo containing multiple packages that provide API fu
|
||||
## Essential Commands
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm install # Install all dependencies
|
||||
pnpm dev # Run all dev servers concurrently
|
||||
@@ -23,6 +24,7 @@ pnpm build:watch # Watch mode with local plugin build
|
||||
```
|
||||
|
||||
### Testing & Code Quality
|
||||
|
||||
```bash
|
||||
pnpm test # Run all tests
|
||||
pnpm lint # Run linting
|
||||
@@ -31,6 +33,7 @@ pnpm type-check # TypeScript type checking
|
||||
```
|
||||
|
||||
### API Development
|
||||
|
||||
```bash
|
||||
cd api && pnpm dev # Run API server (http://localhost:3001)
|
||||
cd api && pnpm test:watch # Run tests in watch mode
|
||||
@@ -38,6 +41,7 @@ cd api && pnpm codegen # Generate GraphQL types
|
||||
```
|
||||
|
||||
### Deployment
|
||||
|
||||
```bash
|
||||
pnpm unraid:deploy <SERVER_IP> # Deploy all to Unraid server
|
||||
```
|
||||
@@ -45,6 +49,7 @@ pnpm unraid:deploy <SERVER_IP> # Deploy all to Unraid server
|
||||
## Architecture Notes
|
||||
|
||||
### API Structure (NestJS)
|
||||
|
||||
- Modules: `auth`, `config`, `plugins`, `emhttp`, `monitoring`
|
||||
- GraphQL API with Apollo Server at `/graphql`
|
||||
- Redux store for state management in `src/store/`
|
||||
@@ -52,26 +57,31 @@ pnpm unraid:deploy <SERVER_IP> # Deploy all to Unraid server
|
||||
- Entry points: `src/index.ts` (server), `src/cli.ts` (CLI)
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- TypeScript imports use `.js` extensions (ESM compatibility)
|
||||
- NestJS dependency injection with decorators
|
||||
- GraphQL schema-first approach with code generation
|
||||
- API plugins follow specific structure (see `api/docs/developer/api-plugins.md`)
|
||||
|
||||
### Authentication
|
||||
|
||||
- API key authentication via headers
|
||||
- Cookie-based session management
|
||||
- Keys stored in `/boot/config/plugins/unraid-api/`
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. Work Intent required before starting development
|
||||
2. Fork from `main` branch
|
||||
3. Reference Work Intent in PR
|
||||
4. No direct pushes to main
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
LOG_LEVEL=debug unraid-api start --debug
|
||||
```
|
||||
|
||||
Enables GraphQL playground at `http://tower.local/graphql`
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
174
api/package.json
174
api/package.json
@@ -52,29 +52,29 @@
|
||||
"unraid-api": "dist/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.11.8",
|
||||
"@apollo/server": "^4.11.2",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@apollo/server": "^4.12.2",
|
||||
"@as-integrations/fastify": "^2.1.1",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/helmet": "^13.0.1",
|
||||
"@graphql-codegen/client-preset": "^4.5.0",
|
||||
"@graphql-tools/load-files": "^7.0.0",
|
||||
"@graphql-tools/merge": "^9.0.8",
|
||||
"@graphql-tools/schema": "^10.0.7",
|
||||
"@graphql-tools/utils": "^10.5.5",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@nestjs/apollo": "^13.0.3",
|
||||
"@graphql-codegen/client-preset": "^4.8.3",
|
||||
"@graphql-tools/load-files": "^7.0.1",
|
||||
"@graphql-tools/merge": "^9.0.24",
|
||||
"@graphql-tools/schema": "^10.0.23",
|
||||
"@graphql-tools/utils": "^10.8.6",
|
||||
"@jsonforms/core": "^3.6.0",
|
||||
"@nestjs/apollo": "^13.1.0",
|
||||
"@nestjs/cache-manager": "^3.0.1",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/common": "^11.1.3",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/core": "^11.1.3",
|
||||
"@nestjs/event-emitter": "^3.0.1",
|
||||
"@nestjs/graphql": "^13.0.3",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
"@nestjs/platform-fastify": "^11.0.11",
|
||||
"@nestjs/graphql": "^13.1.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-fastify": "^11.1.3",
|
||||
"@nestjs/schedule": "^6.0.0",
|
||||
"@nestjs/throttler": "^6.4.0",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@runonflux/nat-upnp": "^1.0.2",
|
||||
"@types/diff": "^8.0.0",
|
||||
"@unraid/libvirt": "^2.1.0",
|
||||
@@ -82,67 +82,67 @@
|
||||
"accesscontrol": "^2.2.1",
|
||||
"bycontract": "^2.0.11",
|
||||
"bytes": "^3.1.2",
|
||||
"cache-manager": "^7.0.0",
|
||||
"cache-manager": "^7.0.1",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"casbin": "^5.32.0",
|
||||
"casbin": "^5.38.0",
|
||||
"change-case": "^5.4.4",
|
||||
"chokidar": "^4.0.1",
|
||||
"chokidar": "^4.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cli-table": "^0.3.11",
|
||||
"command-exists": "^1.2.9",
|
||||
"convert": "^5.8.0",
|
||||
"convert": "^5.12.0",
|
||||
"cookie": "^1.0.2",
|
||||
"cron": "4.3.1",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"diff": "^8.0.0",
|
||||
"dockerode": "^4.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"execa": "^9.5.1",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"diff": "^8.0.2",
|
||||
"dockerode": "^4.0.7",
|
||||
"dotenv": "^16.6.1",
|
||||
"execa": "^9.6.0",
|
||||
"exit-hook": "^4.0.0",
|
||||
"fastify": "^5.2.1",
|
||||
"fastify": "^5.4.0",
|
||||
"filenamify": "^6.0.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"glob": "^11.0.1",
|
||||
"fs-extra": "^11.3.0",
|
||||
"glob": "^11.0.3",
|
||||
"global-agent": "^3.0.0",
|
||||
"got": "^14.4.6",
|
||||
"graphql": "^16.9.0",
|
||||
"got": "^14.4.7",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-fields": "^2.0.3",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"graphql-scalars": "^1.24.2",
|
||||
"graphql-subscriptions": "^3.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^6.0.0",
|
||||
"graphql-ws": "^6.0.5",
|
||||
"ini": "^5.0.0",
|
||||
"ip": "^2.0.1",
|
||||
"jose": "^6.0.0",
|
||||
"jose": "^6.0.11",
|
||||
"json-bigint-patch": "^0.0.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
"multi-ini": "^2.3.2",
|
||||
"mustache": "^4.2.0",
|
||||
"nest-authz": "^2.14.0",
|
||||
"nest-commander": "^3.15.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"nest-authz": "^2.17.0",
|
||||
"nest-commander": "^3.17.0",
|
||||
"nestjs-pino": "^4.4.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-window-polyfill": "^1.0.2",
|
||||
"p-retry": "^6.2.0",
|
||||
"node-window-polyfill": "^1.0.4",
|
||||
"p-retry": "^6.2.1",
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-http-header-strategy": "^1.1.0",
|
||||
"path-type": "^6.0.0",
|
||||
"pino": "^9.5.0",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino": "^9.7.0",
|
||||
"pino-http": "^10.5.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"pm2": "^6.0.0",
|
||||
"pm2": "^6.0.8",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"request": "^2.88.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"semver": "^7.6.3",
|
||||
"semver": "^7.7.2",
|
||||
"strftime": "^0.10.3",
|
||||
"systeminformation": "^5.25.11",
|
||||
"uuid": "^11.0.2",
|
||||
"ws": "^8.18.0",
|
||||
"systeminformation": "^5.27.7",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.3",
|
||||
"zen-observable-ts": "^1.1.0",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.25.69"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"unraid-api-plugin-connect": "workspace:*"
|
||||
@@ -153,67 +153,67 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@graphql-codegen/add": "^5.0.3",
|
||||
"@graphql-codegen/cli": "^5.0.3",
|
||||
"@graphql-codegen/fragment-matcher": "^5.0.2",
|
||||
"@graphql-codegen/import-types-preset": "^3.0.0",
|
||||
"@graphql-codegen/typed-document-node": "^5.0.11",
|
||||
"@graphql-codegen/typescript": "^4.1.1",
|
||||
"@graphql-codegen/typescript-operations": "^4.3.1",
|
||||
"@graphql-codegen/cli": "^5.0.7",
|
||||
"@graphql-codegen/fragment-matcher": "^5.1.0",
|
||||
"@graphql-codegen/import-types-preset": "^3.0.1",
|
||||
"@graphql-codegen/typed-document-node": "^5.1.2",
|
||||
"@graphql-codegen/typescript": "^4.1.6",
|
||||
"@graphql-codegen/typescript-operations": "^4.6.1",
|
||||
"@graphql-codegen/typescript-resolvers": "4.5.1",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||
"@nestjs/testing": "^11.0.11",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
|
||||
"@nestjs/testing": "^11.1.3",
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@swc/core": "^1.10.1",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@swc/core": "^1.12.9",
|
||||
"@types/async-exit-hook": "^2.0.2",
|
||||
"@types/bytes": "^3.1.4",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/cli-table": "^0.3.4",
|
||||
"@types/command-exists": "^1.2.3",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/dockerode": "^3.3.31",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/dockerode": "^3.3.42",
|
||||
"@types/graphql-fields": "^1.3.9",
|
||||
"@types/graphql-type-uuid": "^0.2.6",
|
||||
"@types/ini": "^4.1.1",
|
||||
"@types/ip": "^1.1.3",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/pify": "^6.0.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/mustache": "^4.2.6",
|
||||
"@types/node": "^22.16.0",
|
||||
"@types/pify": "^6.1.0",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/sendmail": "^1.4.7",
|
||||
"@types/stoppable": "^1.1.3",
|
||||
"@types/strftime": "^0.9.8",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/wtfnode": "^0.7.3",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"@vitest/ui": "^3.0.5",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-n": "^17.0.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-n": "^17.20.0",
|
||||
"eslint-plugin-no-relative-import-paths": "^1.6.1",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"graphql-codegen-typescript-validation-schema": "^0.17.0",
|
||||
"jiti": "^2.4.0",
|
||||
"nodemon": "^3.1.7",
|
||||
"prettier": "^3.5.2",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"graphql-codegen-typescript-validation-schema": "^0.17.1",
|
||||
"jiti": "^2.4.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"prettier": "^3.6.2",
|
||||
"rollup-plugin-node-externals": "^8.0.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"tsx": "^4.19.3",
|
||||
"type-fest": "^4.37.0",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"unplugin-swc": "^1.5.1",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-node": "^5.0.0",
|
||||
"vite-tsconfig-paths": "^5.1.0",
|
||||
"vitest": "^3.0.5",
|
||||
"zx": "^8.3.2"
|
||||
"tsx": "^4.20.3",
|
||||
"type-fest": "^4.41.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"unplugin-swc": "^1.5.5",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-node": "^5.0.1",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4",
|
||||
"zx": "^8.6.1"
|
||||
},
|
||||
"overrides": {
|
||||
"eslint": {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@graphql-codegen/cli": "^5.0.3",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@jsonforms/core": "^3.6.0",
|
||||
"@nestjs/apollo": "^13.0.3",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
@@ -49,7 +49,7 @@
|
||||
"execa": "^9.5.1",
|
||||
"fast-check": "^4.1.1",
|
||||
"got": "^14.4.6",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"graphql-subscriptions": "^3.0.0",
|
||||
"graphql-ws": "^6.0.0",
|
||||
@@ -74,7 +74,7 @@
|
||||
"peerDependencies": {
|
||||
"@apollo/client": "^3.11.8",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@jsonforms/core": "^3.6.0",
|
||||
"@nestjs/apollo": "^13.0.3",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
@@ -88,7 +88,7 @@
|
||||
"class-validator": "^0.14.1",
|
||||
"execa": "^9.5.1",
|
||||
"got": "^14.4.6",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"graphql-subscriptions": "^3.0.0",
|
||||
"graphql-ws": "^6.0.0",
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
"description": "Shared utilities and types for Unraid API ecosystem",
|
||||
"devDependencies": {
|
||||
"@graphql-tools/utils": "^10.5.5",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@jsonforms/core": "^3.6.0",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/graphql": "^13.0.3",
|
||||
"@types/bun": "^1.2.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.0",
|
||||
"class-validator": "^0.14.1",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nest-authz": "^2.14.0",
|
||||
@@ -45,11 +45,11 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@graphql-tools/utils": "^10.5.5",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@jsonforms/core": "^3.6.0",
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/graphql": "^13.0.3",
|
||||
"class-validator": "^0.14.1",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-scalars": "^1.23.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nest-authz": "^2.14.0"
|
||||
|
||||
10230
pnpm-lock.yaml
generated
10230
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components/common",
|
||||
"components": "@/components",
|
||||
"composables": "@/composables",
|
||||
"utils": "@/lib/utils",
|
||||
"lib": "@/lib"
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@internationalized/number": "^3.6.0",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@jsonforms/vue": "^3.5.1",
|
||||
"@jsonforms/vue-vanilla": "^3.5.1",
|
||||
"@jsonforms/core": "^3.6.0",
|
||||
"@jsonforms/vue": "^3.6.0",
|
||||
"@jsonforms/vue-vanilla": "^3.6.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -1,19 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionRoot,
|
||||
useForwardPropsEmits,
|
||||
type AccordionRootEmits,
|
||||
type AccordionRootProps,
|
||||
} from 'reka-ui';
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
|
||||
const props = defineProps<AccordionRootProps>();
|
||||
const emits = defineEmits<AccordionRootEmits>();
|
||||
export interface AccordionItemData {
|
||||
value: string;
|
||||
title: string;
|
||||
content?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
export interface AccordionProps {
|
||||
items?: AccordionItemData[];
|
||||
type?: 'single' | 'multiple';
|
||||
collapsible?: boolean;
|
||||
defaultValue?: string | string[];
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<AccordionProps>(), {
|
||||
type: 'single',
|
||||
collapsible: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-bind="forwarded">
|
||||
<AccordionRoot
|
||||
:type="type"
|
||||
:collapsible="collapsible"
|
||||
:default-value="defaultValue"
|
||||
:class="props.class"
|
||||
>
|
||||
<!-- Default slot for direct composition -->
|
||||
<slot />
|
||||
|
||||
<!-- Props-based usage for simple cases -->
|
||||
<template v-if="items && items.length > 0">
|
||||
<AccordionItem
|
||||
v-for="item in items"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:disabled="item.disabled"
|
||||
>
|
||||
<AccordionTrigger>
|
||||
<slot name="trigger" :item="item">
|
||||
{{ item.title }}
|
||||
</slot>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<slot name="content" :item="item">
|
||||
{{ item.content }}
|
||||
</slot>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</template>
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
export { default as Accordion } from './Accordion.vue';
|
||||
export { default as AccordionContent } from './AccordionContent.vue';
|
||||
export { default as AccordionItem } from './AccordionItem.vue';
|
||||
export { default as AccordionTrigger } from './AccordionTrigger.vue';
|
||||
export type { AccordionItemData, AccordionProps } from './Accordion.vue';
|
||||
|
||||
@@ -1,15 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot, useForwardPropsEmits } from 'reka-ui';
|
||||
import type { DialogRootEmits, DialogRootProps } from 'reka-ui';
|
||||
import Button from '@/components/common/button/Button.vue';
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogRoot,
|
||||
DialogScrollContent,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const props = defineProps<DialogRootProps>();
|
||||
const emits = defineEmits<DialogRootEmits>();
|
||||
export interface DialogProps {
|
||||
description?: string;
|
||||
title?: string;
|
||||
triggerText?: string;
|
||||
modelValue?: boolean;
|
||||
showFooter?: boolean;
|
||||
closeButtonText?: string;
|
||||
primaryButtonText?: string;
|
||||
primaryButtonLoading?: boolean;
|
||||
primaryButtonLoadingText?: string;
|
||||
primaryButtonDisabled?: boolean;
|
||||
scrollable?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
const {
|
||||
description,
|
||||
title,
|
||||
triggerText,
|
||||
modelValue,
|
||||
showFooter = true,
|
||||
closeButtonText = 'Close',
|
||||
primaryButtonText,
|
||||
primaryButtonLoading = false,
|
||||
primaryButtonLoadingText,
|
||||
primaryButtonDisabled = false,
|
||||
scrollable = false,
|
||||
size = 'md',
|
||||
} = defineProps<DialogProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
'primary-click': [];
|
||||
}>();
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
emit('update:modelValue', open);
|
||||
};
|
||||
|
||||
const handlePrimaryClick = () => {
|
||||
emit('primary-click');
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-lg',
|
||||
lg: 'max-w-2xl',
|
||||
xl: 'max-w-4xl',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
<DialogRoot :open="modelValue" @update:open="handleOpenChange">
|
||||
<DialogTrigger v-if="triggerText || $slots.trigger">
|
||||
<slot name="trigger">
|
||||
<Button>{{ triggerText }}</Button>
|
||||
</slot>
|
||||
</DialogTrigger>
|
||||
|
||||
<component :is="scrollable ? DialogScrollContent : DialogContent" :class="cn(sizeClasses[size])">
|
||||
<DialogHeader v-if="title || description || $slots.header">
|
||||
<slot name="header">
|
||||
<DialogTitle v-if="title">{{ title }}</DialogTitle>
|
||||
<DialogDescription v-if="description">
|
||||
{{ description }}
|
||||
</DialogDescription>
|
||||
</slot>
|
||||
</DialogHeader>
|
||||
|
||||
<slot />
|
||||
|
||||
<DialogFooter v-if="$slots.footer || showFooter">
|
||||
<slot name="footer">
|
||||
<div class="flex justify-end gap-2">
|
||||
<DialogClose as-child>
|
||||
<Button variant="secondary">{{ closeButtonText }}</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
v-if="primaryButtonText"
|
||||
variant="primary"
|
||||
:disabled="primaryButtonDisabled || primaryButtonLoading"
|
||||
@click="handlePrimaryClick"
|
||||
>
|
||||
<span v-if="primaryButtonLoading && primaryButtonLoadingText">
|
||||
{{ primaryButtonLoadingText }}
|
||||
</span>
|
||||
<span v-else>{{ primaryButtonText }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</slot>
|
||||
</DialogFooter>
|
||||
</component>
|
||||
</DialogRoot>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, useForwardPropsEmits, type DialogCloseProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<DialogCloseProps>();
|
||||
const emits = defineEmits([]);
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose v-bind="forwarded">
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
@@ -1,9 +1,4 @@
|
||||
export { default as Dialog } from './Dialog.vue';
|
||||
export { default as DialogClose } from './DialogClose.vue';
|
||||
export { default as DialogContent } from './DialogContent.vue';
|
||||
export { default as DialogDescription } from './DialogDescription.vue';
|
||||
export { default as DialogFooter } from './DialogFooter.vue';
|
||||
export { default as DialogHeader } from './DialogHeader.vue';
|
||||
export { default as DialogScrollContent } from './DialogScrollContent.vue';
|
||||
export { default as DialogTitle } from './DialogTitle.vue';
|
||||
export { default as DialogTrigger } from './DialogTrigger.vue';
|
||||
|
||||
// Type exports
|
||||
export type { DialogProps } from './Dialog.vue';
|
||||
|
||||
@@ -1,19 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/common/button';
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRoot,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuRootEmits,
|
||||
type DropdownMenuRootProps,
|
||||
} from 'reka-ui';
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>();
|
||||
const emits = defineEmits<DropdownMenuRootEmits>();
|
||||
export interface DropdownMenuItemData {
|
||||
type?: 'item' | 'label' | 'separator';
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const forwarded = useForwardPropsEmits<DropdownMenuRootProps, 'update:open'>(props, emits);
|
||||
export interface DropdownMenuProps {
|
||||
items?: DropdownMenuItemData[];
|
||||
trigger?: string;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
sideOffset?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<DropdownMenuProps>(), {
|
||||
align: 'start',
|
||||
side: 'bottom',
|
||||
sideOffset: 4,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [item: DropdownMenuItemData];
|
||||
}>();
|
||||
|
||||
function handleItemClick(item: DropdownMenuItemData) {
|
||||
item.onClick?.();
|
||||
|
||||
emit('select', item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger v-if="!$slots.trigger" as-child>
|
||||
<Button variant="primary">
|
||||
{{ props.trigger || 'Options' }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuTrigger v-else as-child>
|
||||
<slot name="trigger" />
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent :align="props.align" :side="props.side" :side-offset="props.sideOffset">
|
||||
<!--Slot for direct composition -->
|
||||
<slot name="content" />
|
||||
|
||||
<!-- Props-based items rendering -->
|
||||
<template v-if="props.items && props.items.length > 0">
|
||||
<template v-for="item in props.items" :key="item.label || item.type">
|
||||
<DropdownMenuSeparator v-if="item.type === 'separator'" />
|
||||
|
||||
<DropdownMenuLabel v-else-if="item.type === 'label'">
|
||||
{{ item.label }}
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem v-else :disabled="item.disabled" @click="handleItemClick(item)">
|
||||
{{ item.label }}
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DropdownMenuArrow as RekaDropdownMenuArrow,
|
||||
useForwardProps,
|
||||
type DropdownMenuArrowProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<DropdownMenuArrowProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RekaDropdownMenuArrow
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'fill-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,18 +1,2 @@
|
||||
import DropdownMenu from '@/components/common/dropdown-menu/DropdownMenu.vue';
|
||||
|
||||
export { DropdownMenu };
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue';
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue';
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue';
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue';
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue';
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue';
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue';
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue';
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue';
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue';
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue';
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue';
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue';
|
||||
export { default as DropdownMenuArrow } from './DropdownMenuArrow.vue';
|
||||
export { DropdownMenuPortal } from 'reka-ui';
|
||||
export { default as DropdownMenu } from './DropdownMenu.vue';
|
||||
export type { DropdownMenuItemData, DropdownMenuProps } from './DropdownMenu.vue';
|
||||
|
||||
@@ -1,15 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'reka-ui';
|
||||
import { SelectRoot, useForwardPropsEmits } from 'reka-ui';
|
||||
import {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectRoot,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<SelectRootProps>();
|
||||
const emits = defineEmits<SelectRootEmits>();
|
||||
type SelectValueType = string | number;
|
||||
|
||||
const forwarded = useForwardPropsEmits<SelectRootProps, 'update:modelValue'>(props, emits);
|
||||
type AcceptableValue = SelectValueType | SelectValueType[] | Record<string, unknown> | null;
|
||||
|
||||
interface SelectItemInterface {
|
||||
label: string;
|
||||
value: SelectValueType;
|
||||
disabled?: boolean;
|
||||
class?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface SelectLabelInterface {
|
||||
type: 'label';
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SelectSeparatorInterface {
|
||||
type: 'separator';
|
||||
}
|
||||
|
||||
// Union type for all possible items
|
||||
export type SelectItemType =
|
||||
| SelectItemInterface
|
||||
| SelectLabelInterface
|
||||
| SelectSeparatorInterface
|
||||
| SelectValueType;
|
||||
|
||||
export interface SelectProps {
|
||||
modelValue?: AcceptableValue;
|
||||
items?: SelectItemType[] | SelectItemType[][];
|
||||
placeholder?: string;
|
||||
multiple?: boolean;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
class?: string;
|
||||
valueKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SelectProps>(), {
|
||||
items: () => [],
|
||||
multiple: false,
|
||||
valueKey: 'value',
|
||||
labelKey: 'label',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: AcceptableValue];
|
||||
}>();
|
||||
|
||||
function isStructuredItem(item: SelectItemType): item is SelectItemInterface {
|
||||
return typeof item === 'object' && item !== null && 'value' in item;
|
||||
}
|
||||
|
||||
function isLabelItem(item: SelectItemType): item is SelectLabelInterface {
|
||||
return typeof item === 'object' && item !== null && 'type' in item && item.type === 'label';
|
||||
}
|
||||
|
||||
function isSeparatorItem(item: SelectItemType): item is SelectSeparatorInterface {
|
||||
return typeof item === 'object' && item !== null && 'type' in item && item.type === 'separator';
|
||||
}
|
||||
|
||||
function getItemLabel(item: SelectItemType): string {
|
||||
if (isStructuredItem(item)) {
|
||||
return String(props.labelKey in item ? item[props.labelKey] : (item.label ?? item.value));
|
||||
}
|
||||
|
||||
if (isLabelItem(item)) return item.label;
|
||||
return String(item);
|
||||
}
|
||||
|
||||
// Get value for an item
|
||||
function getItemValue(item: SelectItemType): SelectValueType | null {
|
||||
if (isStructuredItem(item)) {
|
||||
const value = props.valueKey in item ? item[props.valueKey] : item.value;
|
||||
|
||||
return typeof value === 'string' || typeof value === 'number' ? value : null;
|
||||
}
|
||||
|
||||
if (isLabelItem(item) || isSeparatorItem(item)) return null;
|
||||
return item;
|
||||
}
|
||||
|
||||
function isGroupedItems(items: SelectItemType[] | SelectItemType[][]): items is SelectItemType[][] {
|
||||
return Array.isArray(items) && items.length > 0 && Array.isArray(items[0]);
|
||||
}
|
||||
|
||||
const itemGroups = computed(() => {
|
||||
if (!props.items) return [];
|
||||
|
||||
return isGroupedItems(props.items) ? props.items : [props.items];
|
||||
});
|
||||
|
||||
const flatItems = computed(() => {
|
||||
return itemGroups.value.flat();
|
||||
});
|
||||
|
||||
const renderableItems = computed(() => {
|
||||
return flatItems.value.filter((item) => !isLabelItem(item) && !isSeparatorItem(item));
|
||||
});
|
||||
|
||||
const groupedOrderedItems = computed(() => {
|
||||
return itemGroups.value.map((group, groupIndex) => ({
|
||||
groupIndex,
|
||||
items: group.map((item, index) => ({
|
||||
item,
|
||||
index,
|
||||
type: isLabelItem(item) ? 'label' : isSeparatorItem(item) ? 'separator' : 'item',
|
||||
})),
|
||||
}));
|
||||
});
|
||||
|
||||
const isMultipleSelection = computed(() => {
|
||||
return props.multiple && Array.isArray(props.modelValue) && props.modelValue.length > 0;
|
||||
});
|
||||
|
||||
const multipleValueDisplay = computed(() => {
|
||||
if (!isMultipleSelection.value || !Array.isArray(props.modelValue)) return '';
|
||||
|
||||
const values = props.modelValue as SelectValueType[];
|
||||
const displayLabels = values.map((value) => {
|
||||
const item = renderableItems.value.find((item) => {
|
||||
const itemValue = getItemValue(item);
|
||||
|
||||
return itemValue === value;
|
||||
});
|
||||
return item ? getItemLabel(item) : String(value);
|
||||
});
|
||||
|
||||
if (displayLabels.length <= 2) {
|
||||
return displayLabels.join(', ');
|
||||
} else {
|
||||
return `${displayLabels[0]}, ${displayLabels[1]} +${displayLabels.length - 2} more`;
|
||||
}
|
||||
});
|
||||
|
||||
function handleUpdateModelValue(value: AcceptableValue) {
|
||||
emit('update:modelValue', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-bind="forwarded">
|
||||
<slot />
|
||||
<SelectRoot
|
||||
:model-value="props.modelValue"
|
||||
:multiple="props.multiple"
|
||||
:disabled="props.disabled"
|
||||
:required="props.required"
|
||||
:name="props.name"
|
||||
@update:model-value="handleUpdateModelValue"
|
||||
>
|
||||
<SelectTrigger :class="props.class">
|
||||
<slot>
|
||||
<SelectValue :placeholder="props.placeholder">
|
||||
<template v-if="isMultipleSelection">
|
||||
{{ multipleValueDisplay }}
|
||||
</template>
|
||||
</SelectValue>
|
||||
</slot>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<slot name="content-top" />
|
||||
|
||||
<SelectGroup v-for="{ groupIndex, items } in groupedOrderedItems" :key="groupIndex">
|
||||
<template v-for="{ item, index, type } in items" :key="index">
|
||||
<SelectLabel v-if="type === 'label'">
|
||||
{{ getItemLabel(item) }}
|
||||
</SelectLabel>
|
||||
|
||||
<SelectSeparator v-if="type === 'separator'" />
|
||||
|
||||
<SelectItem
|
||||
v-if="type === 'item'"
|
||||
:value="getItemValue(item)!"
|
||||
:disabled="isStructuredItem(item) ? item.disabled : false"
|
||||
:class="isStructuredItem(item) ? item.class : undefined"
|
||||
>
|
||||
<slot name="item" :item="item" :index="index">
|
||||
{{ getItemLabel(item) }}
|
||||
</slot>
|
||||
</SelectItem>
|
||||
</template>
|
||||
</SelectGroup>
|
||||
<slot name="content-bottom" />
|
||||
</SelectContent>
|
||||
</SelectRoot>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,2 @@
|
||||
export { default as Select } from './Select.vue';
|
||||
export { default as SelectContent } from './SelectContent.vue';
|
||||
export { default as SelectGroup } from './SelectGroup.vue';
|
||||
export { default as SelectItem } from './SelectItem.vue';
|
||||
export { default as SelectItemText } from './SelectItemText.vue';
|
||||
export { default as SelectLabel } from './SelectLabel.vue';
|
||||
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue';
|
||||
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue';
|
||||
export { default as SelectSeparator } from './SelectSeparator.vue';
|
||||
export { default as SelectTrigger } from './SelectTrigger.vue';
|
||||
export { default as SelectValue } from './SelectValue.vue';
|
||||
export type { SelectItemType, SelectProps } from './Select.vue';
|
||||
|
||||
@@ -12,7 +12,7 @@ const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem v-bind="forwardedProps" :class="cn(props.class)">
|
||||
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
|
||||
<slot />
|
||||
</AccordionItem>
|
||||
</template>
|
||||
19
unraid-ui/src/components/ui/accordion/AccordionRoot.vue
Normal file
19
unraid-ui/src/components/ui/accordion/AccordionRoot.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccordionRoot,
|
||||
useForwardPropsEmits,
|
||||
type AccordionRootEmits,
|
||||
type AccordionRootProps,
|
||||
} from 'reka-ui';
|
||||
|
||||
const props = defineProps<AccordionRootProps>();
|
||||
const emits = defineEmits<AccordionRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
@@ -16,7 +16,7 @@ const delegatedProps = reactiveOmit(props, 'class');
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-1 items-center justify-between p-2 rounded-md font-medium transition-all border border-border hover:border-muted-foreground focus:border-muted-foreground [&[data-state=open]>svg]:rotate-180',
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
4
unraid-ui/src/components/ui/accordion/index.ts
Normal file
4
unraid-ui/src/components/ui/accordion/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as AccordionRoot } from './AccordionRoot.vue';
|
||||
export { default as AccordionContent } from './AccordionContent.vue';
|
||||
export { default as AccordionItem } from './AccordionItem.vue';
|
||||
export { default as AccordionTrigger } from './AccordionTrigger.vue';
|
||||
11
unraid-ui/src/components/ui/dialog/DialogClose.vue
Normal file
11
unraid-ui/src/components/ui/dialog/DialogClose.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<DialogCloseProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose v-bind="props">
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
@@ -33,7 +33,7 @@ const { teleportTarget } = useTeleport();
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg rounded-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
14
unraid-ui/src/components/ui/dialog/DialogRoot.vue
Normal file
14
unraid-ui/src/components/ui/dialog/DialogRoot.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot, useForwardPropsEmits, type DialogRootEmits, type DialogRootProps } from 'reka-ui';
|
||||
|
||||
const props = defineProps<DialogRootProps>();
|
||||
const emits = defineEmits<DialogRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
@@ -14,16 +14,17 @@ import {
|
||||
} from 'reka-ui';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
|
||||
const props = defineProps<
|
||||
DialogContentProps & { class?: HTMLAttributes['class'] } & { to?: string | HTMLElement }
|
||||
>();
|
||||
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
9
unraid-ui/src/components/ui/dialog/index.ts
Normal file
9
unraid-ui/src/components/ui/dialog/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as DialogRoot } from './DialogRoot.vue';
|
||||
export { default as DialogClose } from './DialogClose.vue';
|
||||
export { default as DialogContent } from './DialogContent.vue';
|
||||
export { default as DialogDescription } from './DialogDescription.vue';
|
||||
export { default as DialogFooter } from './DialogFooter.vue';
|
||||
export { default as DialogHeader } from './DialogHeader.vue';
|
||||
export { default as DialogScrollContent } from './DialogScrollContent.vue';
|
||||
export { default as DialogTitle } from './DialogTitle.vue';
|
||||
export { default as DialogTrigger } from './DialogTrigger.vue';
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { Check } from 'lucide-vue-next';
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -8,16 +9,12 @@ import {
|
||||
type DropdownMenuCheckboxItemEmits,
|
||||
type DropdownMenuCheckboxItemProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>();
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import useTeleport from '@/composables/useTeleport';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuPortal,
|
||||
@@ -8,9 +9,7 @@ import {
|
||||
type DropdownMenuContentEmits,
|
||||
type DropdownMenuContentProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||
@@ -20,29 +19,25 @@ const props = withDefaults(
|
||||
);
|
||||
const emits = defineEmits<DropdownMenuContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal :to="teleportTarget">
|
||||
<DropdownMenuContent
|
||||
v-bind="forwarded"
|
||||
side="bottom"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 rounded-lg bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
'z-50 min-w-32 overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<slot />
|
||||
</div>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
@@ -1,17 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { DropdownMenuItem, useForwardProps, type DropdownMenuItemProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuItemProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
@@ -1,17 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { DropdownMenuLabel, useForwardProps, type DropdownMenuLabelProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuLabelProps & { class?: HTMLAttributes['class']; inset?: boolean }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>();
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits<DropdownMenuRadioGroupProps, 'update:modelValue'>(props, emits);
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { Circle } from 'lucide-vue-next';
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
@@ -8,17 +9,13 @@ import {
|
||||
type DropdownMenuRadioItemEmits,
|
||||
type DropdownMenuRadioItemProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuRoot,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuRootEmits,
|
||||
type DropdownMenuRootProps,
|
||||
} from 'reka-ui';
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>();
|
||||
const emits = defineEmits<DropdownMenuRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { DropdownMenuSeparator, type DropdownMenuSeparatorProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSeparatorProps & {
|
||||
@@ -9,11 +10,7 @@ const props = defineProps<
|
||||
}
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
const props = defineProps<DropdownMenuSubProps>();
|
||||
const emits = defineEmits<DropdownMenuSubEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits<DropdownMenuSubProps, 'update:open'>(props, emits);
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1,21 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
useForwardPropsEmits,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>();
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { ChevronRight } from 'lucide-vue-next';
|
||||
import { DropdownMenuSubTrigger, useForwardProps, type DropdownMenuSubTriggerProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
@@ -3,11 +3,14 @@ import { DropdownMenuTrigger, useForwardProps, type DropdownMenuTriggerProps } f
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>();
|
||||
|
||||
const forwardedProps = useForwardProps<DropdownMenuTriggerProps>(props);
|
||||
const forwardedProps = useForwardProps(props);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
|
||||
<DropdownMenuTrigger
|
||||
class="outline-none cursor-pointer [&[data-state=open]]:cursor-pointer"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
15
unraid-ui/src/components/ui/dropdown-menu/index.ts
Normal file
15
unraid-ui/src/components/ui/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export { default as DropdownMenuRoot } from './DropdownMenuRoot.vue';
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue';
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue';
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue';
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue';
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue';
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue';
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue';
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue';
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue';
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue';
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue';
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue';
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue';
|
||||
export { DropdownMenuPortal } from 'reka-ui';
|
||||
5
unraid-ui/src/components/ui/index.ts
Normal file
5
unraid-ui/src/components/ui/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Export all UI primitives
|
||||
export * from './accordion';
|
||||
export * from './dialog';
|
||||
export * from './dropdown-menu';
|
||||
export * from './select';
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import useTeleport from '@/composables/useTeleport';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import {
|
||||
SelectContent,
|
||||
SelectPortal,
|
||||
@@ -9,34 +10,27 @@ import {
|
||||
type SelectContentEmits,
|
||||
type SelectContentProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from '.';
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
|
||||
const props = withDefaults(defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>(), {
|
||||
forceMount: false,
|
||||
position: 'popper',
|
||||
to: undefined,
|
||||
});
|
||||
|
||||
const emits = defineEmits<SelectContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectPortal :force-mount="forceMount" :to="teleportTarget">
|
||||
<SelectPortal :to="teleportTarget">
|
||||
<SelectContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
@@ -1,15 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { SelectGroup, type SelectGroupProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<SelectGroupProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { Check } from 'lucide-vue-next';
|
||||
import {
|
||||
SelectItem,
|
||||
@@ -8,15 +9,11 @@ import {
|
||||
useForwardProps,
|
||||
type SelectItemProps,
|
||||
} from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<SelectItemProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
15
unraid-ui/src/components/ui/select/SelectRoot.vue
Normal file
15
unraid-ui/src/components/ui/select/SelectRoot.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'reka-ui';
|
||||
import { SelectRoot, useForwardPropsEmits } from 'reka-ui';
|
||||
|
||||
const props = defineProps<SelectRootProps>();
|
||||
const emits = defineEmits<SelectRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</SelectRoot>
|
||||
</template>
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { SelectScrollDownButton, useForwardProps, type SelectScrollDownButtonProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { ChevronUp } from 'lucide-vue-next';
|
||||
import { SelectScrollUpButton, useForwardProps, type SelectScrollUpButtonProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
@@ -1,15 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { SelectSeparator, type SelectSeparatorProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<SelectSeparatorProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -1,16 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { reactiveOmit } from '@vueuse/core';
|
||||
import { ChevronDown } from 'lucide-vue-next';
|
||||
import { SelectIcon, SelectTrigger, useForwardProps, type SelectTriggerProps } from 'reka-ui';
|
||||
import { computed, type HTMLAttributes } from 'vue';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
|
||||
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
const delegatedProps = reactiveOmit(props, 'class');
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
11
unraid-ui/src/components/ui/select/index.ts
Normal file
11
unraid-ui/src/components/ui/select/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { default as SelectRoot } from './SelectRoot.vue';
|
||||
export { default as SelectContent } from './SelectContent.vue';
|
||||
export { default as SelectGroup } from './SelectGroup.vue';
|
||||
export { default as SelectItem } from './SelectItem.vue';
|
||||
export { default as SelectItemText } from './SelectItemText.vue';
|
||||
export { default as SelectLabel } from './SelectLabel.vue';
|
||||
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue';
|
||||
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue';
|
||||
export { default as SelectSeparator } from './SelectSeparator.vue';
|
||||
export { default as SelectTrigger } from './SelectTrigger.vue';
|
||||
export { default as SelectValue } from './SelectValue.vue';
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/common/tooltip';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemText,
|
||||
SelectRoot,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/form/select';
|
||||
} from '@/components/ui/select';
|
||||
import useTeleport from '@/composables/useTeleport';
|
||||
import type { ControlElement } from '@jsonforms/core';
|
||||
import { useJsonFormsControl } from '@jsonforms/vue';
|
||||
@@ -41,20 +41,18 @@ const onSelectOpen = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- The ControlWrapper now handles the v-if based on control.visible -->
|
||||
<Select
|
||||
<SelectRoot
|
||||
v-model="selected"
|
||||
:disabled="!control.enabled"
|
||||
:required="control.required"
|
||||
@update:model-value="onChange"
|
||||
@update:open="onSelectOpen"
|
||||
>
|
||||
<!-- The trigger shows the currently selected value (if any) -->
|
||||
<SelectTrigger>
|
||||
<SelectValue v-if="selected">{{ selected }}</SelectValue>
|
||||
<span v-else>{{ control.schema.default ?? 'Select an option' }}</span>
|
||||
</SelectTrigger>
|
||||
<!-- The content includes the selectable options -->
|
||||
|
||||
<SelectContent :to="teleportTarget">
|
||||
<template v-for="option in options" :key="option.value">
|
||||
<TooltipProvider v-if="option.tooltip" :delay-duration="50">
|
||||
@@ -69,10 +67,11 @@ const onSelectOpen = () => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<SelectItem v-else :value="option.value">
|
||||
<SelectItemText>{{ option.label }}</SelectItemText>
|
||||
</SelectItem>
|
||||
</template>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SelectRoot>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,9 @@ import '@/styles/index.css';
|
||||
// Components
|
||||
export * from '@/components';
|
||||
|
||||
// Component Primitives
|
||||
export * from '@/components/ui';
|
||||
|
||||
// JsonForms
|
||||
export * from '@/forms/renderers';
|
||||
|
||||
|
||||
365
unraid-ui/stories/components/common/Accordion.stories.ts
Normal file
365
unraid-ui/stories/components/common/Accordion.stories.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import { Accordion } from '../../../src/components/common/accordion';
|
||||
import { AccordionContent, AccordionItem, AccordionTrigger } from '../../../src/components/ui/accordion';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Common/Accordion',
|
||||
component: Accordion,
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['single', 'multiple'],
|
||||
},
|
||||
collapsible: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Accordion>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Basic usage with items prop
|
||||
export const BasicUsage: Story = {
|
||||
args: {
|
||||
type: 'single',
|
||||
collapsible: true,
|
||||
items: [
|
||||
{
|
||||
value: 'item-1',
|
||||
title: 'Is it accessible?',
|
||||
content: 'Yes. It adheres to the WAI-ARIA design pattern.',
|
||||
},
|
||||
{
|
||||
value: 'item-2',
|
||||
title: 'Is it styled?',
|
||||
content: "Yes. It comes with default styles that matches the other components' aesthetic.",
|
||||
},
|
||||
{
|
||||
value: 'item-3',
|
||||
title: 'Is it animated?',
|
||||
content: "Yes. It's animated by default, but you can disable it if you prefer.",
|
||||
},
|
||||
],
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { Accordion },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<Accordion
|
||||
:type="args.type"
|
||||
:collapsible="args.collapsible"
|
||||
:items="args.items"
|
||||
:class="args.class"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Items with custom slot content
|
||||
export const ItemsWithCustomSlots: Story = {
|
||||
args: {
|
||||
type: 'single',
|
||||
collapsible: true,
|
||||
items: [
|
||||
{
|
||||
value: 'stats',
|
||||
title: 'User Statistics',
|
||||
content: 'View your usage statistics',
|
||||
},
|
||||
{
|
||||
value: 'settings',
|
||||
title: 'Account Settings',
|
||||
content: 'Manage your account preferences',
|
||||
},
|
||||
{
|
||||
value: 'notifications',
|
||||
title: 'Notifications',
|
||||
content: 'Configure notification preferences',
|
||||
},
|
||||
],
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { Accordion },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<Accordion
|
||||
:type="args.type"
|
||||
:collapsible="args.collapsible"
|
||||
:items="args.items"
|
||||
>
|
||||
<template #trigger="{ item }">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
|
||||
<span style="font-weight: 500;">{{ item.title }}</span>
|
||||
<span style="background: #e0e0e0; padding: 2px 8px; border-radius: 12px; font-size: 12px;">
|
||||
Custom
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content="{ item }">
|
||||
<div style="padding: 16px; background-color: #f8f8f8; border-radius: 4px;">
|
||||
<p style="margin: 0 0 12px 0;">{{ item.content }}</p>
|
||||
<button style="background: #007bff; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">
|
||||
Manage {{ item.title }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Accordion>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Direct composition pattern
|
||||
export const DirectComposition: Story = {
|
||||
render: () => ({
|
||||
components: { Accordion, AccordionItem, AccordionTrigger, AccordionContent },
|
||||
template: `
|
||||
<div>
|
||||
<h3 style="margin-bottom: 16px;">Direct Composition Pattern</h3>
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="permissions">
|
||||
<AccordionTrigger>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>🔒</span>
|
||||
<span>Permissions Management</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div style="padding: 16px;">
|
||||
<p style="margin: 0 0 12px 0;">Manage user permissions and access control</p>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<label><input type="checkbox" /> Read</label>
|
||||
<label><input type="checkbox" /> Write</label>
|
||||
<label><input type="checkbox" /> Delete</label>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="api-keys">
|
||||
<AccordionTrigger>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>🔑</span>
|
||||
<span>API Keys</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div style="padding: 16px;">
|
||||
<p style="margin: 0 0 12px 0;">Create and manage API keys</p>
|
||||
<button style="background: #28a745; color: white; border: none; padding: 8px 16px; border-radius: 4px;">
|
||||
Generate New Key
|
||||
</button>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="webhooks">
|
||||
<AccordionTrigger>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>🪝</span>
|
||||
<span>Webhooks</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div style="padding: 16px;">
|
||||
<p style="margin: 0 0 12px 0;">Configure webhook endpoints</p>
|
||||
<input type="text" placeholder="Webhook URL" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Multiple mode with default values
|
||||
export const MultipleMode: Story = {
|
||||
args: {
|
||||
type: 'multiple',
|
||||
defaultValue: ['item-1', 'item-3'],
|
||||
items: [
|
||||
{
|
||||
value: 'item-1',
|
||||
title: 'First Section (Default Open)',
|
||||
content: 'This section is open by default in multiple mode.',
|
||||
},
|
||||
{
|
||||
value: 'item-2',
|
||||
title: 'Second Section',
|
||||
content: 'Multiple sections can be open at the same time.',
|
||||
},
|
||||
{
|
||||
value: 'item-3',
|
||||
title: 'Third Section (Default Open)',
|
||||
content: 'This section is also open by default.',
|
||||
},
|
||||
],
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { Accordion },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<Accordion
|
||||
:type="args.type"
|
||||
:default-value="args.defaultValue"
|
||||
:items="args.items"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Mixed usage pattern
|
||||
export const MixedUsage: Story = {
|
||||
args: {
|
||||
items: [
|
||||
{
|
||||
value: 'auto-1',
|
||||
title: 'Auto-generated Item 1',
|
||||
content: 'This item comes from the items prop',
|
||||
},
|
||||
{
|
||||
value: 'auto-2',
|
||||
title: 'Auto-generated Item 2',
|
||||
content: 'This item also comes from the items prop',
|
||||
},
|
||||
],
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { Accordion, AccordionItem, AccordionTrigger, AccordionContent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h3 style="margin-bottom: 16px;">Mixed Usage (Direct + Items)</h3>
|
||||
<Accordion type="single" collapsible :items="args.items">
|
||||
<!-- Direct composition items render first -->
|
||||
<AccordionItem value="manual-1">
|
||||
<AccordionTrigger>
|
||||
<span style="color: #007bff;">📝 Manually Added Item</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div style="padding: 16px; background: #e3f2fd; border-radius: 4px;">
|
||||
This item was added using direct composition and appears before items from props.
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<!-- Items from props will render after -->
|
||||
</Accordion>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Dynamic component pattern
|
||||
export const DynamicComponents: Story = {
|
||||
render: () => ({
|
||||
components: { Accordion },
|
||||
setup() {
|
||||
// Mock components
|
||||
const PermissionsPanel = {
|
||||
template: `
|
||||
<div style="padding: 16px; background: #f0f8ff; border-radius: 4px;">
|
||||
<h4 style="margin: 0 0 8px 0;">Permission Settings</h4>
|
||||
<label style="display: block; margin: 4px 0;"><input type="checkbox" checked /> Admin Access</label>
|
||||
<label style="display: block; margin: 4px 0;"><input type="checkbox" /> Editor Access</label>
|
||||
<label style="display: block; margin: 4px 0;"><input type="checkbox" checked /> Viewer Access</label>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const ProfilePanel = {
|
||||
template: `
|
||||
<div style="padding: 16px; background: #f0fff0; border-radius: 4px;">
|
||||
<h4 style="margin: 0 0 8px 0;">User Profile</h4>
|
||||
<input type="text" value="John Doe" style="width: 100%; padding: 8px; margin: 4px 0; border: 1px solid #ddd; border-radius: 4px;" />
|
||||
<input type="email" value="john@example.com" style="width: 100%; padding: 8px; margin: 4px 0; border: 1px solid #ddd; border-radius: 4px;" />
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const SettingsPanel = {
|
||||
template: `
|
||||
<div style="padding: 16px; background: #fff0f0; border-radius: 4px;">
|
||||
<h4 style="margin: 0 0 8px 0;">Application Settings</h4>
|
||||
<label style="display: block; margin: 4px 0;"><input type="checkbox" checked /> Enable notifications</label>
|
||||
<label style="display: block; margin: 4px 0;"><input type="checkbox" /> Dark mode</label>
|
||||
<label style="display: block; margin: 4px 0;"><input type="checkbox" checked /> Auto-save</label>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
|
||||
const componentMap = {
|
||||
permissions: PermissionsPanel,
|
||||
profile: ProfilePanel,
|
||||
settings: SettingsPanel,
|
||||
};
|
||||
|
||||
const items = [
|
||||
{ value: 'item-1', title: 'Permissions', componentType: 'permissions' },
|
||||
{ value: 'item-2', title: 'Profile', componentType: 'profile' },
|
||||
{ value: 'item-3', title: 'Settings', componentType: 'settings' },
|
||||
];
|
||||
|
||||
return { items, componentMap };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h3 style="margin-bottom: 16px;">Dynamic Components Pattern</h3>
|
||||
<Accordion :items="items">
|
||||
<template #content="{ item }">
|
||||
<component :is="componentMap[item.componentType]" />
|
||||
</template>
|
||||
</Accordion>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Disabled items
|
||||
export const DisabledItems: Story = {
|
||||
args: {
|
||||
type: 'single',
|
||||
collapsible: true,
|
||||
items: [
|
||||
{
|
||||
value: 'item-1',
|
||||
title: 'Enabled Item',
|
||||
content: 'This item can be toggled.',
|
||||
},
|
||||
{
|
||||
value: 'item-2',
|
||||
title: 'Disabled Item',
|
||||
content: 'This item cannot be toggled.',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
value: 'item-3',
|
||||
title: 'Another Enabled Item',
|
||||
content: 'This item can also be toggled.',
|
||||
},
|
||||
],
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { Accordion },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<Accordion
|
||||
:type="args.type"
|
||||
:collapsible="args.collapsible"
|
||||
:items="args.items"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
266
unraid-ui/stories/components/common/Dialog.stories.ts
Normal file
266
unraid-ui/stories/components/common/Dialog.stories.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import { ref } from 'vue';
|
||||
import Button from '../../../src/components/common/button/Button.vue';
|
||||
import DialogComponent from '../../../src/components/common/dialog/Dialog.vue';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Common/Dialog',
|
||||
component: DialogComponent,
|
||||
} satisfies Meta<typeof DialogComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Dialog: Story = {
|
||||
args: {
|
||||
title: 'Dialog Title',
|
||||
description: "This is a dialog description that explains what's happening.",
|
||||
triggerText: 'Open Dialog',
|
||||
showFooter: true,
|
||||
closeButtonText: 'Close',
|
||||
primaryButtonText: 'Save Changes',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { DialogComponent },
|
||||
setup() {
|
||||
const isOpen = ref(false);
|
||||
const handlePrimaryClick = () => {
|
||||
console.log('Primary button clicked');
|
||||
isOpen.value = false;
|
||||
};
|
||||
return { args, isOpen, handlePrimaryClick };
|
||||
},
|
||||
template: `
|
||||
<DialogComponent
|
||||
v-model="isOpen"
|
||||
:title="args.title"
|
||||
:description="args.description"
|
||||
:triggerText="args.triggerText"
|
||||
:showFooter="args.showFooter"
|
||||
:closeButtonText="args.closeButtonText"
|
||||
:primaryButtonText="args.primaryButtonText"
|
||||
@primary-click="handlePrimaryClick"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SimpleDialog: Story = {
|
||||
args: {
|
||||
title: 'Delete Item?',
|
||||
description: 'This action cannot be undone.',
|
||||
triggerText: 'Delete',
|
||||
primaryButtonText: 'Confirm Delete',
|
||||
},
|
||||
};
|
||||
|
||||
export const DialogWithoutPrimaryButton: Story = {
|
||||
args: {
|
||||
title: 'Information',
|
||||
description: 'This is just an informational dialog.',
|
||||
triggerText: 'Show Info',
|
||||
showFooter: true,
|
||||
closeButtonText: 'Got it',
|
||||
},
|
||||
};
|
||||
|
||||
export const DialogWithCustomContent: Story = {
|
||||
render: () => ({
|
||||
components: { DialogComponent, Button },
|
||||
setup() {
|
||||
const isOpen = ref(false);
|
||||
return { isOpen };
|
||||
},
|
||||
template: `
|
||||
<DialogComponent v-model="isOpen" title="Custom Content Dialog">
|
||||
<template #trigger>
|
||||
<Button variant="secondary">Custom Trigger Button</Button>
|
||||
</template>
|
||||
<div style="padding: 20px 0;">
|
||||
<p>This dialog has custom content in the body.</p>
|
||||
<p>You can put any HTML or Vue components here.</p>
|
||||
<div style="margin-top: 20px; padding: 20px; background: #f5f5f5; border-radius: 4px;">
|
||||
<code>Custom content area</code>
|
||||
</div>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const DialogWithCustomFooter: Story = {
|
||||
render: () => ({
|
||||
components: { DialogComponent, Button },
|
||||
setup() {
|
||||
const isOpen = ref(false);
|
||||
const handleCancel = () => {
|
||||
console.log('Cancel clicked');
|
||||
isOpen.value = false;
|
||||
};
|
||||
const handleSave = () => {
|
||||
console.log('Save clicked');
|
||||
isOpen.value = false;
|
||||
};
|
||||
const handleSaveAndClose = () => {
|
||||
console.log('Save and Close clicked');
|
||||
isOpen.value = false;
|
||||
};
|
||||
return { isOpen, handleCancel, handleSave, handleSaveAndClose };
|
||||
},
|
||||
template: `
|
||||
<DialogComponent v-model="isOpen" title="Custom Footer">
|
||||
<template #trigger>
|
||||
<Button>Open with Custom Footer</Button>
|
||||
</template>
|
||||
<p>This dialog has a completely custom footer with multiple actions.</p>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%;">
|
||||
<Button variant="ghost" @click="handleCancel">Cancel</Button>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<Button variant="secondary" @click="handleSave">Save</Button>
|
||||
<Button variant="primary" @click="handleSaveAndClose">Save and Close</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DialogComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const DialogWithNoFooter: Story = {
|
||||
render: () => ({
|
||||
components: { DialogComponent },
|
||||
setup() {
|
||||
const isOpen = ref(false);
|
||||
return { isOpen };
|
||||
},
|
||||
template: `
|
||||
<DialogComponent
|
||||
v-model="isOpen"
|
||||
title="No Footer Dialog"
|
||||
description="This dialog has no footer buttons."
|
||||
triggerText="Open Dialog"
|
||||
:showFooter="false"
|
||||
>
|
||||
<p style="padding: 20px 0;">Content without any footer buttons.</p>
|
||||
</DialogComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const DialogControlledProgrammatically: Story = {
|
||||
render: () => ({
|
||||
components: { DialogComponent, Button },
|
||||
setup() {
|
||||
const isOpen = ref(false);
|
||||
const openDialog = () => {
|
||||
isOpen.value = true;
|
||||
};
|
||||
const closeDialog = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
return { isOpen, openDialog, closeDialog };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<Button @click="openDialog">Open Dialog Programmatically</Button>
|
||||
<DialogComponent
|
||||
v-model="isOpen"
|
||||
title="Controlled Dialog"
|
||||
description="This dialog is controlled programmatically without a trigger."
|
||||
>
|
||||
<p>This dialog was opened programmatically!</p>
|
||||
<template #footer>
|
||||
<Button @click="closeDialog">Close</Button>
|
||||
</template>
|
||||
</DialogComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const DialogSizes: Story = {
|
||||
render: () => ({
|
||||
components: { DialogComponent, Button },
|
||||
setup() {
|
||||
const dialogs = ref({
|
||||
sm: false,
|
||||
md: false,
|
||||
lg: false,
|
||||
xl: false,
|
||||
});
|
||||
return { dialogs };
|
||||
},
|
||||
template: `
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
|
||||
<DialogComponent
|
||||
v-model="dialogs.sm"
|
||||
size="sm"
|
||||
title="Small Dialog"
|
||||
description="This is a small dialog (max-width: 24rem)"
|
||||
triggerText="Small (sm)"
|
||||
primaryButtonText="Save"
|
||||
>
|
||||
<p>This dialog has a small size, perfect for simple confirmations or brief messages.</p>
|
||||
</DialogComponent>
|
||||
|
||||
<DialogComponent
|
||||
v-model="dialogs.md"
|
||||
size="md"
|
||||
title="Medium Dialog"
|
||||
description="This is a medium dialog (max-width: 32rem)"
|
||||
triggerText="Medium (md)"
|
||||
primaryButtonText="Save"
|
||||
>
|
||||
<p>This is the default dialog size, suitable for most use cases with moderate content.</p>
|
||||
</DialogComponent>
|
||||
|
||||
<DialogComponent
|
||||
v-model="dialogs.lg"
|
||||
size="lg"
|
||||
title="Large Dialog"
|
||||
description="This is a large dialog (max-width: 42rem)"
|
||||
triggerText="Large (lg)"
|
||||
primaryButtonText="Save"
|
||||
>
|
||||
<p>Large dialogs provide more space for complex forms or detailed content that requires more horizontal space.</p>
|
||||
<div style="margin-top: 16px; padding: 16px; background: #f5f5f5; border-radius: 4px;">
|
||||
<p>Additional content area to demonstrate the increased width.</p>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
|
||||
<DialogComponent
|
||||
v-model="dialogs.xl"
|
||||
size="xl"
|
||||
title="Extra Large Dialog"
|
||||
description="This is an extra large dialog (max-width: 56rem)"
|
||||
triggerText="Extra Large (xl)"
|
||||
primaryButtonText="Save"
|
||||
>
|
||||
<p>Extra large dialogs are ideal for complex interfaces, data tables, or content that needs maximum horizontal space.</p>
|
||||
<div style="margin-top: 16px;">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr style="border-bottom: 1px solid #ddd;">
|
||||
<th style="text-align: left; padding: 8px;">Column 1</th>
|
||||
<th style="text-align: left; padding: 8px;">Column 2</th>
|
||||
<th style="text-align: left; padding: 8px;">Column 3</th>
|
||||
<th style="text-align: left; padding: 8px;">Column 4</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="border-bottom: 1px solid #eee;">
|
||||
<td style="padding: 8px;">Data</td>
|
||||
<td style="padding: 8px;">Data</td>
|
||||
<td style="padding: 8px;">Data</td>
|
||||
<td style="padding: 8px;">Data</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</DialogComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -1,24 +1,18 @@
|
||||
import { DropdownMenu } from '@/components/common/dropdown-menu';
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import { MoreVertical } from 'lucide-vue-next';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuArrow,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/common/dropdown-menu';
|
||||
import { Button } from '@/components/common/button';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Common/DropdownMenu',
|
||||
component: DropdownMenu,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
open: {
|
||||
control: 'boolean',
|
||||
description: 'Controls the open state of the dropdown menu',
|
||||
align: {
|
||||
control: { type: 'select' },
|
||||
options: ['start', 'center', 'end'],
|
||||
},
|
||||
side: {
|
||||
control: { type: 'select' },
|
||||
options: ['top', 'right', 'bottom', 'left'],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof DropdownMenu>;
|
||||
@@ -27,66 +21,226 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Dropdown: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
DropdownMenuArrow,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
Button,
|
||||
// Basic props-based usage
|
||||
export const BasicUsage: Story = {
|
||||
args: {
|
||||
trigger: 'Options',
|
||||
items: [{ label: 'Profile' }, { label: 'Settings' }, { type: 'separator' }, { label: 'Logout' }],
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { DropdownMenu },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div class="bg-gray-200 p-4 h-screen">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="secondary">Open Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
<DropdownMenuArrow />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu
|
||||
:trigger="args.trigger"
|
||||
:items="args.items"
|
||||
:align="args.align"
|
||||
:side="args.side"
|
||||
:side-offset="args.sideOffset"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// User account menu
|
||||
export const UserAccountMenu: Story = {
|
||||
args: {
|
||||
trigger: 'John Doe',
|
||||
items: [
|
||||
{ type: 'label', label: 'john.doe@example.com' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Profile Settings' },
|
||||
{ label: 'Account Security' },
|
||||
{ label: 'Billing & Plans' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Help & Support' },
|
||||
{ label: 'Keyboard Shortcuts', disabled: true },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Sign Out' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// File operations menu
|
||||
export const FileOperationsMenu: Story = {
|
||||
args: {
|
||||
trigger: 'File Actions',
|
||||
items: [
|
||||
{ label: 'New File' },
|
||||
{ label: 'New Folder' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Copy' },
|
||||
{ label: 'Cut' },
|
||||
{ label: 'Paste', disabled: true },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Rename' },
|
||||
{ label: 'Delete' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Properties' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Context menu style
|
||||
export const ContextMenu: Story = {
|
||||
args: {
|
||||
trigger: 'Right Click Me',
|
||||
align: 'start',
|
||||
items: [
|
||||
{ label: 'Back', disabled: true },
|
||||
{ label: 'Forward', disabled: true },
|
||||
{ label: 'Reload' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Save As...' },
|
||||
{ label: 'Print...' },
|
||||
{ label: 'Cast...' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'View Page Source' },
|
||||
{ label: 'Inspect' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Settings menu with grouped items
|
||||
export const SettingsMenu: Story = {
|
||||
args: {
|
||||
trigger: '⚙️ Settings',
|
||||
items: [
|
||||
{ type: 'label', label: 'Appearance' },
|
||||
{ label: 'Theme' },
|
||||
{ label: 'Font Size' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'label', label: 'Privacy' },
|
||||
{ label: 'Clear Browsing Data' },
|
||||
{ label: 'Cookie Settings' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'label', label: 'Advanced' },
|
||||
{ label: 'Developer Options' },
|
||||
{ label: 'Experimental Features' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Dropdown with click handlers
|
||||
export const WithClickHandlers: Story = {
|
||||
args: {
|
||||
trigger: 'Actions',
|
||||
items: [
|
||||
{
|
||||
label: 'Alert',
|
||||
onClick: () => alert('Alert clicked!'),
|
||||
},
|
||||
{
|
||||
label: 'Console Log',
|
||||
onClick: () => console.log('Console log clicked!'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Disabled Action',
|
||||
disabled: true,
|
||||
onClick: () => console.log('This should not fire'),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Different alignments
|
||||
export const AlignmentVariations: Story = {
|
||||
render: () => ({
|
||||
components: { DropdownMenu },
|
||||
template: `
|
||||
<div style="display: flex; justify-content: space-around; padding: 100px 20px;">
|
||||
<DropdownMenu
|
||||
trigger="Align Start"
|
||||
:items="[{ label: 'Item 1' }, { label: 'Item 2' }, { label: 'Item 3' }]"
|
||||
align="start"
|
||||
/>
|
||||
<DropdownMenu
|
||||
trigger="Align Center"
|
||||
:items="[{ label: 'Item 1' }, { label: 'Item 2' }, { label: 'Item 3' }]"
|
||||
align="center"
|
||||
/>
|
||||
<DropdownMenu
|
||||
trigger="Align End"
|
||||
:items="[{ label: 'Item 1' }, { label: 'Item 2' }, { label: 'Item 3' }]"
|
||||
align="end"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const IconDropdown: Story = {
|
||||
// Different side positions
|
||||
export const SidePositions: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
Button,
|
||||
MoreVertical,
|
||||
},
|
||||
template: `
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Duplicate</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
<DropdownMenuArrow />
|
||||
</DropdownMenuContent>
|
||||
components: { DropdownMenu },
|
||||
template: `
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 400px; gap: 40px;">
|
||||
<DropdownMenu
|
||||
trigger="Top"
|
||||
:items="[{ label: 'Item 1' }, { label: 'Item 2' }]"
|
||||
side="top"
|
||||
/>
|
||||
<DropdownMenu
|
||||
trigger="Right"
|
||||
:items="[{ label: 'Item 1' }, { label: 'Item 2' }]"
|
||||
side="right"
|
||||
/>
|
||||
<DropdownMenu
|
||||
trigger="Bottom"
|
||||
:items="[{ label: 'Item 1' }, { label: 'Item 2' }]"
|
||||
side="bottom"
|
||||
/>
|
||||
<DropdownMenu
|
||||
trigger="Left"
|
||||
:items="[{ label: 'Item 1' }, { label: 'Item 2' }]"
|
||||
side="left"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Custom trigger with slot
|
||||
export const CustomTrigger: Story = {
|
||||
render: () => ({
|
||||
components: { DropdownMenu },
|
||||
template: `
|
||||
<DropdownMenu
|
||||
:items="[
|
||||
{ label: 'Edit' },
|
||||
{ label: 'Duplicate' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Archive' },
|
||||
{ label: 'Delete' }
|
||||
]"
|
||||
>
|
||||
<template #trigger>
|
||||
<button style="padding: 8px 16px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Custom Button Trigger
|
||||
</button>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
// Long menu with scroll
|
||||
export const LongMenuWithScroll: Story = {
|
||||
args: {
|
||||
trigger: 'Many Options',
|
||||
items: Array.from({ length: 20 }, (_, i) => ({
|
||||
label: `Option ${i + 1}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
// Empty state
|
||||
export const EmptyState: Story = {
|
||||
args: {
|
||||
trigger: 'Empty Menu',
|
||||
items: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import Bar from "../../../src/components/common/loading/Bar.vue";
|
||||
import Error from "../../../src/components/common/loading/Error.vue";
|
||||
import Spinner from "../../../src/components/common/loading/Spinner.vue";
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import Bar from '../../../src/components/common/loading/Bar.vue';
|
||||
import Error from '../../../src/components/common/loading/Error.vue';
|
||||
import Spinner from '../../../src/components/common/loading/Spinner.vue';
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common/Loading",
|
||||
title: 'Components/Common/Loading',
|
||||
component: Bar,
|
||||
subcomponents: { Bar, Spinner, Error },
|
||||
} satisfies Meta<typeof Bar>;
|
||||
@@ -17,17 +17,17 @@ type ErrorStory = StoryObj<typeof Error>;
|
||||
|
||||
export const LoadingBar: BarStory = {
|
||||
args: {},
|
||||
render: (args) => ({
|
||||
render: () => ({
|
||||
components: { Bar },
|
||||
template: `<div class="w-full max-w-md"><Bar v-bind="args" /></div>`,
|
||||
template: `<div class="w-full max-w-md"><Bar /></div>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const LoadingSpinner: SpinnerStory = {
|
||||
args: {},
|
||||
render: (args) => ({
|
||||
render: () => ({
|
||||
components: { Spinner },
|
||||
template: `<div class="p-4"><Spinner v-bind="args" /></div>`,
|
||||
template: `<div class="p-4"><Spinner /></div>`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export const LoadingError: ErrorStory = {
|
||||
args: {
|
||||
loading: false,
|
||||
error: null,
|
||||
class: "",
|
||||
class: '',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { Error },
|
||||
@@ -50,4 +50,4 @@ export const LoadingError: ErrorStory = {
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,11 +7,7 @@ import SheetFooter from '../../../src/components/common/sheet/SheetFooter.vue';
|
||||
import SheetHeader from '../../../src/components/common/sheet/SheetHeader.vue';
|
||||
import SheetTitle from '../../../src/components/common/sheet/SheetTitle.vue';
|
||||
import SheetTrigger from '../../../src/components/common/sheet/SheetTrigger.vue';
|
||||
import Select from '../../../src/components/form/select/Select.vue';
|
||||
import SelectContent from '../../../src/components/form/select/SelectContent.vue';
|
||||
import SelectItem from '../../../src/components/form/select/SelectItem.vue';
|
||||
import SelectTrigger from '../../../src/components/form/select/SelectTrigger.vue';
|
||||
import SelectValue from '../../../src/components/form/select/SelectValue.vue';
|
||||
import { Select } from '../../../src/components/form/select';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Common',
|
||||
@@ -38,7 +34,7 @@ export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Sheet: Story = {
|
||||
render: (args) => ({
|
||||
render: () => ({
|
||||
components: {
|
||||
SheetComponent,
|
||||
SheetTrigger,
|
||||
@@ -107,10 +103,6 @@ export const SheetWithSelect: Story = {
|
||||
SheetFooter,
|
||||
Button,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -134,16 +126,15 @@ export const SheetWithSelect: Story = {
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium">Theme</label>
|
||||
<Select v-model="theme">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a theme" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="light">Light</SelectItem>
|
||||
<SelectItem value="dark">Dark</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select
|
||||
v-model="theme"
|
||||
placeholder="Select a theme"
|
||||
:items="[
|
||||
{ value: 'light', label: 'Light' },
|
||||
{ value: 'dark', label: 'Dark' },
|
||||
{ value: 'system', label: 'System' }
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,92 +1,349 @@
|
||||
import { Select } from '@/components/form/select';
|
||||
import type { Meta, StoryObj } from '@storybook/vue3';
|
||||
import {
|
||||
Select as SelectComponent,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/form/select';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Form/Select',
|
||||
component: SelectComponent,
|
||||
} satisfies Meta<typeof SelectComponent>;
|
||||
component: Select,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'A custom Select component that accepts an items prop for easy rendering of options. Supports simple arrays, object arrays, and grouped items with labels and separators.',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
placeholder: {
|
||||
control: 'text',
|
||||
description: 'Placeholder text when no value is selected',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the select is disabled',
|
||||
},
|
||||
multiple: {
|
||||
control: 'boolean',
|
||||
description: 'Whether multiple items can be selected',
|
||||
},
|
||||
valueKey: {
|
||||
control: 'text',
|
||||
description: 'Key to use for item values when using object arrays',
|
||||
},
|
||||
labelKey: {
|
||||
control: 'text',
|
||||
description: 'Key to use for item labels when using object arrays',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Select>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Select: Story = {
|
||||
export const SimpleArray: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
SelectComponent,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
},
|
||||
components: { Select },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<SelectComponent>
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Fruits</SelectLabel>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="orange">Orange</SelectItem>
|
||||
<SelectItem value="grape">Grape</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</SelectComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
const value = ref(null);
|
||||
|
||||
export const Grouped: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
SelectComponent,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
},
|
||||
setup() {
|
||||
return { args };
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<SelectComponent>
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select a food" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Fruits</SelectLabel>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="grape">Grape</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Vegetables</SelectLabel>
|
||||
<SelectItem value="carrot">Carrot</SelectItem>
|
||||
<SelectItem value="potato">Potato</SelectItem>
|
||||
<SelectItem value="celery">Celery</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</SelectComponent>
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected: {{ value }}</p>
|
||||
<p class="text-sm">Items: {{ args.items }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select a fruit',
|
||||
items: ['Apple', 'Banana', 'Orange', 'Grape', 'Mango'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ObjectArray: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref(null);
|
||||
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected: {{ value }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select a color',
|
||||
items: [
|
||||
{ value: 'red', label: 'Red' },
|
||||
{ value: 'green', label: 'Green' },
|
||||
{ value: 'blue', label: 'Blue' },
|
||||
{ value: 'yellow', label: 'Yellow' },
|
||||
{ value: 'purple', label: 'Purple' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const GroupedItems: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref(null);
|
||||
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected: {{ value }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select food',
|
||||
items: [
|
||||
[
|
||||
{ type: 'label', label: 'Fruits' },
|
||||
{ value: 'apple', label: 'Apple' },
|
||||
{ value: 'banana', label: 'Banana' },
|
||||
{ value: 'orange', label: 'Orange' },
|
||||
],
|
||||
[
|
||||
{ type: 'label', label: 'Vegetables' },
|
||||
{ value: 'carrot', label: 'Carrot' },
|
||||
{ value: 'lettuce', label: 'Lettuce' },
|
||||
{ value: 'tomato', label: 'Tomato' },
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDisabledItems: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref(null);
|
||||
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected: {{ value }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select an option',
|
||||
items: [
|
||||
{ value: 'active', label: 'Active' },
|
||||
{ value: 'disabled1', label: 'Disabled Option 1', disabled: true },
|
||||
{ value: 'enabled', label: 'Enabled' },
|
||||
{ value: 'disabled2', label: 'Disabled Option 2', disabled: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithSeparators: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref(null);
|
||||
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected: {{ value }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select action',
|
||||
items: [
|
||||
{ value: 'new', label: 'New File' },
|
||||
{ value: 'open', label: 'Open File' },
|
||||
{ type: 'separator' },
|
||||
{ value: 'save', label: 'Save' },
|
||||
{ value: 'saveas', label: 'Save As...' },
|
||||
{ type: 'separator' },
|
||||
{ value: 'exit', label: 'Exit' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ControlledValue: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref('banana');
|
||||
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected value: {{ value }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select a fruit',
|
||||
items: [
|
||||
{ value: 'apple', label: 'Apple' },
|
||||
{ value: 'banana', label: 'Banana' },
|
||||
{ value: 'orange', label: 'Orange' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleSelection: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref(['apple', 'orange']);
|
||||
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:multiple="args.multiple"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected values: {{ Array.isArray(value) ? value.join(', ') : value }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select fruits',
|
||||
multiple: true,
|
||||
items: [
|
||||
{ value: 'apple', label: 'Apple' },
|
||||
{ value: 'banana', label: 'Banana' },
|
||||
{ value: 'orange', label: 'Orange' },
|
||||
{ value: 'grape', label: 'Grape' },
|
||||
{ value: 'mango', label: 'Mango' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const CustomSlots: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref(null);
|
||||
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full" :style="{ backgroundColor: item.color }"></span>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
<p class="text-sm text-muted-foreground">Selected: {{ value }}</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'Select a color',
|
||||
items: [
|
||||
{ value: 'red', label: 'Red', color: '#ef4444' },
|
||||
{ value: 'green', label: 'Green', color: '#22c55e' },
|
||||
{ value: 'blue', label: 'Blue', color: '#3b82f6' },
|
||||
{ value: 'yellow', label: 'Yellow', color: '#eab308' },
|
||||
{ value: 'purple', label: 'Purple', color: '#a855f7' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const DisabledSelect: Story = {
|
||||
render: (args) => ({
|
||||
components: { Select },
|
||||
setup() {
|
||||
const value = ref(null);
|
||||
return { args, value };
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select
|
||||
v-model="value"
|
||||
:items="args.items"
|
||||
:placeholder="args.placeholder"
|
||||
:disabled="args.disabled"
|
||||
:valueKey="args.valueKey"
|
||||
:labelKey="args.labelKey"
|
||||
/>
|
||||
<p class="text-sm text-muted-foreground">Selected: {{ value }}</p>
|
||||
<p class="text-xs text-muted-foreground">The entire select is disabled</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
placeholder: 'This select is disabled',
|
||||
disabled: true,
|
||||
items: [
|
||||
{ value: 'option1', label: 'Option 1' },
|
||||
{ value: 'option2', label: 'Option 2' },
|
||||
{ value: 'option3', label: 'Option 3' },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,10 +77,10 @@ const stubs = {
|
||||
UpcUptimeExpire: { template: '<div data-testid="uptime-expire"></div>', props: ['t'] },
|
||||
UpcServerState: { template: '<div data-testid="server-state"></div>', props: ['t'] },
|
||||
NotificationsSidebar: { template: '<div data-testid="notifications-sidebar"></div>' },
|
||||
UpcDropdownMenu: {
|
||||
DropdownMenu: {
|
||||
template: '<div data-testid="dropdown-menu"><slot name="trigger" /><slot /></div>',
|
||||
props: ['t'],
|
||||
},
|
||||
UpcDropdownContent: { template: '<div data-testid="dropdown-content"></div>', props: ['t'] },
|
||||
UpcDropdownTrigger: { template: '<button data-testid="dropdown-trigger"></button>', props: ['t'] },
|
||||
};
|
||||
|
||||
|
||||
@@ -40,5 +40,9 @@ const MockBrandButton = {
|
||||
vi.mock('@unraid/ui', () => ({
|
||||
cn: mockCn,
|
||||
BrandButton: MockBrandButton,
|
||||
DropdownMenu: {
|
||||
name: 'DropdownMenu',
|
||||
template: '<div><slot name="trigger" /><slot /></div>',
|
||||
},
|
||||
// Add other UI components as needed
|
||||
}));
|
||||
|
||||
@@ -10,18 +10,9 @@ import {
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogScrollContent,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@unraid/ui';
|
||||
import { extractGraphQLErrorMessage } from '~/helpers/functions';
|
||||
|
||||
@@ -185,7 +176,7 @@ async function upsertKey() {
|
||||
const fragmentData = useFragment(API_KEY_FRAGMENT_WITH_KEY, apiKeyResult.create);
|
||||
apiKeyStore.setCreatedKey(fragmentData);
|
||||
}
|
||||
|
||||
|
||||
modalVisible.value = false;
|
||||
editingKey.value = null;
|
||||
newKeyName.value = '';
|
||||
@@ -200,135 +191,119 @@ async function upsertKey() {
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:open="modalVisible"
|
||||
@close="close"
|
||||
@update:open="
|
||||
v-model="modalVisible"
|
||||
size="lg"
|
||||
:title="editingKey ? t('Edit API Key') : t('Create API Key')"
|
||||
:scrollable="true"
|
||||
close-button-text="Cancel"
|
||||
:primary-button-text="editingKey ? 'Save' : 'Create'"
|
||||
:primary-button-loading="loading || postCreateLoading"
|
||||
:primary-button-loading-text="editingKey ? 'Saving...' : 'Creating...'"
|
||||
:primary-button-disabled="loading || postCreateLoading"
|
||||
@update:model-value="
|
||||
(v) => {
|
||||
if (!v) close();
|
||||
}
|
||||
"
|
||||
@primary-click="upsertKey"
|
||||
>
|
||||
<DialogScrollContent class="max-w-800px">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ editingKey ? t('Edit API Key') : t('Create API Key') }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
<form @submit.prevent="upsertKey">
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-name">Name</Label>
|
||||
<Input id="api-key-name" v-model="newKeyName" placeholder="Name" class="mt-1" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-desc">Description</Label>
|
||||
<Input
|
||||
id="api-key-desc"
|
||||
v-model="newKeyDescription"
|
||||
placeholder="Description"
|
||||
class="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-roles">Roles</Label>
|
||||
<Select v-model="newKeyRoles" multiple class="mt-1 w-full">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Roles" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="role in possibleRoles" :key="role" :value="role">{{
|
||||
role
|
||||
}}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-permissions">Permissions</Label>
|
||||
<Accordion id="api-key-permissions" type="single" collapsible class="w-full mt-2">
|
||||
<AccordionItem value="permissions">
|
||||
<AccordionTrigger>
|
||||
<PermissionCounter
|
||||
:permissions="newKeyPermissions"
|
||||
:possible-permissions="possiblePermissions"
|
||||
/>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="flex flex-row justify-end my-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
type="button"
|
||||
@click="
|
||||
areAllPermissionsSelected() ? clearAllPermissions() : selectAllPermissions()
|
||||
"
|
||||
>
|
||||
{{ areAllPermissionsSelected() ? 'Clear All' : 'Select All' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mt-1">
|
||||
<div
|
||||
v-for="perm in possiblePermissions"
|
||||
:key="perm.resource"
|
||||
class="rounded-sm p-2 border"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-semibold">{{ perm.resource }}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
type="button"
|
||||
@click="
|
||||
areAllActionsSelected(perm.resource)
|
||||
? clearAllActions(perm.resource)
|
||||
: selectAllActions(perm.resource)
|
||||
<div class="max-w-800px">
|
||||
<form @submit.prevent="upsertKey">
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-name">Name</Label>
|
||||
<Input id="api-key-name" v-model="newKeyName" placeholder="Name" class="mt-1" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-desc">Description</Label>
|
||||
<Input id="api-key-desc" v-model="newKeyDescription" placeholder="Description" class="mt-1" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-roles">Roles</Label>
|
||||
<Select
|
||||
v-model="newKeyRoles"
|
||||
:items="possibleRoles"
|
||||
:multiple="true"
|
||||
:placeholder="'Select Roles'"
|
||||
class="mt-1 w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<Label for="api-key-permissions">Permissions</Label>
|
||||
<Accordion id="api-key-permissions" type="single" collapsible class="w-full mt-2">
|
||||
<AccordionItem value="permissions">
|
||||
<AccordionTrigger>
|
||||
<PermissionCounter
|
||||
:permissions="newKeyPermissions"
|
||||
:possible-permissions="possiblePermissions"
|
||||
/>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div class="flex flex-row justify-end my-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
type="button"
|
||||
@click="areAllPermissionsSelected() ? clearAllPermissions() : selectAllPermissions()"
|
||||
>
|
||||
{{ areAllPermissionsSelected() ? 'Select None' : 'Select All' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 mt-1">
|
||||
<div
|
||||
v-for="perm in possiblePermissions"
|
||||
:key="perm.resource"
|
||||
class="rounded-sm p-2 border"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="font-semibold">{{ perm.resource }}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
type="button"
|
||||
@click="
|
||||
areAllActionsSelected(perm.resource)
|
||||
? clearAllActions(perm.resource)
|
||||
: selectAllActions(perm.resource)
|
||||
"
|
||||
>
|
||||
{{ areAllActionsSelected(perm.resource) ? 'Select None' : 'Select All' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<label
|
||||
v-for="action in perm.actions"
|
||||
:key="action"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="
|
||||
!!newKeyPermissions.find(
|
||||
(p) => p.resource === perm.resource && p.actions.includes(action)
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ areAllActionsSelected(perm.resource) ? 'Clear All' : 'Select All' }}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<label
|
||||
v-for="action in perm.actions"
|
||||
:key="action"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="
|
||||
!!newKeyPermissions.find(
|
||||
(p) => p.resource === perm.resource && p.actions.includes(action)
|
||||
@change="
|
||||
(e: Event) =>
|
||||
togglePermission(
|
||||
perm.resource,
|
||||
action,
|
||||
(e.target as HTMLInputElement)?.checked
|
||||
)
|
||||
"
|
||||
@change="
|
||||
(e: Event) =>
|
||||
togglePermission(
|
||||
perm.resource,
|
||||
action,
|
||||
(e.target as HTMLInputElement)?.checked
|
||||
)
|
||||
"
|
||||
/>
|
||||
<span class="text-sm">{{ action }}</span>
|
||||
</label>
|
||||
</div>
|
||||
"
|
||||
/>
|
||||
<span class="text-sm">{{ action }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
<div v-if="error" class="text-red-500 mt-2 text-sm">
|
||||
{{ extractGraphQLErrorMessage(error) }}
|
||||
</div>
|
||||
</form>
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" @click="close">Cancel</Button>
|
||||
<Button variant="primary" :disabled="loading || postCreateLoading" @click="upsertKey()">
|
||||
<span v-if="loading || postCreateLoading">
|
||||
{{ editingKey ? 'Saving...' : 'Creating...' }}
|
||||
</span>
|
||||
<span v-else>{{ editingKey ? 'Save' : 'Create' }}</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogScrollContent>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
<div v-if="error" class="text-red-500 mt-2 text-sm">
|
||||
{{ extractGraphQLErrorMessage(error) }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -4,12 +4,6 @@ import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable';
|
||||
import {
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
@@ -42,6 +36,14 @@ const { mutate: recalculateOverview } = useMutation(resetOverview);
|
||||
const { determineTeleportTarget } = useTeleport();
|
||||
const importance = ref<Importance | undefined>(undefined);
|
||||
|
||||
const filterItems = [
|
||||
{ type: 'label' as const, label: 'Notification Types' },
|
||||
{ label: 'All Types', value: 'all' },
|
||||
{ label: 'Alert', value: Importance.ALERT },
|
||||
{ label: 'Info', value: Importance.INFO },
|
||||
{ label: 'Warning', value: Importance.WARNING },
|
||||
];
|
||||
|
||||
const confirmAndArchiveAll = async () => {
|
||||
if (confirm('This will archive all notifications on your Unraid server. Continue?')) {
|
||||
await archiveAll();
|
||||
@@ -176,26 +178,16 @@ const prepareToViewNotifications = () => {
|
||||
</TabsContent>
|
||||
|
||||
<Select
|
||||
:items="filterItems"
|
||||
placeholder="Filter By"
|
||||
class="h-auto"
|
||||
@update:model-value="
|
||||
(val: unknown) => {
|
||||
const strVal = String(val);
|
||||
importance = strVal === 'all' || !strVal ? undefined : (strVal as Importance);
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="h-auto">
|
||||
<SelectValue class="text-gray-400 leading-6" placeholder="Filter By" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Notification Types</SelectLabel>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem :value="Importance.ALERT"> Alert </SelectItem>
|
||||
<SelectItem :value="Importance.INFO">Info</SelectItem>
|
||||
<SelectItem :value="Importance.WARNING">Warning</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TabsContent value="unread" class="flex-col flex-1 min-h-0">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
|
||||
import { DropdownMenu } from '@unraid/ui';
|
||||
import { devConfig } from '~/helpers/env';
|
||||
|
||||
import type { Server } from '~/types/server';
|
||||
@@ -143,11 +144,16 @@ onMounted(() => {
|
||||
|
||||
<NotificationsSidebar />
|
||||
|
||||
<UpcDropdownMenu :t="t">
|
||||
<DropdownMenu align="end" side="bottom" :side-offset="4">
|
||||
<template #trigger>
|
||||
<UpcDropdownTrigger :t="t" />
|
||||
</template>
|
||||
</UpcDropdownMenu>
|
||||
<template #content>
|
||||
<div class="max-w-[350px] sm:min-w-[350px]">
|
||||
<UpcDropdownContent :t="t" />
|
||||
</div>
|
||||
</template>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { DropdownMenu, DropdownMenuArrow, DropdownMenuContent, DropdownMenuTrigger } from '@unraid/ui';
|
||||
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
defineProps<{ t: ComposerTranslation }>();
|
||||
|
||||
const open = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu v-model:open="open">
|
||||
<DropdownMenuTrigger>
|
||||
<slot name="trigger" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent :side-offset="4" :align="'end'" :side="'bottom'" class="w-[350px]">
|
||||
<UpcDropdownContent :t="t" />
|
||||
<DropdownMenuArrow :rounded="true" class="fill-popover" :height="10" :width="16" />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
@@ -11,15 +11,12 @@ import {
|
||||
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
|
||||
const props = defineProps<{ t: ComposerTranslation }>();
|
||||
|
||||
const dropdownStore = useDropdownStore();
|
||||
const { dropdownVisible } = storeToRefs(dropdownStore);
|
||||
const { errors } = storeToRefs(useErrorsStore());
|
||||
const { rebootType, state, stateData } = storeToRefs(useServerStore());
|
||||
const { available: osUpdateAvailable } = storeToRefs(useUpdateOsStore());
|
||||
@@ -43,15 +40,14 @@ const title = computed((): string => {
|
||||
if (showErrorIcon.value) {
|
||||
return props.t('Learn more about the error');
|
||||
}
|
||||
return dropdownVisible.value ? props.t('Close Dropdown') : props.t('Open Dropdown');
|
||||
return props.t('Open Dropdown');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="group text-18px border-0 relative flex flex-row justify-end items-center h-full gap-x-8px opacity-100 hover:opacity-75 focus:opacity-75 transition-opacity text-header-text-primary"
|
||||
class="group text-18px border-0 relative flex flex-row justify-end items-center h-full gap-x-8px opacity-100 hover:opacity-75 transition-opacity text-header-text-primary"
|
||||
:title="title"
|
||||
@click="dropdownStore.dropdownToggle()"
|
||||
>
|
||||
<template v-if="errors.length && errors[0].level">
|
||||
<InformationCircleIcon
|
||||
|
||||
4
web/helpers/globals.d.ts
vendored
4
web/helpers/globals.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
declare global {
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var csrf_token: string;
|
||||
}
|
||||
|
||||
// an export or import statement is required to make this file a module
|
||||
export {};
|
||||
export {};
|
||||
|
||||
@@ -89,10 +89,10 @@
|
||||
"@floating-ui/vue": "^1.1.5",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@jsonforms/core": "^3.5.1",
|
||||
"@jsonforms/vue": "^3.5.1",
|
||||
"@jsonforms/vue-vanilla": "^3.5.1",
|
||||
"@jsonforms/vue-vuetify": "^3.5.1",
|
||||
"@jsonforms/core": "^3.6.0",
|
||||
"@jsonforms/vue": "^3.6.0",
|
||||
"@jsonforms/vue-vanilla": "^3.6.0",
|
||||
"@jsonforms/vue-vuetify": "^3.6.0",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@pinia/nuxt": "^0.11.0",
|
||||
"@unraid/shared-callbacks": "^1.1.1",
|
||||
@@ -105,7 +105,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"focus-trap": "^7.6.2",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^6.0.0",
|
||||
"hex-to-rgba": "^2.0.1",
|
||||
|
||||
Reference in New Issue
Block a user