feat: tailwind v4 (#1522)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Streamlined Tailwind CSS integration using Vite plugin, eliminating
the need for separate Tailwind config files.
* Updated theme and color variables for improved consistency and
maintainability.

* **Style**
* Standardized spacing, sizing, and font classes across all components
using Tailwind’s default scale.
* Reduced excessive gaps, padding, and font sizes for a more compact and
cohesive UI.
* Updated gradient, border, and shadow classes to match Tailwind v4
conventions.
* Replaced custom pixel-based classes with Tailwind’s bracketed
arbitrary value syntax where needed.
* Replaced focus outline styles from `outline-none` to `outline-hidden`
for consistent focus handling.
* Updated flex shrink/grow utility classes to use newer shorthand forms.
* Converted several component templates to use self-closing tags for
cleaner markup.
  * Adjusted icon sizes and spacing for improved visual balance.

* **Chores**
* Removed legacy Tailwind/PostCSS configuration files and related
scripts.
* Updated and cleaned up package dependencies for Tailwind v4 and
related plugins.
  * Removed unused or redundant build scripts and configuration exports.
  * Updated documentation to reflect new Tailwind v4 usage.
  * Removed Prettier Tailwind plugin from formatting configurations.
* Removed Nuxt Tailwind module in favor of direct Vite plugin
integration.
  * Cleaned up ESLint config by removing Prettier integration.

* **Bug Fixes**
  * Corrected invalid or outdated Tailwind class names and syntax.
* Fixed issues with max-width and other utility classes for improved
layout consistency.

* **Tests**
* Updated test assertions to match new class names and styling
conventions.

* **Documentation**
* Revised README and internal notes to clarify Tailwind v4 adoption and
configuration changes.
* Added new development notes emphasizing Tailwind v4 usage and
documentation references.

* **UI Components**
* Enhanced BrandButton stories with detailed variant, size, and padding
showcases for better visual testing.
* Improved theme store to apply dark mode class on both `<html>` and
`<body>` elements for compatibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-07-21 09:58:02 -04:00
committed by GitHub
parent 1a8da6d92b
commit 2c62e0ad09
149 changed files with 1805 additions and 2345 deletions

View File

@@ -14,7 +14,26 @@
"Bash(mv:*)",
"Bash(ls:*)",
"mcp__ide__getDiagnostics",
"Bash(pnpm --filter \"*connect*\" test connect-status-writer.service.spec)"
"Bash(pnpm --filter \"*connect*\" test connect-status-writer.service.spec)",
"Bash(pnpm storybook:*)",
"Bash(rm:*)",
"Bash(pnpm add:*)",
"Bash(pnpm install:*)",
"Bash(pkill:*)",
"Bash(true)",
"Bash(timeout 15 pnpm storybook)",
"WebFetch(domain:tailwindcss.com)",
"Bash(pnpm list:*)",
"Bash(pnpm remove:*)",
"WebFetch(domain:github.com)",
"mcp__browsermcp__browser_navigate",
"Bash(clear)",
"Bash(git log:*)",
"Bash(pnpm --filter ./unraid-ui build)",
"Bash(pnpm --filter @unraid/ui build)",
"Bash(pnpm --filter @unraid/web build)",
"Bash(python3:*)",
"Bash(pnpm tailwind:build:*)"
]
},
"enableAllProjectMcpServers": false

View File

@@ -135,3 +135,8 @@ Enables GraphQL playground at `http://tower.local/graphql`
- Place all mock declarations at the top level
- Use factory functions for module mocks to avoid hoisting issues
- Clear mocks between tests to ensure isolation
## Development Memories
- We are using tailwind v4 we do not need a tailwind config anymore
- always search the internet for tailwind v4 documentation when making tailwind related style changes

View File

