mirror of
https://github.com/unraid/api.git
synced 2025-12-30 13:09:52 -06:00
feat: unraid ui component library (#976)
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -24,6 +24,7 @@ build/Release
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
unraid-ui/node_modules/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
@@ -64,6 +65,7 @@ test/__temp__/*
|
||||
|
||||
# Built files
|
||||
dist
|
||||
unraid-ui/storybook-static
|
||||
|
||||
# Typescript
|
||||
typescript
|
||||
@@ -74,7 +76,7 @@ typescript
|
||||
# Github actions
|
||||
RELEASE_NOTES.md
|
||||
|
||||
# Docker Deploy Folder
|
||||
# Docker Deploy Folder
|
||||
deploy/*
|
||||
!deploy/.gitkeep
|
||||
|
||||
@@ -89,4 +91,4 @@ deploy/*
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
fb_keepalive
|
||||
fb_keepalive
|
||||
|
||||
2
unraid-ui/.npmrc
Normal file
2
unraid-ui/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
46
unraid-ui/.storybook/main.ts
Normal file
46
unraid-ui/.storybook/main.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { StorybookConfig } from "@storybook/vue3-vite";
|
||||
import { resolve } from "path";
|
||||
import { mergeConfig } from "vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../stories/**/*.stories.@(js|mjs|ts)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-controls",
|
||||
{
|
||||
name: "@storybook/addon-postcss",
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require("postcss"),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/vue3-vite",
|
||||
options: {
|
||||
docgen: "vue-component-meta",
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
return mergeConfig(config, {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "../src"),
|
||||
"@/components": resolve(__dirname, "../src/components"),
|
||||
"@/lib": resolve(__dirname, "../src/lib"),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
postcss: "./postcss.config.js",
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
16
unraid-ui/.storybook/preview.ts
Normal file
16
unraid-ui/.storybook/preview.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Preview } from "@storybook/vue3";
|
||||
import "../src/styles/globals.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
10
unraid-ui/.storybook/tailwind.config.ts
Normal file
10
unraid-ui/.storybook/tailwind.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import baseConfig from "../tailwind.config";
|
||||
|
||||
export default {
|
||||
...baseConfig,
|
||||
content: [
|
||||
"../src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
"../stories/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"../.storybook/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
};
|
||||
9
unraid-ui/.storybook/tsconfig.json
Normal file
9
unraid-ui/.storybook/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["../src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["../stories/**/*", "../src/**/*"]
|
||||
}
|
||||
122
unraid-ui/README.md
Normal file
122
unraid-ui/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Unraid UI
|
||||
|
||||
A Vue 3 component library providing a set of reusable, accessible UI components for Unraid development.
|
||||
|
||||
## Features
|
||||
|
||||
- ⚡️ Built with Vue 3 and TypeScript
|
||||
- 🎭 Storybook documentation
|
||||
- ✅ Tested components
|
||||
- 🎪 Built on top of TailwindCSS and Shadcn/UI
|
||||
|
||||
## Installation
|
||||
|
||||
Make sure you have the peer dependencies installed:
|
||||
|
||||
```bash
|
||||
npm install vue@^3.3.0 tailwindcss@^3.0.0
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Add CSS
|
||||
|
||||
Import the component library styles in your main entry file:
|
||||
|
||||
```typescript
|
||||
import "@unraid/ui/style.css";
|
||||
```
|
||||
|
||||
### 2. Configure TailwindCSS
|
||||
|
||||
Add the following to your `tailwind.config.js`:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
content: [
|
||||
// ... your content paths
|
||||
"./node_modules/@unraid/ui/**/*.{js,vue,ts}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||
};
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { Button } from "@unraid/ui";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button variant="primary"> Click me </Button>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Local Development
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Start Storybook development server:
|
||||
|
||||
```bash
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
This will start Storybook at [http://localhost:6006](http://localhost:6006)
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
Run tests with UI:
|
||||
|
||||
```bash
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
Generate coverage report:
|
||||
|
||||
```bash
|
||||
npm run coverage
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
- `dev`: Start development server
|
||||
- `build`: Build for production
|
||||
- `preview`: Preview production build
|
||||
- `test`: Run tests
|
||||
- `test:ui`: Run tests with UI
|
||||
- `coverage`: Generate test coverage
|
||||
- `clean`: Remove build artifacts
|
||||
- `typecheck`: Run type checking
|
||||
- `storybook`: Start Storybook development server
|
||||
- `build-storybook`: Build Storybook for production
|
||||
|
||||
## License
|
||||
19
unraid-ui/components.json
Normal file
19
unraid-ui/components.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "default",
|
||||
"typescript": true,
|
||||
"tsConfigPath": "./tsconfig.json",
|
||||
"tailwind": {
|
||||
"config": "./tailwind.config.js",
|
||||
"css": "./src/styles/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"framework": "vite",
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"types": "@/types"
|
||||
}
|
||||
}
|
||||
11111
unraid-ui/package-lock.json
generated
Normal file
11111
unraid-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
unraid-ui/package.json
Normal file
74
unraid-ui/package.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "@unraid/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && vue-tsc --emitDeclarationOnly",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"coverage": "vitest run --coverage",
|
||||
"clean": "rm -rf dist",
|
||||
"prebuild": "npm run clean",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": "^3.0.0",
|
||||
"vue": "^3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@vueuse/core": "^10.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-vue-next": "^0.468.0",
|
||||
"radix-vue": "^1.9.11",
|
||||
"shadcn-vue": "^0.11.3",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-controls": "^8.4.7",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/vue3-vite": "^8.4.7",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@testing-library/vue": "^8.0.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/testing-library__vue": "^5.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"@vitest/coverage-v8": "^1.0.0",
|
||||
"@vitest/ui": "^1.0.0",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"@vue/tsconfig": "^0.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"happy-dom": "^12.0.0",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.0.0",
|
||||
"vitest": "^1.0.0",
|
||||
"vue": "^3.3.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./style.css": "./dist/style.css"
|
||||
}
|
||||
}
|
||||
6
unraid-ui/postcss.config.js
Normal file
6
unraid-ui/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
127
unraid-ui/src/components/common/badge/Badge.vue
Normal file
127
unraid-ui/src/components/common/badge/Badge.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { UiBadgeProps } from "@/types/badge";
|
||||
|
||||
const props = withDefaults(defineProps<UiBadgeProps>(), {
|
||||
color: "gray",
|
||||
icon: undefined,
|
||||
iconRight: undefined,
|
||||
iconStyles: "",
|
||||
size: "16px",
|
||||
});
|
||||
|
||||
const computedStyleClasses = computed(() => {
|
||||
let colorClasses = "";
|
||||
let textSize = "";
|
||||
let iconSize = "";
|
||||
switch (props.color) {
|
||||
case "red":
|
||||
colorClasses =
|
||||
"bg-unraid-red text-white group-hover:bg-orange-dark group-focus:bg-orange-dark";
|
||||
break;
|
||||
case "yellow":
|
||||
colorClasses =
|
||||
"bg-yellow-100 text-black group-hover:bg-yellow-200 group-focus:bg-yellow-200";
|
||||
break;
|
||||
case "green":
|
||||
colorClasses =
|
||||
"bg-green-200 text-green-800 group-hover:bg-green-300 group-focus:bg-green-300";
|
||||
break;
|
||||
case "blue":
|
||||
colorClasses =
|
||||
"bg-blue-100 text-blue-800 group-hover:bg-blue-200 group-focus:bg-blue-200";
|
||||
break;
|
||||
case "indigo":
|
||||
colorClasses =
|
||||
"bg-indigo-100 text-indigo-800 group-hover:bg-indigo-200 group-focus:bg-indigo-200";
|
||||
break;
|
||||
case "purple":
|
||||
colorClasses =
|
||||
"bg-purple-100 text-purple-800 group-hover:bg-purple-200 group-focus:bg-purple-200";
|
||||
break;
|
||||
case "pink":
|
||||
colorClasses =
|
||||
"bg-pink-100 text-pink-800 group-hover:bg-pink-200 group-focus:bg-pink-200";
|
||||
break;
|
||||
case "orange":
|
||||
colorClasses =
|
||||
"bg-orange text-white group-hover:bg-orange-dark group-focus:bg-orange-dark";
|
||||
break;
|
||||
case "black":
|
||||
colorClasses =
|
||||
"bg-black text-white group-hover:bg-gray-800 group-focus:bg-gray-800";
|
||||
break;
|
||||
case "white":
|
||||
colorClasses =
|
||||
"bg-white text-black group-hover:bg-gray-100 group-focus:bg-gray-100";
|
||||
break;
|
||||
case "transparent":
|
||||
colorClasses =
|
||||
"bg-transparent text-black group-hover:bg-gray-100 group-focus:bg-gray-100";
|
||||
break;
|
||||
case "current":
|
||||
colorClasses =
|
||||
"bg-current text-black group-hover:bg-gray-100 group-focus:bg-gray-100";
|
||||
break;
|
||||
case "gray":
|
||||
colorClasses =
|
||||
"bg-gray-200 text-gray-800 group-hover:bg-gray-300 group-focus:bg-gray-300";
|
||||
break;
|
||||
case "custom":
|
||||
colorClasses = "";
|
||||
break;
|
||||
}
|
||||
switch (props.size) {
|
||||
case "12px":
|
||||
textSize = "text-12px px-8px py-4px gap-4px";
|
||||
iconSize = "w-12px";
|
||||
break;
|
||||
case "14px":
|
||||
textSize = "text-14px px-8px py-4px gap-8px";
|
||||
iconSize = "w-14px";
|
||||
break;
|
||||
case "16px":
|
||||
textSize = "text-16px px-12px py-8px gap-8px";
|
||||
iconSize = "w-16px";
|
||||
break;
|
||||
case "18px":
|
||||
textSize = "text-18px px-12px py-8px gap-8px";
|
||||
iconSize = "w-18px";
|
||||
break;
|
||||
case "20px":
|
||||
textSize = "text-20px px-16px py-12px gap-8px";
|
||||
iconSize = "w-20px";
|
||||
break;
|
||||
case "24px":
|
||||
textSize = "text-24px px-16px py-12px gap-8px";
|
||||
iconSize = "w-24px";
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
badge: `${textSize} ${colorClasses}`,
|
||||
icon: `${iconSize} ${props.iconStyles}`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="inline-flex items-center rounded-full font-semibold leading-none transition-all duration-200 ease-in-out"
|
||||
:class="[computedStyleClasses.badge]"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="flex-shrink-0"
|
||||
:class="computedStyleClasses.icon"
|
||||
/>
|
||||
<slot />
|
||||
<component
|
||||
:is="iconRight"
|
||||
v-if="iconRight"
|
||||
class="flex-shrink-0"
|
||||
:class="computedStyleClasses.icon"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
3
unraid-ui/src/components/common/badge/index.ts
Normal file
3
unraid-ui/src/components/common/badge/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Badge from "./Badge.vue";
|
||||
|
||||
export { Badge };
|
||||
54
unraid-ui/src/components/common/button/Button.test.ts
Normal file
54
unraid-ui/src/components/common/button/Button.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/vue";
|
||||
import Button from "./Button.vue";
|
||||
|
||||
describe("Button", () => {
|
||||
it("renders correctly with default props", () => {
|
||||
render(Button, {
|
||||
slots: {
|
||||
default: "Click me",
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText("Click me")).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders with different variants", () => {
|
||||
const { rerender } = render(Button, {
|
||||
props: { variant: "destructive" },
|
||||
slots: { default: "Delete" },
|
||||
});
|
||||
|
||||
rerender({
|
||||
props: { variant: "outline" },
|
||||
slots: { default: "Delete" },
|
||||
});
|
||||
});
|
||||
|
||||
it("renders with different sizes", () => {
|
||||
const { rerender } = render(Button, {
|
||||
props: { size: "sm" },
|
||||
slots: { default: "Small Button" },
|
||||
});
|
||||
|
||||
rerender({
|
||||
props: { size: "lg" },
|
||||
slots: { default: "Large Button" },
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts and applies additional classes", () => {
|
||||
render(Button, {
|
||||
props: {
|
||||
class: "custom-class",
|
||||
},
|
||||
slots: {
|
||||
default: "Custom Button",
|
||||
},
|
||||
});
|
||||
|
||||
const button = screen.getByRole("button");
|
||||
|
||||
expect(button.classList.contains("custom-class")).toBe(true);
|
||||
});
|
||||
});
|
||||
35
unraid-ui/src/components/common/button/Button.vue
Normal file
35
unraid-ui/src/components/common/button/Button.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ButtonVariants } from "./button.variants";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
variant?:
|
||||
| "primary"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link";
|
||||
size?: "sm" | "md" | "lg" | "icon";
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
});
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return cn(
|
||||
ButtonVariants({ variant: props.variant, size: props.size }),
|
||||
props.class
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :class="buttonClass">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
30
unraid-ui/src/components/common/button/button.variants.ts
Normal file
30
unraid-ui/src/components/common/button/button.variants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export const ButtonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
sm: "h-9 rounded-md px-3",
|
||||
md: "h-10 px-4 py-2",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
},
|
||||
}
|
||||
);
|
||||
2
unraid-ui/src/components/common/button/index.ts
Normal file
2
unraid-ui/src/components/common/button/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Button } from "./Button.vue";
|
||||
export { ButtonVariants } from "./button.variants";
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuRoot,
|
||||
type DropdownMenuRootEmits,
|
||||
type DropdownMenuRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
|
||||
const props = defineProps<DropdownMenuRootProps>();
|
||||
const emits = defineEmits<DropdownMenuRootEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
type DropdownMenuCheckboxItemEmits,
|
||||
type DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuCheckboxItemProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const emits = defineEmits<DropdownMenuCheckboxItemEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuCheckboxItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Check class="w-4 h-4" />
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuCheckboxItem>
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
type DropdownMenuContentEmits,
|
||||
type DropdownMenuContentProps,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
sideOffset: 4,
|
||||
class: undefined,
|
||||
}
|
||||
);
|
||||
const emits = defineEmits<DropdownMenuContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md 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
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuGroup, type DropdownMenuGroupProps } from "radix-vue";
|
||||
|
||||
const props = defineProps<DropdownMenuGroupProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuGroup v-bind="props">
|
||||
<slot />
|
||||
</DropdownMenuGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
type DropdownMenuItemProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuItemProps & { class?: HTMLAttributes["class"]; inset?: boolean }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuLabel,
|
||||
type DropdownMenuLabelProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuLabelProps & { class?: HTMLAttributes["class"]; inset?: boolean }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuLabel
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuLabel>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuRadioGroup,
|
||||
type DropdownMenuRadioGroupEmits,
|
||||
type DropdownMenuRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
|
||||
const props = defineProps<DropdownMenuRadioGroupProps>();
|
||||
const emits = defineEmits<DropdownMenuRadioGroupEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioGroup v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuRadioGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
type DropdownMenuRadioItemEmits,
|
||||
type DropdownMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { Circle } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuRadioItemProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const emits = defineEmits<DropdownMenuRadioItemEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuRadioItem
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</DropdownMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</DropdownMenuRadioItem>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
type DropdownMenuSeparatorProps,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSeparatorProps & {
|
||||
class?: HTMLAttributes["class"];
|
||||
}
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSeparator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DropdownMenuSub,
|
||||
type DropdownMenuSubEmits,
|
||||
type DropdownMenuSubProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
|
||||
const props = defineProps<DropdownMenuSubProps>();
|
||||
const emits = defineEmits<DropdownMenuSubEmits>();
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSub v-bind="forwarded">
|
||||
<slot />
|
||||
</DropdownMenuSub>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSubContentProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
const emits = defineEmits<DropdownMenuSubContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DropdownMenuSubContent>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
type DropdownMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronRight } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DropdownMenuSubTriggerProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuSubTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</DropdownMenuSubTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DropdownMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</DropdownMenuTrigger>
|
||||
</template>
|
||||
16
unraid-ui/src/components/common/dropdown-menu/index.ts
Normal file
16
unraid-ui/src/components/common/dropdown-menu/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export { DropdownMenuPortal } from 'radix-vue'
|
||||
|
||||
export { default as DropdownMenu } from './DropdownMenu.vue'
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
||||
12
unraid-ui/src/components/common/loading/Bar.vue
Normal file
12
unraid-ui/src/components/common/loading/Bar.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = withDefaults(defineProps<{ class?: string }>(), { class: "" });
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="cn('h-5 animate-pulse bg-gray-300 w-full', props.class)"
|
||||
role="progressbar"
|
||||
aria-label="Loading"
|
||||
/>
|
||||
</template>
|
||||
62
unraid-ui/src/components/common/loading/Error.vue
Normal file
62
unraid-ui/src/components/common/loading/Error.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { ShieldExclamationIcon } from "@heroicons/vue/24/solid";
|
||||
import LoadingSpinner from "./Spinner.vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* A default container for displaying loading and error states.
|
||||
*
|
||||
* By default, this component will expand to full height and display contents
|
||||
* in the center of the container.
|
||||
*
|
||||
* Any slot/child will only render when a loading/error state isn't displayed.
|
||||
*
|
||||
* Exposes a 'retry' event (user-triggered during error state).
|
||||
*
|
||||
* @example
|
||||
* <LoadingError @retry="retryFunction" :loading="loading" :error="error" />
|
||||
*
|
||||
* <LoadingError :loading="loading" :error="error">
|
||||
* <p>Only displayed when both loading and error are false.</p>
|
||||
* </LoadingError>
|
||||
*/
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: string;
|
||||
/** hasdfsa */
|
||||
loading: boolean;
|
||||
error: Error | null | undefined;
|
||||
}>(),
|
||||
{ class: "" }
|
||||
);
|
||||
|
||||
defineEmits(["retry"]);
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn('h-full flex flex-col items-center justify-center gap-3', props.class)
|
||||
"
|
||||
>
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="contents">
|
||||
<LoadingSpinner />
|
||||
<p>Loading Notifications...</p>
|
||||
</div>
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="space-y-3">
|
||||
<div class="flex justify-center">
|
||||
<ShieldExclamationIcon class="h-10 text-unraid-red" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold">{{ `Error` }}</h3>
|
||||
<p>{{ error.message }}</p>
|
||||
</div>
|
||||
<Button type="button" class="w-full" @click="$emit('retry')"
|
||||
>Try Again</Button
|
||||
>
|
||||
</div>
|
||||
<!-- Default state -->
|
||||
<slot v-else />
|
||||
</div>
|
||||
</template>
|
||||
19
unraid-ui/src/components/common/loading/Spinner.vue
Normal file
19
unraid-ui/src/components/common/loading/Spinner.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = withDefaults(defineProps<{ class?: string }>(), { class: "" });
|
||||
</script>
|
||||
<template>
|
||||
<!-- adapted from https://tw-elements.com/docs/standard/components/spinners/ -->
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'inline-block h-8 w-8 animate-spin rounded-full border-2 border-solid border-current border-e-transparent align-[-0.125em] text-primary motion-reduce:animate-[spin_1.5s_linear_infinite]',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
role="status"
|
||||
>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</template>
|
||||
5
unraid-ui/src/components/common/loading/index.ts
Normal file
5
unraid-ui/src/components/common/loading/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Bar from "./Bar.vue";
|
||||
import Error from "./Error.vue";
|
||||
import Spinner from "./Spinner.vue";
|
||||
|
||||
export { Bar, Error, Spinner };
|
||||
34
unraid-ui/src/components/common/scroll-area/ScrollArea.vue
Normal file
34
unraid-ui/src/components/common/scroll-area/ScrollArea.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
type ScrollAreaRootProps,
|
||||
ScrollAreaViewport,
|
||||
} from "radix-vue";
|
||||
import ScrollBar from "./ScrollBar.vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
ScrollAreaRootProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaRoot
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('relative overflow-hidden', props.class)"
|
||||
>
|
||||
<ScrollAreaViewport class="h-full w-full rounded-[inherit]">
|
||||
<slot />
|
||||
</ScrollAreaViewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</template>
|
||||
41
unraid-ui/src/components/common/scroll-area/ScrollBar.vue
Normal file
41
unraid-ui/src/components/common/scroll-area/ScrollBar.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
ScrollAreaScrollbar,
|
||||
type ScrollAreaScrollbarProps,
|
||||
ScrollAreaThumb,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
orientation: "vertical",
|
||||
class: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaScrollbar
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' &&
|
||||
'h-full w-2.5 border-l border-l-transparent p-px',
|
||||
orientation === 'horizontal' &&
|
||||
'h-2.5 flex-col border-t border-t-transparent p-px',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaScrollbar>
|
||||
</template>
|
||||
2
unraid-ui/src/components/common/scroll-area/index.ts
Normal file
2
unraid-ui/src/components/common/scroll-area/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ScrollArea } from './ScrollArea.vue'
|
||||
export { default as ScrollBar } from './ScrollBar.vue'
|
||||
55
unraid-ui/src/components/common/sheet/Sheet.vue
Normal file
55
unraid-ui/src/components/common/sheet/Sheet.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted } from "vue";
|
||||
import {
|
||||
DialogRoot,
|
||||
useForwardPropsEmits,
|
||||
type DialogRootEmits,
|
||||
type DialogRootProps,
|
||||
} from "radix-vue";
|
||||
|
||||
const MOBILE_VIEWPORT =
|
||||
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" as const;
|
||||
|
||||
const props = defineProps<DialogRootProps & { class?: string }>();
|
||||
const emits = defineEmits<DialogRootEmits>();
|
||||
|
||||
const getViewport = (): string => {
|
||||
return (
|
||||
document.querySelector('meta[name="viewport"]')?.getAttribute("content") ??
|
||||
"width=1300"
|
||||
);
|
||||
};
|
||||
const updateViewport = (viewport: string): void => {
|
||||
if (window.innerWidth < 500) {
|
||||
const meta = document.querySelector('meta[name="viewport"]');
|
||||
if (meta) {
|
||||
meta.setAttribute("content", viewport);
|
||||
} else {
|
||||
const meta = document.createElement("meta");
|
||||
meta.name = "viewport";
|
||||
meta.content = viewport;
|
||||
document.head.appendChild(meta);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
const initialViewport = ref(getViewport());
|
||||
const openListener = (opened: boolean) => {
|
||||
if (opened) {
|
||||
updateViewport(MOBILE_VIEWPORT);
|
||||
} else {
|
||||
updateViewport(initialViewport.value);
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
updateViewport(initialViewport.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="forwarded" @update:open="openListener">
|
||||
<slot />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
11
unraid-ui/src/components/common/sheet/SheetClose.vue
Normal file
11
unraid-ui/src/components/common/sheet/SheetClose.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose v-bind="props">
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
62
unraid-ui/src/components/common/sheet/SheetContent.vue
Normal file
62
unraid-ui/src/components/common/sheet/SheetContent.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { X } from "lucide-vue-next";
|
||||
import { type SheetVariants, sheetVariants } from ".";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
class?: HTMLAttributes["class"];
|
||||
side?: SheetVariants["side"];
|
||||
padding?: SheetVariants["padding"];
|
||||
disabled?: boolean;
|
||||
forceMount?: boolean;
|
||||
to?: string | HTMLElement | Element;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<SheetContentProps>();
|
||||
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, side, padding, ...delegated } = props;
|
||||
return delegated;
|
||||
});
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal
|
||||
:disabled="disabled"
|
||||
:force-mount="forceMount"
|
||||
:to="to as HTMLElement"
|
||||
>
|
||||
<DialogOverlay
|
||||
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
|
||||
/>
|
||||
<DialogContent
|
||||
:class="cn(sheetVariants({ side, padding }), props.class)"
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
|
||||
>
|
||||
<X class="w-4 h-4 text-muted-foreground" />
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
||||
24
unraid-ui/src/components/common/sheet/SheetDescription.vue
Normal file
24
unraid-ui/src/components/common/sheet/SheetDescription.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { DialogDescription, type DialogDescriptionProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DialogDescriptionProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
||||
19
unraid-ui/src/components/common/sheet/SheetFooter.vue
Normal file
19
unraid-ui/src/components/common/sheet/SheetFooter.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
14
unraid-ui/src/components/common/sheet/SheetHeader.vue
Normal file
14
unraid-ui/src/components/common/sheet/SheetHeader.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('flex flex-col gap-y-2 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
24
unraid-ui/src/components/common/sheet/SheetTitle.vue
Normal file
24
unraid-ui/src/components/common/sheet/SheetTitle.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { DialogTitle, type DialogTitleProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
DialogTitleProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
:class="cn('text-lg font-medium text-foreground', props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
||||
11
unraid-ui/src/components/common/sheet/SheetTrigger.vue
Normal file
11
unraid-ui/src/components/common/sheet/SheetTrigger.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
||||
36
unraid-ui/src/components/common/sheet/index.ts
Normal file
36
unraid-ui/src/components/common/sheet/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
|
||||
export { default as Sheet } from "./Sheet.vue";
|
||||
export { default as SheetTrigger } from "./SheetTrigger.vue";
|
||||
export { default as SheetClose } from "./SheetClose.vue";
|
||||
export { default as SheetContent } from "./SheetContent.vue";
|
||||
export { default as SheetHeader } from "./SheetHeader.vue";
|
||||
export { default as SheetTitle } from "./SheetTitle.vue";
|
||||
export { default as SheetDescription } from "./SheetDescription.vue";
|
||||
export { default as SheetFooter } from "./SheetFooter.vue";
|
||||
|
||||
export const sheetVariants = cva(
|
||||
"fixed z-50 bg-muted dark:bg-background gap-4 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
padding: {
|
||||
none: "",
|
||||
md: "p-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
padding: "md",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export type SheetVariants = VariantProps<typeof sheetVariants>;
|
||||
15
unraid-ui/src/components/common/tabs/Tabs.vue
Normal file
15
unraid-ui/src/components/common/tabs/Tabs.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { TabsRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { TabsRootEmits, TabsRootProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<TabsRootProps>()
|
||||
const emits = defineEmits<TabsRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</TabsRoot>
|
||||
</template>
|
||||
29
unraid-ui/src/components/common/tabs/TabsContent.vue
Normal file
29
unraid-ui/src/components/common/tabs/TabsContent.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TabsContent, type TabsContentProps } from "radix-vue";
|
||||
import { computed, type HTMLAttributes } from "vue";
|
||||
|
||||
const props = defineProps<
|
||||
TabsContentProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsContent
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
class="data-[state=active]:flex data-[state=inactive]:hidden"
|
||||
>
|
||||
<slot />
|
||||
</TabsContent>
|
||||
</template>
|
||||
29
unraid-ui/src/components/common/tabs/TabsList.vue
Normal file
29
unraid-ui/src/components/common/tabs/TabsList.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { TabsList, type TabsListProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
TabsListProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsList
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'inline-flex items-center justify-center rounded-md bg-input p-1.5 text-foreground',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</TabsList>
|
||||
</template>
|
||||
33
unraid-ui/src/components/common/tabs/TabsTrigger.vue
Normal file
33
unraid-ui/src/components/common/tabs/TabsTrigger.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { TabsTrigger, type TabsTriggerProps, useForwardProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
TabsTriggerProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TabsTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded px-4.5 py-2.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="truncate">
|
||||
<slot />
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
</template>
|
||||
4
unraid-ui/src/components/common/tabs/index.ts
Normal file
4
unraid-ui/src/components/common/tabs/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Tabs } from './Tabs.vue'
|
||||
export { default as TabsTrigger } from './TabsTrigger.vue'
|
||||
export { default as TabsList } from './TabsList.vue'
|
||||
export { default as TabsContent } from './TabsContent.vue'
|
||||
14
unraid-ui/src/components/common/tooltip/Tooltip.vue
Normal file
14
unraid-ui/src/components/common/tooltip/Tooltip.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { TooltipRoot, type TooltipRootEmits, type TooltipRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||
|
||||
const props = defineProps<TooltipRootProps>()
|
||||
const emits = defineEmits<TooltipRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</TooltipRoot>
|
||||
</template>
|
||||
52
unraid-ui/src/components/common/tooltip/TooltipContent.vue
Normal file
52
unraid-ui/src/components/common/tooltip/TooltipContent.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
TooltipContent,
|
||||
type TooltipContentEmits,
|
||||
type TooltipContentProps,
|
||||
TooltipPortal,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import useTeleport from "@/composables/useTeleport";
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(),
|
||||
{
|
||||
sideOffset: 4,
|
||||
class: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
const emits = defineEmits<TooltipContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
|
||||
const { teleportTarget } = useTeleport();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipPortal :to="teleportTarget as HTMLElement" defer>
|
||||
<TooltipContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</template>
|
||||
11
unraid-ui/src/components/common/tooltip/TooltipProvider.vue
Normal file
11
unraid-ui/src/components/common/tooltip/TooltipProvider.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { TooltipProvider, type TooltipProviderProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<TooltipProviderProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipProvider v-bind="props">
|
||||
<slot />
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
11
unraid-ui/src/components/common/tooltip/TooltipTrigger.vue
Normal file
11
unraid-ui/src/components/common/tooltip/TooltipTrigger.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { TooltipTrigger, type TooltipTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<TooltipTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipTrigger v-bind="props">
|
||||
<slot />
|
||||
</TooltipTrigger>
|
||||
</template>
|
||||
4
unraid-ui/src/components/common/tooltip/index.ts
Normal file
4
unraid-ui/src/components/common/tooltip/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Tooltip } from './Tooltip.vue'
|
||||
export { default as TooltipContent } from './TooltipContent.vue'
|
||||
export { default as TooltipTrigger } from './TooltipTrigger.vue'
|
||||
export { default as TooltipProvider } from './TooltipProvider.vue'
|
||||
85
unraid-ui/src/components/form/Lightswitch.vue
Normal file
85
unraid-ui/src/components/form/Lightswitch.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @todo complete this component
|
||||
*/
|
||||
import { Switch, SwitchGroup, SwitchLabel } from "@headlessui/vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
description?: string; // @todo setup
|
||||
label: string;
|
||||
}>(),
|
||||
{
|
||||
description: "",
|
||||
}
|
||||
);
|
||||
|
||||
const checked = ref(false);
|
||||
</script>
|
||||
<template>
|
||||
<SwitchGroup as="div">
|
||||
<div class="flex flex-shrink-0 items-center gap-16px">
|
||||
<Switch
|
||||
v-model="checked"
|
||||
:class="[
|
||||
checked ? 'bg-green-500' : 'bg-gray-200',
|
||||
'relative inline-flex h-24px w-[44px] flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
checked ? 'translate-x-20px' : 'translate-x-0',
|
||||
'pointer-events-none relative inline-block h-20px w-20px transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
checked
|
||||
? 'opacity-0 duration-100 ease-out'
|
||||
: 'opacity-100 duration-200 ease-in',
|
||||
'absolute inset-0 flex h-full w-full items-center justify-center transition-opacity',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
class="h-12px w-12px text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path
|
||||
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
:class="[
|
||||
checked
|
||||
? 'opacity-100 duration-200 ease-in'
|
||||
: 'opacity-0 duration-100 ease-out',
|
||||
'absolute inset-0 flex h-full w-full items-center justify-center transition-opacity',
|
||||
]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
class="h-12px w-12px text-green-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path
|
||||
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</Switch>
|
||||
<SwitchLabel class="text-14px">
|
||||
{{ label }}
|
||||
</SwitchLabel>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
</template>
|
||||
42
unraid-ui/src/components/form/Switch.vue
Normal file
42
unraid-ui/src/components/form/Switch.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { Switch, SwitchGroup, SwitchLabel } from "@headlessui/vue";
|
||||
|
||||
export interface Props {
|
||||
label?: string;
|
||||
// propChecked?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
label: "",
|
||||
// propChecked: false,
|
||||
});
|
||||
|
||||
const checked = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchGroup>
|
||||
<div class="flex items-center gap-8px p-8px rounded">
|
||||
<Switch
|
||||
v-model="checked"
|
||||
:class="
|
||||
checked
|
||||
? 'bg-gradient-to-r from-unraid-red to-orange'
|
||||
: 'bg-transparent'
|
||||
"
|
||||
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
|
||||
>
|
||||
<span
|
||||
v-show="!checked"
|
||||
class="absolute z-0 inset-0 opacity-10 bg-primary"
|
||||
/>
|
||||
<span
|
||||
:class="checked ? 'translate-x-[26px]' : 'translate-x-[2px]'"
|
||||
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
|
||||
/>
|
||||
</Switch>
|
||||
<SwitchLabel>{{ label }}</SwitchLabel>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
</template>
|
||||
32
unraid-ui/src/components/form/input/Input.vue
Normal file
32
unraid-ui/src/components/form/input/Input.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: string | number;
|
||||
modelValue?: string | number;
|
||||
class?: HTMLAttributes["class"];
|
||||
}>();
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "update:modelValue", payload: string | number): void;
|
||||
}>();
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="modelValue"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
1
unraid-ui/src/components/form/input/index.ts
Normal file
1
unraid-ui/src/components/form/input/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Input } from './Input.vue'
|
||||
27
unraid-ui/src/components/form/label/Label.vue
Normal file
27
unraid-ui/src/components/form/label/Label.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { Label, type LabelProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Label
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</Label>
|
||||
</template>
|
||||
1
unraid-ui/src/components/form/label/index.ts
Normal file
1
unraid-ui/src/components/form/label/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Label } from './Label.vue'
|
||||
15
unraid-ui/src/components/form/select/Select.vue
Normal file
15
unraid-ui/src/components/form/select/Select.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'radix-vue'
|
||||
import { SelectRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
|
||||
const props = defineProps<SelectRootProps>()
|
||||
const emits = defineEmits<SelectRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</SelectRoot>
|
||||
</template>
|
||||
76
unraid-ui/src/components/form/select/SelectContent.vue
Normal file
76
unraid-ui/src/components/form/select/SelectContent.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectContent,
|
||||
type SelectContentEmits,
|
||||
type SelectContentProps,
|
||||
SelectPortal,
|
||||
SelectViewport,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from ".";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<
|
||||
SelectContentProps & {
|
||||
class?: HTMLAttributes["class"];
|
||||
disabled?: boolean;
|
||||
forceMount?: boolean;
|
||||
to?: string | HTMLElement | Element;
|
||||
}
|
||||
>(),
|
||||
{
|
||||
position: "popper",
|
||||
class: undefined,
|
||||
to: "#modals",
|
||||
}
|
||||
);
|
||||
const emits = defineEmits<SelectContentEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectPortal
|
||||
:disabled="disabled"
|
||||
:force-mount="forceMount"
|
||||
:to="to as HTMLElement"
|
||||
>
|
||||
<SelectContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover 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',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectViewport
|
||||
:class="
|
||||
cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[--radix-select-trigger-height] w-full min-w-[--radix-select-trigger-width]'
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</template>
|
||||
21
unraid-ui/src/components/form/select/SelectGroup.vue
Normal file
21
unraid-ui/src/components/form/select/SelectGroup.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { SelectGroup, type SelectGroupProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SelectGroupProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectGroup :class="cn('p-1 w-full', props.class)" v-bind="delegatedProps">
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</template>
|
||||
46
unraid-ui/src/components/form/select/SelectItem.vue
Normal file
46
unraid-ui/src/components/form/select/SelectItem.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
type SelectItemProps,
|
||||
SelectItemText,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SelectItemProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItem
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</SelectItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectItemText>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</template>
|
||||
11
unraid-ui/src/components/form/select/SelectItemText.vue
Normal file
11
unraid-ui/src/components/form/select/SelectItemText.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectItemText, type SelectItemTextProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<SelectItemTextProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItemText v-bind="props">
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</template>
|
||||
17
unraid-ui/src/components/form/select/SelectLabel.vue
Normal file
17
unraid-ui/src/components/form/select/SelectLabel.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { SelectLabel, type SelectLabelProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SelectLabelProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel
|
||||
:class="cn('py-1.5 pl-8 pr-2 text-sm font-semibold', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectScrollDownButton,
|
||||
type SelectScrollDownButtonProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronDown } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SelectScrollDownButtonProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollDownButton
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||
"
|
||||
>
|
||||
<slot>
|
||||
<ChevronDown class="h-4 w-4" />
|
||||
</slot>
|
||||
</SelectScrollDownButton>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectScrollUpButton,
|
||||
type SelectScrollUpButtonProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronUp } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SelectScrollUpButtonProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollUpButton
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||
"
|
||||
>
|
||||
<slot>
|
||||
<ChevronUp class="h-4 w-4" />
|
||||
</slot>
|
||||
</SelectScrollUpButton>
|
||||
</template>
|
||||
22
unraid-ui/src/components/form/select/SelectSeparator.vue
Normal file
22
unraid-ui/src/components/form/select/SelectSeparator.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import { SelectSeparator, type SelectSeparatorProps } from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SelectSeparatorProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('-mx-1 my-1 h-px bg-muted', props.class)"
|
||||
/>
|
||||
</template>
|
||||
40
unraid-ui/src/components/form/select/SelectTrigger.vue
Normal file
40
unraid-ui/src/components/form/select/SelectTrigger.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SelectIcon,
|
||||
SelectTrigger,
|
||||
type SelectTriggerProps,
|
||||
useForwardProps,
|
||||
} from "radix-vue";
|
||||
import { ChevronDown } from "lucide-vue-next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SelectTriggerProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectTrigger
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-4.5 py-3 text-base ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<SelectIcon as-child>
|
||||
<ChevronDown class="w-4 h-4 opacity-50 shrink-0" />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
</template>
|
||||
11
unraid-ui/src/components/form/select/SelectValue.vue
Normal file
11
unraid-ui/src/components/form/select/SelectValue.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { SelectValue, type SelectValueProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<SelectValueProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectValue v-bind="props">
|
||||
<slot />
|
||||
</SelectValue>
|
||||
</template>
|
||||
11
unraid-ui/src/components/form/select/index.ts
Normal file
11
unraid-ui/src/components/form/select/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export { default as Select } from './Select.vue'
|
||||
export { default as SelectValue } from './SelectValue.vue'
|
||||
export { default as SelectTrigger } from './SelectTrigger.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 SelectSeparator } from './SelectSeparator.vue'
|
||||
export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'
|
||||
export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'
|
||||
45
unraid-ui/src/components/form/switch/Switch.vue
Normal file
45
unraid-ui/src/components/form/switch/Switch.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { type HTMLAttributes, computed } from "vue";
|
||||
import {
|
||||
SwitchRoot,
|
||||
type SwitchRootEmits,
|
||||
type SwitchRootProps,
|
||||
SwitchThumb,
|
||||
useForwardPropsEmits,
|
||||
} from "radix-vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps<
|
||||
SwitchRootProps & { class?: HTMLAttributes["class"] }
|
||||
>();
|
||||
|
||||
const emits = defineEmits<SwitchRootEmits>();
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props;
|
||||
|
||||
return delegated;
|
||||
});
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<SwitchThumb
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</SwitchRoot>
|
||||
</template>
|
||||
5
unraid-ui/src/components/form/switch/index.ts
Normal file
5
unraid-ui/src/components/form/switch/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import Switch from "./Switch.vue";
|
||||
import SwitchHeadlessUI from "./SwitchHeadlessUI.vue";
|
||||
import Lightswitch from "./Lightswitch.vue";
|
||||
|
||||
export { Switch, SwitchHeadlessUI, Lightswitch };
|
||||
34
unraid-ui/src/components/layout/CardWrapper.vue
Normal file
34
unraid-ui/src/components/layout/CardWrapper.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
error?: boolean;
|
||||
hover?: boolean;
|
||||
increasedPadding?: boolean;
|
||||
padding?: boolean;
|
||||
warning?: boolean;
|
||||
}>(),
|
||||
{
|
||||
error: false,
|
||||
hover: true,
|
||||
increasedPadding: false,
|
||||
padding: true,
|
||||
warning: false,
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="group/card text-left relative flex flex-col flex-1 border-2 border-solid rounded-md shadow-md"
|
||||
:class="[
|
||||
padding && 'p-4',
|
||||
increasedPadding && 'md:p-6',
|
||||
hover && 'hover:shadow-orange/50 transition-all',
|
||||
error && 'text-white bg-unraid-red border-unraid-red',
|
||||
warning && 'text-black bg-yellow-100 border-yellow-100',
|
||||
!error && !warning && 'text-foreground bg-background border-muted',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
16
unraid-ui/src/components/layout/PageContainer.vue
Normal file
16
unraid-ui/src/components/layout/PageContainer.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(defineProps<{
|
||||
maxWidth?: string;
|
||||
}>(), {
|
||||
maxWidth: 'max-w-1024px',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid gap-y-24px w-full mx-auto px-16px"
|
||||
:class="maxWidth"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
4
unraid-ui/src/components/layout/index.ts
Normal file
4
unraid-ui/src/components/layout/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CardWrapper from "./CardWrapper.vue";
|
||||
import PageContainer from "./PageContainer.vue";
|
||||
|
||||
export { CardWrapper, PageContainer };
|
||||
27
unraid-ui/src/composables/useTeleport.ts
Normal file
27
unraid-ui/src/composables/useTeleport.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
const useTeleport = () => {
|
||||
const teleportTarget = ref<string | HTMLElement | Element>("#modals");
|
||||
|
||||
const determineTeleportTarget = () => {
|
||||
const myModalsComponent = document.querySelector("unraid-modals");
|
||||
if (!myModalsComponent?.shadowRoot) return;
|
||||
|
||||
const potentialTarget = myModalsComponent.shadowRoot.querySelector("#modals");
|
||||
if (!potentialTarget) return;
|
||||
|
||||
teleportTarget.value = potentialTarget;
|
||||
console.log("[determineTeleportTarget] teleportTarget", teleportTarget.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
determineTeleportTarget();
|
||||
});
|
||||
|
||||
return {
|
||||
teleportTarget,
|
||||
determineTeleportTarget,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTeleport;
|
||||
7
unraid-ui/src/env.d.ts
vendored
Normal file
7
unraid-ui/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
133
unraid-ui/src/index.ts
Normal file
133
unraid-ui/src/index.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// Lib
|
||||
import { cn, scaleRemFactor } from "@/lib/utils";
|
||||
|
||||
// Components
|
||||
import { Badge } from "@/components/common/badge";
|
||||
import { Button, ButtonVariants } from "@/components/common/button";
|
||||
import { CardWrapper, PageContainer } from "@/components/layout";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
} from "@/components/common/dropdown-menu";
|
||||
import { Bar, Error, Spinner } from "@/components/common/loading";
|
||||
import { Input } from "@/components/form/input";
|
||||
import { Label } from "@/components/form/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectItemText,
|
||||
SelectLabel,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/form/select";
|
||||
import {
|
||||
Switch,
|
||||
SwitchHeadlessUI,
|
||||
Lightswitch,
|
||||
} from "@/components/form/switch";
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@/components/common/tabs";
|
||||
import { ScrollArea, ScrollBar } from "@/components/common/scroll-area";
|
||||
import {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetClose,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from "@/components/common/sheet";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
TooltipProvider,
|
||||
} from "@/components/common/tooltip";
|
||||
|
||||
// Composables
|
||||
import useTeleport from "@/composables/useTeleport";
|
||||
|
||||
// Export
|
||||
export {
|
||||
Bar,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonVariants,
|
||||
CardWrapper,
|
||||
cn,
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
Error,
|
||||
Input,
|
||||
Label,
|
||||
PageContainer,
|
||||
scaleRemFactor,
|
||||
ScrollBar,
|
||||
ScrollArea,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectItemText,
|
||||
SelectLabel,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
Spinner,
|
||||
Switch,
|
||||
SwitchHeadlessUI,
|
||||
Lightswitch,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
TooltipProvider,
|
||||
useTeleport,
|
||||
};
|
||||
27
unraid-ui/src/lib/tailwind-rem-to-rem/index.ts
Normal file
27
unraid-ui/src/lib/tailwind-rem-to-rem/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import { scaleRemFactor } from "../utils";
|
||||
import defaultTheme from "tailwindcss/defaultTheme";
|
||||
import plugin from "tailwindcss/plugin";
|
||||
|
||||
export default plugin.withOptions(
|
||||
() => {
|
||||
return function () {
|
||||
// Plugin functionality can be added here if needed in the future
|
||||
};
|
||||
},
|
||||
(options?: {
|
||||
baseFontSize?: number;
|
||||
newFontSize?: number;
|
||||
}): Partial<Config> => {
|
||||
const baseFontSize = options?.baseFontSize ?? 16;
|
||||
const newFontSize = options?.newFontSize ?? 10;
|
||||
|
||||
return {
|
||||
theme: scaleRemFactor(
|
||||
defaultTheme,
|
||||
baseFontSize,
|
||||
newFontSize
|
||||
) as Config["theme"],
|
||||
};
|
||||
}
|
||||
);
|
||||
59
unraid-ui/src/lib/utils.ts
Normal file
59
unraid-ui/src/lib/utils.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
type RemToPxInput = unknown;
|
||||
|
||||
function isFunction(
|
||||
input: RemToPxInput
|
||||
): input is (...args: unknown[]) => boolean {
|
||||
return typeof input === "function";
|
||||
}
|
||||
|
||||
export function scaleRemFactor(
|
||||
input: RemToPxInput,
|
||||
baseFontSize = 16,
|
||||
newFontSize = 10
|
||||
): unknown {
|
||||
const scaleFactor = baseFontSize / newFontSize; // baseFontSize / newFontSize;
|
||||
if (input == null) return input;
|
||||
|
||||
if (typeof input === "string") {
|
||||
return input.replace(
|
||||
/(\d*\.?\d+)rem$/,
|
||||
(_, val) => `${(parseFloat(val) * scaleFactor).toFixed(4)}rem`
|
||||
);
|
||||
}
|
||||
|
||||
if (Array.isArray(input))
|
||||
return input.map((val) => scaleRemFactor(val, baseFontSize, newFontSize));
|
||||
|
||||
if (typeof input === "object") {
|
||||
const ret: Record<string, RemToPxInput> = {};
|
||||
const obj = input as Record<string, RemToPxInput>;
|
||||
for (const key in obj) {
|
||||
ret[key] = scaleRemFactor(obj[key], baseFontSize, newFontSize);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (isFunction(input)) {
|
||||
return function (...args: unknown[]): unknown {
|
||||
const replacedArgs = args.map((arg) => {
|
||||
if (typeof arg === "string") {
|
||||
return arg.replace(
|
||||
/(\d*\.?\d+)rem/g,
|
||||
(_, val) => `${(parseFloat(val) * scaleFactor).toFixed(4)}rem`
|
||||
);
|
||||
}
|
||||
return arg;
|
||||
});
|
||||
return input(...replacedArgs);
|
||||
};
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
55
unraid-ui/src/styles/globals.css
Normal file
55
unraid-ui/src/styles/globals.css
Normal file
@@ -0,0 +1,55 @@
|
||||
/* @tailwind directives */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
2
unraid-ui/src/styles/index.css
Normal file
2
unraid-ui/src/styles/index.css
Normal file
@@ -0,0 +1,2 @@
|
||||
/* global styles for unraid-ui */
|
||||
@import "./global.css";
|
||||
26
unraid-ui/src/types/badge.ts
Normal file
26
unraid-ui/src/types/badge.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import type { Component } from "vue";
|
||||
|
||||
export type UiBadgePropsColor =
|
||||
| "gray"
|
||||
| "red"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "blue"
|
||||
| "indigo"
|
||||
| "purple"
|
||||
| "pink"
|
||||
| "orange"
|
||||
| "black"
|
||||
| "white"
|
||||
| "transparent"
|
||||
| "current"
|
||||
| "custom";
|
||||
|
||||
export interface UiBadgeProps {
|
||||
color?: UiBadgePropsColor;
|
||||
icon?: typeof XCircleIcon | Component;
|
||||
iconRight?: typeof XCircleIcon | Component;
|
||||
iconStyles?: string;
|
||||
size?: "12px" | "14px" | "16px" | "18px" | "20px" | "24px";
|
||||
}
|
||||
31
unraid-ui/src/types/button.ts
Normal file
31
unraid-ui/src/types/button.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Component } from "vue";
|
||||
|
||||
export type ButtonStyle =
|
||||
| "black"
|
||||
| "fill"
|
||||
| "gray"
|
||||
| "outline"
|
||||
| "outline-black"
|
||||
| "outline-white"
|
||||
| "underline"
|
||||
| "underline-hover-red"
|
||||
| "white"
|
||||
| "none";
|
||||
export interface ButtonProps {
|
||||
btnStyle?: ButtonStyle;
|
||||
btnType?: "button" | "submit" | "reset";
|
||||
class?: string | string[] | Record<string, boolean> | undefined;
|
||||
click?: () => void;
|
||||
disabled?: boolean;
|
||||
download?: boolean;
|
||||
external?: boolean;
|
||||
href?: string;
|
||||
icon?: Component;
|
||||
iconRight?: Component;
|
||||
iconRightHoverDisplay?: boolean;
|
||||
// iconRightHoverAnimate?: boolean;
|
||||
noPadding?: boolean;
|
||||
size?: "12px" | "14px" | "16px" | "18px" | "20px" | "24px";
|
||||
text?: string;
|
||||
title?: string;
|
||||
}
|
||||
56
unraid-ui/stories/components/common/Button.stories.ts
Normal file
56
unraid-ui/stories/components/common/Button.stories.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import ButtonComponent from "../../../src/components/common/button/Button.vue";
|
||||
|
||||
interface ButtonStoryProps {
|
||||
variant:
|
||||
| "primary"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link";
|
||||
size: "sm" | "md" | "lg" | "icon";
|
||||
text: string;
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common",
|
||||
component: ButtonComponent,
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: [
|
||||
"primary",
|
||||
"destructive",
|
||||
"outline",
|
||||
"secondary",
|
||||
"ghost",
|
||||
"link",
|
||||
],
|
||||
},
|
||||
size: {
|
||||
control: "select",
|
||||
options: ["sm", "md", "lg", "icon"],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ButtonComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ButtonStoryProps>;
|
||||
|
||||
export const Button: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
text: "Click me",
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { ButtonComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template:
|
||||
'<ButtonComponent :variant="args.variant" :size="args.size">{{ args.text }}</ButtonComponent>',
|
||||
}),
|
||||
};
|
||||
274
unraid-ui/tailwind.config.ts
Normal file
274
unraid-ui/tailwind.config.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import "dotenv/config";
|
||||
import type { Config } from "tailwindcss";
|
||||
import type { PluginAPI } from "tailwindcss/types/config";
|
||||
import typography from "@tailwindcss/typography";
|
||||
import animate from "tailwindcss-animate";
|
||||
import remToRem from "./src/lib/tailwind-rem-to-rem";
|
||||
|
||||
export default <Partial<Config>>{
|
||||
content: ["./src/components/**/*.{js,vue,ts}", "./src/composables/**/*.vue"],
|
||||
darkMode: ["selector"],
|
||||
safelist: [
|
||||
"dark",
|
||||
"DropdownWrapper_blip",
|
||||
"unraid_mark_1",
|
||||
"unraid_mark_2",
|
||||
"unraid_mark_3",
|
||||
"unraid_mark_4",
|
||||
"unraid_mark_6",
|
||||
"unraid_mark_7",
|
||||
"unraid_mark_8",
|
||||
"unraid_mark_9",
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: "clear-sans,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji",
|
||||
},
|
||||
colors: {
|
||||
inherit: "inherit",
|
||||
transparent: "transparent",
|
||||
|
||||
black: "#1c1b1b",
|
||||
"grey-darkest": "#222",
|
||||
"grey-darker": "#606f7b",
|
||||
"grey-dark": "#383735",
|
||||
"grey-mid": "#999999",
|
||||
grey: "#e0e0e0",
|
||||
"grey-light": "#dae1e7",
|
||||
"grey-lighter": "#f1f5f8",
|
||||
"grey-lightest": "#f2f2f2",
|
||||
white: "#ffffff",
|
||||
|
||||
// unraid colors
|
||||
"yellow-accent": "#E9BF41",
|
||||
"orange-dark": "#f15a2c",
|
||||
orange: "#ff8c2f",
|
||||
// palettes generated from https://uicolors.app/create
|
||||
"unraid-red": {
|
||||
DEFAULT: "#E22828",
|
||||
"50": "#fef2f2",
|
||||
"100": "#ffe1e1",
|
||||
"200": "#ffc9c9",
|
||||
"300": "#fea3a3",
|
||||
"400": "#fc6d6d",
|
||||
"500": "#f43f3f",
|
||||
"600": "#e22828",
|
||||
"700": "#bd1818",
|
||||
"800": "#9c1818",
|
||||
"900": "#821a1a",
|
||||
"950": "#470808",
|
||||
},
|
||||
|
||||
"unraid-green": {
|
||||
DEFAULT: "#63A659",
|
||||
"50": "#f5f9f4",
|
||||
"100": "#e7f3e5",
|
||||
"200": "#d0e6cc",
|
||||
"300": "#aad1a4",
|
||||
"400": "#7db474",
|
||||
"500": "#63a659",
|
||||
"600": "#457b3e",
|
||||
"700": "#396134",
|
||||
"800": "#314e2d",
|
||||
"900": "#284126",
|
||||
"950": "#122211",
|
||||
},
|
||||
"header-text-primary": "var(--header-text-primary)",
|
||||
"header-text-secondary": "var(--header-text-secondary)",
|
||||
"header-background-color": "var(--header-background-color)",
|
||||
// ShadCN
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
// Unfortunately due to webGUI CSS setting base HTML font-size to .65% or something we must use pixel values for web components
|
||||
fontSize: {
|
||||
"10px": "10px",
|
||||
"12px": "12px",
|
||||
"14px": "14px",
|
||||
"16px": "16px",
|
||||
"18px": "18px",
|
||||
"20px": "20px",
|
||||
"24px": "24px",
|
||||
"30px": "30px",
|
||||
},
|
||||
spacing: {
|
||||
"4.5": "1.125rem",
|
||||
"-8px": "-8px",
|
||||
"2px": "2px",
|
||||
"4px": "4px",
|
||||
"6px": "6px",
|
||||
"8px": "8px",
|
||||
"10px": "10px",
|
||||
"12px": "12px",
|
||||
"14px": "14px",
|
||||
"16px": "16px",
|
||||
"20px": "20px",
|
||||
"24px": "24px",
|
||||
"28px": "28px",
|
||||
"32px": "32px",
|
||||
"36px": "36px",
|
||||
"40px": "40px",
|
||||
"64px": "64px",
|
||||
"80px": "80px",
|
||||
"90px": "90px",
|
||||
"150px": "150px",
|
||||
"160px": "160px",
|
||||
"200px": "200px",
|
||||
"260px": "260px",
|
||||
"300px": "300px",
|
||||
"310px": "310px",
|
||||
"350px": "350px",
|
||||
"448px": "448px",
|
||||
"512px": "512px",
|
||||
"640px": "640px",
|
||||
"800px": "800px",
|
||||
},
|
||||
minWidth: {
|
||||
"86px": "86px",
|
||||
"160px": "160px",
|
||||
"260px": "260px",
|
||||
"300px": "300px",
|
||||
"310px": "310px",
|
||||
"350px": "350px",
|
||||
"800px": "800px",
|
||||
},
|
||||
maxWidth: {
|
||||
"86px": "86px",
|
||||
"160px": "160px",
|
||||
"260px": "260px",
|
||||
"300px": "300px",
|
||||
"310px": "310px",
|
||||
"350px": "350px",
|
||||
"640px": "640px",
|
||||
"800px": "800px",
|
||||
"1024px": "1024px",
|
||||
},
|
||||
screens: {
|
||||
"2xs": "470px",
|
||||
xs: "530px",
|
||||
tall: { raw: "(min-height: 700px)" },
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
"collapsible-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-collapsible-content-height)" },
|
||||
},
|
||||
"collapsible-up": {
|
||||
from: { height: "var(--radix-collapsible-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"collapsible-down": "collapsible-down 0.2s ease-in-out",
|
||||
"collapsible-up": "collapsible-up 0.2s ease-in-out",
|
||||
},
|
||||
/**
|
||||
* @todo modify prose classes to use pixels for webgui…sadge https://tailwindcss.com/docs/typography-plugin#customizing-the-default-theme
|
||||
*/
|
||||
|
||||
typography: (theme: PluginAPI["theme"]) => ({
|
||||
DEFAULT: {
|
||||
css: {
|
||||
color: theme("colors.foreground"),
|
||||
a: {
|
||||
color: theme("colors.primary"),
|
||||
textDecoration: "underline",
|
||||
"&:hover": {
|
||||
color: theme("colors.primary-foreground"),
|
||||
},
|
||||
},
|
||||
"--tw-prose-body": theme("colors.foreground"),
|
||||
"--tw-prose-headings": theme("colors.foreground"),
|
||||
"--tw-prose-lead": theme("colors.foreground"),
|
||||
"--tw-prose-links": theme("colors.primary"),
|
||||
"--tw-prose-bold": theme("colors.foreground"),
|
||||
"--tw-prose-counters": theme("colors.foreground"),
|
||||
"--tw-prose-bullets": theme("colors.foreground"),
|
||||
"--tw-prose-hr": theme("colors.foreground"),
|
||||
"--tw-prose-quotes": theme("colors.foreground"),
|
||||
"--tw-prose-quote-borders": theme("colors.foreground"),
|
||||
"--tw-prose-captions": theme("colors.foreground"),
|
||||
"--tw-prose-code": theme("colors.foreground"),
|
||||
"--tw-prose-pre-code": theme("colors.foreground"),
|
||||
"--tw-prose-pre-bg": theme("colors.background"),
|
||||
"--tw-prose-th-borders": theme("colors.foreground"),
|
||||
"--tw-prose-td-borders": theme("colors.foreground"),
|
||||
"--tw-prose-invert-body": theme("colors.background"),
|
||||
"--tw-prose-invert-headings": theme("colors.background"),
|
||||
"--tw-prose-invert-lead": theme("colors.background"),
|
||||
"--tw-prose-invert-links": theme("colors.primary"),
|
||||
"--tw-prose-invert-bold": theme("colors.background"),
|
||||
"--tw-prose-invert-counters": theme("colors.background"),
|
||||
"--tw-prose-invert-bullets": theme("colors.background"),
|
||||
"--tw-prose-invert-hr": theme("colors.background"),
|
||||
"--tw-prose-invert-quotes": theme("colors.background"),
|
||||
"--tw-prose-invert-quote-borders": theme("colors.background"),
|
||||
"--tw-prose-invert-captions": theme("colors.background"),
|
||||
"--tw-prose-invert-code": theme("colors.background"),
|
||||
"--tw-prose-invert-pre-code": theme("colors.background"),
|
||||
"--tw-prose-invert-pre-bg": theme("colors.foreground"),
|
||||
"--tw-prose-invert-th-borders": theme("colors.background"),
|
||||
"--tw-prose-invert-td-borders": theme("colors.background"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
typography,
|
||||
animate,
|
||||
remToRem({
|
||||
baseFontSize: 16,
|
||||
newFontSize: Number(process.env.VITE_TAILWIND_BASE_FONT_SIZE) || 10,
|
||||
}),
|
||||
],
|
||||
};
|
||||
10
unraid-ui/test/setup.ts
Normal file
10
unraid-ui/test/setup.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/// <reference types="vitest/globals" />
|
||||
import { config } from "@vue/test-utils";
|
||||
import { cleanup } from "@testing-library/vue";
|
||||
|
||||
// Setup Vue Test Utils global config
|
||||
config.global.stubs = {};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
26
unraid-ui/tsconfig.app.json
Normal file
26
unraid-ui/tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/components/*": ["./src/components/*"],
|
||||
"@/lib/*": ["./src/lib/*"],
|
||||
"@/types/*": ["./src/types/*"]
|
||||
},
|
||||
"types": ["vite/client", "vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
20
unraid-ui/tsconfig.json
Normal file
20
unraid-ui/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@/styles": ["./src/styles"],
|
||||
"@/components": ["./src/components"],
|
||||
"@/composables": ["./src/composables"],
|
||||
"@/lib": ["./src/lib"],
|
||||
"@/types": ["./src/types"]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"exclude": ["node_modules", "**/*.copy.vue", "**/*copy.vue"],
|
||||
"references": [{ "path": "./tsconfig.test.json" }]
|
||||
}
|
||||
11
unraid-ui/tsconfig.node.json
Normal file
11
unraid-ui/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
17
unraid-ui/tsconfig.test.json
Normal file
17
unraid-ui/tsconfig.test.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist/test",
|
||||
"types": [
|
||||
"node",
|
||||
"happy-dom",
|
||||
"vitest/globals",
|
||||
"@vue/test-utils",
|
||||
"@testing-library/vue"
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.copy.vue", "**/*copy.vue"]
|
||||
}
|
||||
56
unraid-ui/vite.config.ts
Normal file
56
unraid-ui/vite.config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vite";
|
||||
import { resolve } from "path";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
include: ["src/**/*.ts", "src/**/*.vue"],
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, "src/index.ts"),
|
||||
name: "Unraid UI",
|
||||
formats: ["es", "umd"],
|
||||
fileName: "index",
|
||||
},
|
||||
cssCodeSplit: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
external: ["vue"],
|
||||
output: {
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (
|
||||
typeof assetInfo.source === "string" &&
|
||||
assetInfo.source.includes("style.css")
|
||||
)
|
||||
return "css/style.[hash].css";
|
||||
return "assets/[name].[hash][extname]";
|
||||
},
|
||||
globals: {
|
||||
vue: "Vue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "./src"),
|
||||
"@/components": resolve(__dirname, "./src/components"),
|
||||
"@/composables": resolve(__dirname, "./src/composables"),
|
||||
"@/lib": resolve(__dirname, "./src/lib"),
|
||||
"@/styles": resolve(__dirname, "./src/styles"),
|
||||
"@/types": resolve(__dirname, "./src/types"),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "happy-dom",
|
||||
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
},
|
||||
});
|
||||
21
unraid-ui/vitest.config.ts
Normal file
21
unraid-ui/vitest.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vitest/config";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { resolve } from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "happy-dom",
|
||||
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "./src"),
|
||||
"@/components": resolve(__dirname, "./src/components"),
|
||||
"@/lib": resolve(__dirname, "./src/lib"),
|
||||
"@/types": resolve(__dirname, "./src/types"),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -4,6 +4,12 @@ import type { PluginAPI } from 'tailwindcss/types/config';
|
||||
|
||||
// @ts-expect-error - just trying to get this to build @fixme
|
||||
export default <Partial<Config>>{
|
||||
content: [
|
||||
'./components/**/*.{js,vue,ts}',
|
||||
'./layouts/**/*.vue',
|
||||
'./pages/**/*.vue',
|
||||
'../unraid-ui/src/**/*.{vue,ts}',
|
||||
],
|
||||
darkMode: ['selector'],
|
||||
safelist: [
|
||||
'dark',
|
||||
|
||||
Reference in New Issue
Block a user