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:
Michael Datelle
2025-01-15 11:15:52 -05:00
committed by GitHub
parent 8d386043ae
commit 6669a963af
72 changed files with 11981 additions and 6912 deletions

View File

@@ -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
View File

@@ -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
}

View File

@@ -19,3 +19,4 @@
".DS_Store"
]
}

10
unraid-ui/.eslintrc.js Normal file
View 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'
},
};

View File

@@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 105,
"singleQuote": true,
"plugins": ["prettier-plugin-tailwindcss", "@ianvs/prettier-plugin-sort-imports"]
}

View File

@@ -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,
],
},
},
});
};
},
};

View File

@@ -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}"
],
};

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}
}

View 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>

View 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>

View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
import BrandLoading from './BrandLoading.vue';
</script>
<template>
<BrandLoading gradient-start="#ffffff" gradient-stop="#ffffff" />
</template>

View 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>

View 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>

View 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",
},
}
);

View 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";

View File

@@ -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>

View 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",
},
}
);

View File

@@ -1,3 +1,2 @@
import Badge from "./Badge.vue";
export { Badge };
export { default as Badge } from "./Badge.vue";
export { badgeVariants } from "./badge.variants";

View File

@@ -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
);
});

View File

@@ -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: {

View File

@@ -1,2 +1,2 @@
export { default as Button } from "./Button.vue";
export { ButtonVariants } from "./button.variants";
export { buttonVariants } from "./button.variants";

View File

@@ -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"
>

View File

@@ -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";

View 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",
},
}
);

View File

@@ -0,0 +1 @@
export { default as Lightswitch } from "./Lightswitch.vue";

View File

@@ -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";

View File

@@ -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,

View File

@@ -1,2 +1,3 @@
/* global styles for unraid-ui */
@import "./global.css";
@import "./globals.css";

View 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>;

View File

@@ -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";
}

View File

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

View 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"
/>
`,
}),
};

View 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>
`,
}),
};

View File

@@ -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>
`,
}),
};

View 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>
`,
}),
};

View File

@@ -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>
`,
}),
};

View 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>
`,
}),
};

View File

@@ -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>
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View 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" />
`,
}),
};

View 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>
`,
}),
};

View 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" />
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View 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>
`,
}),
};

View File

@@ -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>;

View File

@@ -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"]
}

View File

@@ -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" }]
}

View File

@@ -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"]
}

View File

@@ -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}'],
},
});
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()"

View File

@@ -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>

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

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

View File

@@ -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>;