mirror of
https://github.com/unraid/api.git
synced 2025-12-31 21:49:57 -06:00
refactor: unraid ui cleanup and migration (#998)
Co-authored-by: Eli Bosley <ekbosley@gmail.com> Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com> Co-authored-by: mdatelle <mike@datelle.net> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Zack Spear <zackspear@users.noreply.github.com>
This commit is contained in:
51
.github/workflows/main.yml
vendored
51
.github/workflows/main.yml
vendored
@@ -101,7 +101,45 @@ jobs:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/api/deploy/release/*.tgz
|
||||
|
||||
build-unraid-ui:
|
||||
name: Build Unraid UI Library
|
||||
defaults:
|
||||
run:
|
||||
working-directory: unraid-ui
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: |
|
||||
unraid-ui/package-lock.json
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Make Built Node Artifact
|
||||
run: |
|
||||
mkdir unraid-ui-dist
|
||||
mv dist/ unraid-ui-dist/dist/
|
||||
mv package.json unraid-ui-dist/package.json
|
||||
ls unraid-ui-dist
|
||||
|
||||
- name: Upload Artifact to Github
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-ui
|
||||
path: unraid-ui/unraid-ui-dist
|
||||
|
||||
build-web:
|
||||
needs: [build-unraid-ui]
|
||||
name: Build Web App
|
||||
environment:
|
||||
name: production
|
||||
@@ -126,9 +164,20 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
cache-dependency-path: |
|
||||
web/package-lock.json
|
||||
node-version-file: "web/.nvmrc"
|
||||
|
||||
- name: Remove Existing Unraid UI folder
|
||||
run: |
|
||||
rm -r ../unraid-ui
|
||||
|
||||
- name: Download Artifact for Unraid UI
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-ui
|
||||
path: unraid-ui
|
||||
|
||||
- name: Installing node deps
|
||||
run: npm install
|
||||
|
||||
|
||||
27
.vscode/settings.json
vendored
27
.vscode/settings.json
vendored
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.page": "php"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "never",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "flat",
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
}
|
||||
"files.associations": {
|
||||
"*.page": "php"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "never",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"i18n-ally.localesPaths": [
|
||||
"locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "flat",
|
||||
"eslint.experimental.useFlatConfig": true
|
||||
}
|
||||
|
||||
1
.vscode/sftp-template.json
vendored
1
.vscode/sftp-template.json
vendored
@@ -19,3 +19,4 @@
|
||||
".DS_Store"
|
||||
]
|
||||
}
|
||||
|
||||
10
unraid-ui/.eslintrc.js
Normal file
10
unraid-ui/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"],
|
||||
rules: {
|
||||
// override/add rules settings here, such as:
|
||||
// 'vue/no-unused-vars': 'error'
|
||||
},
|
||||
};
|
||||
8
unraid-ui/.prettierrc.json
Normal file
8
unraid-ui/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 105,
|
||||
"singleQuote": true,
|
||||
"plugins": ["prettier-plugin-tailwindcss", "@ianvs/prettier-plugin-sort-imports"]
|
||||
}
|
||||
@@ -1,22 +1,12 @@
|
||||
import type { StorybookConfig } from "@storybook/vue3-vite";
|
||||
import { resolve } from "path";
|
||||
import { mergeConfig } from "vite";
|
||||
import { dirname, join } from "path";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../stories/**/*.stories.@(js|mjs|ts)"],
|
||||
stories: ["../stories/**/*.stories.@(js|jsx|ts|tsx)"],
|
||||
addons: [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-controls",
|
||||
{
|
||||
name: "@storybook/addon-postcss",
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require("postcss"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"@storybook/addon-interactions"
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/vue3-vite",
|
||||
@@ -27,19 +17,27 @@ const config: StorybookConfig = {
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
return mergeConfig(config, {
|
||||
async viteFinal(config) {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "../src"),
|
||||
"@/components": resolve(__dirname, "../src/components"),
|
||||
"@/lib": resolve(__dirname, "../src/lib"),
|
||||
"@": join(dirname(new URL(import.meta.url).pathname), "../src"),
|
||||
"@/components": join(dirname(new URL(import.meta.url).pathname), "../src/components"),
|
||||
"@/lib": join(dirname(new URL(import.meta.url).pathname), "../src/lib"),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
postcss: "./postcss.config.js",
|
||||
postcss: {
|
||||
plugins: [
|
||||
(await import("tailwindcss")).default({
|
||||
config: "./tailwind.config.ts",
|
||||
}),
|
||||
(await import("autoprefixer")).default,
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ 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}",
|
||||
"../src/components/**/*.{js,vue,ts}",
|
||||
"../src/composables/**/*.{js,vue,ts}",
|
||||
"../stories/**/*.stories.{js,ts,jsx,tsx,mdx}"
|
||||
],
|
||||
};
|
||||
|
||||
2917
unraid-ui/package-lock.json
generated
2917
unraid-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,16 +3,17 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"tailwind.config.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && vue-tsc --emitDeclarationOnly",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
@@ -36,12 +37,12 @@
|
||||
"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"
|
||||
"tailwind-merge": "^2.5.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-controls": "^8.4.7",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/addon-essentials": "^8.4.7",
|
||||
"@storybook/addon-interactions": "^8.4.7",
|
||||
"@storybook/addon-links": "^8.4.7",
|
||||
"@storybook/vue3-vite": "^8.4.7",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@testing-library/vue": "^8.0.0",
|
||||
@@ -54,9 +55,14 @@
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"@vue/tsconfig": "^0.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"happy-dom": "^12.0.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "3.4.2",
|
||||
"tailwindcss": "^3.0.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-dts": "^3.0.0",
|
||||
@@ -64,11 +70,25 @@
|
||||
"vue": "^3.3.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.30.1"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./style.css": "./dist/style.css"
|
||||
"./styles": "./dist/style.css",
|
||||
"./styles/*": "./src/styles/*",
|
||||
"./tailwind.config": {
|
||||
"types": "./dist/tailwind.config.d.ts",
|
||||
"import": "./dist/tailwind.config.js",
|
||||
"default": "./dist/tailwind.config.js"
|
||||
},
|
||||
"./theme/preset": {
|
||||
"types": "./dist/theme/preset.d.ts",
|
||||
"import": "./dist/theme/preset.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
unraid-ui/src/components/brand/BrandButton.vue
Normal file
91
unraid-ui/src/components/brand/BrandButton.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { brandButtonVariants } from './brand-button.variants';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface BrandButtonProps {
|
||||
variant?: 'fill' | 'black' | 'gray' | 'outline' | 'outline-black' | 'outline-white' | 'underline' | 'underline-hover-red' | 'white' | 'none';
|
||||
size?: '12px' | '14px' | '16px' | '18px' | '20px' | '24px';
|
||||
padding?: 'default' | 'none';
|
||||
btnType?: 'button' | 'submit' | 'reset';
|
||||
class?: string;
|
||||
click?: () => void;
|
||||
disabled?: boolean;
|
||||
external?: boolean;
|
||||
href?: string;
|
||||
icon?: any;
|
||||
iconRight?: any;
|
||||
iconRightHoverDisplay?: boolean;
|
||||
text?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BrandButtonProps>(), {
|
||||
variant: 'fill',
|
||||
size: '16px',
|
||||
padding: 'default',
|
||||
btnType: 'button',
|
||||
class: undefined,
|
||||
click: undefined,
|
||||
disabled: false,
|
||||
external: false,
|
||||
href: undefined,
|
||||
icon: undefined,
|
||||
iconRight: undefined,
|
||||
iconRightHoverDisplay: false,
|
||||
text: '',
|
||||
title: '',
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
|
||||
const classes = computed(() => {
|
||||
const iconSize = `w-${props.size}`;
|
||||
|
||||
return {
|
||||
button: cn(brandButtonVariants({ variant: props.variant, size: props.size, padding: props.padding }), props.class),
|
||||
icon: `${iconSize} fill-current flex-shrink-0`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="href ? 'a' : 'button'"
|
||||
:disabled="disabled"
|
||||
:href="href"
|
||||
:rel="external ? 'noopener noreferrer' : ''"
|
||||
:target="external ? '_blank' : ''"
|
||||
:type="!href ? btnType : ''"
|
||||
:class="classes.button"
|
||||
:title="title"
|
||||
@click="click ?? $emit('click')"
|
||||
>
|
||||
<div
|
||||
v-if="variant === 'fill'"
|
||||
class="absolute -top-[2px] -right-[2px] -bottom-[2px] -left-[2px] -z-10 bg-gradient-to-r from-unraid-red to-orange opacity-100 transition-all rounded-md group-hover:opacity-60 group-focus:opacity-60"
|
||||
/>
|
||||
<div
|
||||
v-if="variant === 'outline'"
|
||||
class="absolute -top-[2px] -right-[2px] -bottom-[2px] -left-[2px] -z-10 bg-gradient-to-r from-unraid-red to-orange opacity-0 transition-all rounded-md group-hover:opacity-100 group-focus:opacity-100"
|
||||
/>
|
||||
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
:class="classes.icon"
|
||||
/>
|
||||
|
||||
{{ text }}
|
||||
<slot />
|
||||
|
||||
<component
|
||||
:is="iconRight"
|
||||
v-if="iconRight"
|
||||
:class="[
|
||||
classes.icon,
|
||||
iconRightHoverDisplay && 'opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-all',
|
||||
]"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
134
unraid-ui/src/components/brand/BrandLoading.vue
Normal file
134
unraid-ui/src/components/brand/BrandLoading.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
title?: string,
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
title: 'Loading',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 133.52 76.97"
|
||||
:class="`unraid_mark`"
|
||||
role="img"
|
||||
>
|
||||
<title>{{ title }}</title>
|
||||
<desc>Unraid logo animating with a wave like effect</desc>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="unraidLoadingGradient"
|
||||
x1="23.76"
|
||||
y1="81.49"
|
||||
x2="109.76"
|
||||
y2="-4.51"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" :stop-color="gradientStart" />
|
||||
<stop offset="1" :stop-color="gradientStop" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="m70,19.24zm57,0l6.54,0l0,38.49l-6.54,0l0,-38.49z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_9"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm47.65,11.9l-6.55,0l0,-23.79l6.55,0l0,23.79z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_8"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm31.77,-4.54l-6.54,0l0,-14.7l6.54,0l0,14.7z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_7"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm15.9,11.9l-6.54,0l0,-23.79l6.54,0l0,23.79z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_6"
|
||||
/>
|
||||
<path
|
||||
d="m63.49,19.24l6.51,0l0,38.49l-6.51,0l0,-38.49z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_5"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-22.38,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_4"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-38.26,43.03l6.55,0l0,14.73l-6.55,0l0,-14.73z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_3"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-54.13,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_2"
|
||||
/>
|
||||
<path
|
||||
d="m70,19.24zm-63.46,38.49l-6.54,0l0,-38.49l6.54,0l0,38.49z"
|
||||
fill="url(#unraidLoadingGradient)"
|
||||
class="unraid_mark_1"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
animation: mark_2 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_3 {
|
||||
animation: mark_3 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_6,
|
||||
.unraid_mark_8 {
|
||||
animation: mark_6 1.5s ease infinite;
|
||||
}
|
||||
.unraid_mark_7 {
|
||||
animation: mark_7 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes mark_2 {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_3 {
|
||||
50% {
|
||||
transform: translateY(-62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_6 {
|
||||
50% {
|
||||
transform: translateY(40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes mark_7 {
|
||||
50% {
|
||||
transform: translateY(62px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
7
unraid-ui/src/components/brand/BrandLoadingWhite.vue
Normal file
7
unraid-ui/src/components/brand/BrandLoadingWhite.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import BrandLoading from './BrandLoading.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BrandLoading gradient-start="#ffffff" gradient-stop="#ffffff" />
|
||||
</template>
|
||||
38
unraid-ui/src/components/brand/BrandLogo.vue
Normal file
38
unraid-ui/src/components/brand/BrandLogo.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 222.36 39.04"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="unraidLogo"
|
||||
x1="47.53"
|
||||
y1="79.1"
|
||||
x2="170.71"
|
||||
y2="-44.08"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" :stop-color="gradientStart" />
|
||||
<stop offset="1" :stop-color="gradientStop" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<title>Unraid Logo</title>
|
||||
<path
|
||||
d="M146.7,29.47H135l-3,9h-6.49L138.93,0h8l13.41,38.49h-7.09L142.62,6.93l-5.83,16.88h8ZM29.69,0V25.4c0,8.91-5.77,13.64-14.9,13.64S0,34.31,0,25.4V0H6.54V25.4c0,5.17,3.19,7.92,8.25,7.92s8.36-2.75,8.36-7.92V0ZM50.86,12v26.5H44.31V0h6.11l17,26.5V0H74V38.49H67.9ZM171.29,0h6.54V38.49h-6.54Zm51.07,24.69c0,9-5.88,13.8-15.17,13.8H192.67V0H207.3c9.18,0,15.06,4.78,15.06,13.8ZM215.82,13.8c0-5.28-3.3-8.14-8.52-8.14h-8.08V32.77h8c5.33,0,8.63-2.8,8.63-8.08ZM108.31,23.92c4.34-1.6,6.93-5.28,6.93-11.55C115.24,3.68,110.18,0,102.48,0H88.84V38.49h6.55V5.66h6.87c3.8,0,6.21,1.82,6.21,6.71s-2.41,6.76-6.21,6.76H98.88l9.21,19.36h7.53Z"
|
||||
fill="url(#unraidLogo)"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
44
unraid-ui/src/components/brand/BrandLogoConnect.vue
Normal file
44
unraid-ui/src/components/brand/BrandLogoConnect.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
export interface Props {
|
||||
gradientStart?: string;
|
||||
gradientStop?: string;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-name="Layer 1" viewBox="0 0 954.29 142.4">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1="-57.82"
|
||||
x2="923.39"
|
||||
y1="71.2"
|
||||
y2="71.2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" :stop-color="gradientStart" />
|
||||
<stop offset="1" :stop-color="gradientStop" />
|
||||
</linearGradient>
|
||||
<linearGradient id="b" xlink:href="#a" x2="923.39" />
|
||||
<linearGradient id="c" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
|
||||
<linearGradient id="d" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
|
||||
<linearGradient id="e" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
|
||||
<linearGradient id="f" xlink:href="#a" x2="923.39" />
|
||||
<linearGradient id="g" xlink:href="#a" y1="12.16" y2="12.16" />
|
||||
<linearGradient id="h" xlink:href="#a" x2="923.39" y1="86.94" y2="86.94" />
|
||||
</defs>
|
||||
<path fill="url(#a)" d="M54.39 0C20.96 0 0 17.4 0 49.84v42.52c0 32.63 20.96 50.04 53.99 50.04s53.8-16.81 53.8-48.06v-.99H84.25v.99c0 17.8-11.47 27.49-30.26 27.49s-30.46-10.28-30.46-29.47V49.84c0-18.99 11.67-29.47 30.85-29.47s29.86 9.89 29.86 27.69v.79h23.54v-.79C107.79 16.81 87.02 0 54.39 0Z" />
|
||||
<path fill="url(#b)" d="M197.58 0c-33.42 0-54.59 17.4-54.59 49.84v42.52c0 32.63 21.16 50.04 54.19 50.04s54.59-17.4 54.59-50.04V49.84C251.77 17.4 230.61 0 197.58 0Zm30.66 92.36c0 19.18-11.87 29.47-31.05 29.47s-30.66-10.28-30.66-29.47V49.84c0-18.99 11.87-29.47 31.05-29.47s30.66 10.48 30.66 29.47v42.52Z" />
|
||||
<path fill="url(#c)" d="M373.8 97.31 312.49 1.98h-21.95v138.44h23.53V45.09l61.32 95.33h21.95V1.98H373.8v95.33z" />
|
||||
<path fill="url(#d)" d="M521.35 97.31 460.04 1.98h-21.96v138.44h23.54V45.09l61.31 95.33h21.95V1.98h-23.53v95.33z" />
|
||||
<path fill="url(#e)" d="M585.63 140.42h92.95v-20.57h-69.42V81.29h59.54V60.92h-59.54V22.35h69.42V1.98h-92.95v138.44z" />
|
||||
<path fill="url(#f)" d="M766.8 0c-33.43 0-54.39 17.4-54.39 49.84v42.52c0 32.63 20.96 50.04 53.99 50.04s53.8-16.81 53.8-48.06v-.99h-23.54v.99c0 17.8-11.47 27.49-30.26 27.49s-30.46-10.28-30.46-29.47V49.84c0-18.99 11.67-29.47 30.85-29.47s29.86 9.89 29.86 27.69v.79h23.54v-.79c0-31.25-20.77-48.06-53.4-48.06Z" />
|
||||
<path fill="url(#g)" d="M846.11 1.98h108.18v20.37H846.11z" />
|
||||
<path fill="url(#h)" d="M888.43 33.45h23.54v106.97h-23.54z" />
|
||||
</svg>
|
||||
</template>
|
||||
70
unraid-ui/src/components/brand/brand-button.variants.ts
Normal file
70
unraid-ui/src/components/brand/brand-button.variants.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export const brandButtonVariants = cva(
|
||||
"group text-center font-semibold leading-none relative z-0 flex flex-row items-center justify-center border-2 border-solid shadow-none cursor-pointer rounded-md hover:shadow-md focus:shadow-md disabled:opacity-25 disabled:hover:opacity-25 disabled:focus:opacity-25 disabled:cursor-not-allowed",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
fill: "[&]:text-white bg-transparent border-transparent",
|
||||
black: "[&]:text-white bg-black border-black transition hover:text-black focus:text-black hover:bg-grey focus:bg-grey hover:border-grey focus:border-grey",
|
||||
gray: "text-black bg-grey transition hover:text-white focus:text-white hover:bg-grey-mid focus:bg-grey-mid hover:border-grey-mid focus:border-grey-mid",
|
||||
outline: "[&]:text-orange bg-transparent border-orange hover:text-white focus:text-white",
|
||||
"outline-black": "text-black bg-transparent border-black hover:text-black focus:text-black hover:bg-grey focus:bg-grey hover:border-grey focus:border-grey",
|
||||
"outline-white": "text-white bg-transparent border-white hover:text-black focus:text-black hover:bg-white focus:bg-white",
|
||||
underline: "opacity-75 underline border-transparent transition hover:text-primary hover:bg-muted hover:border-muted focus:text-primary focus:bg-muted focus:border-muted hover:opacity-100 focus:opacity-100",
|
||||
"underline-hover-red": "opacity-75 underline border-transparent transition hover:text-white hover:bg-unraid-red hover:border-unraid-red focus:text-white focus:bg-unraid-red focus:border-unraid-red hover:opacity-100 focus:opacity-100",
|
||||
white: "text-black bg-white transition hover:bg-grey focus:bg-grey",
|
||||
none: "",
|
||||
},
|
||||
size: {
|
||||
"12px": "text-12px gap-4px",
|
||||
"14px": "text-14px gap-8px",
|
||||
"16px": "text-16px gap-8px",
|
||||
"18px": "text-18px gap-8px",
|
||||
"20px": "text-20px gap-8px",
|
||||
"24px": "text-24px gap-8px",
|
||||
},
|
||||
padding: {
|
||||
default: "",
|
||||
none: "p-0",
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
size: "12px",
|
||||
padding: "default",
|
||||
class: "p-8px",
|
||||
},
|
||||
{
|
||||
size: "14px",
|
||||
padding: "default",
|
||||
class: "p-8px",
|
||||
},
|
||||
{
|
||||
size: "16px",
|
||||
padding: "default",
|
||||
class: "p-12px",
|
||||
},
|
||||
{
|
||||
size: "18px",
|
||||
padding: "default",
|
||||
class: "p-12px",
|
||||
},
|
||||
{
|
||||
size: "20px",
|
||||
padding: "default",
|
||||
class: "p-16px",
|
||||
},
|
||||
{
|
||||
size: "24px",
|
||||
padding: "default",
|
||||
class: "p-16px",
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: "fill",
|
||||
size: "16px",
|
||||
padding: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
6
unraid-ui/src/components/brand/index.ts
Normal file
6
unraid-ui/src/components/brand/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as BrandButton } from "./BrandButton.vue";
|
||||
export { brandButtonVariants } from "./brand-button.variants";
|
||||
export { default as BrandLoading } from "./BrandLoading.vue";
|
||||
export { default as BrandLoadingWhite } from "./BrandLoadingWhite.vue";
|
||||
export { default as BrandLogo } from "./BrandLogo.vue";
|
||||
export { default as BrandLogoConnect } from "./BrandLogoConnect.vue";
|
||||
@@ -1,127 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { UiBadgeProps } from "@/types/badge";
|
||||
import type { Component } from "vue";
|
||||
import { badgeVariants } from "./badge.variants";
|
||||
|
||||
const props = withDefaults(defineProps<UiBadgeProps>(), {
|
||||
color: "gray",
|
||||
export interface BadgeProps {
|
||||
variant?: "red" | "yellow" | "green" | "blue" | "indigo" | "purple" |
|
||||
"pink" | "orange" | "black" | "white" | "transparent" | "current" | "gray" | "custom";
|
||||
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
icon?: Component;
|
||||
iconRight?: Component;
|
||||
iconStyles?: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BadgeProps>(), {
|
||||
variant: "gray",
|
||||
size: "md",
|
||||
icon: undefined,
|
||||
iconRight: undefined,
|
||||
iconStyles: "",
|
||||
size: "16px",
|
||||
class: "",
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
const badgeClasses = computed(() => {
|
||||
const iconSizes = {
|
||||
xs: "w-12px",
|
||||
sm: "w-14px",
|
||||
md: "w-16px",
|
||||
lg: "w-18px",
|
||||
xl: "w-20px",
|
||||
"2xl": "w-24px",
|
||||
} as const;
|
||||
|
||||
return {
|
||||
badge: `${textSize} ${colorClasses}`,
|
||||
icon: `${iconSize} ${props.iconStyles}`,
|
||||
badge: badgeVariants({ variant: props.variant, size: props.size }),
|
||||
icon: `${iconSizes[props.size ?? "md"]} ${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]"
|
||||
>
|
||||
<span :class="[badgeClasses.badge, props.class]">
|
||||
<component
|
||||
:is="icon"
|
||||
v-if="icon"
|
||||
class="flex-shrink-0"
|
||||
:class="computedStyleClasses.icon"
|
||||
:class="badgeClasses.icon"
|
||||
/>
|
||||
<slot />
|
||||
<component
|
||||
:is="iconRight"
|
||||
v-if="iconRight"
|
||||
class="flex-shrink-0"
|
||||
:class="computedStyleClasses.icon"
|
||||
:class="badgeClasses.icon"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
37
unraid-ui/src/components/common/badge/badge.variants.ts
Normal file
37
unraid-ui/src/components/common/badge/badge.variants.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full font-semibold leading-none transition-all duration-200 ease-in-out unraid-ui-badge-test",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
red: "bg-unraid-red text-white hover:bg-orange-dark",
|
||||
yellow: "bg-yellow-100 text-black hover:bg-yellow-200",
|
||||
green: "bg-green-200 text-green-800 hover:bg-green-300",
|
||||
blue: "bg-blue-100 text-blue-800 hover:bg-blue-200",
|
||||
indigo: "bg-indigo-100 text-indigo-800 hover:bg-indigo-200",
|
||||
purple: "bg-purple-100 text-purple-800 hover:bg-purple-200",
|
||||
pink: "bg-pink-100 text-pink-800 hover:bg-pink-200",
|
||||
orange: "bg-orange text-white hover:bg-orange-dark",
|
||||
black: "bg-black text-white hover:bg-gray-800",
|
||||
white: "bg-white text-black hover:bg-gray-100",
|
||||
transparent: "bg-transparent text-black hover:bg-gray-100",
|
||||
current: "bg-current text-current hover:bg-gray-100",
|
||||
gray: "bg-gray-200 text-gray-800 hover:bg-gray-300",
|
||||
custom: "",
|
||||
},
|
||||
size: {
|
||||
xs: "text-12px px-8px py-4px gap-4px",
|
||||
sm: "text-14px px-8px py-4px gap-8px",
|
||||
md: "text-16px px-12px py-8px gap-8px",
|
||||
lg: "text-18px px-12px py-8px gap-8px",
|
||||
xl: "text-20px px-16px py-12px gap-8px",
|
||||
"2xl": "text-24px px-16px py-12px gap-8px",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "gray",
|
||||
size: "md",
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1,3 +1,2 @@
|
||||
import Badge from "./Badge.vue";
|
||||
|
||||
export { Badge };
|
||||
export { default as Badge } from "./Badge.vue";
|
||||
export { badgeVariants } from "./badge.variants";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ButtonVariants } from "./button.variants";
|
||||
import { buttonVariants } from "./button.variants";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
export interface ButtonProps {
|
||||
variant?:
|
||||
| "primary"
|
||||
| "destructive"
|
||||
@@ -15,14 +15,14 @@ interface Props {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
});
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
return cn(
|
||||
ButtonVariants({ variant: props.variant, size: props.size }),
|
||||
buttonVariants({ variant: props.variant, size: props.size }),
|
||||
props.class
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export const ButtonVariants = cva(
|
||||
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: {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as Button } from "./Button.vue";
|
||||
export { ButtonVariants } from "./button.variants";
|
||||
export { buttonVariants } from "./button.variants";
|
||||
|
||||
@@ -4,54 +4,52 @@ 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 { sheetVariants } from "./sheet.variants";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SheetContentProps extends DialogContentProps {
|
||||
export interface SheetContentProps {
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
padding?: "none" | "md";
|
||||
class?: HTMLAttributes["class"];
|
||||
side?: SheetVariants["side"];
|
||||
padding?: SheetVariants["padding"];
|
||||
disabled?: boolean;
|
||||
forceMount?: boolean;
|
||||
to?: string | HTMLElement | Element;
|
||||
to?: string | HTMLElement;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||
side: "right",
|
||||
padding: "md",
|
||||
});
|
||||
|
||||
const props = defineProps<SheetContentProps>();
|
||||
|
||||
const emits = defineEmits<DialogContentEmits>();
|
||||
|
||||
const sheetClass = computed(() => {
|
||||
return cn(
|
||||
sheetVariants({ side: props.side, padding: props.padding }),
|
||||
props.class,
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
>
|
||||
<DialogPortal :disabled="disabled" :force-mount="forceMount" :to="to">
|
||||
<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 }"
|
||||
>
|
||||
<DialogContent :class="sheetClass" v-bind="forwarded">
|
||||
<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"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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";
|
||||
@@ -8,29 +6,4 @@ 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>;
|
||||
export { sheetVariants } from "./sheet.variants";
|
||||
|
||||
23
unraid-ui/src/components/common/sheet/sheet.variants.ts
Normal file
23
unraid-ui/src/components/common/sheet/sheet.variants.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export const sheetVariants = cva(
|
||||
"fixed z-50 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",
|
||||
},
|
||||
}
|
||||
);
|
||||
1
unraid-ui/src/components/form/lightswitch/index.ts
Normal file
1
unraid-ui/src/components/form/lightswitch/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Lightswitch } from "./Lightswitch.vue";
|
||||
@@ -1,5 +1,2 @@
|
||||
import Switch from "./Switch.vue";
|
||||
import SwitchHeadlessUI from "./SwitchHeadlessUI.vue";
|
||||
import Lightswitch from "./Lightswitch.vue";
|
||||
|
||||
export { Switch, SwitchHeadlessUI, Lightswitch };
|
||||
export { default as Switch } from "./Switch.vue";
|
||||
export { default as SwitchHeadlessUI } from "./SwitchHeadlessUI.vue";
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
// Styles
|
||||
import "./styles/index.css";
|
||||
|
||||
// Config
|
||||
import tailwindConfig from "../tailwind.config";
|
||||
|
||||
// Lib
|
||||
import { cn, scaleRemFactor } from "@/lib/utils";
|
||||
|
||||
// Components
|
||||
import { Badge } from "@/components/common/badge";
|
||||
import { Button, ButtonVariants } from "@/components/common/button";
|
||||
import {
|
||||
BrandButton,
|
||||
brandButtonVariants,
|
||||
BrandLoading,
|
||||
BrandLoadingWhite,
|
||||
BrandLogo,
|
||||
BrandLogoConnect
|
||||
} from "@/components/brand";
|
||||
import { Button, buttonVariants } from "@/components/common/button";
|
||||
import { CardWrapper, PageContainer } from "@/components/layout";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -24,6 +38,7 @@ import {
|
||||
import { Bar, Error, Spinner } from "@/components/common/loading";
|
||||
import { Input } from "@/components/form/input";
|
||||
import { Label } from "@/components/form/label";
|
||||
import { Lightswitch } from "@/components/form/lightswitch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -37,17 +52,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/form/select";
|
||||
import {
|
||||
Switch,
|
||||
SwitchHeadlessUI,
|
||||
Lightswitch,
|
||||
} from "@/components/form/switch";
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@/components/common/tabs";
|
||||
import { Switch, SwitchHeadlessUI } from "@/components/form/switch";
|
||||
import { ScrollArea, ScrollBar } from "@/components/common/scroll-area";
|
||||
import {
|
||||
Sheet,
|
||||
@@ -59,6 +64,12 @@ import {
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from "@/components/common/sheet";
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@/components/common/tabs";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@@ -73,8 +84,14 @@ import useTeleport from "@/composables/useTeleport";
|
||||
export {
|
||||
Bar,
|
||||
Badge,
|
||||
BrandButton,
|
||||
brandButtonVariants,
|
||||
BrandLoading,
|
||||
BrandLoadingWhite,
|
||||
BrandLogo,
|
||||
BrandLogoConnect,
|
||||
Button,
|
||||
ButtonVariants,
|
||||
buttonVariants,
|
||||
CardWrapper,
|
||||
cn,
|
||||
DropdownMenu,
|
||||
@@ -120,6 +137,7 @@ export {
|
||||
Spinner,
|
||||
Switch,
|
||||
SwitchHeadlessUI,
|
||||
tailwindConfig,
|
||||
Lightswitch,
|
||||
Tabs,
|
||||
TabsList,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
/* global styles for unraid-ui */
|
||||
@import "./global.css";
|
||||
@import "./globals.css";
|
||||
|
||||
|
||||
245
unraid-ui/src/theme/preset.ts
Normal file
245
unraid-ui/src/theme/preset.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import type { PluginAPI } from "tailwindcss/types/config";
|
||||
import typography from "@tailwindcss/typography";
|
||||
import animate from "tailwindcss-animate";
|
||||
|
||||
export const unraidPreset = {
|
||||
darkMode: ['selector', '[data-mode="dark"]'],
|
||||
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",
|
||||
"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))",
|
||||
},
|
||||
},
|
||||
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",
|
||||
},
|
||||
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],
|
||||
} satisfies Partial<Config>;
|
||||
@@ -1,26 +1,9 @@
|
||||
import type { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type { Component } from "vue";
|
||||
import { badgeVariants } from "@/components/common/badge/badge.variants";
|
||||
|
||||
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;
|
||||
export interface UiBadgeProps extends VariantProps<typeof badgeVariants> {
|
||||
icon?: Component;
|
||||
iconRight?: Component;
|
||||
iconStyles?: string;
|
||||
size?: "12px" | "14px" | "16px" | "18px" | "20px" | "24px";
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
}
|
||||
35
unraid-ui/stories/components/brand/BrandButton.stories.ts
Normal file
35
unraid-ui/stories/components/brand/BrandButton.stories.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import BrandButton from "../../../src/components/brand/BrandButton.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Brand",
|
||||
component: BrandButton,
|
||||
} satisfies Meta<typeof BrandButton>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Button: Story = {
|
||||
args: {
|
||||
variant: "fill",
|
||||
size: "14px",
|
||||
padding: "default",
|
||||
text: "Click me",
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BrandButton },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<BrandButton
|
||||
:variant="args.variant"
|
||||
:size="args.size"
|
||||
:padding="args.padding"
|
||||
:text="args.text"
|
||||
:class="args.class"
|
||||
/>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
39
unraid-ui/stories/components/brand/BrandLoading.stories.ts
Normal file
39
unraid-ui/stories/components/brand/BrandLoading.stories.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import BrandLoading from "../../../src/components/brand/BrandLoading.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Brand",
|
||||
component: BrandLoading,
|
||||
argTypes: {
|
||||
gradientStart: { control: 'color' },
|
||||
gradientStop: { control: 'color' },
|
||||
title: { control: 'text' },
|
||||
},
|
||||
} satisfies Meta<typeof BrandLoading>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
title: 'Loading',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BrandLoading },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div class="w-[200px]">
|
||||
<BrandLoading
|
||||
:gradient-start="args.gradientStart"
|
||||
:gradient-stop="args.gradientStop"
|
||||
:title="args.title"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import BrandLoadingWhite from "../../../src/components/brand/BrandLoadingWhite.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Brand",
|
||||
component: BrandLoadingWhite,
|
||||
decorators: [
|
||||
() => ({
|
||||
template: '<div class="bg-black p-8 rounded-md"><story/></div>',
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof BrandLoadingWhite>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const LoadingWhite: Story = {
|
||||
render: () => ({
|
||||
components: { BrandLoadingWhite },
|
||||
template: `
|
||||
<div class="w-[200px]">
|
||||
<BrandLoadingWhite />
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
36
unraid-ui/stories/components/brand/BrandLogo.stories.ts
Normal file
36
unraid-ui/stories/components/brand/BrandLogo.stories.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import BrandLogo from "../../../src/components/brand/BrandLogo.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Brand",
|
||||
component: BrandLogo,
|
||||
argTypes: {
|
||||
gradientStart: { control: 'color' },
|
||||
gradientStop: { control: 'color' },
|
||||
},
|
||||
} satisfies Meta<typeof BrandLogo>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Logo: Story = {
|
||||
args: {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BrandLogo },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div class="w-[300px]">
|
||||
<BrandLogo
|
||||
:gradient-start="args.gradientStart"
|
||||
:gradient-stop="args.gradientStop"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import BrandLogoConnect from "../../../src/components/brand/BrandLogoConnect.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Brand",
|
||||
component: BrandLogoConnect,
|
||||
argTypes: {
|
||||
gradientStart: { control: 'color' },
|
||||
gradientStop: { control: 'color' },
|
||||
},
|
||||
} satisfies Meta<typeof BrandLogoConnect>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const LogoConnect: Story = {
|
||||
args: {
|
||||
gradientStart: '#e32929',
|
||||
gradientStop: '#ff8d30',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BrandLogoConnect },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div class="w-[300px]">
|
||||
<BrandLogoConnect
|
||||
:gradient-start="args.gradientStart"
|
||||
:gradient-stop="args.gradientStop"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
35
unraid-ui/stories/components/common/Badge.stories.ts
Normal file
35
unraid-ui/stories/components/common/Badge.stories.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import BadgeComponent from "../../../src/components/common/badge/Badge.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common",
|
||||
component: BadgeComponent,
|
||||
} satisfies Meta<typeof BadgeComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Badge: Story = {
|
||||
args: {
|
||||
variant: "gray",
|
||||
size: "md",
|
||||
default: "Badge",
|
||||
class: ""
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { BadgeComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<BadgeComponent
|
||||
:variant="args.variant"
|
||||
:size="args.size"
|
||||
:class="args.class"
|
||||
>
|
||||
{{ args.default }}
|
||||
</BadgeComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -1,56 +1,34 @@
|
||||
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>;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Button: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
size: "md",
|
||||
text: "Click me",
|
||||
default: "Click me",
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { ButtonComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template:
|
||||
'<ButtonComponent :variant="args.variant" :size="args.size">{{ args.text }}</ButtonComponent>',
|
||||
template: `
|
||||
<ButtonComponent
|
||||
:variant="args.variant"
|
||||
:size="args.size"
|
||||
:class="args.class"
|
||||
>
|
||||
{{ args.default }}
|
||||
</ButtonComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
78
unraid-ui/stories/components/common/DropdownMenu.stories.ts
Normal file
78
unraid-ui/stories/components/common/DropdownMenu.stories.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { MoreVertical } from "lucide-vue-next";
|
||||
|
||||
import Button from "../../../src/components/common/button/Button.vue";
|
||||
import DropdownMenu from "../../../src/components/common/dropdown-menu/DropdownMenu.vue";
|
||||
import DropdownMenuContent from "../../../src/components/common/dropdown-menu/DropdownMenuContent.vue";
|
||||
import DropdownMenuItem from "../../../src/components/common/dropdown-menu/DropdownMenuItem.vue";
|
||||
import DropdownMenuLabel from "../../../src/components/common/dropdown-menu/DropdownMenuLabel.vue";
|
||||
import DropdownMenuSeparator from "../../../src/components/common/dropdown-menu/DropdownMenuSeparator.vue";
|
||||
import DropdownMenuTrigger from "../../../src/components/common/dropdown-menu/DropdownMenuTrigger.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common/DropdownMenu",
|
||||
component: DropdownMenu,
|
||||
} satisfies Meta<typeof DropdownMenu>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Dropdown: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
Button,
|
||||
},
|
||||
template: `
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="secondary">Open Menu</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Profile</DropdownMenuItem>
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const IconDropdown: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
Button,
|
||||
MoreVertical,
|
||||
},
|
||||
template: `
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreVertical class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Duplicate</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
53
unraid-ui/stories/components/common/Loading.stories.ts
Normal file
53
unraid-ui/stories/components/common/Loading.stories.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import Bar from "../../../src/components/common/loading/Bar.vue";
|
||||
import Error from "../../../src/components/common/loading/Error.vue";
|
||||
import Spinner from "../../../src/components/common/loading/Spinner.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common/Loading",
|
||||
component: Bar,
|
||||
subcomponents: { Bar, Spinner, Error },
|
||||
} satisfies Meta<typeof Bar>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type BarStory = StoryObj<typeof Bar>;
|
||||
type SpinnerStory = StoryObj<typeof Spinner>;
|
||||
type ErrorStory = StoryObj<typeof Error>;
|
||||
|
||||
export const LoadingBar: BarStory = {
|
||||
args: {},
|
||||
render: (args) => ({
|
||||
components: { Bar },
|
||||
template: `<div class="w-full max-w-md"><Bar v-bind="args" /></div>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const LoadingSpinner: SpinnerStory = {
|
||||
args: {},
|
||||
render: (args) => ({
|
||||
components: { Spinner },
|
||||
template: `<div class="p-4"><Spinner v-bind="args" /></div>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const LoadingError: ErrorStory = {
|
||||
args: {
|
||||
loading: false,
|
||||
error: null,
|
||||
class: "",
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { Error },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
|
||||
<Error v-bind="args">
|
||||
<div class="text-center">Content when not loading or error</div>
|
||||
</Error>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
69
unraid-ui/stories/components/common/ScrollArea.stories.ts
Normal file
69
unraid-ui/stories/components/common/ScrollArea.stories.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import ScrollArea from "../../../src/components/common/scroll-area/ScrollArea.vue";
|
||||
import ScrollBar from "../../../src/components/common/scroll-area/ScrollBar.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common/ScrollArea",
|
||||
component: ScrollArea,
|
||||
subcomponents: { ScrollBar },
|
||||
} satisfies Meta<typeof ScrollArea>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Vertical: Story = {
|
||||
args: {
|
||||
class: "rounded-md border",
|
||||
style: {
|
||||
height: "200px",
|
||||
width: "350px",
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { ScrollArea, ScrollBar },
|
||||
setup() {
|
||||
const items = Array(30).fill(0).map((_, i) => `Content ${i + 1}`);
|
||||
return { args, items };
|
||||
},
|
||||
template: `
|
||||
<ScrollArea v-bind="args">
|
||||
<div class="p-4">
|
||||
<div class="space-y-1">
|
||||
<div v-for="(item, i) in items" :key="i" class="text-sm">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollBar orientation="vertical" />
|
||||
</ScrollArea>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Horizontal: Story = {
|
||||
args: {
|
||||
class: "rounded-md border",
|
||||
style: {
|
||||
height: "80px",
|
||||
width: "350px",
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { ScrollArea, ScrollBar },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<ScrollArea v-bind="args">
|
||||
<div class="flex p-4">
|
||||
${Array(50)
|
||||
.fill(0)
|
||||
.map((_, i) => `<div class="flex-shrink-0 mr-2">Content ${i + 1}</div>`)
|
||||
.join("")}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
84
unraid-ui/stories/components/common/Sheet.stories.ts
Normal file
84
unraid-ui/stories/components/common/Sheet.stories.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import SheetComponent from "../../../src/components/common/sheet/Sheet.vue";
|
||||
import SheetTrigger from "../../../src/components/common/sheet/SheetTrigger.vue";
|
||||
import SheetContent from "../../../src/components/common/sheet/SheetContent.vue";
|
||||
import SheetHeader from "../../../src/components/common/sheet/SheetHeader.vue";
|
||||
import SheetTitle from "../../../src/components/common/sheet/SheetTitle.vue";
|
||||
import SheetDescription from "../../../src/components/common/sheet/SheetDescription.vue";
|
||||
import SheetFooter from "../../../src/components/common/sheet/SheetFooter.vue";
|
||||
import Button from "../../../src/components/common/button/Button.vue";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common",
|
||||
component: SheetComponent,
|
||||
subcomponents: {
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetFooter
|
||||
},
|
||||
} satisfies Meta<typeof SheetComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Sheet: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
SheetComponent,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
Button,
|
||||
},
|
||||
template: `
|
||||
<div class="inline-flex items-center gap-4 p-4">
|
||||
<SheetComponent>
|
||||
<SheetTrigger>
|
||||
<Button variant="outline">Open Right</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Edit Profile</SheetTitle>
|
||||
<SheetDescription>
|
||||
Make changes to your profile here. Click save when you're done.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div class="py-6">Sheet content goes here...</div>
|
||||
<SheetFooter>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button>Save changes</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</SheetComponent>
|
||||
|
||||
<SheetComponent>
|
||||
<SheetTrigger>
|
||||
<Button variant="outline">Open Left</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Left Side Sheet</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div class="py-6">Content from the left side</div>
|
||||
</SheetContent>
|
||||
</SheetComponent>
|
||||
|
||||
<SheetComponent>
|
||||
<SheetTrigger>
|
||||
<Button variant="outline">Open Top</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="top" padding="none">
|
||||
<div class="p-4">Top sheet with no padding variant</div>
|
||||
</SheetContent>
|
||||
</SheetComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
41
unraid-ui/stories/components/common/Tabs.stories.ts
Normal file
41
unraid-ui/stories/components/common/Tabs.stories.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { Tabs as TabsComponent, TabsList, TabsTrigger, TabsContent } from "../../../src/components/common/tabs";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common",
|
||||
component: TabsComponent,
|
||||
} satisfies Meta<typeof TabsComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Tabs: Story = {
|
||||
args: {
|
||||
defaultValue: "tab1",
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { TabsComponent, TabsList, TabsTrigger, TabsContent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<TabsComponent :default-value="args.defaultValue" class="w-[400px]">
|
||||
<TabsList>
|
||||
<TabsTrigger value="tab1">Account</TabsTrigger>
|
||||
<TabsTrigger value="tab2">Password</TabsTrigger>
|
||||
<TabsTrigger value="tab3">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="tab1">
|
||||
<div class="p-4">Account settings content</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="tab2">
|
||||
<div class="p-4">Password settings content</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="tab3">
|
||||
<div class="p-4">Other settings content</div>
|
||||
</TabsContent>
|
||||
</TabsComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
41
unraid-ui/stories/components/common/Tooltip.stories.ts
Normal file
41
unraid-ui/stories/components/common/Tooltip.stories.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { Tooltip as TooltipComponent, TooltipTrigger, TooltipContent, TooltipProvider } from "../../../src/components/common/tooltip";
|
||||
import { Button } from "../../../src/components/common/button";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Common",
|
||||
component: TooltipComponent,
|
||||
} satisfies Meta<typeof TooltipComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Tooltip: Story = {
|
||||
args: {
|
||||
defaultOpen: false,
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { TooltipProvider, TooltipComponent, TooltipTrigger, TooltipContent, Button },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div id="modals"></div>
|
||||
<div class="p-20 flex items-center justify-start">
|
||||
<TooltipProvider>
|
||||
<TooltipComponent :default-open="args.defaultOpen">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="outline">Hover me</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Add to library</p>
|
||||
</TooltipContent>
|
||||
</TooltipComponent>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
24
unraid-ui/stories/components/form/Input.stories.ts
Normal file
24
unraid-ui/stories/components/form/Input.stories.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { Input as InputComponent } from "../../../src/components/form/input";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form/Input",
|
||||
component: InputComponent,
|
||||
} satisfies Meta<typeof InputComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Input: Story = {
|
||||
render: (args) => ({
|
||||
components: { InputComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<InputComponent placeholder="Type something..." v-bind="args" />
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
23
unraid-ui/stories/components/form/Label.stories.ts
Normal file
23
unraid-ui/stories/components/form/Label.stories.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { Label as LabelComponent } from "../../../src/components/form/label";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form",
|
||||
component: LabelComponent,
|
||||
} satisfies Meta<typeof LabelComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Label: Story = {
|
||||
render: (args) => ({
|
||||
components: { LabelComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<LabelComponent>Email address</LabelComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
26
unraid-ui/stories/components/form/Lightswitch.stories.ts
Normal file
26
unraid-ui/stories/components/form/Lightswitch.stories.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { Lightswitch as LightswitchComponent } from "../../../src/components/form/lightswitch";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form/Lightswitch",
|
||||
component: LightswitchComponent,
|
||||
} satisfies Meta<typeof LightswitchComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Lightswitch: Story = {
|
||||
args: {
|
||||
label: "Enable notifications",
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { LightswitchComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<LightswitchComponent v-bind="args" />
|
||||
`,
|
||||
}),
|
||||
};
|
||||
96
unraid-ui/stories/components/form/Select.stories.ts
Normal file
96
unraid-ui/stories/components/form/Select.stories.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import {
|
||||
Select as SelectComponent,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../src/components/form/select";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form/Select",
|
||||
component: SelectComponent,
|
||||
} satisfies Meta<typeof SelectComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Select: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
SelectComponent,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
},
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div id="modals"></div>
|
||||
<SelectComponent>
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Fruits</SelectLabel>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="orange">Orange</SelectItem>
|
||||
<SelectItem value="grape">Grape</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</SelectComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Grouped: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
SelectComponent,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
},
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div id="modals"></div>
|
||||
<SelectComponent>
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select a food" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Fruits</SelectLabel>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="grape">Grape</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Vegetables</SelectLabel>
|
||||
<SelectItem value="carrot">Carrot</SelectItem>
|
||||
<SelectItem value="potato">Potato</SelectItem>
|
||||
<SelectItem value="celery">Celery</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</SelectComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
27
unraid-ui/stories/components/form/Switch.stories.ts
Normal file
27
unraid-ui/stories/components/form/Switch.stories.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { Switch as SwitchComponent } from "../../../src/components/form/switch";
|
||||
import { Label } from "../../../src/components/form/label";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Form",
|
||||
component: SwitchComponent,
|
||||
} satisfies Meta<typeof SwitchComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Switch: Story = {
|
||||
render: (args) => ({
|
||||
components: { SwitchComponent, Label },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div class="flex items-center space-x-2">
|
||||
<SwitchComponent v-bind="args" />
|
||||
<Label>Airplane Mode</Label>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
62
unraid-ui/stories/components/layout/CardWrapper.stories.ts
Normal file
62
unraid-ui/stories/components/layout/CardWrapper.stories.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { CardWrapper as CardWrapperComponent } from "../../../src/components/layout";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Layout/CardWrapper",
|
||||
component: CardWrapperComponent,
|
||||
} satisfies Meta<typeof CardWrapperComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const CardWrapper: Story = {
|
||||
render: (args) => ({
|
||||
components: { CardWrapperComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<CardWrapperComponent v-bind="args">
|
||||
<h3 class="text-lg font-semibold mb-2">Card Title</h3>
|
||||
<p>This is some example content inside the card wrapper.</p>
|
||||
</CardWrapperComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
error: true,
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { CardWrapperComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<CardWrapperComponent v-bind="args">
|
||||
<h3 class="text-lg font-semibold mb-2">Error State</h3>
|
||||
<p>This card shows the error state styling.</p>
|
||||
</CardWrapperComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
warning: true,
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { CardWrapperComponent },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<CardWrapperComponent v-bind="args">
|
||||
<h3 class="text-lg font-semibold mb-2">Warning State</h3>
|
||||
<p>This card shows the warning state styling.</p>
|
||||
</CardWrapperComponent>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
57
unraid-ui/stories/components/layout/PageContainer.stories.ts
Normal file
57
unraid-ui/stories/components/layout/PageContainer.stories.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3";
|
||||
import { PageContainer as PageContainerComponent } from "../../../src/components/layout";
|
||||
import { CardWrapper } from "../../../src/components/layout";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Layout/PageContainer",
|
||||
component: PageContainerComponent,
|
||||
} satisfies Meta<typeof PageContainerComponent>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const PageContainer: Story = {
|
||||
render: (args) => ({
|
||||
components: { PageContainerComponent, CardWrapper },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div class="bg-muted/20 p-4">
|
||||
<PageContainerComponent v-bind="args">
|
||||
<CardWrapper>
|
||||
<h3 class="text-lg font-semibold mb-2">Section 1</h3>
|
||||
<p>This content is constrained by the PageContainer.</p>
|
||||
</CardWrapper>
|
||||
<CardWrapper>
|
||||
<h3 class="text-lg font-semibold mb-2">Section 2</h3>
|
||||
<p>Another section to demonstrate the grid gap.</p>
|
||||
</CardWrapper>
|
||||
</PageContainerComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const CustomMaxWidth: Story = {
|
||||
args: {
|
||||
maxWidth: 'max-w-2xl',
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { PageContainerComponent, CardWrapper },
|
||||
setup() {
|
||||
return { args };
|
||||
},
|
||||
template: `
|
||||
<div class="bg-muted/20 p-4">
|
||||
<PageContainerComponent v-bind="args">
|
||||
<CardWrapper>
|
||||
<h3 class="text-lg font-semibold mb-2">Narrower Container</h3>
|
||||
<p>This container uses a custom max-width value.</p>
|
||||
</CardWrapper>
|
||||
</PageContainerComponent>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
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";
|
||||
import { unraidPreset } from "./src/theme/preset";
|
||||
|
||||
export default <Partial<Config>>{
|
||||
content: ["./src/components/**/*.{js,vue,ts}", "./src/composables/**/*.vue"],
|
||||
darkMode: ["selector"],
|
||||
export default {
|
||||
presets: [unraidPreset],
|
||||
content: [
|
||||
"./src/components/**/*.{js,vue,ts}",
|
||||
"./src/composables/**/*.{js,vue,ts}",
|
||||
"./stories/**/*.stories.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
safelist: [
|
||||
"dark",
|
||||
"DropdownWrapper_blip",
|
||||
@@ -19,256 +19,13 @@ export default <Partial<Config>>{
|
||||
"unraid_mark_7",
|
||||
"unraid_mark_8",
|
||||
"unraid_mark_9",
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
{
|
||||
pattern: /^text-(header-text-secondary|orange-dark)$/,
|
||||
variants: ['group-hover', 'group-focus']
|
||||
},
|
||||
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"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
pattern: /^(underline|no-underline)$/,
|
||||
variants: ['group-hover', 'group-focus']
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
typography,
|
||||
animate,
|
||||
remToRem({
|
||||
baseFontSize: 16,
|
||||
newFontSize: Number(process.env.VITE_TAILWIND_BASE_FONT_SIZE) || 10,
|
||||
}),
|
||||
],
|
||||
};
|
||||
} satisfies Partial<Config>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
@@ -13,6 +14,7 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
@@ -22,5 +24,5 @@
|
||||
},
|
||||
"types": ["vite/client", "vitest/globals"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "**/*.config.ts"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"incremental": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
@@ -10,11 +12,20 @@
|
||||
"@/components": ["./src/components"],
|
||||
"@/composables": ["./src/composables"],
|
||||
"@/lib": ["./src/lib"],
|
||||
"@/types": ["./src/types"]
|
||||
"@/types": ["./src/types"],
|
||||
"@/theme": ["./src/theme"]
|
||||
},
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"files": ["tailwind.config.ts"],
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"./tailwind.config.ts",
|
||||
"src/theme/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "**/*.copy.vue", "**/*copy.vue"],
|
||||
"references": [{ "path": "./tsconfig.test.json" }]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"@testing-library/vue"
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "tailwind.config.ts"],
|
||||
"exclude": ["node_modules", "**/*.copy.vue", "**/*copy.vue"]
|
||||
}
|
||||
|
||||
@@ -1,56 +1,82 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from "vite";
|
||||
import { resolve } from "path";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import dts from "vite-plugin-dts";
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import tailwindcss from 'tailwindcss';
|
||||
|
||||
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",
|
||||
},
|
||||
export default function createConfig() {
|
||||
return defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
...(process.env.npm_lifecycle_script?.includes('storybook')
|
||||
? []
|
||||
: [
|
||||
dts({
|
||||
insertTypesEntry: true,
|
||||
include: ['src/**/*.ts', 'src/**/*.vue', 'tailwind.config.ts'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: true,
|
||||
copyDtsFiles: true,
|
||||
}),
|
||||
]),
|
||||
],
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
},
|
||||
},
|
||||
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"),
|
||||
build: {
|
||||
cssCodeSplit: false,
|
||||
rollupOptions: {
|
||||
external: ['vue', 'tailwindcss'],
|
||||
input: {
|
||||
index: resolve(__dirname, 'src/index.ts'),
|
||||
tailwind: resolve(__dirname, 'tailwind.config.ts'),
|
||||
preset: resolve(__dirname, 'src/theme/preset.ts'),
|
||||
},
|
||||
preserveEntrySignatures: 'allow-extension',
|
||||
output: {
|
||||
exports: 'named',
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
tailwindcss: 'tailwindcss',
|
||||
},
|
||||
format: 'es',
|
||||
preserveModules: true,
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name === 'style.css') {
|
||||
return 'style.css';
|
||||
}
|
||||
return '[name][extname]';
|
||||
},
|
||||
entryFileNames: (chunkInfo) => {
|
||||
if (chunkInfo.name === 'tailwind') {
|
||||
return '[name].config.js';
|
||||
} else {
|
||||
return '[name].js';
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
target: 'esnext',
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
},
|
||||
},
|
||||
test: {
|
||||
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'),
|
||||
'@/composables': resolve(__dirname, './src/composables'),
|
||||
'@/lib': resolve(__dirname, './src/lib'),
|
||||
'@/styles': resolve(__dirname, './src/styles'),
|
||||
'@/types': resolve(__dirname, './src/types'),
|
||||
'@/theme': resolve(__dirname, './src/theme'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
// eslint-disable vue/no-v-html
|
||||
import { BrandButton } from '@unraid/ui';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const serverStore = useServerStore();
|
||||
@@ -33,7 +30,7 @@ const { authAction, stateData } = storeToRefs(serverStore);
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
@import '../assets/main.css';
|
||||
</style>
|
||||
|
||||
@@ -15,15 +15,12 @@ else
|
||||
echo "Third party plugins found - PLEASE CHECK YOUR UNRAID NOTIFICATIONS AND WAIT FOR THE MESSAGE THAT IT IS SAFE TO REBOOT!"
|
||||
fi
|
||||
*/
|
||||
import { PageContainer } from '@unraid/ui';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onBeforeMount } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
export interface Props {
|
||||
@@ -56,7 +53,7 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPageContainer>
|
||||
<PageContainer>
|
||||
<UpdateOsStatus
|
||||
:title="t('Downgrade Unraid OS')"
|
||||
:subtitle="subtitle"
|
||||
@@ -70,15 +67,12 @@ onBeforeMount(() => {
|
||||
:version="restoreVersion"
|
||||
:t="t"
|
||||
/>
|
||||
<UpdateOsThirdPartyDrivers
|
||||
v-if="rebootType === 'thirdPartyDriversDownloading'"
|
||||
:t="t"
|
||||
/>
|
||||
</UiPageContainer>
|
||||
<UpdateOsThirdPartyDrivers v-if="rebootType === 'thirdPartyDriversDownloading'" :t="t" />
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
@import '../assets/main.css';
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ArrowDownTrayIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { BrandButton } from '@unraid/ui';
|
||||
import { CONNECT_FORUMS, CONTACT, DISCORD, WEBGUI_GRAPHQL } from '~/helpers/urls';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -19,7 +17,11 @@ const downloadUrl = computed(() => new URL(`/graphql/api/logs?apiKey=${apiKey.va
|
||||
<div class="whitespace-normal flex flex-col gap-y-16px max-w-3xl">
|
||||
<span>
|
||||
{{ t('The primary method of support for Unraid Connect is through our forums and Discord.') }}
|
||||
{{ t('If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.') }}
|
||||
{{
|
||||
t(
|
||||
'If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.'
|
||||
)
|
||||
}}
|
||||
{{ t('The logs may contain sensitive information so do not post them publicly.') }}
|
||||
</span>
|
||||
<span class="flex flex-col gap-y-16px">
|
||||
@@ -36,15 +38,30 @@ const downloadUrl = computed(() => new URL(`/graphql/api/logs?apiKey=${apiKey.va
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row items-baseline gap-8px">
|
||||
<a :href="CONNECT_FORUMS.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
|
||||
<a
|
||||
:href="CONNECT_FORUMS.toString()"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px"
|
||||
>
|
||||
{{ t('Unraid Connect Forums') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
<a :href="DISCORD.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
|
||||
<a
|
||||
:href="DISCORD.toString()"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px"
|
||||
>
|
||||
{{ t('Unraid Discord') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
<a :href="CONTACT.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
|
||||
<a
|
||||
:href="CONTACT.toString()"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px"
|
||||
>
|
||||
{{ t('Unraid Contact Page') }}
|
||||
<ArrowTopRightOnSquareIcon class="w-16px" />
|
||||
</a>
|
||||
@@ -54,7 +71,7 @@ const downloadUrl = computed(() => new URL(`/graphql/api/logs?apiKey=${apiKey.va
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
@import '../assets/main.css';
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
BellAlertIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
import { BellAlertIcon, ExclamationTriangleIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
|
||||
import { Badge } from '@unraid/ui';
|
||||
import { getReleaseNotesUrl, WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
import type { UiBadgeProps, UiBadgePropsColor } from '~/types/ui/badge';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -27,23 +18,22 @@ const { osVersion, rebootType, stateDataError } = storeToRefs(serverStore);
|
||||
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
|
||||
const { rebootTypeText } = storeToRefs(updateOsActionsStore);
|
||||
|
||||
export interface UpdateOsStatus extends UserProfileLink {
|
||||
badge: UiBadgeProps;
|
||||
}
|
||||
const updateOsStatus = computed(() => {
|
||||
if (stateDataError.value) { // only allowed to update when server is does not have a state error
|
||||
if (stateDataError.value) {
|
||||
// only allowed to update when server is does not have a state error
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rebootTypeText.value) {
|
||||
return {
|
||||
badge: {
|
||||
color: 'yellow' as UiBadgePropsColor,
|
||||
color: 'yellow',
|
||||
icon: ExclamationTriangleIcon,
|
||||
},
|
||||
href: rebootType.value === 'downgrade'
|
||||
? WEBGUI_TOOLS_DOWNGRADE.toString()
|
||||
: WEBGUI_TOOLS_UPDATE.toString(),
|
||||
href:
|
||||
rebootType.value === 'downgrade'
|
||||
? WEBGUI_TOOLS_DOWNGRADE.toString()
|
||||
: WEBGUI_TOOLS_UPDATE.toString(),
|
||||
text: t(rebootTypeText.value),
|
||||
};
|
||||
}
|
||||
@@ -51,13 +41,13 @@ const updateOsStatus = computed(() => {
|
||||
if (availableWithRenewal.value || available.value) {
|
||||
return {
|
||||
badge: {
|
||||
color: 'orange' as UiBadgePropsColor,
|
||||
color: 'orange',
|
||||
icon: BellAlertIcon,
|
||||
},
|
||||
click: () => { updateOsStore.setModalOpen(true); },
|
||||
text: availableWithRenewal.value
|
||||
? t('Update Released')
|
||||
: t('Update Available'),
|
||||
click: () => {
|
||||
updateOsStore.setModalOpen(true);
|
||||
},
|
||||
text: availableWithRenewal.value ? t('Update Released') : t('Update Available'),
|
||||
title: availableWithRenewal.value
|
||||
? t('Unraid OS {0} Released', [availableWithRenewal.value])
|
||||
: t('Unraid OS {0} Update Available', [available.value]),
|
||||
@@ -77,15 +67,15 @@ const updateOsStatus = computed(() => {
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<UiBadge
|
||||
color="custom"
|
||||
<Badge
|
||||
variant="custom"
|
||||
:icon="InformationCircleIcon"
|
||||
icon-styles="text-header-text-secondary"
|
||||
size="14px"
|
||||
size="sm"
|
||||
class="text-header-text-secondary group-hover:text-orange-dark group-focus:text-orange-dark group-hover:underline group-focus:underline"
|
||||
>
|
||||
{{ osVersion }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
</a>
|
||||
<component
|
||||
:is="updateOsStatus.href ? 'a' : 'button'"
|
||||
@@ -95,14 +85,14 @@ const updateOsStatus = computed(() => {
|
||||
class="group"
|
||||
@click="updateOsStatus.click?.()"
|
||||
>
|
||||
<UiBadge
|
||||
<Badge
|
||||
v-if="updateOsStatus.badge"
|
||||
:color="updateOsStatus.badge.color"
|
||||
:icon="updateOsStatus.badge.icon"
|
||||
size="12px"
|
||||
size="xs"
|
||||
>
|
||||
{{ updateOsStatus.text }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
<template v-else>
|
||||
{{ updateOsStatus.text }}
|
||||
</template>
|
||||
@@ -111,7 +101,6 @@ const updateOsStatus = computed(() => {
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
</style>
|
||||
|
||||
@@ -15,15 +15,12 @@ else
|
||||
echo "Third party plugins found - PLEASE CHECK YOUR UNRAID NOTIFICATIONS AND WAIT FOR THE MESSAGE THAT IT IS SAFE TO REBOOT!"
|
||||
fi
|
||||
*/
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { BrandLoading, PageContainer } from '@unraid/ui';
|
||||
import { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useServerStore } from '~/store/server';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -46,7 +43,9 @@ const subtitle = computed(() => {
|
||||
});
|
||||
|
||||
/** when we're not prompting for reboot /Tools/Update will automatically send the user to account.unraid.net/server/update-os */
|
||||
const showLoader = computed(() => window.location.pathname === WEBGUI_TOOLS_UPDATE.pathname && rebootType.value === '');
|
||||
const showLoader = computed(
|
||||
() => window.location.pathname === WEBGUI_TOOLS_UPDATE.pathname && rebootType.value === ''
|
||||
);
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (showLoader.value) {
|
||||
@@ -57,7 +56,7 @@ onBeforeMount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPageContainer>
|
||||
<PageContainer>
|
||||
<BrandLoading v-if="showLoader" class="mx-auto my-12 max-w-160px" />
|
||||
<UpdateOsStatus
|
||||
v-else
|
||||
@@ -66,16 +65,14 @@ onBeforeMount(() => {
|
||||
:subtitle="subtitle"
|
||||
:t="t"
|
||||
/>
|
||||
<UpdateOsThirdPartyDrivers
|
||||
v-if="rebootType === 'thirdPartyDriversDownloading'"
|
||||
:t="t"
|
||||
/>
|
||||
</UiPageContainer>
|
||||
<UpdateOsThirdPartyDrivers v-if="rebootType === 'thirdPartyDriversDownloading'" :t="t" />
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
@import '../assets/main.css';
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
@@ -124,6 +121,4 @@ onBeforeMount(() => {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
|
||||
@@ -6,19 +6,16 @@ import {
|
||||
InformationCircleIcon,
|
||||
LifebuoyIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import dayjs from 'dayjs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
import { BrandButton, CardWrapper } from '@unraid/ui';
|
||||
import useDateTimeHelper from '~/composables/dateTime';
|
||||
import { FORUMS_BUG_REPORT } from '~/helpers/urls';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
import dayjs from 'dayjs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
t: ComposerTranslation;
|
||||
@@ -30,9 +27,12 @@ const serverStore = useServerStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
|
||||
const { dateTimeFormat } = storeToRefs(serverStore);
|
||||
const {
|
||||
outputDateTimeFormatted: formattedReleaseDate,
|
||||
} = useDateTimeHelper(dateTimeFormat.value, props.t, true, dayjs(props.releaseDate, 'YYYY-MM-DD').valueOf());
|
||||
const { outputDateTimeFormatted: formattedReleaseDate } = useDateTimeHelper(
|
||||
dateTimeFormat.value,
|
||||
props.t,
|
||||
true,
|
||||
dayjs(props.releaseDate, 'YYYY-MM-DD').valueOf()
|
||||
);
|
||||
|
||||
const diagnosticsButton = ref<UserProfileLink | undefined>({
|
||||
click: () => {
|
||||
@@ -55,12 +55,10 @@ const downgradeButton = ref<UserProfileLink>({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCardWrapper :increased-padding="true">
|
||||
<CardWrapper :increased-padding="true">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-20px sm:gap-24px">
|
||||
<div class="grid gap-y-16px">
|
||||
<h3
|
||||
class="font-semibold leading-normal flex flex-row items-start justify-start gap-8px"
|
||||
>
|
||||
<h3 class="font-semibold leading-normal flex flex-row items-start justify-start gap-8px">
|
||||
<ArrowUturnDownIcon class="w-20px shrink-0" />
|
||||
<span class="leading-none inline-flex flex-wrap justify-start items-baseline gap-8px">
|
||||
<span class="text-20px">
|
||||
@@ -76,28 +74,45 @@ const downgradeButton = ref<UserProfileLink>({
|
||||
</h3>
|
||||
<div class="prose text-16px leading-relaxed opacity-75 whitespace-normal">
|
||||
<p>{{ t(`Downgrades are only recommended if you're unable to solve a critical issue.`) }}</p>
|
||||
<p>{{ t('In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.') }}</p>
|
||||
<p>{{ t('Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.') }} </p>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
'In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p>
|
||||
{{
|
||||
t(
|
||||
'Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="downgradeButton" class="flex flex-col flex-shrink-0 gap-16px flex-grow items-stretch">
|
||||
<BrandButton
|
||||
:btn-style="'underline'"
|
||||
:variant="'underline'"
|
||||
:icon="InformationCircleIcon"
|
||||
:text="t('{0} Release Notes', [version])"
|
||||
@click="updateOsActionsStore.viewReleaseNotes(t('{0} Release Notes', [version]), '/boot/previous/changes.txt')"
|
||||
@click="
|
||||
updateOsActionsStore.viewReleaseNotes(
|
||||
t('{0} Release Notes', [version]),
|
||||
'/boot/previous/changes.txt'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<BrandButton
|
||||
v-if="diagnosticsButton"
|
||||
:btn-style="'gray'"
|
||||
:variant="'gray'"
|
||||
:icon="diagnosticsButton.icon"
|
||||
:name="diagnosticsButton.name"
|
||||
:text="diagnosticsButton.text"
|
||||
@click="diagnosticsButton.click"
|
||||
/>
|
||||
<BrandButton
|
||||
:btn-style="'gray'"
|
||||
:variant="'gray'"
|
||||
:external="true"
|
||||
:href="FORUMS_BUG_REPORT.toString()"
|
||||
:icon="LifebuoyIcon"
|
||||
@@ -113,11 +128,11 @@ const downgradeButton = ref<UserProfileLink>({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UiCardWrapper>
|
||||
</CardWrapper>
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
@import '../../assets/main.css';
|
||||
</style>
|
||||
|
||||
@@ -8,19 +8,18 @@ import {
|
||||
InformationCircleIcon,
|
||||
XCircleIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import { WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
|
||||
import { Badge, BrandButton } from '@unraid/ui';
|
||||
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
|
||||
import useDateTimeHelper from '~/composables/dateTime';
|
||||
import { WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import type { ButtonProps } from '~/types/ui/button';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
|
||||
|
||||
export interface Props {
|
||||
downgradeNotAvailable?: boolean;
|
||||
restoreVersion?: string | undefined;
|
||||
@@ -42,16 +41,15 @@ const serverStore = useServerStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
|
||||
const { dateTimeFormat, osVersion, rebootType, rebootVersion, regExp, regUpdatesExpired } = storeToRefs(serverStore);
|
||||
const { dateTimeFormat, osVersion, rebootType, rebootVersion, regExp, regUpdatesExpired } =
|
||||
storeToRefs(serverStore);
|
||||
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
|
||||
const { ineligibleText, rebootTypeText, status } = storeToRefs(updateOsActionsStore);
|
||||
|
||||
const updateAvailable = computed(() => available.value || availableWithRenewal.value);
|
||||
|
||||
const {
|
||||
outputDateTimeReadableDiff: readableDiffRegExp,
|
||||
outputDateTimeFormatted: formattedRegExp,
|
||||
} = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
|
||||
const { outputDateTimeReadableDiff: readableDiffRegExp, outputDateTimeFormatted: formattedRegExp } =
|
||||
useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
|
||||
|
||||
const regExpOutput = computed(() => {
|
||||
if (!regExp.value) {
|
||||
@@ -67,16 +65,16 @@ const regExpOutput = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const showRebootButton = computed(() => rebootType.value === 'downgrade' || rebootType.value === 'update');
|
||||
const showRebootButton = computed(
|
||||
() => rebootType.value === 'downgrade' || rebootType.value === 'update'
|
||||
);
|
||||
|
||||
const checkButton = computed((): ButtonProps => {
|
||||
if (showRebootButton.value || props.showExternalDowngrade) {
|
||||
return {
|
||||
btnStyle: 'outline',
|
||||
click: () => {
|
||||
props.showExternalDowngrade
|
||||
? accountStore.downgradeOs()
|
||||
: accountStore.updateOs();
|
||||
props.showExternalDowngrade ? accountStore.downgradeOs() : accountStore.updateOs();
|
||||
},
|
||||
icon: ArrowTopRightOnSquareIcon,
|
||||
text: props.t('More options'),
|
||||
@@ -124,9 +122,9 @@ const checkButton = computed((): ButtonProps => {
|
||||
:title="t('View release notes')"
|
||||
@click="updateOsActionsStore.viewReleaseNotes(t('{0} Release Notes', [osVersion]))"
|
||||
>
|
||||
<UiBadge :icon="InformationCircleIcon" class="underline">
|
||||
<Badge :icon="InformationCircleIcon" variant="gray" size="md">
|
||||
{{ t('Current Version {0}', [osVersion]) }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
</button>
|
||||
|
||||
<a
|
||||
@@ -135,75 +133,68 @@ const checkButton = computed((): ButtonProps => {
|
||||
class="group"
|
||||
:title="t('Learn more and fix')"
|
||||
>
|
||||
<UiBadge
|
||||
:color="'yellow'"
|
||||
<Badge
|
||||
variant="yellow"
|
||||
:icon="ExclamationTriangleIcon"
|
||||
:title="regExpOutput?.text"
|
||||
class="underline"
|
||||
>
|
||||
{{ t('Key ineligible for future releases') }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
</a>
|
||||
<UiBadge
|
||||
<Badge
|
||||
v-else-if="ineligibleText && availableWithRenewal"
|
||||
:color="'yellow'"
|
||||
variant="yellow"
|
||||
:icon="ExclamationTriangleIcon"
|
||||
:title="regExpOutput?.text"
|
||||
>
|
||||
{{ t('Key ineligible for {0}', [availableWithRenewal]) }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
|
||||
<UiBadge
|
||||
v-if="status === 'checking'"
|
||||
:color="'orange'"
|
||||
:icon="BrandLoadingWhite"
|
||||
>
|
||||
<Badge v-if="status === 'checking'" variant="orange" :icon="BrandLoadingWhite">
|
||||
{{ t('Checking...') }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
<template v-else>
|
||||
<UiBadge
|
||||
<Badge
|
||||
v-if="rebootType === ''"
|
||||
:color="updateAvailable ? 'orange' : 'green'"
|
||||
:variant="updateAvailable ? 'orange' : 'green'"
|
||||
:icon="updateAvailable ? BellAlertIcon : CheckCircleIcon"
|
||||
>
|
||||
{{ (available
|
||||
? t('Unraid {0} Available', [available])
|
||||
: (availableWithRenewal
|
||||
? t('Up-to-date with eligible releases')
|
||||
: t('Up-to-date')))
|
||||
{{
|
||||
available
|
||||
? t('Unraid {0} Available', [available])
|
||||
: availableWithRenewal
|
||||
? t('Up-to-date with eligible releases')
|
||||
: t('Up-to-date')
|
||||
}}
|
||||
</UiBadge>
|
||||
<UiBadge
|
||||
v-else
|
||||
:color="'yellow'"
|
||||
:icon="ExclamationTriangleIcon"
|
||||
>
|
||||
</Badge>
|
||||
<Badge v-else variant="yellow" :icon="ExclamationTriangleIcon">
|
||||
{{ t(rebootTypeText) }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
<UiBadge
|
||||
v-if="downgradeNotAvailable"
|
||||
:color="'gray'"
|
||||
:icon="XCircleIcon"
|
||||
>
|
||||
<Badge v-if="downgradeNotAvailable" variant="gray" :icon="XCircleIcon">
|
||||
{{ t('No downgrade available') }}
|
||||
</UiBadge>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div class="inline-flex flex-col flex-shrink-0 gap-16px flex-grow items-center md:items-end">
|
||||
<span v-if="showRebootButton">
|
||||
<BrandButton
|
||||
btn-style="fill"
|
||||
variant="fill"
|
||||
:icon="ArrowPathIcon"
|
||||
:text="rebootType === 'downgrade' ? t('Reboot Now to Downgrade to {0}', [rebootVersion]) : t('Reboot Now to Update to {0}', [rebootVersion])"
|
||||
:text="
|
||||
rebootType === 'downgrade'
|
||||
? t('Reboot Now to Downgrade to {0}', [rebootVersion])
|
||||
: t('Reboot Now to Update to {0}', [rebootVersion])
|
||||
"
|
||||
@click="updateOsActionsStore.rebootServer()"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<BrandButton
|
||||
:btn-style="checkButton.btnStyle"
|
||||
:variant="checkButton.btnStyle"
|
||||
:icon="checkButton.icon"
|
||||
:text="checkButton.text"
|
||||
@click="checkButton.click"
|
||||
@@ -212,7 +203,7 @@ const checkButton = computed((): ButtonProps => {
|
||||
|
||||
<span v-if="rebootType !== ''">
|
||||
<BrandButton
|
||||
btn-style="outline"
|
||||
variant="outline"
|
||||
:icon="XCircleIcon"
|
||||
:text="t('Cancel {0}', [rebootType === 'downgrade' ? t('Downgrade') : t('Update')])"
|
||||
@click="updateOsStore.cancelUpdate()"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
// @todo ensure key installs and updateOs can be handled at the same time
|
||||
// @todo with multiple actions of key install and update after successful key install, rather than showing default success message, show a message to have them confirm the update
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDoubleDownIcon,
|
||||
@@ -10,10 +9,7 @@ import {
|
||||
WrenchScrewdriverIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { WEBGUI_CONNECT_SETTINGS, WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
@@ -21,6 +17,8 @@ import { useInstallKeyStore } from '~/store/installKey';
|
||||
// import { usePromoStore } from '~/store/promo';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
@@ -38,21 +36,10 @@ const installKeyStore = useInstallKeyStore();
|
||||
const serverStore = useServerStore();
|
||||
const updateOsActionStore = useUpdateOsActionsStore();
|
||||
|
||||
const {
|
||||
accountAction,
|
||||
accountActionHide,
|
||||
accountActionStatus,
|
||||
accountActionType,
|
||||
} = storeToRefs(accountStore);
|
||||
const {
|
||||
callbackStatus,
|
||||
} = storeToRefs(callbackActionsStore);
|
||||
const {
|
||||
keyActionType,
|
||||
keyUrl,
|
||||
keyInstallStatus,
|
||||
keyType,
|
||||
} = storeToRefs(installKeyStore);
|
||||
const { accountAction, accountActionHide, accountActionStatus, accountActionType } =
|
||||
storeToRefs(accountStore);
|
||||
const { callbackStatus } = storeToRefs(callbackActionsStore);
|
||||
const { keyActionType, keyUrl, keyInstallStatus, keyType } = storeToRefs(installKeyStore);
|
||||
const {
|
||||
connectPluginInstalled,
|
||||
refreshServerStateStatus,
|
||||
@@ -80,7 +67,9 @@ const isSettingsPage = ref<boolean>(document.location.pathname === '/Settings/Ma
|
||||
|
||||
const heading = computed(() => {
|
||||
if (updateOsStatus.value === 'confirming') {
|
||||
return callbackTypeDowngrade.value ? props.t('Downgrade Unraid OS confirmation required') : props.t('Update Unraid OS confirmation required');
|
||||
return callbackTypeDowngrade.value
|
||||
? props.t('Downgrade Unraid OS confirmation required')
|
||||
: props.t('Update Unraid OS confirmation required');
|
||||
}
|
||||
switch (callbackStatus.value) {
|
||||
case 'error':
|
||||
@@ -94,19 +83,37 @@ const heading = computed(() => {
|
||||
});
|
||||
const subheading = computed(() => {
|
||||
if (updateOsStatus.value === 'confirming') {
|
||||
return callbackTypeDowngrade.value ? props.t('Please confirm the downgrade details below') : props.t('Please confirm the update details below');
|
||||
return callbackTypeDowngrade.value
|
||||
? props.t('Please confirm the downgrade details below')
|
||||
: props.t('Please confirm the update details below');
|
||||
}
|
||||
if (callbackStatus.value === 'error') {
|
||||
return props.t('Something went wrong'); /** @todo show actual error messages */
|
||||
}
|
||||
if (callbackStatus.value === 'loading') { return props.t('Please keep this window open while we perform some actions'); }
|
||||
if (callbackStatus.value === 'loading') {
|
||||
return props.t('Please keep this window open while we perform some actions');
|
||||
}
|
||||
if (callbackStatus.value === 'success') {
|
||||
if (accountActionType.value === 'signIn') { return props.t('You\'re one step closer to enhancing your Unraid experience'); }
|
||||
if (keyActionType.value === 'purchase') { return props.t('Thank you for purchasing an Unraid {0} Key!', [keyType.value]); }
|
||||
if (keyActionType.value === 'replace') { return props.t('Your {0} Key has been replaced!', [keyType.value]); }
|
||||
if (keyActionType.value === 'trialExtend') { return props.t('Your Trial key has been extended!'); }
|
||||
if (keyActionType.value === 'trialStart') { return props.t('Your free Trial key provides all the functionality of an Unleashed Registration key'); }
|
||||
if (keyActionType.value === 'upgrade') { return props.t('Thank you for upgrading to an Unraid {0} Key!', [keyType.value]); }
|
||||
if (accountActionType.value === 'signIn') {
|
||||
return props.t("You're one step closer to enhancing your Unraid experience");
|
||||
}
|
||||
if (keyActionType.value === 'purchase') {
|
||||
return props.t('Thank you for purchasing an Unraid {0} Key!', [keyType.value]);
|
||||
}
|
||||
if (keyActionType.value === 'replace') {
|
||||
return props.t('Your {0} Key has been replaced!', [keyType.value]);
|
||||
}
|
||||
if (keyActionType.value === 'trialExtend') {
|
||||
return props.t('Your Trial key has been extended!');
|
||||
}
|
||||
if (keyActionType.value === 'trialStart') {
|
||||
return props.t(
|
||||
'Your free Trial key provides all the functionality of an Unleashed Registration key'
|
||||
);
|
||||
}
|
||||
if (keyActionType.value === 'upgrade') {
|
||||
return props.t('Thank you for upgrading to an Unraid {0} Key!', [keyType.value]);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
@@ -137,30 +144,50 @@ const cancelUpdateOs = () => {
|
||||
// close();
|
||||
// };
|
||||
|
||||
const keyInstallStatusCopy = computed((): { text: string; } => {
|
||||
const keyInstallStatusCopy = computed((): { text: string } => {
|
||||
let txt1 = props.t('Installing');
|
||||
let txt2 = props.t('Installed');
|
||||
let txt3 = props.t('Install');
|
||||
switch (keyInstallStatus.value) {
|
||||
case 'installing':
|
||||
if (keyActionType.value === 'trialExtend') { txt1 = props.t('Installing Extended Trial'); }
|
||||
if (keyActionType.value === 'recover') { txt1 = props.t('Installing Recovered'); }
|
||||
if (keyActionType.value === 'renew') { txt1 = props.t('Installing Extended'); }
|
||||
if (keyActionType.value === 'replace') { txt1 = props.t('Installing Replaced'); }
|
||||
if (keyActionType.value === 'trialExtend') {
|
||||
txt1 = props.t('Installing Extended Trial');
|
||||
}
|
||||
if (keyActionType.value === 'recover') {
|
||||
txt1 = props.t('Installing Recovered');
|
||||
}
|
||||
if (keyActionType.value === 'renew') {
|
||||
txt1 = props.t('Installing Extended');
|
||||
}
|
||||
if (keyActionType.value === 'replace') {
|
||||
txt1 = props.t('Installing Replaced');
|
||||
}
|
||||
return {
|
||||
text: props.t('{0} {1} Key…', [txt1, keyType.value]),
|
||||
};
|
||||
case 'success':
|
||||
if (keyActionType.value === 'renew' || keyActionType.value === 'trialExtend') { txt2 = props.t('Extension Installed'); }
|
||||
if (keyActionType.value === 'recover') { txt2 = props.t('Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt2 = props.t('Replaced'); }
|
||||
if (keyActionType.value === 'renew' || keyActionType.value === 'trialExtend') {
|
||||
txt2 = props.t('Extension Installed');
|
||||
}
|
||||
if (keyActionType.value === 'recover') {
|
||||
txt2 = props.t('Recovered');
|
||||
}
|
||||
if (keyActionType.value === 'replace') {
|
||||
txt2 = props.t('Replaced');
|
||||
}
|
||||
return {
|
||||
text: props.t('{1} Key {0} Successfully', [txt2, keyType.value]),
|
||||
};
|
||||
case 'failed':
|
||||
if (keyActionType.value === 'trialExtend') { txt3 = props.t('Install Extended'); }
|
||||
if (keyActionType.value === 'recover') { txt3 = props.t('Install Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt3 = props.t('Install Replaced'); }
|
||||
if (keyActionType.value === 'trialExtend') {
|
||||
txt3 = props.t('Install Extended');
|
||||
}
|
||||
if (keyActionType.value === 'recover') {
|
||||
txt3 = props.t('Install Recovered');
|
||||
}
|
||||
if (keyActionType.value === 'replace') {
|
||||
txt3 = props.t('Install Replaced');
|
||||
}
|
||||
return {
|
||||
text: props.t('Failed to {0} {1} Key', [txt3, keyType.value]),
|
||||
};
|
||||
@@ -172,31 +199,32 @@ const keyInstallStatusCopy = computed((): { text: string; } => {
|
||||
}
|
||||
});
|
||||
|
||||
const accountActionStatusCopy = computed((): { text: string; } => {
|
||||
const accountActionStatusCopy = computed((): { text: string } => {
|
||||
switch (accountActionStatus.value) {
|
||||
case 'waiting':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('Signing In')
|
||||
: props.t('Signing Out'),
|
||||
text: accountAction.value?.type === 'signIn' ? props.t('Signing In') : props.t('Signing Out'),
|
||||
};
|
||||
case 'updating':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('Signing in {0}…', [accountAction.value.user?.preferred_username])
|
||||
: props.t('Signing out {0}…', [username.value]),
|
||||
text:
|
||||
accountAction.value?.type === 'signIn'
|
||||
? props.t('Signing in {0}…', [accountAction.value.user?.preferred_username])
|
||||
: props.t('Signing out {0}…', [username.value]),
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('{0} Signed In Successfully', [accountAction.value.user?.preferred_username])
|
||||
: props.t('{0} Signed Out Successfully', [username.value]),
|
||||
text:
|
||||
accountAction.value?.type === 'signIn'
|
||||
? props.t('{0} Signed In Successfully', [accountAction.value.user?.preferred_username])
|
||||
: props.t('{0} Signed Out Successfully', [username.value]),
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
text: accountAction.value?.type === 'signIn'
|
||||
? props.t('Sign In Failed')
|
||||
: props.t('Sign Out Failed'),
|
||||
text:
|
||||
accountAction.value?.type === 'signIn'
|
||||
? props.t('Sign In Failed')
|
||||
: props.t('Sign Out Failed'),
|
||||
};
|
||||
case 'ready':
|
||||
default:
|
||||
@@ -214,7 +242,9 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
*/
|
||||
const showUpdateEligibility = computed(() => {
|
||||
// rather than specifically targeting 'Starter' and 'Unleashed' we'll target all keys that are not 'Basic', 'Plus', 'Pro', 'Lifetime', or 'Trial'
|
||||
if (!keyType.value) { return false; }
|
||||
if (!keyType.value) {
|
||||
return false;
|
||||
}
|
||||
return !['Basic', 'Plus', 'Pro', 'Lifetime', 'Trial'].includes(keyType.value);
|
||||
});
|
||||
</script>
|
||||
@@ -245,20 +275,13 @@ const showUpdateEligibility = computed(() => {
|
||||
:text="keyInstallStatusCopy.text"
|
||||
>
|
||||
<div v-if="keyType === 'Trial'" class="opacity-75 italic mt-4px">
|
||||
<UpcUptimeExpire
|
||||
v-if="refreshServerStateStatus === 'done'"
|
||||
:for-expire="true"
|
||||
:t="t"
|
||||
/>
|
||||
<UpcUptimeExpire v-if="refreshServerStateStatus === 'done'" :for-expire="true" :t="t" />
|
||||
<p v-else>
|
||||
{{ t('Calculating trial expiration…') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="showUpdateEligibility" class="opacity-75 italic mt-4px">
|
||||
<RegistrationUpdateExpiration
|
||||
v-if="refreshServerStateStatus === 'done'"
|
||||
:t="t"
|
||||
/>
|
||||
<RegistrationUpdateExpiration v-if="refreshServerStateStatus === 'done'" :t="t" />
|
||||
<p v-else>
|
||||
{{ t('Calculating OS Update Eligibility…') }}
|
||||
</p>
|
||||
@@ -276,7 +299,10 @@ const showUpdateEligibility = computed(() => {
|
||||
{{ t('Copy your Key URL: {0}', [keyUrl]) }}
|
||||
</p>
|
||||
<p>
|
||||
<a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">
|
||||
<a
|
||||
href="/Tools/Registration"
|
||||
class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition"
|
||||
>
|
||||
{{ t('Then go to Tools > Registration to manually install it') }}
|
||||
</a>
|
||||
</p>
|
||||
@@ -284,7 +310,11 @@ const showUpdateEligibility = computed(() => {
|
||||
</UpcCallbackFeedbackStatus>
|
||||
|
||||
<UpcCallbackFeedbackStatus
|
||||
v-if="stateDataError && callbackStatus !== 'loading' && (keyInstallStatus === 'success' || keyInstallStatus === 'failed')"
|
||||
v-if="
|
||||
stateDataError &&
|
||||
callbackStatus !== 'loading' &&
|
||||
(keyInstallStatus === 'success' || keyInstallStatus === 'failed')
|
||||
"
|
||||
:error="true"
|
||||
:text="t('Post Install License Key Error')"
|
||||
>
|
||||
@@ -322,7 +352,11 @@ const showUpdateEligibility = computed(() => {
|
||||
</p>
|
||||
|
||||
<p class="text-14px italic opacity-75">
|
||||
{{ callbackTypeDowngrade ? t('This downgrade will require a reboot') : t('This update will require a reboot') }}
|
||||
{{
|
||||
callbackTypeDowngrade
|
||||
? t('This downgrade will require a reboot')
|
||||
: t('This update will require a reboot')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,12 +366,7 @@ const showUpdateEligibility = computed(() => {
|
||||
<template v-if="callbackStatus === 'success' || updateOsStatus === 'confirming'" #footer>
|
||||
<div class="flex flex-row justify-center gap-16px">
|
||||
<template v-if="callbackStatus === 'success'">
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:icon="XMarkIcon"
|
||||
:text="closeText"
|
||||
@click="close"
|
||||
/>
|
||||
<BrandButton btn-style="underline" :icon="XMarkIcon" :text="closeText" @click="close" />
|
||||
|
||||
<template v-if="connectPluginInstalled && accountActionType === 'signIn'">
|
||||
<BrandButton
|
||||
@@ -372,7 +401,9 @@ const showUpdateEligibility = computed(() => {
|
||||
/>
|
||||
<BrandButton
|
||||
:icon="CheckIcon"
|
||||
:text="callbackTypeDowngrade ? t('Confirm and start downgrade') : t('Confirm and start update')"
|
||||
:text="
|
||||
callbackTypeDowngrade ? t('Confirm and start downgrade') : t('Confirm and start update')
|
||||
"
|
||||
@click="confirmUpdateOs"
|
||||
/>
|
||||
</template>
|
||||
@@ -390,8 +421,8 @@ const showUpdateEligibility = computed(() => {
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
|
||||
.unraid_mark_2,
|
||||
.unraid_mark_4 {
|
||||
@@ -440,6 +471,4 @@ const showUpdateEligibility = computed(() => {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import { BrandButton, BrandLoadingWhite } from '@unraid/ui';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import type { ComposerTranslation } from 'vue-i18n';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUnraidApiStore } from '~/store/unraidApi';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
|
||||
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
|
||||
|
||||
defineProps<{ t: ComposerTranslation; }>();
|
||||
defineProps<{ t: ComposerTranslation }>();
|
||||
|
||||
const { expireTime, connectPluginInstalled, state, stateData } = storeToRefs(useServerStore());
|
||||
const { unraidApiStatus, unraidApiRestartAction } = storeToRefs(useUnraidApiStore());
|
||||
|
||||
const showExpireTime = computed(() => (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value > 0);
|
||||
const showExpireTime = computed(
|
||||
() => (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value > 0
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,21 +23,24 @@ const showExpireTime = computed(() => (state.value === 'TRIAL' || state.value ==
|
||||
class="text-center prose text-16px leading-relaxed whitespace-normal opacity-75 gap-y-8px"
|
||||
v-html="t(stateData.message)"
|
||||
/>
|
||||
<UpcUptimeExpire
|
||||
v-if="showExpireTime"
|
||||
class="text-center opacity-75 mt-12px"
|
||||
:t="t"
|
||||
/>
|
||||
<UpcUptimeExpire v-if="showExpireTime" class="text-center opacity-75 mt-12px" :t="t" />
|
||||
</header>
|
||||
<template v-if="stateData.actions">
|
||||
<ul v-if="connectPluginInstalled && unraidApiStatus !== 'online'" class="list-reset flex flex-col gap-y-8px px-16px">
|
||||
<ul
|
||||
v-if="connectPluginInstalled && unraidApiStatus !== 'online'"
|
||||
class="list-reset flex flex-col gap-y-8px px-16px"
|
||||
>
|
||||
<li>
|
||||
<BrandButton
|
||||
class="w-full"
|
||||
:disabled="unraidApiStatus === 'connecting' || unraidApiStatus === 'restarting'"
|
||||
:icon="unraidApiStatus === 'restarting' ? BrandLoadingWhite : unraidApiRestartAction?.icon"
|
||||
:text="unraidApiStatus === 'restarting' ? t('Restarting unraid-api…') : t('Restart unraid-api')"
|
||||
:title="unraidApiStatus === 'restarting' ? t('Restarting unraid-api…') : t('Restart unraid-api')"
|
||||
:text="
|
||||
unraidApiStatus === 'restarting' ? t('Restarting unraid-api…') : t('Restart unraid-api')
|
||||
"
|
||||
:title="
|
||||
unraidApiStatus === 'restarting' ? t('Restarting unraid-api…') : t('Restart unraid-api')
|
||||
"
|
||||
@click="unraidApiRestartAction?.click?.()"
|
||||
/>
|
||||
</li>
|
||||
@@ -51,9 +51,9 @@ const showExpireTime = computed(() => (state.value === 'TRIAL' || state.value ==
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
@import '../../assets/main.css';
|
||||
|
||||
.DropdownWrapper_blip {
|
||||
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-foreground);
|
||||
|
||||
12489
web/package-lock.json
generated
12489
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,7 @@
|
||||
"@heroicons/vue": "^2.2.0",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@pinia/nuxt": "^0.9.0",
|
||||
"@unraid/ui": "file:../unraid-ui",
|
||||
"@vue/apollo-composable": "^4.2.1",
|
||||
"@vueuse/components": "^12.0.0",
|
||||
"@vueuse/integrations": "^12.0.0",
|
||||
@@ -87,6 +88,9 @@
|
||||
"vue-i18n": "^10.0.5",
|
||||
"wretch": "^2.11.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.30.1"
|
||||
},
|
||||
"overrides": {
|
||||
"vue": "latest",
|
||||
"radix-vue": {
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ExclamationTriangleIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { ExclamationTriangleIcon } from '@heroicons/vue/24/solid';
|
||||
import { BrandButton, BrandLogo } from '@unraid/ui';
|
||||
import { serverState } from '~/_data/serverState';
|
||||
import type { SendPayloads } from '~/store/callback';
|
||||
import type { UiBadgePropsColor } from '~/types/ui/badge';
|
||||
import type { ButtonStyle } from '~/types/ui/button';
|
||||
import AES from 'crypto-js/aes';
|
||||
import BrandButton from '~/components/Brand/Button.vue';
|
||||
|
||||
const { registerEntry } = useCustomElements();
|
||||
onBeforeMount(() => {
|
||||
registerEntry('UnraidComponents');
|
||||
});
|
||||
|
||||
useHead({
|
||||
meta: [
|
||||
{ name: 'viewport',
|
||||
content: 'width=1300', }
|
||||
]
|
||||
})
|
||||
meta: [{ name: 'viewport', content: 'width=1300' }],
|
||||
});
|
||||
|
||||
const valueToMakeCallback = ref<SendPayloads | undefined>();
|
||||
const callbackDestination = ref<string>('');
|
||||
@@ -42,6 +36,19 @@ const createCallbackUrl = (payload: SendPayloads, sendType: string) => {
|
||||
callbackDestination.value = destinationUrl.toString(); // differs from callbackActions.send
|
||||
};
|
||||
|
||||
const variants = [
|
||||
'fill',
|
||||
'black',
|
||||
'gray',
|
||||
'outline',
|
||||
'outline-black',
|
||||
'outline-white',
|
||||
'underline',
|
||||
'underline-hover-red',
|
||||
'white',
|
||||
'none',
|
||||
] as const;
|
||||
|
||||
onMounted(() => {
|
||||
createCallbackUrl(
|
||||
[
|
||||
@@ -59,35 +66,6 @@ onMounted(() => {
|
||||
'forUpc'
|
||||
);
|
||||
});
|
||||
|
||||
const badgeColors = [
|
||||
'black',
|
||||
'white',
|
||||
'red',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'indigo',
|
||||
'purple',
|
||||
'pink',
|
||||
'orange',
|
||||
'transparent',
|
||||
'current',
|
||||
'gray',
|
||||
'custom',
|
||||
] as UiBadgePropsColor[];
|
||||
|
||||
const buttonColors = [
|
||||
'black',
|
||||
'fill',
|
||||
'gray',
|
||||
'outline',
|
||||
'outline-black',
|
||||
'outline-white',
|
||||
'underline',
|
||||
'underline-hover-red',
|
||||
'white',
|
||||
] as ButtonStyle[];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -162,17 +140,16 @@ const buttonColors = [
|
||||
</code>
|
||||
</div>
|
||||
<div class="bg-background">
|
||||
<hr class="border-black dark:border-white" />
|
||||
<h2 class="text-xl font-semibold font-mono">Legacy Badge Components</h2>
|
||||
<template v-for="color in badgeColors" :key="color">
|
||||
<UiBadge size="14px" :icon="ExclamationTriangleIcon" :color="color">{{ color }}</UiBadge>
|
||||
</template>
|
||||
</div>
|
||||
<div class="bg-background">
|
||||
<hr class="border-black dark:border-white" />
|
||||
<h2 class="text-xl font-semibold font-mono">Legacy Button Components</h2>
|
||||
<template v-for="color in buttonColors" :key="color">
|
||||
<BrandButton type="button" size="14px" :icon="ExclamationTriangleIcon" :btn-style="color as ButtonStyle">{{ color }}</BrandButton>
|
||||
<hr class="border-black dark:border-white" />
|
||||
<h2 class="text-xl font-semibold font-mono">Brand Button Component</h2>
|
||||
<template v-for="variant in variants" :key="variant">
|
||||
<BrandButton
|
||||
:variant="variant"
|
||||
type="button"
|
||||
size="14px"
|
||||
:icon="ExclamationTriangleIcon"
|
||||
>{{ variant }}</BrandButton
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,6 +159,10 @@ const buttonColors = [
|
||||
</template>
|
||||
|
||||
<style lang="postcss">
|
||||
/* Import unraid-ui globals first */
|
||||
@import '@unraid/ui/styles';
|
||||
@import '../assets/main.css';
|
||||
|
||||
code {
|
||||
@apply rounded-lg bg-gray-200 p-1 text-black shadow;
|
||||
}
|
||||
|
||||
@@ -1,283 +1,34 @@
|
||||
import 'dotenv/config';
|
||||
import tailwindConfig from '@unraid/ui/tailwind.config';
|
||||
import type { Config } from 'tailwindcss';
|
||||
import type { PluginAPI } from 'tailwindcss/types/config';
|
||||
import remToRem from './utils/tailwind-rem-to-rem';
|
||||
|
||||
// @ts-expect-error - just trying to get this to build @fixme
|
||||
export default <Partial<Config>>{
|
||||
export default {
|
||||
presets: [tailwindConfig],
|
||||
content: [
|
||||
// Web components
|
||||
'./components/**/*.ce.{js,vue,ts}',
|
||||
// Regular Vue components
|
||||
'./components/**/*.{js,vue,ts}',
|
||||
'./layouts/**/*.vue',
|
||||
'./pages/**/*.vue',
|
||||
'../unraid-ui/src/**/*.{vue,ts}',
|
||||
],
|
||||
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'),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
mode: 'jit',
|
||||
safelist: [],
|
||||
plugins: [
|
||||
require('@tailwindcss/typography'),
|
||||
require('tailwindcss-animate'),
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
require('./utils/tailwind-rem-to-rem').default({
|
||||
remToRem({
|
||||
baseFontSize: 16,
|
||||
/**
|
||||
* The font size where the web components will be rendered in production.
|
||||
* Required due to the webgui using the 62.5% font-size "trick".
|
||||
* Set an env to 16 for local development and 10 for everything else.
|
||||
*/
|
||||
newFontSize: process.env.VITE_TAILWIND_BASE_FONT_SIZE ?? 10,
|
||||
newFontSize: Number(process.env.VITE_TAILWIND_BASE_FONT_SIZE ?? 10),
|
||||
}),
|
||||
],
|
||||
};
|
||||
theme: {
|
||||
extend: {
|
||||
// web-specific extensions only
|
||||
},
|
||||
},
|
||||
} satisfies Partial<Config>;
|
||||
|
||||
Reference in New Issue
Block a user