@@ -1,5 +1,5 @@
{
"version": "4.9.5",
"version": "4.10.0",
"extraOrigins": [
"https://google.com",
"https://test.com"

View File

@@ -247,6 +247,347 @@ A field whose value conforms to the standard URL format as specified in RFC3986:
"""
scalar URL
type DiskPartition {
"""The name of the partition"""
name: String!
"""The filesystem type of the partition"""
fsType: DiskFsType!
"""The size of the partition in bytes"""
size: Float!
}
"""The type of filesystem on the disk partition"""
enum DiskFsType {
XFS
BTRFS
VFAT
ZFS
EXT4
NTFS
}
type Disk implements Node {
id: PrefixedID!
"""The device path of the disk (e.g. /dev/sdb)"""
device: String!
"""The type of disk (e.g. SSD, HDD)"""
type: String!
"""The model name of the disk"""
name: String!
"""The manufacturer of the disk"""
vendor: String!
"""The total size of the disk in bytes"""
size: Float!
"""The number of bytes per sector"""
bytesPerSector: Float!
"""The total number of cylinders on the disk"""
totalCylinders: Float!
"""The total number of heads on the disk"""
totalHeads: Float!
"""The total number of sectors on the disk"""
totalSectors: Float!
"""The total number of tracks on the disk"""
totalTracks: Float!
"""The number of tracks per cylinder"""
tracksPerCylinder: Float!
"""The number of sectors per track"""
sectorsPerTrack: Float!
"""The firmware revision of the disk"""
firmwareRevision: String!
"""The serial number of the disk"""
serialNum: String!
"""The interface type of the disk"""
interfaceType: DiskInterfaceType!
"""The SMART status of the disk"""
smartStatus: DiskSmartStatus!
"""The current temperature of the disk in Celsius"""
temperature: Float
"""The partitions on the disk"""
partitions: [DiskPartition!]!
}
"""The type of interface the disk uses to connect to the system"""
enum DiskInterfaceType {
SAS
SATA
USB
PCIE
UNKNOWN
}
"""
The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk
"""
enum DiskSmartStatus {
OK
UNKNOWN
}
type KeyFile {
location: String
contents: String
}
type Registration implements Node {
id: PrefixedID!
type: registrationType
keyFile: KeyFile
state: RegistrationState
expiration: String
updateExpiration: String
}
enum registrationType {
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
INVALID
TRIAL
}
enum RegistrationState {
TRIAL
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
EEXPIRED
EGUID
EGUID1
ETRIAL
ENOKEYFILE
ENOKEYFILE1
ENOKEYFILE2
ENOFLASH
ENOFLASH1
ENOFLASH2
ENOFLASH3
ENOFLASH4
ENOFLASH5
ENOFLASH6
ENOFLASH7
EBLACKLISTED
EBLACKLISTED1
EBLACKLISTED2
ENOCONN
}
type Vars implements Node {
id: PrefixedID!
"""Unraid version"""
version: String
maxArraysz: Int
maxCachesz: Int
"""Machine hostname"""
name: String
timeZone: String
comment: String
security: String
workgroup: String
domain: String
domainShort: String
hideDotFiles: Boolean
localMaster: Boolean
enableFruit: String
"""Should a NTP server be used for time sync?"""
useNtp: Boolean
"""NTP Server 1"""
ntpServer1: String
"""NTP Server 2"""
ntpServer2: String
"""NTP Server 3"""
ntpServer3: String
"""NTP Server 4"""
ntpServer4: String
domainLogin: String
sysModel: String
sysArraySlots: Int
sysCacheSlots: Int
sysFlashSlots: Int
useSsl: Boolean
"""Port for the webui via HTTP"""
port: Int
"""Port for the webui via HTTPS"""
portssl: Int
localTld: String
bindMgt: Boolean
"""Should telnet be enabled?"""
useTelnet: Boolean
porttelnet: Int
useSsh: Boolean
portssh: Int
startPage: String
startArray: Boolean
spindownDelay: String
queueDepth: String
spinupGroups: Boolean
defaultFormat: String
defaultFsType: String
shutdownTimeout: Int
luksKeyfile: String
pollAttributes: String
pollAttributesDefault: String
pollAttributesStatus: String
nrRequests: Int
nrRequestsDefault: Int
nrRequestsStatus: String
mdNumStripes: Int
mdNumStripesDefault: Int
mdNumStripesStatus: String
mdSyncWindow: Int
mdSyncWindowDefault: Int
mdSyncWindowStatus: String
mdSyncThresh: Int
mdSyncThreshDefault: Int
mdSyncThreshStatus: String
mdWriteMethod: Int
mdWriteMethodDefault: String
mdWriteMethodStatus: String
shareDisk: String
shareUser: String
shareUserInclude: String
shareUserExclude: String
shareSmbEnabled: Boolean
shareNfsEnabled: Boolean
shareAfpEnabled: Boolean
shareInitialOwner: String
shareInitialGroup: String
shareCacheEnabled: Boolean
shareCacheFloor: String
shareMoverSchedule: String
shareMoverLogging: Boolean
fuseRemember: String
fuseRememberDefault: String
fuseRememberStatus: String
fuseDirectio: String
fuseDirectioDefault: String
fuseDirectioStatus: String
shareAvahiEnabled: Boolean
shareAvahiSmbName: String
shareAvahiSmbModel: String
shareAvahiAfpName: String
shareAvahiAfpModel: String
safeMode: Boolean
startMode: String
configValid: Boolean
configError: ConfigErrorState
joinStatus: String
deviceCount: Int
flashGuid: String
flashProduct: String
flashVendor: String
regCheck: String
regFile: String
regGuid: String
regTy: registrationType
regState: RegistrationState
"""Registration owner"""
regTo: String
regTm: String
regTm2: String
regGen: String
sbName: String
sbVersion: String
sbUpdated: String
sbEvents: Int
sbState: String
sbClean: Boolean
sbSynced: Int
sbSyncErrs: Int
sbSynced2: Int
sbSyncExit: String
sbNumDisks: Int
mdColor: String
mdNumDisks: Int
mdNumDisabled: Int
mdNumInvalid: Int
mdNumMissing: Int
mdNumNew: Int
mdNumErased: Int
mdResync: Int
mdResyncCorr: String
mdResyncPos: String
mdResyncDb: String
mdResyncDt: String
mdResyncAction: String
mdResyncSize: Int
mdState: String
mdVersion: String
cacheNumDevices: Int
cacheSbNumDisks: Int
fsState: String
"""Human friendly string of array events happening"""
fsProgress: String
"""
Percentage from 0 - 100 while upgrading a disk or swapping parity drives
"""
fsCopyPrcnt: Int
fsNumMounted: Int
fsNumUnmountable: Int
fsUnmountableMask: String
"""Total amount of user shares"""
shareCount: Int
"""Total amount shares with SMB enabled"""
shareSmbCount: Int
"""Total amount shares with NFS enabled"""
shareNfsCount: Int
"""Total amount shares with AFP enabled"""
shareAfpCount: Int
shareMoverActive: Boolean
csrfToken: String
}
"""Possible error states for configuration"""
enum ConfigErrorState {
UNKNOWN_ERROR
INELIGIBLE
INVALID
NO_KEY_SERVER
WITHDRAWN
}
type Permission {
resource: Resource!
actions: [String!]!
@@ -620,102 +961,6 @@ enum ThemeName {
white
}
type DiskPartition {
"""The name of the partition"""
name: String!
"""The filesystem type of the partition"""
fsType: DiskFsType!
"""The size of the partition in bytes"""
size: Float!
}
"""The type of filesystem on the disk partition"""
enum DiskFsType {
XFS
BTRFS
VFAT
ZFS
EXT4
NTFS
}
type Disk implements Node {
id: PrefixedID!
"""The device path of the disk (e.g. /dev/sdb)"""
device: String!
"""The type of disk (e.g. SSD, HDD)"""
type: String!
"""The model name of the disk"""
name: String!
"""The manufacturer of the disk"""
vendor: String!
"""The total size of the disk in bytes"""
size: Float!
"""The number of bytes per sector"""
bytesPerSector: Float!
"""The total number of cylinders on the disk"""
totalCylinders: Float!
"""The total number of heads on the disk"""
totalHeads: Float!
"""The total number of sectors on the disk"""
totalSectors: Float!
"""The total number of tracks on the disk"""
totalTracks: Float!
"""The number of tracks per cylinder"""
tracksPerCylinder: Float!
"""The number of sectors per track"""
sectorsPerTrack: Float!
"""The firmware revision of the disk"""
firmwareRevision: String!
"""The serial number of the disk"""
serialNum: String!
"""The interface type of the disk"""
interfaceType: DiskInterfaceType!
"""The SMART status of the disk"""
smartStatus: DiskSmartStatus!
"""The current temperature of the disk in Celsius"""
temperature: Float
"""The partitions on the disk"""
partitions: [DiskPartition!]!
}
"""The type of interface the disk uses to connect to the system"""
enum DiskInterfaceType {
SAS
SATA
USB
PCIE
UNKNOWN
}
"""
The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk
"""
enum DiskSmartStatus {
OK
UNKNOWN
}
type InfoApps implements Node {
id: PrefixedID!
@@ -1106,60 +1351,6 @@ type Owner {
avatar: String!
}
type KeyFile {
location: String
contents: String
}
type Registration implements Node {
id: PrefixedID!
type: registrationType
keyFile: KeyFile
state: RegistrationState
expiration: String
updateExpiration: String
}
enum registrationType {
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
INVALID
TRIAL
}
enum RegistrationState {
TRIAL
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
EEXPIRED
EGUID
EGUID1
ETRIAL
ENOKEYFILE
ENOKEYFILE1
ENOKEYFILE2
ENOFLASH
ENOFLASH1
ENOFLASH2
ENOFLASH3
ENOFLASH4
ENOFLASH5
ENOFLASH6
ENOFLASH7
EBLACKLISTED
EBLACKLISTED1
EBLACKLISTED2
ENOCONN
}
type ProfileModel implements Node {
id: PrefixedID!
username: String!
@@ -1225,197 +1416,6 @@ type Settings implements Node {
api: ApiConfig!
}
type Vars implements Node {
id: PrefixedID!
"""Unraid version"""
version: String
maxArraysz: Int
maxCachesz: Int
"""Machine hostname"""
name: String
timeZone: String
comment: String
security: String
workgroup: String
domain: String
domainShort: String
hideDotFiles: Boolean
localMaster: Boolean
enableFruit: String
"""Should a NTP server be used for time sync?"""
useNtp: Boolean
"""NTP Server 1"""
ntpServer1: String
"""NTP Server 2"""
ntpServer2: String
"""NTP Server 3"""
ntpServer3: String
"""NTP Server 4"""
ntpServer4: String
domainLogin: String
sysModel: String
sysArraySlots: Int
sysCacheSlots: Int
sysFlashSlots: Int
useSsl: Boolean
"""Port for the webui via HTTP"""
port: Int
"""Port for the webui via HTTPS"""
portssl: Int
localTld: String
bindMgt: Boolean
"""Should telnet be enabled?"""
useTelnet: Boolean
porttelnet: Int
useSsh: Boolean
portssh: Int
startPage: String
startArray: Boolean
spindownDelay: String
queueDepth: String
spinupGroups: Boolean
defaultFormat: String
defaultFsType: String
shutdownTimeout: Int
luksKeyfile: String
pollAttributes: String
pollAttributesDefault: String
pollAttributesStatus: String
nrRequests: Int
nrRequestsDefault: Int
nrRequestsStatus: String
mdNumStripes: Int
mdNumStripesDefault: Int
mdNumStripesStatus: String
mdSyncWindow: Int
mdSyncWindowDefault: Int
mdSyncWindowStatus: String
mdSyncThresh: Int
mdSyncThreshDefault: Int
mdSyncThreshStatus: String
mdWriteMethod: Int
mdWriteMethodDefault: String
mdWriteMethodStatus: String
shareDisk: String
shareUser: String
shareUserInclude: String
shareUserExclude: String
shareSmbEnabled: Boolean
shareNfsEnabled: Boolean
shareAfpEnabled: Boolean
shareInitialOwner: String
shareInitialGroup: String
shareCacheEnabled: Boolean
shareCacheFloor: String
shareMoverSchedule: String
shareMoverLogging: Boolean
fuseRemember: String
fuseRememberDefault: String
fuseRememberStatus: String
fuseDirectio: String
fuseDirectioDefault: String
fuseDirectioStatus: String
shareAvahiEnabled: Boolean
shareAvahiSmbName: String
shareAvahiSmbModel: String
shareAvahiAfpName: String
shareAvahiAfpModel: String
safeMode: Boolean
startMode: String
configValid: Boolean
configError: ConfigErrorState
joinStatus: String
deviceCount: Int
flashGuid: String
flashProduct: String
flashVendor: String
regCheck: String
regFile: String
regGuid: String
regTy: registrationType
regState: RegistrationState
"""Registration owner"""
regTo: String
regTm: String
regTm2: String
regGen: String
sbName: String
sbVersion: String
sbUpdated: String
sbEvents: Int
sbState: String
sbClean: Boolean
sbSynced: Int
sbSyncErrs: Int
sbSynced2: Int
sbSyncExit: String
sbNumDisks: Int
mdColor: String
mdNumDisks: Int
mdNumDisabled: Int
mdNumInvalid: Int
mdNumMissing: Int
mdNumNew: Int
mdNumErased: Int
mdResync: Int
mdResyncCorr: String
mdResyncPos: String
mdResyncDb: String
mdResyncDt: String
mdResyncAction: String
mdResyncSize: Int
mdState: String
mdVersion: String
cacheNumDevices: Int
cacheSbNumDisks: Int
fsState: String
"""Human friendly string of array events happening"""
fsProgress: String
"""
Percentage from 0 - 100 while upgrading a disk or swapping parity drives
"""
fsCopyPrcnt: Int
fsNumMounted: Int
fsNumUnmountable: Int
fsUnmountableMask: String
"""Total amount of user shares"""
shareCount: Int
"""Total amount shares with SMB enabled"""
shareSmbCount: Int
"""Total amount shares with NFS enabled"""
shareNfsCount: Int
"""Total amount shares with AFP enabled"""
shareAfpCount: Int
shareMoverActive: Boolean
csrfToken: String
}
"""Possible error states for configuration"""
enum ConfigErrorState {
UNKNOWN_ERROR
INELIGIBLE
INVALID
NO_KEY_SERVER
WITHDRAWN
}
type VmDomain implements Node {
"""The unique identifier for the vm (uuid)"""
id: PrefixedID!

View File

@@ -26,6 +26,7 @@
"@nestjs/core",
"@parcel/watcher",
"@swc/core",
"@tailwindcss/oxide",
"@unraid/libvirt",
"core-js",
"cpu-features",
@@ -33,10 +34,12 @@
"esbuild",
"nestjs-pino",
"protobufjs",
"sharp",
"simple-git-hooks",
"ssh2",
"unrs-resolver",
"vue-demi"
"vue-demi",
"workerd"
]
},
"dependencies": {

1285
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,5 +4,5 @@
"tabWidth": 2,
"printWidth": 105,
"singleQuote": true,
"plugins": ["prettier-plugin-tailwindcss", "@ianvs/prettier-plugin-sort-imports"]
"plugins": ["@ianvs/prettier-plugin-sort-imports"]
}

View File

@@ -19,29 +19,21 @@ const config: StorybookConfig = {
staticDirs: ['./static'],
async viteFinal(config) {
const storybookDir = dirname(new URL(import.meta.url).pathname);
return {
...config,
root: dirname(require.resolve('@storybook/builder-vite')),
plugins: [...(config.plugins ?? [])],
resolve: {
alias: {
'@': 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'),
'@': join(storybookDir, '../src'),
'@/components': join(storybookDir, '../src/components'),
'@/lib': join(storybookDir, '../src/lib'),
},
},
optimizeDeps: {
include: [...(config.optimizeDeps?.include ?? []), '@unraid/tailwind-rem-to-rem'],
},
css: {
postcss: {
plugins: [
(await import('tailwindcss')).default({
config: './tailwind.config.ts',
}),
(await import('autoprefixer')).default,
],
},
},
};
},
};

View File

@@ -1,6 +1,7 @@
import type { Preview } from '@storybook/vue3-vite';
import { registerAllComponents } from '../src/register';
import '@/styles/index.css';
import '@/../.storybook/static/index.css';
registerAllComponents({
pathToSharedCss: '/index.css',

View File

@@ -27,36 +27,6 @@ Import the component library styles in your main entry file:
import '@unraid/ui/style.css';
```
### 2. Configure TailwindCSS
Create a `tailwind.config.ts` file with the following configuration:
```typescript
import tailwindConfig from '@unraid/ui/tailwind.config.ts';
import type { Config } from 'tailwindcss';
export default {
presets: [tailwindConfig],
content: [
// ... your content paths
'./components/**/*.{js,vue,ts}',
'./layouts/**/*.vue',
'./pages/**/*.vue',
],
theme: {
extend: {
// your theme extensions
},
},
} satisfies Partial<Config>;
```
This configuration:
- Uses the Unraid UI library's Tailwind config as a preset
- Properly types your configuration with TypeScript
- Allows you to extend the base theme while maintaining all Unraid UI defaults
## Usage
```vue
@@ -249,7 +219,7 @@ const meta = {
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline'],
options: ['primary', 'secondary', 'outline-solid'],
},
size: {
control: 'select',

View File

@@ -3,7 +3,6 @@
"style": "default",
"typescript": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,

View File

@@ -9,11 +9,9 @@
"types": "./dist/index.d.ts",
"sideEffects": false,
"files": [
"dist",
"tailwind.config.ts"
"dist"
],
"scripts": {
"prepare": "pnpm build",
"// Development": "",
"dev": "vite",
"preview": "vite preview",
@@ -34,11 +32,11 @@
"preunraid:deploy": "pnpm build:wc",
"unraid:deploy": "just deploy",
"// Storybook": "",
"prestorybook": "pnpm storybook:css",
"storybook": "storybook dev -p 6006",
"storybook:css": "node scripts/build-style.mjs",
"prebuild-storybook": "pnpm storybook:css",
"build-storybook": "storybook build",
"storybook": "pnpm tailwind:watch & pnpm storybook:dev",
"storybook:dev": "storybook dev -p 6006",
"build-storybook": "pnpm tailwind:build && storybook build",
"tailwind:build": "tailwindcss -i ./src/styles/globals.css -o ./.storybook/static/index.css",
"tailwind:watch": "pnpm tailwind:build --watch",
"// Cloudflare Workers Deployment": "",
"deploy:storybook": "pnpm build-storybook && wrangler deploy",
"deploy:storybook:staging": "pnpm build-storybook && wrangler deploy --env staging"
@@ -54,6 +52,7 @@
"@jsonforms/core": "3.6.0",
"@jsonforms/vue": "3.6.0",
"@jsonforms/vue-vanilla": "3.6.0",
"@tailwindcss/cli": "^4.1.11",
"@vueuse/core": "13.5.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
@@ -64,6 +63,7 @@
"reka-ui": "2.3.2",
"shadcn-vue": "2.2.0",
"tailwind-merge": "2.6.0",
"tw-animate-css": "^1.3.5",
"vue-sonner": "1.3.2"
},
"devDependencies": {
@@ -72,19 +72,18 @@
"@storybook/addon-links": "9.0.17",
"@storybook/builder-vite": "9.0.17",
"@storybook/vue3-vite": "9.0.17",
"@tailwindcss/typography": "0.5.16",
"@tailwindcss/vite": "^4.1.11",
"@testing-library/vue": "8.1.0",
"@types/jsdom": "21.1.7",
"@types/node": "22.16.4",
"@types/testing-library__vue": "5.3.0",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@unraid/tailwind-rem-to-rem": "1.1.0",
"@unraid/tailwind-rem-to-rem": "2.0.0",
"@vitejs/plugin-vue": "6.0.0",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"@vue/test-utils": "2.4.6",
"@vue/tsconfig": "0.7.0",
"autoprefixer": "10.4.21",
"concurrently": "9.2.0",
"eslint": "9.31.0",
"eslint-config-prettier": "10.1.5",
@@ -96,14 +95,10 @@
"happy-dom": "18.0.1",
"jiti": "2.4.2",
"postcss": "8.5.6",
"postcss-import": "16.1.1",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.14",
"rimraf": "6.0.1",
"storybook": "9.0.17",
"tailwind-rem-to-rem": "github:unraid/tailwind-rem-to-rem",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "1.0.7",
"tailwindcss": "4.1.11",
"typescript": "5.8.3",
"typescript-eslint": "8.37.0",
"vite": "7.0.4",
@@ -124,20 +119,7 @@
"require": "./dist/index.cjs"
},
"./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"
},
"./tailwind.config.ts": {
"import": "./tailwind.config.ts",
"default": "./tailwind.config.ts"
},
"./theme/preset": {
"types": "./dist/theme/preset.d.ts",
"import": "./dist/theme/preset.js"
}
"./styles/*": "./src/styles/*"
},
"packageManager": "pnpm@10.13.1"
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,30 +0,0 @@
import fs from 'fs/promises';
import autoprefixer from 'autoprefixer';
import postcss from 'postcss';
import postcssImport from 'postcss-import';
import tailwindcss from 'tailwindcss';
/**
* Helper script for storybook to build the CSS file for the components. This is used to ensure that modals render using the shadow styles.
*/
process.env.VITE_TAILWIND_BASE_FONT_SIZE = 16;
const inputPath = './src/styles/index.css';
const outputPath = './.storybook/static/index.css'; // served from root: /index.css
const css = await fs.readFile(inputPath, 'utf8');
const result = await postcss([
postcssImport(),
tailwindcss({ config: './tailwind.config.ts' }),
autoprefixer(),
]).process(css, {
from: inputPath,
to: outputPath,
});
await fs.mkdir('./.storybook/static', { recursive: true });
await fs.writeFile(outputPath, result.css);
console.log('✅ CSS built for Storybook:', outputPath);

View File

@@ -42,18 +42,17 @@ const props = withDefaults(defineProps<BrandButtonProps>(), {
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`,
icon: 'w-[var(--icon-size)] fill-current shrink-0',
iconSize: props.size ?? '16px',
};
});
const needsBrandGradientBackground = computed(() => {
return ['outline', 'outline-primary'].includes(props.variant ?? '');
return ['outline-solid', 'outline-primary'].includes(props.variant ?? '');
});
</script>
@@ -71,15 +70,20 @@ const needsBrandGradientBackground = computed(() => {
>
<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"
class="absolute -top-[2px] -right-[2px] -bottom-[2px] -left-[2px] -z-10 bg-linear-to-r from-unraid-red to-orange opacity-100 transition-all rounded-md group-hover:opacity-60 group-focus:opacity-60"
/>
<!-- gives outline buttons the brand gradient background -->
<div
v-if="needsBrandGradientBackground"
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"
class="absolute -top-[2px] -right-[2px] -bottom-[2px] -left-[2px] -z-10 bg-linear-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" />
<component
:is="icon"
v-if="icon"
:class="classes.icon"
:style="{ '--icon-size': classes.iconSize }"
/>
{{ text }}
<slot />
@@ -92,6 +96,7 @@ const needsBrandGradientBackground = computed(() => {
iconRightHoverDisplay &&
'opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-all',
]"
:style="{ '--icon-size': classes.iconSize }"
/>
</component>
</template>

View File

@@ -21,15 +21,15 @@ export const brandButtonVariants = cva(
'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: '',
none: 'border-transparent hover:shadow-none focus:shadow-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',
'12px': 'text-xs gap-1',
'14px': 'text-sm gap-2',
'16px': 'text-base gap-2',
'18px': 'text-lg gap-2',
'20px': 'text-xl gap-2',
'24px': 'text-2xl gap-2',
},
padding: {
default: '',
@@ -41,32 +41,32 @@ export const brandButtonVariants = cva(
{
size: '12px',
padding: 'default',
class: 'p-8px',
class: 'p-2',
},
{
size: '14px',
padding: 'default',
class: 'p-8px',
class: 'p-2',
},
{
size: '16px',
padding: 'default',
class: 'p-12px',
class: 'p-3',
},
{
size: '18px',
padding: 'default',
class: 'p-12px',
class: 'p-3',
},
{
size: '20px',
padding: 'default',
class: 'p-16px',
class: 'p-4',
},
{
size: '24px',
padding: 'default',
class: 'p-16px',
class: 'p-4',
},
],
defaultVariants: {

View File

@@ -23,12 +23,12 @@ const props = withDefaults(defineProps<BadgeProps>(), {
const badgeClasses = computed(() => {
const iconSizes = {
xs: 'w-12px',
sm: 'w-14px',
md: 'w-16px',
lg: 'w-18px',
xl: 'w-20px',
'2xl': 'w-24px',
xs: 'w-3',
sm: 'w-3.5',
md: 'w-4',
lg: 'w-4.5',
xl: 'w-5',
'2xl': 'w-6',
} as const;
return {
@@ -40,8 +40,8 @@ const badgeClasses = computed(() => {
<template>
<span :class="[badgeClasses.badge, props.class]">
<component :is="icon" v-if="icon" class="flex-shrink-0" :class="badgeClasses.icon" />
<component :is="icon" v-if="icon" class="shrink-0" :class="badgeClasses.icon" />
<slot />
<component :is="iconRight" v-if="iconRight" class="flex-shrink-0" :class="badgeClasses.icon" />
<component :is="iconRight" v-if="iconRight" class="shrink-0" :class="badgeClasses.icon" />
</span>
</template>

View File

@@ -22,12 +22,12 @@ export const badgeVariants = cva(
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',
xs: 'text-xs px-2 py-1 gap-1',
sm: 'text-sm px-2 py-1 gap-2',
md: 'text-base px-3 py-2 gap-2',
lg: 'text-lg px-3 py-2 gap-2',
xl: 'text-xl px-4 py-3 gap-2',
'2xl': 'text-2xl px-4 py-3 gap-2',
},
},
defaultVariants: {

View File

@@ -20,7 +20,7 @@ describe('Button', () => {
});
rerender({
props: { variant: 'outline' },
props: { variant: 'outline-solid' },
slots: { default: 'Delete' },
});
});

View File

@@ -2,7 +2,7 @@ import type { VariantProps } from 'class-variance-authority';
import { cva } from 'class-variance-authority';
export const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-base 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',
'inline-flex items-center justify-center rounded-md text-base font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {

View File

@@ -35,7 +35,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'z-50 w-72 rounded-md bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 w-72 rounded-md bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)
"

View File

@@ -46,12 +46,12 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
<template>
<DialogPortal :disabled="disabled" :force-mount="forceMount" :to="teleportTarget">
<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"
class="fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<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"
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
>
<X class="w-4 h-4 text-muted-foreground" />
</DialogClose>

View File

@@ -22,7 +22,7 @@ const forwarded = useForwardProps(delegatedProps);
cn(
'inline-flex items-center justify-center rounded-full text-muted-foreground/50 w-10 h-10',
// Disabled
'group-data-[disabled]:text-muted-foreground group-data-[disabled]:opacity-50',
'group-data-disabled:text-muted-foreground group-data-disabled:opacity-50',
// Active
'group-data-[state=active]:bg-primary group-data-[state=active]:text-primary-foreground',
// Completed

View File

@@ -24,7 +24,7 @@ const forwarded = useForwardProps(delegatedProps);
'flex flex-col items-start gap-1',
'md:flex-row md:items-center md:gap-2',
'group transition-all duration-200',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-80',
'data-disabled:pointer-events-none data-disabled:opacity-80',
props.class
)
"

View File

@@ -22,7 +22,7 @@ const forwarded = useForwardProps(delegatedProps);
cn(
'hidden md:block bg-muted md:w-24 md:h-px md:my-0',
// Disabled
'group-data-[disabled]:bg-muted group-data-[disabled]:opacity-75',
'group-data-disabled:bg-muted group-data-disabled:opacity-75',
// Completed
'group-data-[state=completed]:bg-accent-foreground',
props.class

View File

@@ -16,7 +16,7 @@ const delegatedProps = computed(() => {
v-bind="delegatedProps"
:class="
cn(
'flex mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'flex mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
props.class
)
"

View File

@@ -19,7 +19,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'inline-flex items-center justify-center whitespace-nowrap rounded px-4.5 py-2.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
'inline-flex items-center justify-center whitespace-nowrap rounded px-4.5 py-2.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-xs',
props.class
)
"

View File

@@ -30,7 +30,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"

View File

@@ -21,7 +21,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default gap-2 select-none justify-between items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
'relative flex cursor-default gap-2 select-none justify-between items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
props.class
)
"

View File

@@ -33,7 +33,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="{ ...forwarded, ...$attrs }"
:class="
cn(
'z-50 w-[200px] rounded-md border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'z-50 w-[200px] rounded-md border bg-popover text-popover-foreground shadow-md outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class
@@ -45,7 +45,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
cn(
'p-1',
position === 'popper' &&
'h-[--reka-combobox-trigger-height] w-full min-w-[--reka-combobox-trigger-width]'
'h-(--reka-combobox-trigger-height) w-full min-w-(--reka-combobox-trigger-width)'
)
"
>

View File

@@ -24,7 +24,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
v-model="modelValue"
:class="
cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"

View File

@@ -13,18 +13,18 @@ const checked = ref(false);
</script>
<template>
<SwitchGroup as="div">
<div class="flex flex-shrink-0 items-center gap-16px">
<div class="flex shrink-0 items-center gap-4">
<Switch
v-model="checked"
:class="[
checked ? 'bg-green-500' : 'bg-gray-200',
'relative inline-flex h-24px w-[44px] flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2',
'relative inline-flex h-6 w-[44px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2',
]"
>
<span
:class="[
checked ? 'translate-x-20px' : 'translate-x-0',
'pointer-events-none relative inline-block h-20px w-20px transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
checked ? 'translate-x-5' : 'translate-x-0',
'pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out',
]"
>
<span
@@ -34,7 +34,7 @@ const checked = ref(false);
]"
aria-hidden="true"
>
<svg class="h-12px w-12px text-gray-400" fill="none" viewBox="0 0 12 12">
<svg class="h-3 w-3 text-gray-400" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
@@ -51,7 +51,7 @@ const checked = ref(false);
]"
aria-hidden="true"
>
<svg class="h-12px w-12px text-green-500" fill="currentColor" viewBox="0 0 12 12">
<svg class="h-3 w-3 text-green-500" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
@@ -59,7 +59,7 @@ const checked = ref(false);
</span>
</span>
</Switch>
<SwitchLabel class="text-14px">
<SwitchLabel class="text-sm">
{{ label }}
</SwitchLabel>
</div>

View File

@@ -12,7 +12,7 @@ const props = defineProps<{
<div
:class="
cn(
'relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5',
'relative has-data-[slot=increment]:*:data-[slot=input]:pr-5 has-data-[slot=decrement]:*:data-[slot=input]:pl-5',
props.class
)
"

View File

@@ -13,7 +13,7 @@ const props = defineProps<{
data-slot="input"
:class="
cn(
'flex h-10 w-full rounded-md border border-input bg-background py-2 text-sm text-center ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-md border border-input bg-background py-2 text-sm text-center ring-offset-background placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
props.class
)
"

View File

@@ -27,7 +27,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
props.class
)
"

View File

@@ -4,13 +4,13 @@ withDefaults(
maxWidth?: string;
}>(),
{
maxWidth: 'max-w-1024px',
maxWidth: 'max-w-[1024px]',
}
);
</script>
<template>
<div class="grid gap-y-24px w-full mx-auto px-16px" :class="maxWidth">
<div class="grid gap-y-6 w-full mx-auto px-4" :class="maxWidth">
<slot />
</div>
</template>

View File

@@ -44,7 +44,7 @@ const { teleportTarget } = useTeleport();
<DialogClose
v-if="showCloseButton !== false"
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-accent data-[state=open]:text-muted-foreground"
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>

View File

@@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class
)
"

View File

@@ -18,7 +18,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
inset && 'pl-8',
props.class
)

View File

@@ -25,7 +25,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
v-bind="forwarded"
:class="
cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class
)
"

View File

@@ -17,7 +17,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden focus:bg-accent data-[state=open]:bg-accent',
props.class
)
"

View File

@@ -8,7 +8,7 @@ const forwardedProps = useForwardProps(props);
<template>
<DropdownMenuTrigger
class="outline-none cursor-pointer [&[data-state=open]]:cursor-pointer"
class="outline-hidden cursor-pointer data-[state=open]:cursor-pointer"
v-bind="forwardedProps"
>
<slot />

View File

@@ -48,7 +48,7 @@ const { teleportTarget } = useTeleport();
cn(
'p-1',
position === 'popper' &&
'h-[--reka-select-trigger-height] w-full min-w-[--reka-select-trigger-width]'
'h-(--reka-select-trigger-height) w-full min-w-(--reka-select-trigger-width)'
)
"
>

View File

@@ -23,7 +23,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
props.class
)
"

View File

@@ -17,7 +17,7 @@ const forwardedProps = useForwardProps(delegatedProps);
v-bind="forwardedProps"
:class="
cn(
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate text-start',
props.class
)
"

View File

@@ -99,7 +99,7 @@ if (control.value.data !== undefined && control.value.data !== null) {
<ComboboxItem
:value="suggestion.value"
@select="handleSelect"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden hover:bg-accent hover:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground"
>
<span>{{ suggestion.label || suggestion.value }}</span>
</ComboboxItem>
@@ -113,7 +113,7 @@ if (control.value.data !== undefined && control.value.data !== null) {
v-else
:value="suggestion.value"
@select="handleSelect"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground"
class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden hover:bg-accent hover:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground"
>
<span>{{ suggestion.label || suggestion.value }}</span>
</ComboboxItem>

View File

@@ -13,7 +13,7 @@ const { control } = useJsonFormsControl(props);
<template>
<!-- Only render the wrapper if the control is visible -->
<div v-if="control.visible" class="flex-grow">
<div v-if="control.visible" class="grow">
<!-- Render the actual control passed via the default slot -->
<slot />
<!-- Automatically display errors below the control -->

View File

@@ -46,7 +46,7 @@ const togglePasswordVisibility = () => {
<Input
v-model="value"
:type="inputType"
:class="cn('flex-grow', classOverride, { 'pr-10': isPassword })"
:class="cn('grow', classOverride, { 'pr-10': isPassword })"
:disabled="!control.enabled"
:required="control.required"
:placeholder="control.schema.description"

View File

@@ -70,7 +70,7 @@ const descriptionClass = computed(() => {
<template>
<!-- Use the computed isVisible based on renderer.value.visible -->
<div class="flex flex-col gap-2 flex-shrink-0">
<div class="flex flex-col gap-2 shrink-0">
<!-- Replace native label with the Label component -->
<Label v-if="labelText" :class="labelClass">{{ labelText }}</Label>
<!-- Use v-html with the parsedDescription ref -->

View File

@@ -145,7 +145,10 @@ const getStepState = (stepIndex: number): StepState => {
<!-- Render elements for the current step -->
<!-- Added key to force re-render on step change, ensuring correct elements display -->
<div class="current-step-content rounded-md border p-4 shadow" :key="`step-content-${currentStep}`">
<div
class="current-step-content rounded-md border p-4 shadow-sm"
:key="`step-content-${currentStep}`"
>
<DispatchRenderer
v-for="(element, index) in currentStepElements"
:key="`${layout.path}-${index}-step-${currentStep}`"

View File

@@ -13,8 +13,5 @@ export * from '@/forms/renderers';
// Lib
export * from '@/lib/utils';
// Config
export { default as tailwindConfig } from '../tailwind.config';
// Composables
export { default as useTeleport } from '@/composables/useTeleport';

View File

@@ -1,98 +1,336 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
@import 'tw-animate-css';
/* Force generation of all color utilities for custom colors across packages */
@source inline("{bg,text,border,ring,fill,stroke}-{unraid-green,unraid-red}{,-50,-100,-200,-300,-400,-500,-600,-700,-800,-900,-950}");
@source inline("{bg,text,border,ring,fill,stroke}-{yellow-accent,orange-dark,orange}");
@source inline("{from,via,to}-{unraid-red,orange}");
@source inline("bg-linear-to-{r,l,t,b,tr,tl,br,bl}");
@source "../**/*.vue";
@source "../stories/**/*.ts";
@custom-variant dark (&:where(.dark, .dark *));
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border);
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
/* Fix for Tailwind v4 border styles not working */
* {
--tw-border-style: solid;
}
}
@theme static {
/* Container settings */
--container-center: true;
--container-padding: 2rem;
--container-screen-2xl: 1400px;
/* Font families */
--font-sans:
clear-sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
/* Grid template columns */
--grid-template-columns-settings: 35% 1fr;
/* Border color default */
--default-border-color: var(--color-border);
/* Color palette */
--color-inherit: inherit;
--color-transparent: transparent;
--color-black: #1c1b1b;
--color-grey-darkest: #222;
--color-grey-darker: #606f7b;
--color-grey-dark: #383735;
--color-grey-mid: #999999;
--color-grey: #e0e0e0;
--color-grey-light: #dae1e7;
--color-grey-lighter: #f1f5f8;
--color-grey-lightest: #f2f2f2;
--color-white: #ffffff;
/* Unraid colors */
--color-yellow-accent: #e9bf41;
--color-orange-dark: #f15a2c;
--color-orange: #ff8c2f;
/* Unraid red palette */
--color-unraid-red: #e22828;
--color-unraid-red-50: #fef2f2;
--color-unraid-red-100: #ffe1e1;
--color-unraid-red-200: #ffc9c9;
--color-unraid-red-300: #fea3a3;
--color-unraid-red-400: #fc6d6d;
--color-unraid-red-500: #f43f3f;
--color-unraid-red-600: #e22828;
--color-unraid-red-700: #bd1818;
--color-unraid-red-800: #9c1818;
--color-unraid-red-900: #821a1a;
--color-unraid-red-950: #470808;
/* Unraid green palette */
--color-unraid-green: #63a659;
--color-unraid-green-50: #f5f9f4;
--color-unraid-green-100: #e7f3e5;
--color-unraid-green-200: #d0e6cc;
--color-unraid-green-300: #aad1a4;
--color-unraid-green-400: #7db474;
--color-unraid-green-500: #63a659;
--color-unraid-green-600: #457b3e;
--color-unraid-green-700: #396134;
--color-unraid-green-800: #314e2d;
--color-unraid-green-900: #284126;
--color-unraid-green-950: #122211;
/* Header colors */
--color-header-text-primary: var(--header-text-primary);
--color-header-text-secondary: var(--header-text-secondary);
--color-header-background-color: var(--header-background-color);
/* Font sizes */
--font-10px: 10px;
--font-12px: 12px;
--font-14px: 14px;
--font-16px: 16px;
--font-18px: 18px;
--font-20px: 20px;
--font-24px: 24px;
--font-30px: 30px;
/* Spacing */
--spacing-4_5: 1.125rem;
--spacing--8px: -8px;
--spacing-2px: 2px;
--spacing-4px: 4px;
--spacing-6px: 6px;
--spacing-8px: 8px;
--spacing-10px: 10px;
--spacing-12px: 12px;
--spacing-14px: 14px;
--spacing-16px: 16px;
--spacing-20px: 20px;
--spacing-24px: 24px;
--spacing-28px: 28px;
--spacing-32px: 32px;
--spacing-36px: 36px;
--spacing-40px: 40px;
--spacing-64px: 64px;
--spacing-80px: 80px;
--spacing-90px: 90px;
--spacing-150px: 150px;
--spacing-160px: 160px;
--spacing-200px: 200px;
--spacing-260px: 260px;
--spacing-300px: 300px;
--spacing-310px: 310px;
--spacing-350px: 350px;
--spacing-448px: 448px;
--spacing-512px: 512px;
--spacing-640px: 640px;
--spacing-800px: 800px;
/* Width and Height values */
--width-36px: 36px;
--height-36px: 36px;
/* Min/Max widths */
--min-width-86px: 86px;
--min-width-160px: 160px;
--min-width-260px: 260px;
--min-width-300px: 300px;
--min-width-310px: 310px;
--min-width-350px: 350px;
--min-width-800px: 800px;
--max-width-86px: 86px;
--max-width-160px: 160px;
--max-width-260px: 260px;
--max-width-300px: 300px;
--max-width-310px: 310px;
--max-width-350px: 350px;
--max-width-640px: 640px;
--max-width-800px: 800px;
--max-width-1024px: 1024px;
/* Breakpoints */
--breakpoint-*: initial;
/* Animations */
--animate-mark-2: mark-2 1.5s ease infinite;
--animate-mark-3: mark-3 1.5s ease infinite;
--animate-mark-6: mark-6 1.5s ease infinite;
--animate-mark-7: mark-7 1.5s ease infinite;
/* Keyframes */
--keyframes-mark-2: 50% { transform: translateY(-40px) } to { transform: translateY(0) };
--keyframes-mark-3: 50% { transform: translateY(-62px) } to { transform: translateY(0) };
--keyframes-mark-6: 50% { transform: translateY(40px) } to { transform: translateY(0) };
--keyframes-mark-7: 50% { transform: translateY(62px) } to { transform: translateY(0) };
/* Radius */
--radius: 0.5rem;
/* Text Resizing */
--text-xs: 1.2rem; /* 12px at 10px base */
--text-sm: 1.4rem; /* 14px at 10px base */
--text-base: 1.6rem; /* 16px at 10px base */
--text-lg: 1.8rem; /* 18px at 10px base */
--text-xl: 2rem; /* 20px at 10px base */
--text-2xl: 2.4rem; /* 24px at 10px base */
--text-3xl: 3rem; /* 30px at 10px base */
--text-4xl: 3.6rem; /* 36px at 10px base */
--text-5xl: 4.8rem; /* 48px at 10px base */
--text-6xl: 6rem; /* 60px at 10px base */
--text-7xl: 7.2rem; /* 72px at 10px base */
--text-8xl: 9.6rem; /* 96px at 10px base */
--text-9xl: 12.8rem; /* 128px at 10px base */
--spacing: 0.4rem; /* 4px at 10px base */
}
/* Hybrid theme system: Native CSS + Theme Store fallback */
@layer base {
/* Light mode defaults */
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
/* For web components: inherit CSS variables from the host */
:host {
--background: inherit;
--foreground: inherit;
--muted: inherit;
--muted-foreground: inherit;
--popover: inherit;
--popover-foreground: inherit;
--card: inherit;
--card-foreground: inherit;
--border: inherit;
--input: inherit;
--primary: inherit;
--primary-foreground: inherit;
--secondary: inherit;
--secondary-foreground: inherit;
--accent: inherit;
--accent-foreground: inherit;
--destructive: inherit;
--destructive-foreground: inherit;
--ring: inherit;
--chart-1: inherit;
--chart-2: inherit;
--chart-3: inherit;
--chart-4: inherit;
--chart-5: inherit;
}
/* Class-based dark mode support for web components using :host-context */
:host-context(.dark) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/* Alternative class-based dark mode support for specific Unraid themes */
:host-context(.dark[data-theme='black']),
:host-context(.dark[data-theme='gray']) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--border: 0 0% 14.9%;
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
}
/* Define the theme colors that reference the existing CSS variables from web/themes */
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1, 12 76% 61%));
--color-chart-2: hsl(var(--chart-2, 173 58% 39%));
--color-chart-3: hsl(var(--chart-3, 197 37% 24%));
--color-chart-4: hsl(var(--chart-4, 43 74% 66%));
--color-chart-5: hsl(var(--chart-5, 27 87% 67%));
}

View File

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

View File

View File

@@ -1,271 +0,0 @@
import typography from '@tailwindcss/typography';
import type { Config } from 'tailwindcss';
import animate from 'tailwindcss-animate';
import type { PluginAPI } from 'tailwindcss/types/config';
export const unraidPreset = {
darkMode: ['selector', '[data-mode="dark"]'],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
borderColor: {
DEFAULT: 'hsl(var(--border))',
},
fontFamily: {
sans: 'clear-sans,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji',
},
gridTemplateColumns: {
settings: '35% 1fr',
},
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' },
},
'mark-2': {
'50%': { transform: 'translateY(-40px)' },
'100%': { transform: 'translateY(0)' },
},
'mark-3': {
'50%': { transform: 'translateY(-62px)' },
'100%': { transform: 'translateY(0)' },
},
'mark-6': {
'50%': { transform: 'translateY(40px)' },
'100%': { transform: 'translateY(0)' },
},
'mark-7': {
'50%': { transform: 'translateY(62px)' },
'100%': { transform: 'translateY(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',
'mark-2': 'mark-2 1.5s ease infinite',
'mark-3': 'mark-3 1.5s ease infinite',
'mark-6': 'mark-6 1.5s ease infinite',
'mark-7': 'mark-7 1.5s ease infinite',
},
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

@@ -4,32 +4,113 @@ import { BrandButton } from '../../../src/components/brand/index.js';
const meta = {
title: 'Components/Brand',
component: BrandButton,
parameters: {
layout: 'centered',
},
argTypes: {
variant: {
control: { type: 'select' },
options: [
'fill',
'black',
'gray',
'outline',
'outline-primary',
'outline-black',
'outline-white',
'underline',
'underline-hover-red',
'white',
'none',
],
},
size: {
control: { type: 'select' },
options: ['12px', '14px', '16px', '18px', '20px', '24px'],
},
padding: {
control: { type: 'select' },
options: ['default', 'none', 'lean'],
},
disabled: {
control: { type: 'boolean' },
},
text: {
control: { type: 'text' },
},
},
args: {
variant: 'fill',
size: '16px',
padding: 'default',
text: 'Click me',
disabled: false,
},
} 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) => ({
export const Default: Story = {};
export const AllVariants: Story = {
render: () => ({
components: { BrandButton },
setup() {
return { args };
},
template: `
<BrandButton
:variant="args.variant"
:size="args.size"
:padding="args.padding"
:text="args.text"
:class="args.class"
/>
<div class="grid grid-cols-3 gap-4 p-4">
<div class="bg-gray-900 p-2 rounded">
<BrandButton variant="fill" text="Fill" />
</div>
<BrandButton variant="black" text="Black" />
<BrandButton variant="gray" text="Gray" />
<BrandButton variant="outline" text="Outline" />
<BrandButton variant="outline-primary" text="Outline Primary" />
<BrandButton variant="outline-black" text="Outline Black" />
<div class="bg-gray-900 p-2 rounded">
<BrandButton variant="outline-white" text="Outline White" />
</div>
<BrandButton variant="underline" text="Underline" />
<BrandButton variant="underline-hover-red" text="Underline Hover Red" />
<BrandButton variant="white" text="White" />
<BrandButton variant="none" text="None" />
</div>
`,
}),
};
export const AllSizes: Story = {
render: () => ({
components: { BrandButton },
template: `
<div class="flex flex-col gap-4 p-4">
<BrandButton size="12px" text="12px" />
<BrandButton size="14px" text="14px" />
<BrandButton size="16px" text="16px" />
<BrandButton size="18px" text="18px" />
<BrandButton size="20px" text="20px" />
<BrandButton size="24px" text="24px" />
</div>
`,
}),
};
export const AllPadding: Story = {
render: () => ({
components: { BrandButton },
template: `
<div class="flex flex-col gap-4 p-4">
<BrandButton padding="default" text="Default Padding" />
<BrandButton padding="none" text="No Padding" />
<BrandButton padding="lean" text="Lean Padding" />
</div>
`,
}),
};
export const Disabled: Story = {
args: {
disabled: true,
text: 'Disabled Button',
},
};

View File

@@ -60,7 +60,7 @@ export const Horizontal: Story = {
<div class="flex p-4">
${Array(50)
.fill(0)
.map((_, i) => `<div class="flex-shrink-0 mr-2">Content ${i + 1}</div>`)
.map((_, i) => `<div class="shrink-0 mr-2">Content ${i + 1}</div>`)
.join('')}
</div>
<ScrollBar orientation="horizontal" />

View File

@@ -1,116 +0,0 @@
import tailwindRemToRem from '@unraid/tailwind-rem-to-rem';
import type { Config } from 'tailwindcss';
import tailwindcssAnimate from 'tailwindcss-animate';
/* eslint-disable no-relative-import-paths/no-relative-import-paths */
import { unraidPreset } from './src/theme/preset';
export default {
darkMode: ['class'],
presets: [unraidPreset],
content: [
'./src/components/**/*.{js,vue,ts}',
'./src/components/**/*.ce.{js,vue,ts}',
'./src/composables/**/*.{js,vue,ts}',
'./stories/**/*.stories.{js,ts,jsx,mdx}',
'./index.html',
],
safelist: [
'dark',
'unraid_mark_1',
'unraid_mark_2',
'unraid_mark_3',
'unraid_mark_4',
'unraid_mark_6',
'unraid_mark_7',
'unraid_mark_8',
'unraid_mark_9',
{
pattern: /^text-(header-text-secondary|orange-dark)$/,
variants: ['group-hover', 'group-focus'],
},
{
pattern: /^(underline|no-underline)$/,
variants: ['group-hover', 'group-focus'],
},
],
plugins: [
tailwindRemToRem({
baseFontSize: 16,
newFontSize: Number(process.env.VITE_TAILWIND_BASE_FONT_SIZE ?? 10),
}),
tailwindcssAnimate,
],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))',
},
},
keyframes: {
'accordion-down': {
from: {
height: '0',
},
to: {
height: 'var(--reka-accordion-content-height)',
},
},
'accordion-up': {
from: {
height: 'var(--reka-accordion-content-height)',
},
to: {
height: '0',
},
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
} satisfies Partial<Config>;

View File

@@ -41,7 +41,6 @@
"src/**/*.tsx",
"src/**/*.vue",
"src/**/*.ce.vue",
"tailwind.config.ts",
"src/theme/**/*.ts",
"**/*.config.ts",
"eslint.config.ts",

View File

@@ -1,31 +1,30 @@
/// <reference types="vitest" />
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export default function createConfig() {
return defineConfig({
plugins: [
tailwindcss(),
vue(),
...(process.env.npm_lifecycle_script?.includes('storybook')
? []
: [
dts({
insertTypesEntry: true,
include: ['src/**/*.ts', 'src/**/*.vue', 'tailwind.config.ts'],
include: ['src/**/*.ts', 'src/**/*.vue'],
outDir: 'dist',
rollupTypes: true,
copyDtsFiles: true,
}),
]),
],
css: {
postcss: {
plugins: [tailwindcss()],
},
},
build: {
cssCodeSplit: false,
rollupOptions: {
@@ -36,8 +35,6 @@ export default function createConfig() {
],
input: {
index: resolve(__dirname, 'src/index.ts'),
tailwind: resolve(__dirname, 'tailwind.config.ts'),
preset: resolve(__dirname, 'src/theme/preset.ts'),
},
preserveEntrySignatures: 'allow-extension',
output: {

View File

@@ -1,12 +1,12 @@
import { fileURLToPath, URL } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite';
import vueDevTools from 'vite-plugin-vue-devtools';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueDevTools()],
plugins: [tailwindcss(), vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
@@ -15,11 +15,6 @@ export default defineConfig({
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
css: {
postcss: {
plugins: [tailwindcss()],
},
},
build: {
outDir: 'dist-wc',
manifest: 'ui.manifest.json',

View File

@@ -7,7 +7,7 @@ const config = {
tabWidth: 2,
printWidth: 105,
singleQuote: true,
plugins: ['prettier-plugin-tailwindcss', '@ianvs/prettier-plugin-sort-imports'],
plugins: ['@ianvs/prettier-plugin-sort-imports'],
// decorators-legacy lets the import sorter transform files with decorators
importOrderParserPlugins: ['typescript', 'decorators-legacy'],
importOrder: [

View File

@@ -164,7 +164,7 @@ describe('KeyActions', () => {
const button = wrapper.findComponent(BrandButton);
expect(button.props('class')).toContain('sm:max-w-300px');
expect(button.props('class')).toContain('sm:max-w-[300px]');
});
it('passes all required props to BrandButton component', () => {

View File

@@ -167,7 +167,7 @@ describe('Modal', () => {
expect(modalDiv.classes()).toContain('border-green-600/10');
});
it('disables shadow when disableShadow is true', async () => {
it('disables shadow-sm when disableShadow is true', async () => {
wrapper = mount(Modal, {
props: {
t,
@@ -208,7 +208,7 @@ describe('Modal', () => {
it('applies overlay color and opacity classes', async () => {
const overlayColor = 'bg-blue-500';
const overlayOpacity = 'bg-opacity-50';
const overlayOpacity = 'bg-blue-500/50';
wrapper = mount(Modal, {
props: {

View File

@@ -245,7 +245,7 @@ describe('UserProfile.ce.vue', () => {
expect(mockCopy).toHaveBeenCalledTimes(1);
expect(mockCopy).toHaveBeenCalledWith(initialServerData.lanIp);
const copiedMessage = wrapper.find('.text-white.text-12px');
const copiedMessage = wrapper.find('.text-white.text-xs');
expect(copiedMessage.exists()).toBe(true);
expect(copiedMessage.text()).toContain(t('LAN IP Copied'));
@@ -265,7 +265,7 @@ describe('UserProfile.ce.vue', () => {
expect(copyLanIpSpy).toHaveBeenCalledTimes(1);
expect(mockCopy).not.toHaveBeenCalled();
const notSupportedMessage = wrapper.find('.text-white.text-12px');
const notSupportedMessage = wrapper.find('.text-white.text-xs');
expect(notSupportedMessage.exists()).toBe(true);
expect(notSupportedMessage.text()).toContain(t('LAN IP {0}', [initialServerData.lanIp]));

View File

@@ -1,7 +1,30 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import { NuxtLayout, NuxtPage } from '#components';
import { devConfig } from '~/helpers/env';
onMounted(() => {
document.documentElement.setAttribute('data-env', devConfig.NODE_ENV || 'production');
// Override text sizes back to 16px base in dev mode (from 10px base in globals.css)
if (devConfig.NODE_ENV === 'development') {
document.documentElement.style.setProperty('--text-xs', '0.75rem'); /* 12px */
document.documentElement.style.setProperty('--text-sm', '0.875rem'); /* 14px */
document.documentElement.style.setProperty('--text-base', '1rem'); /* 16px */
document.documentElement.style.setProperty('--text-lg', '1.125rem'); /* 18px */
document.documentElement.style.setProperty('--text-xl', '1.25rem'); /* 20px */
document.documentElement.style.setProperty('--text-2xl', '1.5rem'); /* 24px */
document.documentElement.style.setProperty('--text-3xl', '1.875rem'); /* 30px */
document.documentElement.style.setProperty('--text-4xl', '2.25rem'); /* 36px */
document.documentElement.style.setProperty('--text-5xl', '3rem'); /* 48px */
document.documentElement.style.setProperty('--text-6xl', '3.75rem'); /* 60px */
document.documentElement.style.setProperty('--text-7xl', '4.5rem'); /* 72px */
document.documentElement.style.setProperty('--text-8xl', '6rem'); /* 96px */
document.documentElement.style.setProperty('--text-9xl', '8rem'); /* 128px */
document.documentElement.style.setProperty('--spacing', '0.25rem'); /* 4px */
}
});
</script>
<template>

View File

@@ -1,45 +1,109 @@
@tailwind base;
@tailwind components;
@import 'tailwindcss';
@import '@unraid/ui/styles';
/*
darkTheme
alpha: '#1c1b1b',
beta: '#f2f2f2',
gamma: '#999999',
/* Force generation of all color utilities for custom colors across packages */
@source inline("{bg,text,border,ring,fill,stroke}-{unraid-green,unraid-red}{,-50,-100,-200,-300,-400,-500,-600,-700,-800,-900,-950}");
@source inline("{bg,text,border,ring,fill,stroke}-{yellow-accent,orange-dark,orange}");
lightTheme
alpha: '#f2f2f2',
beta: '#1c1b1b',
gamma: '#999999',
*/
/* Scan unraid-ui package for class usage */
@source "../unraid-ui/src/**/*.{vue,ts,js}";
body {
--color-alpha: #1c1b1b;
--color-beta: #f2f2f2;
--color-gamma: #999999;
--color-gamma-opaque: rgba(153, 153, 153, .5);
--color-customgradient-start: rgba(242, 242, 242, .0);
--color-customgradient-end: rgba(242, 242, 242, .85);
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, .15);
--ring-offset-shadow: 0 0 --var(--color-beta);
--ring-shadow: 0 0 --var(--color-beta);
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Header colors */
--color-header-text-primary: var(--header-text-primary);
--color-header-text-secondary: var(--header-text-secondary);
--color-header-background-color: var(--header-background-color);
/* Legacy colors */
--color-alpha: var(--color-alpha);
--color-beta: var(--color-beta);
--color-gamma: var(--color-gamma);
--color-gamma-opaque: var(--color-gamma-opaque);
--color-customgradient-start: var(--color-customgradient-start);
--color-customgradient-end: var(--color-customgradient-end);
/* Gradients */
--color-header-gradient-start: var(--header-gradient-start);
--color-header-gradient-end: var(--header-gradient-end);
--color-banner-gradient: var(--banner-gradient);
}
/* .button-gradient-border-to-bg {
background: linear-gradient(to right,#e03237 0%,#fd8c3c 100%) left top no-repeat,linear-gradient(to right,#e03237 0%,#fd8c3c 100%) left bottom no-repeat,linear-gradient(to top,#e03237 0%,#e03237 100%) left bottom no-repeat,linear-gradient(to top,#fd8c3c 0%,#fd8c3c 100%) right bottom no-repeat;
background-size: 100% 2px,100% 2px,2px 100%,2px 100%;
&:hover,
&:focus {
background: linear-gradient(to right,#E22828 0%,#FF8C2F 100%);
@layer utilities {
:host {
--tw-divide-y-reverse: 0;
--tw-border-style: solid;
--tw-font-weight: initial;
--tw-tracking: initial;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-rotate-x: rotateX(0);
--tw-rotate-y: rotateY(0);
--tw-rotate-z: rotateZ(0);
--tw-skew-x: skewX(0);
--tw-skew-y: skewY(0);
--tw-space-x-reverse: 0;
--tw-gradient-position: initial;
--tw-gradient-from: #0000;
--tw-gradient-via: #0000;
--tw-gradient-to: #0000;
--tw-gradient-stops: initial;
--tw-gradient-via-stops: initial;
--tw-gradient-from-position: 0%;
--tw-gradient-via-position: 50%;
--tw-gradient-to-position: 100%;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-duration: initial;
--tw-ease: initial;
}
} */
/* Ensure this is always at the bottom @see https://tailwindcss.com/docs/content-configuration#working-with-third-party-libraries */
@tailwind utilities;
}
@layer base {
body {
@apply bg-background text-foreground;
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: hsl(var(--border));
}
}
body {
--color-alpha: #1c1b1b;
--color-beta: #f2f2f2;
--color-gamma: #999999;
--color-gamma-opaque: rgba(153, 153, 153, 0.5);
--color-customgradient-start: rgba(242, 242, 242, 0);
--color-customgradient-end: rgba(242, 242, 242, 0.85);
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, 0.15);
--ring-offset-shadow: 0 0 --var(--color-beta);
--ring-shadow: 0 0 --var(--color-beta);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}

View File

@@ -41,4 +41,4 @@ const config: CodegenConfig = {
},
};
export default config;
export default config;

View File

@@ -67,8 +67,8 @@ const docsButtons = computed<BrandButtonProps[]>(() => {
:title-in-main="partnerInfo?.hasPartnerLogo"
:description="description"
overlay-color="bg-background"
overlay-opacity="bg-opacity-100"
max-width="max-w-800px"
overlay-opacity="bg-background/100"
max-width="max-w-[800px]"
:modal-vertical-center="false"
:disable-shadow="true"
>
@@ -77,7 +77,7 @@ const docsButtons = computed<BrandButtonProps[]>(() => {
</template>
<template #footer>
<div class="w-full flex gap-8px justify-center mx-auto">
<div class="w-full flex gap-2 justify-center mx-auto">
<BrandButton
:text="t('Activate Now')"
:icon-right="ArrowTopRightOnSquareIcon"

View File

@@ -18,5 +18,5 @@ const { darkMode } = storeToRefs(useThemeStore());
:src="partnerInfo?.partnerLogoUrl"
class="w-72"
:class="{ invert: darkMode && partnerInfo.hasPartnerLogo }"
/>
>
</template>

View File

@@ -72,12 +72,12 @@ const steps: readonly Step[] = [
</script>
<template>
<Stepper :default-value="activeStep" class="text-foreground flex w-full items-start gap-2 text-16px">
<Stepper :default-value="activeStep" class="text-foreground flex w-full items-start gap-2 text-base">
<StepperItem
v-for="step in steps"
:key="step.step"
v-slot="{ state }: { state: StepState }"
class="relative flex w-full flex-col items-center justify-center data-[disabled]:opacity-100"
class="relative flex w-full flex-col items-center justify-center data-disabled:opacity-100"
:step="step.step"
:disabled="true"
>

View File

@@ -57,7 +57,7 @@ watchEffect(() => {
</script>
<template>
<div id="modals" ref="modals" class="relative z-[99999]">
<div id="modals" ref="modals" class="relative z-99999">
<Dialog
v-model="showModal"
:show-footer="false"
@@ -70,9 +70,9 @@ watchEffect(() => {
<ActivationPartnerLogo />
</div>
<h1 class="text-center text-20px sm:text-24px font-semibold mt-4">{{ title }}</h1>
<h1 class="text-center text-xl sm:text-2xl font-semibold mt-4">{{ title }}</h1>
<div class="sm:max-w-lg mx-auto mt-2 text-center">
<p class="text-18px sm:text-20px opacity-75">
<p class="text-lg sm:text-xl opacity-75">
{{ t(`First, you'll create your device's login credentials, then you'll activate your Unraid license—your device's operating system (OS).`) }}
</p>
</div>
@@ -89,7 +89,7 @@ watchEffect(() => {
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -38,4 +38,4 @@ export const ACTIVATION_CODE_QUERY = graphql(/* GraphQL */ `
}
}
}
`);
`);

View File

@@ -207,7 +207,7 @@ async function upsertKey() {
"
@primary-click="upsertKey"
>
<div class="max-w-800px">
<div class="max-w-[800px]">
<form @submit.prevent="upsertKey">
<div class="mb-2">
<Label for="api-key-name">Name</Label>
@@ -290,7 +290,7 @@ async function upsertKey() {
(e.target as HTMLInputElement)?.checked
)
"
/>
>
<span class="text-sm">{{ action }}</span>
</label>
</div>

View File

@@ -127,7 +127,7 @@ async function copyKeyValue(keyValue: string) {
}}</Badge>
</div>
</div>
<div class="flex gap-2 flex-shrink-0">
<div class="flex gap-2 shrink-0">
<Button variant="secondary" size="sm" @click="openCreateModal(key)">Edit</Button>
<Button variant="destructive" size="sm" @click="_deleteKey(key.id)">Delete</Button>
</div>

View File

@@ -27,4 +27,4 @@ export function actionVariant(action: string):
default:
return 'gray';
}
}
}

View File

@@ -6,7 +6,7 @@ import ApiKeyManager from '~/components/ApiKey/ApiKeyManager.vue';
<ApiKeyManager />
</div>
</template>
<style lang="postcss">
<style>
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -13,10 +13,10 @@ const { authAction, stateData } = storeToRefs(serverStore);
</script>
<template>
<div class="whitespace-normal flex flex-col gap-y-16px max-w-3xl">
<div class="whitespace-normal flex flex-col gap-y-4 max-w-3xl">
<span v-if="stateData.error" class="text-unraid-red font-semibold">
<h3 class="text-16px mb-8px">{{ t(stateData.heading) }}</h3>
<span class="text-14px" v-html="t(stateData.message)" />
<h3 class="text-base mb-2">{{ t(stateData.heading) }}</h3>
<span class="text-sm" v-html="t(stateData.message)" />
</span>
<span v-if="authAction">
<BrandButton
@@ -31,7 +31,7 @@ const { authAction, stateData } = storeToRefs(serverStore);
</div>
</template>
<style lang="postcss">
<style>
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -19,15 +19,15 @@ const { avatar, connectPluginInstalled, registered, username } = storeToRefs(ser
</script>
<template>
<figure class="group relative z-0 flex items-center justify-center w-36px h-36px rounded-full bg-gradient-to-r from-unraid-red to-orange">
<figure class="group relative z-0 flex items-center justify-center w-9 h-9 rounded-full bg-linear-to-r from-unraid-red to-orange">
<img
v-if="avatar && connectPluginInstalled && registered"
:src="avatar"
:alt="username"
class="absolute z-10 inset-0 w-36px h-36px rounded-full overflow-hidden"
class="absolute z-10 inset-0 w-9 h-9 rounded-full overflow-hidden"
>
<template v-else>
<BrandMark gradient-start="#fff" gradient-stop="#fff" class="opacity-100 absolute z-10 w-36px px-4px" />
<BrandMark gradient-start="#fff" gradient-stop="#fff" class="opacity-100 absolute z-10 w-9 px-[4px]" />
</template>
</figure>
</template>

View File

@@ -15,7 +15,7 @@ onBeforeMount(() => {
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -87,7 +87,7 @@ watch([form], () => {
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -107,14 +107,14 @@ const onChange = ({ data }: { data: Record<string, unknown> }) => {
>
<template v-if="connectPluginInstalled">
<Label>Account Status:</Label>
<div v-html="'<unraid-auth></unraid-auth>'"></div>
<div v-html="'<unraid-auth></unraid-auth>'"/>
</template>
<Label>Download Unraid API Logs:</Label>
<div
v-html="
'<unraid-download-api-logs></unraid-download-api-logs>'
"
></div>
/>
</div>
<!-- auto-generated settings form -->
<div class="mt-6 pl-3 [&_.vertical-layout]:space-y-6">
@@ -136,7 +136,6 @@ const onChange = ({ data }: { data: Record<string, unknown> }) => {
</div>
<div class="col-start-2 ml-10 space-y-4">
<BrandButton
variant="outline-primary"
padding="lean"
size="12px"
class="leading-normal"
@@ -151,7 +150,7 @@ const onChange = ({ data }: { data: Record<string, unknown> }) => {
</div>
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '../../assets/main.css';

View File

@@ -24,7 +24,7 @@ import { CogIcon } from '@heroicons/vue/24/solid';
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -78,7 +78,7 @@ onBeforeMount(() => {
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -16,7 +16,7 @@ const downloadUrl = computed(() => {
</script>
<template>
<div class="whitespace-normal flex flex-col gap-y-16px max-w-3xl">
<div class="whitespace-normal flex flex-col gap-y-4 max-w-3xl">
<span>
{{ t('The primary method of support for Unraid Connect is through our forums and Discord.') }}
{{
@@ -26,7 +26,7 @@ const downloadUrl = computed(() => {
}}
{{ t('The logs may contain sensitive information so do not post them publicly.') }}
</span>
<span class="flex flex-col gap-y-16px">
<span class="flex flex-col gap-y-4">
<div class="flex">
<BrandButton
class="grow-0 shrink-0"
@@ -39,40 +39,40 @@ const downloadUrl = computed(() => {
/>
</div>
<div class="flex flex-row items-baseline gap-8px">
<div class="flex flex-row items-baseline gap-2">
<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"
class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-2"
>
{{ t('Unraid Connect Forums') }}
<ArrowTopRightOnSquareIcon class="w-16px" />
<ArrowTopRightOnSquareIcon class="w-4" />
</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"
class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-2"
>
{{ t('Unraid Discord') }}
<ArrowTopRightOnSquareIcon class="w-16px" />
<ArrowTopRightOnSquareIcon class="w-4" />
</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"
class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-2"
>
{{ t('Unraid Contact Page') }}
<ArrowTopRightOnSquareIcon class="w-16px" />
<ArrowTopRightOnSquareIcon class="w-4" />
</a>
</div>
</span>
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -27,7 +27,7 @@ const { selector, serverState } = storeToRefs(store);
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -90,7 +90,7 @@ const updateOsStatus = computed(() => {
:src="'/webGui/images/UN-logotype-gradient.svg'"
class="w-[160px] h-auto max-h-[30px] object-contain"
alt="Unraid Logo"
/>
>
</a>
<div class="flex flex-wrap justify-start gap-2">
@@ -101,7 +101,7 @@ const updateOsStatus = computed(() => {
target="_blank"
rel="noopener"
>
<InformationCircleIcon class="fill-current w-3 h-3 xs:w-4 xs:h-4 flex-shrink-0" />
<InformationCircleIcon class="fill-current w-3 h-3 xs:w-4 xs:h-4 shrink-0" />
{{ osVersion }}
</a>
<component
@@ -128,7 +128,7 @@ const updateOsStatus = computed(() => {
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';

View File

@@ -46,10 +46,10 @@ const filteredKeyActions = computed((): ServerStateDataAction[] | undefined => {
</script>
<template>
<ul v-if="filteredKeyActions" class="flex flex-col gap-y-8px">
<ul v-if="filteredKeyActions" class="flex flex-col gap-y-2">
<li v-for="action in filteredKeyActions" :key="action.name">
<BrandButton
:class="cn('w-full', props.maxWidth ? 'sm:max-w-300px' : '')"
:class="cn('w-full', props.maxWidth ? 'sm:max-w-[300px]' : '')"
:disabled="action?.disabled"
:external="action?.external"
:href="action?.href"

View File

@@ -192,7 +192,7 @@ watch(selectedLogFile, (newValue) => {
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -371,7 +371,7 @@ defineExpose({ refreshLogContent });
<div
class="w-2 h-2 rounded-full bg-green-500 animate-pulse cursor-help"
aria-hidden="true"
></div>
/>
</TooltipTrigger>
<TooltipContent>
<p>Watching log file</p>
@@ -427,7 +427,7 @@ defineExpose({ refreshLogContent });
<!-- Loading indicator for loading more content -->
<div
v-if="state.isLoadingMore"
class="sticky top-0 z-10 bg-muted/80 backdrop-blur-sm border-b border-border rounded-md mx-2 mt-2"
class="sticky top-0 z-10 bg-muted/80 backdrop-blur-xs border-b border-border rounded-md mx-2 mt-2"
>
<div class="flex items-center justify-center p-2 text-xs text-primary-foreground">
<ArrowPathIcon class="h-3 w-3 mr-2 animate-spin" aria-hidden="true" />
@@ -439,12 +439,12 @@ defineExpose({ refreshLogContent });
class="font-mono whitespace-pre-wrap p-4 m-0 text-xs leading-6 hljs"
:class="{ 'theme-dark': isDarkMode, 'theme-light': !isDarkMode }"
v-html="logContent"
></pre>
/>
</div>
</div>
</template>
<style>
<style scoped>
/* Define CSS variables for both light and dark themes */
:root {
/* Light theme colors (default) - adjusted for better readability */

View File

@@ -8,4 +8,4 @@ export const LOG_FILE_SUBSCRIPTION = graphql(/* GraphQL */ `
totalLines
}
}
`);
`);

View File

@@ -39,7 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
titleInMain: false,
headerJustifyCenter: true,
overlayColor: 'bg-black',
overlayOpacity: 'bg-opacity-80',
overlayOpacity: 'bg-black/80',
modalVerticalCenter: true,
disableShadow: false,
disableOverlayClose: false,
@@ -83,7 +83,7 @@ const computedVerticalCenter = computed<string>(() => {
@keyup.esc="closeModal"
>
<div
class="fixed inset-0 flex flex-col min-h-screen w-screen items-center p-8px sm:p-16px overflow-y-auto"
class="fixed inset-0 flex flex-col min-h-screen w-screen items-center p-2 sm:p-4 overflow-y-auto"
:class="computedVerticalCenter"
>
<TransitionChild
@@ -119,11 +119,11 @@ const computedVerticalCenter = computed<string>(() => {
success ? 'shadow-green-600/30 border-green-600/10' : '',
!error && !success && !disableShadow ? 'shadow-orange/10 border-white/10' : '',
]"
class="text-16px text-foreground bg-background text-left relative z-10 mx-auto flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all"
class="text-base text-foreground bg-background text-left relative z-10 mx-auto flex flex-col justify-around border-2 border-solid transform overflow-hidden rounded-lg transition-all"
>
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px sm:block">
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-1 pr-1 sm:block">
<button
class="rounded-md text-foreground bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
class="rounded-md text-foreground bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-hidden focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
type="button"
@click="closeModal"
>
@@ -133,9 +133,9 @@ const computedVerticalCenter = computed<string>(() => {
</div>
<header
class="relative z-0 grid items-start gap-2 p-16px md:p-24px rounded-t"
class="relative z-0 grid items-start gap-2 p-4 md:p-6 rounded-t"
:class="{
'sm:pr-40px': showCloseX,
'sm:pr-10': showCloseX,
'justify-between': !headerJustifyCenter,
'justify-center': headerJustifyCenter,
}"
@@ -145,7 +145,7 @@ const computedVerticalCenter = computed<string>(() => {
<h1
v-if="title && !titleInMain"
:id="ariaLablledById"
class="text-center text-20px sm:text-24px font-semibold flex flex-wrap justify-center gap-x-4px"
class="text-center text-xl sm:text-2xl font-semibold flex flex-wrap justify-center gap-x-1"
>
{{ title }}
<slot name="headerTitle" />
@@ -156,26 +156,26 @@ const computedVerticalCenter = computed<string>(() => {
<div
v-if="$slots['main'] || description"
class="relative max-h-[65vh] tall:max-h-[75vh] flex flex-col gap-y-16px sm:gap-y-24px p-16px md:p-24px overflow-y-auto"
class="relative max-h-[65vh] tall:max-h-[75vh] flex flex-col gap-y-4 sm:gap-y-6 p-4 md:p-6 overflow-y-auto"
:class="[centerContent && 'text-center', !disableShadow && 'shadow-inner']"
>
<div class="flex flex-col gap-y-12px">
<div class="flex flex-col gap-y-3">
<h1
v-if="title && titleInMain"
:id="ariaLablledById"
class="text-center text-20px sm:text-24px font-semibold flex flex-wrap justify-center gap-x-4px"
class="text-center text-xl sm:text-2xl font-semibold flex flex-wrap justify-center gap-x-1"
>
{{ title }}
<slot name="headerTitle" />
</h1>
<h2 v-if="description" class="text-18px sm:text-20px opacity-75" v-html="description" />
<h2 v-if="description" class="text-lg sm:text-xl opacity-75" v-html="description" />
</div>
<div v-if="$slots['main']">
<slot name="main" />
</div>
</div>
<footer v-if="$slots['footer']" class="text-14px relative p-16px md:p-24px">
<footer v-if="$slots['footer']" class="text-sm relative p-4 md:p-6">
<div class="absolute z-0 inset-0 opacity-10 bg-popover" />
<div class="relative z-10">
<slot name="footer" />

View File

@@ -22,7 +22,7 @@ const { modalVisible: apiKeyModalVisible } = storeToRefs(useApiKeyStore());
</script>
<template>
<div id="modals" ref="modals" class="relative z-[99999]">
<div id="modals" ref="modals" class="relative z-99999">
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
<UpcTrial :t="t" :open="trialModalVisible" />
<UpdateOsCheckUpdateResponseModal :t="t" :open="updateOsModalVisible" />
@@ -32,7 +32,7 @@ const { modalVisible: apiKeyModalVisible } = storeToRefs(useApiKeyStore());
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -131,7 +131,7 @@ const prepareToViewNotifications = () => {
</SheetTrigger>
<SheetContent
side="right"
class="w-full max-w-[100vw] sm:max-w-[540px] max-h-screen h-screen min-h-screen px-0 flex flex-col gap-5 pb-0"
class="w-full max-w-screen sm:max-w-[540px] max-h-screen h-screen min-h-screen px-0 flex flex-col gap-5 pb-0"
>
<div class="relative flex flex-col h-full w-full">
<SheetHeader class="ml-1 px-6 items-baseline gap-1 pb-2">
@@ -188,7 +188,7 @@ const prepareToViewNotifications = () => {
importance = strVal === 'all' || !strVal ? undefined : (strVal as Importance);
}
"
></Select>
/>
</div>
<TabsContent value="unread" class="flex-col flex-1 min-h-0">

View File

@@ -204,7 +204,7 @@ provide('isSubmitting', isCreating);
</script>
<template>
<div class="bg-white rounded-lg border border-gray-200 shadow-sm">
<div class="bg-white rounded-lg border border-gray-200 shadow-xs">
<div class="p-6">
<h2 class="text-xl font-medium mb-4">Configure RClone Remote</h2>
@@ -239,7 +239,7 @@ provide('isSubmitting', isCreating);
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
</style>

View File

@@ -126,7 +126,7 @@ declare global {
<div
v-if="showConfigModal"
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"

View File

@@ -33,7 +33,7 @@ const getLinkedRemote = (remote: string | unknown): string => {
</script>
<template>
<div class="bg-white rounded-lg border border-gray-200 shadow-sm p-4">
<div class="bg-white rounded-lg border border-gray-200 shadow-xs p-4">
<div class="flex justify-between items-start">
<div class="space-y-1">
<h3 class="text-lg font-medium">{{ remote.name }}</h3>
@@ -74,4 +74,4 @@ const getLinkedRemote = (remote: string | unknown): string => {
</div>
</div>
</div>
</template>
</template>

View File

@@ -271,22 +271,22 @@ const items = computed((): RegistrationItemProps[] => {
<template>
<div>
<PageContainer class="max-w-800px">
<PageContainer class="max-w-[800px]">
<CardWrapper :increased-padding="true">
<div class="flex flex-col gap-20px sm:gap-24px">
<header class="flex flex-col gap-y-16px">
<div class="flex flex-col gap-5 sm:gap-6">
<header class="flex flex-col gap-y-4">
<h3
class="text-20px md:text-24px font-semibold leading-normal flex flex-row items-center gap-8px"
class="text-xl md:text-2xl font-semibold leading-normal flex flex-row items-center gap-2"
:class="serverErrors.length ? 'text-unraid-red' : 'text-green-500'"
>
<component :is="headingIcon" class="w-24px h-24px" />
<component :is="headingIcon" class="w-6 h-6" />
<span>
{{ heading }}
</span>
</h3>
<div
v-if="subheading"
class="prose text-16px leading-relaxed whitespace-normal opacity-75"
class="prose text-base leading-relaxed whitespace-normal opacity-75"
v-html="subheading"
/>
<span v-if="authAction" class="grow-0">
@@ -325,7 +325,7 @@ const items = computed((): RegistrationItemProps[] => {
</div>
</template>
<style lang="postcss">
<style >
/* Import unraid-ui globals first */
@import '@unraid/ui/styles';
@import '~/assets/main.css';

View File

@@ -26,10 +26,10 @@ const evenBgColor = computed(() => {
error && 'text-white bg-unraid-red',
warning && 'text-black bg-yellow-100',
]"
class="text-16px p-12px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-5 sm:gap-16px items-baseline rounded"
class="text-base p-3 grid grid-cols-1 gap-1 sm:px-5 sm:grid-cols-5 sm:gap-4 items-baseline rounded"
>
<dt v-if="label" class="font-semibold leading-normal sm:col-span-2 flex flex-row sm:justify-end sm:text-right items-center gap-x-8px">
<ShieldExclamationIcon v-if="error" class="w-16px h-16px fill-current" />
<dt v-if="label" class="font-semibold leading-normal sm:col-span-2 flex flex-row sm:justify-end sm:text-right items-center gap-x-2">
<ShieldExclamationIcon v-if="error" class="w-4 h-4 fill-current" />
<span v-html="label" />
</dt>
<dd

View File

@@ -19,7 +19,7 @@ defineProps<{
</script>
<template>
<div class="flex flex-wrap items-center justify-between gap-8px">
<div class="flex flex-wrap items-center justify-between gap-2">
<BrandButton
v-if="keyLinkedStatus !== 'linked' && keyLinkedStatus !== 'checking'"
variant="none"
@@ -41,7 +41,7 @@ defineProps<{
{{ t(keyLinkedOutput.text ?? 'Unknown') }}
</Badge>
<span class="inline-flex flex-wrap-items-start gap-8px">
<span class="inline-flex flex-wrap-items-start gap-2">
<BrandButton
v-if="keyLinkedStatus === 'notLinked'"
variant="underline"
@@ -50,7 +50,7 @@ defineProps<{
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Link Key')"
:title="t('Learn more and link your key to your account')"
class="text-14px"
class="text-sm"
@click="accountStore.linkKey"
/>
<BrandButton
@@ -59,7 +59,7 @@ defineProps<{
:external="true"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
class="text-sm"
@click="accountStore.myKeys"
/>
</span>

Some files were not shown because too many files have changed in this diff Show More